diff --git a/.github/workflows/api-security.yml b/.github/workflows/api-security.yml index e8a8efc879..cf5403df99 100644 --- a/.github/workflows/api-security.yml +++ b/.github/workflows/api-security.yml @@ -1,14 +1,14 @@ -name: 'API: Security' +name: "API: Security" on: push: branches: - - 'master' - - 'v5.*' + - "master" + - "v5.*" pull_request: branches: - - 'master' - - 'v5.*' + - "master" + - "v5.*" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -26,7 +26,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - "3.12" defaults: run: working-directory: ./api @@ -61,8 +61,9 @@ jobs: - name: Safety if: steps.check-changes.outputs.any_changed == 'true' - run: poetry run safety check --ignore 79023,79027 + run: poetry run safety check --ignore 79023,79027,84420 # TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0 + # TODO: 84420 from `azure-core`, that we need fix alltogether with `azure-cli-core` and `knack` - name: Vulture if: steps.check-changes.outputs.any_changed == 'true' diff --git a/.github/workflows/sdk-refresh-oci-regions.yml b/.github/workflows/sdk-refresh-oci-regions.yml index 337e3e2cda..35cbf37828 100644 --- a/.github/workflows/sdk-refresh-oci-regions.yml +++ b/.github/workflows/sdk-refresh-oci-regions.yml @@ -57,8 +57,6 @@ jobs: title: 'feat(oraclecloud): Update commercial regions' labels: | status/waiting-for-revision - severity/low - provider/oraclecloud no-changelog body: | ### Description diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a4164bc43..0aaf524047 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -85,7 +85,6 @@ repos: args: ["--directory=./"] pass_filenames: false - - repo: https://github.com/hadolint/hadolint rev: v2.13.0-beta hooks: @@ -121,7 +120,8 @@ repos: description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities" # TODO: Botocore needs urllib3 1.X so we need to ignore these vulnerabilities 77744,77745. Remove this once we upgrade to urllib3 2.X # TODO: 79023 & 79027 knack ReDoS until `azure-cli-core` (via `cartography`) allows `knack` >=0.13.0 - entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027' + # TODO: 84420 from `azure-core`, that we need fix alltogether with `azure-cli-core` and `knack` + entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79023,79027,84420' language: system - id: vulture diff --git a/README.md b/README.md index d583a33a15..9e5be11c53 100644 --- a/README.md +++ b/README.md @@ -104,15 +104,15 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically | Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface | |---|---|---|---|---|---|---| -| AWS | 585 | 84 | 40 | 17 | Official | UI, API, CLI | -| Azure | 169 | 22 | 17 | 13 | Official | UI, API, CLI | -| GCP | 100 | 17 | 14 | 7 | Official | UI, API, CLI | -| Kubernetes | 84 | 7 | 7 | 9 | Official | UI, API, CLI | -| GitHub | 20 | 2 | 1 | 2 | Official | UI, API, CLI | -| M365 | 72 | 7 | 4 | 4 | Official | UI, API, CLI | -| OCI | 52 | 14 | 1 | 12 | Official | UI, API, CLI | -| Alibaba Cloud | 64 | 9 | 2 | 9 | Official | UI, API, CLI | -| Cloudflare | 29 | 3 | 0 | 5 | Official | CLI | +| AWS | 572 | 83 | 41 | 17 | Official | UI, API, CLI | +| Azure | 165 | 20 | 18 | 13 | Official | UI, API, CLI | +| GCP | 100 | 13 | 15 | 11 | Official | UI, API, CLI | +| Kubernetes | 83 | 7 | 7 | 9 | Official | UI, API, CLI | +| GitHub | 21 | 2 | 1 | 2 | Official | UI, API, CLI | +| M365 | 75 | 7 | 4 | 4 | Official | UI, API, CLI | +| OCI | 51 | 13 | 3 | 12 | Official | UI, API, CLI | +| Alibaba Cloud | 61 | 9 | 3 | 9 | Official | UI, API, CLI | +| Cloudflare | 29 | 2 | 0 | 5 | Official | CLI, API | | IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI | | MongoDB Atlas | 10 | 3 | 0 | 3 | Official | UI, API, CLI | | LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI | diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 17d4eda45b..6eb74e7f53 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the **Prowler API** are documented in this file. - Support CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057) - Support CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061) - Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065) +- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077) ### 🔐 Security diff --git a/api/src/backend/api/attack_paths/views_helpers.py b/api/src/backend/api/attack_paths/views_helpers.py index cb14c7f44b..b3e860a4b8 100644 --- a/api/src/backend/api/attack_paths/views_helpers.py +++ b/api/src/backend/api/attack_paths/views_helpers.py @@ -5,7 +5,6 @@ from typing import Any, Iterable from rest_framework.exceptions import APIException, ValidationError from api.attack_paths import database as graph_database, AttackPathsQueryDefinition -from api.models import AttackPathsScan from config.custom_logging import BackendLogger from tasks.jobs.attack_paths.config import INTERNAL_LABELS @@ -80,12 +79,12 @@ def prepare_query_parameters( def execute_attack_paths_query( - attack_paths_scan: AttackPathsScan, + database_name: str, definition: AttackPathsQueryDefinition, parameters: dict[str, Any], ) -> dict[str, Any]: try: - with graph_database.get_session(attack_paths_scan.graph_database) as session: + with graph_database.get_session(database_name) as session: result = session.run(definition.cypher, parameters) return _serialize_graph(result.graph()) diff --git a/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json b/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json index fdf310458a..8474c542c9 100644 --- a/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json +++ b/api/src/backend/api/fixtures/dev/8_dev_attack_paths_scans.json @@ -9,8 +9,6 @@ "state": "completed", "progress": 100, "update_tag": 1693586667, - "graph_database": "db-a7f0f6de-6f8e-4b3a-8cbe-3f6dd9012345", - "is_graph_database_deleted": false, "task": null, "inserted_at": "2024-09-01T17:24:37Z", "updated_at": "2024-09-01T17:44:37Z", @@ -30,8 +28,6 @@ "state": "executing", "progress": 48, "update_tag": 1697625000, - "graph_database": "db-4a2fb2af-8a60-4d7d-9cae-4ca65e098765", - "is_graph_database_deleted": false, "task": null, "inserted_at": "2024-10-18T10:55:57Z", "updated_at": "2024-10-18T10:56:15Z", diff --git a/api/src/backend/api/migrations/0077_remove_attackpathsscan_graph_database_indexes.py b/api/src/backend/api/migrations/0077_remove_attackpathsscan_graph_database_indexes.py new file mode 100644 index 0000000000..0498b66e92 --- /dev/null +++ b/api/src/backend/api/migrations/0077_remove_attackpathsscan_graph_database_indexes.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.15 on 2026-02-16 09:24 + +from django.contrib.postgres.operations import RemoveIndexConcurrently +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0076_openstack_provider"), + ] + + operations = [ + RemoveIndexConcurrently( + model_name="attackpathsscan", + name="aps_active_graph_idx", + ), + RemoveIndexConcurrently( + model_name="attackpathsscan", + name="aps_completed_graph_idx", + ), + ] diff --git a/api/src/backend/api/migrations/0078_remove_attackpathsscan_graph_database_fields.py b/api/src/backend/api/migrations/0078_remove_attackpathsscan_graph_database_fields.py new file mode 100644 index 0000000000..89c9558817 --- /dev/null +++ b/api/src/backend/api/migrations/0078_remove_attackpathsscan_graph_database_fields.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.15 on 2026-02-16 09:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0077_remove_attackpathsscan_graph_database_indexes"), + ] + + operations = [ + migrations.RemoveField( + model_name="attackpathsscan", + name="graph_database", + ), + migrations.RemoveField( + model_name="attackpathsscan", + name="is_graph_database_deleted", + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index eb09bba683..29dc2db91d 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -691,8 +691,6 @@ class AttackPathsScan(RowLevelSecurityProtectedModel): update_tag = models.BigIntegerField( null=True, blank=True, help_text="Cartography update tag (epoch)" ) - graph_database = models.CharField(max_length=63, null=True, blank=True) - is_graph_database_deleted = models.BooleanField(default=False) ingestion_exceptions = models.JSONField(default=dict, null=True, blank=True) class Meta(RowLevelSecurityProtectedModel.Meta): @@ -719,21 +717,6 @@ class AttackPathsScan(RowLevelSecurityProtectedModel): fields=["tenant_id", "scan_id"], name="aps_scan_lookup_idx", ), - models.Index( - fields=["tenant_id", "provider_id"], - name="aps_active_graph_idx", - include=["graph_database", "id"], - condition=Q(is_graph_database_deleted=False), - ), - models.Index( - fields=["tenant_id", "provider_id", "-completed_at"], - name="aps_completed_graph_idx", - include=["graph_database", "id"], - condition=Q( - state=StateChoices.COMPLETED, - is_graph_database_deleted=False, - ), - ), ] class JSONAPIMeta: diff --git a/api/src/backend/api/tests/test_attack_paths.py b/api/src/backend/api/tests/test_attack_paths.py index 4208107710..1b6e765164 100644 --- a/api/src/backend/api/tests/test_attack_paths.py +++ b/api/src/backend/api/tests/test_attack_paths.py @@ -89,7 +89,6 @@ def test_execute_attack_paths_query_serializes_graph( parameters=[], ) parameters = {"provider_uid": "123"} - attack_paths_scan = SimpleNamespace(graph_database="tenant-db") node = attack_paths_graph_stub_classes.Node( element_id="node-1", @@ -123,15 +122,17 @@ def test_execute_attack_paths_query_serializes_graph( session_ctx.__enter__.return_value = session session_ctx.__exit__.return_value = False + database_name = "db-tenant-test-tenant-id" + with patch( "api.attack_paths.views_helpers.graph_database.get_session", return_value=session_ctx, ) as mock_get_session: result = views_helpers.execute_attack_paths_query( - attack_paths_scan, definition, parameters + database_name, definition, parameters ) - mock_get_session.assert_called_once_with("tenant-db") + mock_get_session.assert_called_once_with(database_name) session.run.assert_called_once_with(definition.cypher, parameters) assert result["nodes"][0]["id"] == "node-1" assert result["nodes"][0]["properties"]["complex"]["items"][0] == "value" @@ -149,7 +150,7 @@ def test_execute_attack_paths_query_wraps_graph_errors( cypher="MATCH (n) RETURN n", parameters=[], ) - attack_paths_scan = SimpleNamespace(graph_database="tenant-db") + database_name = "db-tenant-test-tenant-id" parameters = {"provider_uid": "123"} class ExplodingContext: @@ -168,7 +169,7 @@ def test_execute_attack_paths_query_wraps_graph_errors( ): with pytest.raises(APIException): views_helpers.execute_attack_paths_query( - attack_paths_scan, definition, parameters + database_name, definition, parameters ) mock_logger.error.assert_called_once() diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 02aefa18ff..e9109e61fb 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -3922,7 +3922,6 @@ class TestAttackPathsScanViewSet: attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], - graph_database="tenant-db", ) query_definition = AttackPathsQueryDefinition( id="aws-rds", @@ -3953,10 +3952,16 @@ class TestAttackPathsScanViewSet: ], } + expected_db_name = f"db-tenant-{attack_paths_scan.provider.tenant_id}" + with ( patch( "api.v1.views.get_query_by_id", return_value=query_definition ) as mock_get_query, + patch( + "api.v1.views.graph_database.get_database_name", + return_value=expected_db_name, + ) as mock_get_db_name, patch( "api.v1.views.attack_paths_views_helpers.prepare_query_parameters", return_value=prepared_parameters, @@ -3978,17 +3983,18 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_200_OK mock_get_query.assert_called_once_with("aws-rds") + mock_get_db_name.assert_called_once_with(attack_paths_scan.provider.tenant_id) mock_prepare.assert_called_once_with( query_definition, {}, attack_paths_scan.provider.uid, ) mock_execute.assert_called_once_with( - attack_paths_scan, + expected_db_name, query_definition, prepared_parameters, ) - mock_clear_cache.assert_called_once_with(attack_paths_scan.graph_database) + mock_clear_cache.assert_called_once_with(expected_db_name) result = response.json()["data"] attributes = result["attributes"] assert attributes["nodes"] == graph_payload["nodes"] @@ -4019,31 +4025,6 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_400_BAD_REQUEST assert "must be completed" in response.json()["errors"][0]["detail"] - def test_run_attack_paths_query_requires_graph_database( - self, - authenticated_client, - providers_fixture, - scans_fixture, - create_attack_paths_scan, - ): - provider = providers_fixture[0] - attack_paths_scan = create_attack_paths_scan( - provider, - scan=scans_fixture[0], - graph_database=None, - ) - - response = authenticated_client.post( - reverse( - "attack-paths-scans-queries-run", kwargs={"pk": attack_paths_scan.id} - ), - data=self._run_payload(), - content_type=API_JSON_CONTENT_TYPE, - ) - - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert "does not reference a graph database" in str(response.json()) - def test_run_attack_paths_query_unknown_query( self, authenticated_client, diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 0c61622d06..bba1689f7d 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -2428,15 +2428,6 @@ class AttackPathsScanViewSet(BaseRLSViewSet): } ) - if not attack_paths_scan.graph_database: - logger.error( - f"The Attack Paths Scan {attack_paths_scan.id} does not reference a graph database" - ) - return Response( - {"detail": "The Attack Paths scan does not reference a graph database"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - payload = attack_paths_views_helpers.normalize_run_payload(request.data) serializer = AttackPathsQueryRunRequestSerializer(data=payload) serializer.is_valid(raise_exception=True) @@ -2450,6 +2441,9 @@ class AttackPathsScanViewSet(BaseRLSViewSet): {"id": "Unknown Attack Paths query for the selected provider"} ) + database_name = graph_database.get_database_name( + attack_paths_scan.provider.tenant_id + ) parameters = attack_paths_views_helpers.prepare_query_parameters( query_definition, serializer.validated_data.get("parameters", {}), @@ -2457,9 +2451,9 @@ class AttackPathsScanViewSet(BaseRLSViewSet): ) graph = attack_paths_views_helpers.execute_attack_paths_query( - attack_paths_scan, query_definition, parameters + database_name, query_definition, parameters ) - graph_database.clear_cache(attack_paths_scan.graph_database) + graph_database.clear_cache(database_name) status_code = status.HTTP_200_OK if not graph.get("nodes"): diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index e2c22e5112..189d5a31d5 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -1625,7 +1625,6 @@ def create_attack_paths_scan(): scan=None, state=StateChoices.COMPLETED, progress=0, - graph_database="tenant-db", **extra_fields, ): scan_instance = scan or Scan.objects.create( @@ -1642,7 +1641,6 @@ def create_attack_paths_scan(): "scan": scan_instance, "state": state, "progress": progress, - "graph_database": graph_database, } payload.update(extra_fields) diff --git a/api/src/backend/tasks/jobs/attack_paths/db_utils.py b/api/src/backend/tasks/jobs/attack_paths/db_utils.py index f3b53a48c9..5c11c5373c 100644 --- a/api/src/backend/tasks/jobs/attack_paths/db_utils.py +++ b/api/src/backend/tasks/jobs/attack_paths/db_utils.py @@ -66,7 +66,6 @@ def starting_attack_paths_scan( attack_paths_scan.state = StateChoices.EXECUTING attack_paths_scan.started_at = datetime.now(tz=timezone.utc) attack_paths_scan.update_tag = cartography_config.update_tag - attack_paths_scan.graph_database = cartography_config.neo4j_database attack_paths_scan.save( update_fields=[ @@ -74,7 +73,6 @@ def starting_attack_paths_scan( "state", "started_at", "update_tag", - "graph_database", ] ) @@ -118,38 +116,6 @@ def update_attack_paths_scan_progress( attack_paths_scan.save(update_fields=["progress"]) -def get_old_attack_paths_scans( - tenant_id: str, - provider_id: str, - attack_paths_scan_id: str, -) -> list[ProwlerAPIAttackPathsScan]: - """ - An `old_attack_paths_scan` is any `completed` Attack Paths scan for the same provider, - with its graph database not deleted, excluding the current Attack Paths scan. - """ - - with rls_transaction(tenant_id): - completed_scans_qs = ( - ProwlerAPIAttackPathsScan.objects.filter( - provider_id=provider_id, - state=StateChoices.COMPLETED, - is_graph_database_deleted=False, - ) - .exclude(id=attack_paths_scan_id) - .all() - ) - - return list(completed_scans_qs) - - -def update_old_attack_paths_scan( - old_attack_paths_scan: ProwlerAPIAttackPathsScan, -) -> None: - with rls_transaction(old_attack_paths_scan.tenant_id): - old_attack_paths_scan.is_graph_database_deleted = True - old_attack_paths_scan.save(update_fields=["is_graph_database_deleted"]) - - def fail_attack_paths_scan( tenant_id: str, scan_id: str, diff --git a/api/src/backend/tasks/jobs/attack_paths/scan.py b/api/src/backend/tasks/jobs/attack_paths/scan.py index 759804b0a4..bb4d735796 100644 --- a/api/src/backend/tasks/jobs/attack_paths/scan.py +++ b/api/src/backend/tasks/jobs/attack_paths/scan.py @@ -193,30 +193,6 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]: f"{prowler_api_provider.provider.upper()} provider {prowler_api_provider.id}" ) - # TODO - # This piece of code delete old Neo4j databases for this tenant's provider - # When we clean all of these databases we need to: - # - Delete this block - # - Delete function from `db_utils` the functions get_old_attack_paths_scans` & `update_old_attack_paths_scan` - # - Remove `graph_database` & `is_graph_database_deleted` from the AttackPathsScan model: - # - Check indexes - # - Create migration - # - The use of `attack_paths_scan.graph_database` on `views` and `views_helpers` - # - Tests - old_attack_paths_scans = db_utils.get_old_attack_paths_scans( - prowler_api_provider.tenant_id, - prowler_api_provider.id, - attack_paths_scan.id, - ) - for old_attack_paths_scan in old_attack_paths_scans: - old_graph_database = old_attack_paths_scan.graph_database - if old_graph_database and old_graph_database != tenant_database_name: - logger.info( - f"Dropping old Neo4j database {old_graph_database} for provider {prowler_api_provider.id}" - ) - graph_database.drop_database(old_graph_database) - db_utils.update_old_attack_paths_scan(old_attack_paths_scan) - logger.info(f"Dropping temporary Neo4j database {tmp_database_name}") graph_database.drop_database(tmp_database_name) diff --git a/api/src/backend/tasks/tests/test_attack_paths_scan.py b/api/src/backend/tasks/tests/test_attack_paths_scan.py index dee0a2d3e1..4dc147aa51 100644 --- a/api/src/backend/tasks/tests/test_attack_paths_scan.py +++ b/api/src/backend/tasks/tests/test_attack_paths_scan.py @@ -28,10 +28,6 @@ class TestAttackPathsRun: "tasks.jobs.attack_paths.scan.utils.call_within_event_loop", side_effect=lambda fn, *a, **kw: fn(*a, **kw), ) - @patch( - "tasks.jobs.attack_paths.scan.db_utils.get_old_attack_paths_scans", - return_value=[], - ) @patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan") @patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress") @patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan") @@ -76,7 +72,6 @@ class TestAttackPathsRun: mock_starting, mock_update_progress, mock_finish, - mock_get_old_scans, mock_event_loop, mock_drop_db, tenants_fixture, diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 4ff3729962..2f39207079 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🚀 Added +- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033) - OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003) - `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833) - `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832) @@ -16,9 +17,11 @@ All notable changes to the **Prowler SDK** are documented in this file. - CSA CCM 4.0 for the Oracle Cloud provider [(#10057)](https://github.com/prowler-cloud/prowler/pull/10057) - OCI regions updater script and CI workflow [(#10020)](https://github.com/prowler-cloud/prowler/pull/10020) - `image` provider for container image scanning with Trivy integration [(#9984)](https://github.com/prowler-cloud/prowler/pull/9984) -- Private registry authentication for the Image provider via environment variables (`REGISTRY_USERNAME`/`REGISTRY_PASSWORD`, `REGISTRY_TOKEN`) [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985) -- Registry scan mode for Image provider: enumerate and scan all images from OCI, Docker Hub, and ECR registries with `--registry`, `--image-filter`, `--tag-filter`, `--max-images`, `--registry-insecure`, and `--registry-list` flags, including public Docker Hub namespace scanning without credentials +- OpenStack compute 7 new checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944) - CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061) +- ECS Exec (ECS-006) privilege escalation detection via `ecs:ExecuteCommand` + `ecs:DescribeTasks` [(#10066)](https://github.com/prowler-cloud/prowler/pull/10066) +- Registry scan mode for `image` provider: enumerate and scan all images from OCI standard, Docker Hub, and ECR [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985) + ### 🔄 Changed @@ -26,7 +29,16 @@ All notable changes to the **Prowler SDK** are documented in this file. - Parallelize Cloudflare zone API calls with threading to improve scan performance [(#9982)](https://github.com/prowler-cloud/prowler/pull/9982) - Update GCP API Keys service metadata to new format [(#9637)](https://github.com/prowler-cloud/prowler/pull/9637) - Update GCP BigQuery service metadata to new format [(#9638)](https://github.com/prowler-cloud/prowler/pull/9638) +- Update GCP Cloud SQL service metadata to new format [(#9639)](https://github.com/prowler-cloud/prowler/pull/9639) - Update GCP Cloud Storage service metadata to new format [(#9640)](https://github.com/prowler-cloud/prowler/pull/9640) +- Update GCP Compute Engine service metadata to new format [(#9641)](https://github.com/prowler-cloud/prowler/pull/9641) +- Update GCP Dataproc service metadata to new format [(#9642)](https://github.com/prowler-cloud/prowler/pull/9642) +- Update GCP DNS service metadata to new format [(#9643)](https://github.com/prowler-cloud/prowler/pull/9643) +- Update GCP GCR service metadata to new format [(#9644)](https://github.com/prowler-cloud/prowler/pull/9644) +- Update GCP GKE service metadata to new format [(#9645)](https://github.com/prowler-cloud/prowler/pull/9645) +- Update GCP IAM service metadata to new format [(#9646)](https://github.com/prowler-cloud/prowler/pull/9646) +- Update GCP KMS service metadata to new format [(#9647)](https://github.com/prowler-cloud/prowler/pull/9647) +- Update GCP Logging service metadata to new format [(#9648)](https://github.com/prowler-cloud/prowler/pull/9648) ### 🔐 Security @@ -39,6 +51,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed - `pip install prowler` failing on systems without C compiler due to `netifaces` transitive dependency from `openstacksdk` [(#10055)](https://github.com/prowler-cloud/prowler/pull/10055) +- `kms_key_not_publicly_accessible` false negative for specific KMS actions (e.g., `kms:DescribeKey`, `kms:Decrypt`) with unrestricted principals [(#10071)](https://github.com/prowler-cloud/prowler/pull/10071) --- diff --git a/prowler/compliance/github/cis_1.0_github.json b/prowler/compliance/github/cis_1.0_github.json index f016acaabb..e675470169 100644 --- a/prowler/compliance/github/cis_1.0_github.json +++ b/prowler/compliance/github/cis_1.0_github.json @@ -778,7 +778,9 @@ { "Id": "1.3.9", "Description": "Confirm the domains an organization owns with a \"Verified\" badge.", - "Checks": [], + "Checks": [ + "organization_verified_badge" + ], "Attributes": [ { "Section": "1 Source Code", diff --git a/prowler/providers/aws/services/iam/lib/privilege_escalation.py b/prowler/providers/aws/services/iam/lib/privilege_escalation.py index 99d3e4dad9..9fded2d5c9 100644 --- a/prowler/providers/aws/services/iam/lib/privilege_escalation.py +++ b/prowler/providers/aws/services/iam/lib/privilege_escalation.py @@ -254,6 +254,11 @@ privilege_escalation_policies_combination = { "iam:PassRole", "ecs:RunTask", }, + # Prerequisite: Running ECS task with ECS Exec enabled and admin task role + "ECS+ExecuteCommand": { + "ecs:ExecuteCommand", + "ecs:DescribeTasks", + }, # SageMaker-based privilege escalation patterns "PassRole+SageMakerCreateNotebookInstance": { "iam:PassRole", diff --git a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json index 4d801f8da9..157e93faca 100644 --- a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json @@ -24,9 +24,9 @@ ], "Remediation": { "Code": { - "CLI": "aws kms put-key-policy --key-id --policy-name default --policy '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam:::root\"},\"Action\":\"kms:*\",\"Resource\":\"*\"}]}'", + "CLI": "aws kms put-key-policy --key-id --policy-name default --policy '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam:::root\"},\"Action\":\"kms:\\*\",\"Resource\":\"\\*\"}]}'", "NativeIaC": "```yaml\n# CloudFormation: restrict KMS key policy to account root (removes any public access)\nResources:\n :\n Type: AWS::KMS::Key\n Properties:\n KeyPolicy:\n Version: '2012-10-17'\n Statement:\n - Effect: Allow\n Principal:\n AWS: arn:aws:iam:::root # Critical: only account root can access; prevents public \"*\" principals\n Action: kms:*\n Resource: '*'\n```", - "Other": "1. Open AWS Console > Key Management Service (KMS)\n2. Select the affected key and go to the Key policy tab\n3. Click Edit and remove any statement with Principal set to \"*\" (or AWS: \"*\")\n4. Ensure a statement exists that allows only arn:aws:iam:::root\n5. Save changes", + "Other": "1. Open AWS Console > Key Management Service (KMS)\n2. Select the affected key and go to the Key policy tab\n3. Click Edit and remove any statement with Principal set to \"\\*\" (or AWS: \"\\*\")\n4. Ensure a statement exists that allows only arn:aws:iam:::root\n5. Save changes", "Terraform": "```hcl\n# Restrict KMS key policy to the account root to avoid any public (\"*\") principals\ndata \"aws_caller_identity\" \"current\" {}\n\nresource \"aws_kms_key\" \"\" {\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [\n {\n Effect = \"Allow\"\n Principal = { AWS = \"arn:aws:iam::${data.aws_caller_identity.current.account_id}:root\" } # Critical: limit to account root to remove public access\n Action = \"kms:*\"\n Resource = \"*\"\n }\n ]\n })\n}\n```" }, "Recommendation": { diff --git a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py index 408034647a..011760ac03 100644 --- a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py +++ b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py @@ -19,7 +19,7 @@ class kms_key_not_publicly_accessible(Check): if is_policy_public( key.policy, kms_client.audited_account, - not_allowed_actions=["kms:*"], + not_allowed_actions=[], ): report.status = "FAIL" report.status_extended = ( diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json index a450f39a7e..318b018df6 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_automated_backups/cloudsql_instance_automated_backups.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_automated_backups", - "CheckTitle": "Ensure That Cloud SQL Database Instances Are Configured With Automated Backups", + "CheckTitle": "Cloud SQL database instance has automated backups configured", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That Cloud SQL Database Instances Are Configured With Automated Backups", - "Risk": "Backups provide a way to restore a Cloud SQL instance to recover lost data or recover from a problem with that instance. Automated backups need to be set for any instance that contains data that should be protected from loss or damage. This recommendation is applicable for SQL Server, PostgreSql, MySql generation 1 and MySql generation 2 instances.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL instances** are checked for **automated backups** being configured to run on a schedule and support point-in-time recovery.", + "Risk": "Absent **automated backups**, unintended deletes, corruption, or ransomware can become irreversible. This degrades data **integrity** and **availability**, removes point-in-time recovery options, and widens `RPO`/`RTO`, causing prolonged outages and incomplete restoration after incidents or schema changes.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-automated-backups.html", + "https://cloud.google.com/sql/docs/mysql/backup-recovery/backups", + "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --backup-start-time <[HH:MM]>", + "CLI": "gcloud sql instances patch --backup-start-time ", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-automated-backups.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Click your instance name, then click Edit\n3. In the Backups section, enable Automated backups and set a Start time\n4. Click Save to apply", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"POSTGRES_14\"\n region = \"\"\n\n settings {\n tier = \"db-custom-1-3840\"\n\n backup_configuration {\n enabled = true # Critical: turns on automated backups\n start_time = \"02:00\" # Critical: required to enable backups and set start time\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to have all SQL database instances set to enable automated backups.", - "Url": "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + "Text": "Enable **automated backups** on all Cloud SQL instances holding important data. Set retention and schedules to meet `RPO`/`RTO`, and enable point-in-time recovery. Apply **least privilege** to backup access, use **separation of duties**, consider cross-region resilience, and regularly test restores with monitoring and alerts for failures.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_automated_backups" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json index 6a3779e604..79b7614822 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_local_infile_flag/cloudsql_instance_mysql_local_infile_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_mysql_local_infile_flag", - "CheckTitle": "Ensure That the Local_infile Database Flag for a Cloud SQL MySQL Instance Is Set to Off", + "CheckTitle": "Cloud SQL MySQL instance has the local_infile database flag set to off", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Local_infile Database Flag for a Cloud SQL MySQL Instance Is Set to Off", - "Risk": "The local_infile flag controls the server-side LOCAL capability for LOAD DATA statements. Depending on the local_infile setting, the server refuses or permits local data loading by clients that have LOCAL enabled on the client side.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for MySQL** instances are evaluated for the `local_infile` database flag being explicitly set to `off`, disabling use of `LOAD DATA LOCAL`.\n\nInstances where `local_infile` is absent or not `off` are identified.", + "Risk": "With `local_infile` enabled, clients can send local files via `LOAD DATA LOCAL`. A stolen credential or SQL injection can coerce clients to leak files and mass-ingest unvetted data, compromising **confidentiality** and **integrity**, and aiding lateral movement through secrets imported into the database.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-local-infile-flag.html", + "https://cloud.google.com/sql/docs/mysql/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags local_infile=off", + "CLI": "gcloud sql instances patch --database-flags=local_infile=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-local-infile-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_1#terraform" + "Other": "1. In Google Cloud Console, go to SQL\n2. Select the MySQL instance and click Edit\n3. In Database flags, add or locate \"local_infile\" and set it to Off\n4. Click Save to apply changes", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"MYSQL_8_0\"\n region = \"\"\n\n settings {\n tier = \"\"\n # Critical: disables LOCAL INFILE to pass the check\n database_flags {\n name = \"local_infile\" # sets the specific flag\n value = \"off\" # required value for compliance\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set the local_infile database flag for a Cloud SQL MySQL instance to off.", - "Url": "https://cloud.google.com/sql/docs/mysql/flags" + "Text": "Keep `local_infile` set to `off`. Use governed import channels (e.g., controlled object storage imports) and enforce **least privilege** for bulk-loading. Apply **separation of duties** between ingestion and admin roles, validate file sources and formats, and monitor high-volume loads. *If ever needed, enable only briefly for vetted tasks.*", + "Url": "https://hub.prowler.com/check/cloudsql_instance_mysql_local_infile_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json index 5b5dda6342..d50c19d28b 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_mysql_skip_show_database_flag/cloudsql_instance_mysql_skip_show_database_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_mysql_skip_show_database_flag", - "CheckTitle": "Ensure Skip_show_database Database Flag for Cloud SQL MySQL Instance Is Set to On", + "CheckTitle": "Cloud SQL MySQL instance has skip_show_database flag set to on", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure Skip_show_database Database Flag for Cloud SQL MySQL Instance Is Set to On", - "Risk": "'skip_show_database' database flag prevents people from using the SHOW DATABASES statement if they do not have the SHOW DATABASES privilege.", + "Severity": "low", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL MySQL** instances configure the `skip_show_database` database flag to `on`, limiting use of `SHOW DATABASES` to accounts with the `SHOW DATABASES` privilege.", + "Risk": "Without `skip_show_database` set to `on`, database names can be exposed to unprivileged users, reducing **confidentiality**. Attackers can perform schema **enumeration** and targeted probing, enabling **lateral movement** and privilege escalation against specific datasets.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-skip-show-database-flag.html", + "https://cloud.google.com/sql/docs/mysql/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags skip_show_database=on", + "CLI": "gcloud sql instances patch --database-flags=skip_show_database=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-skip-show-database-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open your MySQL instance and click Edit\n3. Under Flags, click Add item, select skip_show_database, set value to ON\n4. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"MYSQL_8_0\"\n region = \"\"\n\n settings {\n tier = \"db-custom-1-3840\"\n\n database_flags {\n name = \"skip_show_database\" # Critical: enforce hiding databases from users without SHOW DATABASES privilege\n value = \"on\" # Critical: set flag to 'on' to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set skip_show_database database flag for Cloud SQL Mysql instance to on.", - "Url": "https://cloud.google.com/sql/docs/mysql/flags" + "Text": "Set `skip_show_database` to `on` for all Cloud SQL MySQL instances. Enforce **least privilege** by granting `SHOW DATABASES` only when necessary and reviewing roles regularly. Use **defense in depth**: monitor access and admin actions, and plan changes in maintenance windows as flag updates may trigger restarts.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_mysql_skip_show_database_flag" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json index 922d5136fc..6791e595a3 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_enable_pgaudit_flag/cloudsql_instance_postgres_enable_pgaudit_flag.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_enable_pgaudit_flag", - "CheckTitle": "Ensure That 'cloudsql.enable_pgaudit' Database Flag for each Cloud Sql Postgresql Instance Is Set to 'on' For Centralized Logging", + "CheckTitle": "Cloud SQL PostgreSQL instance has 'cloudsql.enable_pgaudit' flag set to 'on'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That 'cloudsql.enable_pgaudit' Database Flag for each Cloud Sql Postgresql Instance Is Set to 'on' For Centralized Logging", - "Risk": "Ensure cloudsql.enable_pgaudit database flag for Cloud SQL PostgreSQL instance is set to on to allow for centralized logging.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances are evaluated for the database flag `cloudsql.enable_pgaudit` being set to `on`", + "Risk": "Without `cloudsql.enable_pgaudit`, **database activity** lacks granular audit trails. Undetected reads/writes enable insider abuse, credential reuse, or SQL injection without evidence, harming **confidentiality** and **integrity**. Poor traceability slows incident response, forensics, and undermines compliance.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/postgre-sql-audit-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags", + "https://cloud.google.com/sql/docs/postgres/pg-audit" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags cloudsql.enable_pgaudit=On", + "CLI": "gcloud sql instances patch --database-flags cloudsql.enable_pgaudit=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/postgre-sql-audit-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL\n2. Select your PostgreSQL instance and click Edit\n3. In Database flags, click Add item\n4. Set Flag to cloudsql.enable_pgaudit and Value to on\n5. Click Save and restart if prompted", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"us-central1\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-custom-1-3840\"\n database_flags {\n name = \"cloudsql.enable_pgaudit\" # Critical: enable pgAudit\n value = \"on\" # Critical: set flag to 'on' to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "As numerous other recommendations in this section consist of turning on flags for logging purposes, your organization will need a way to manage these logs. You may have a solution already in place. If you do not, consider installing and enabling the open source pgaudit extension within PostgreSQL and enabling its corresponding flag of cloudsql.enable_pgaudit. This flag and installing the extension enables database auditing in PostgreSQL through the open-source pgAudit extension. This extension provides detailed session and object logging to comply with government, financial, & ISO standards and provides auditing capabilities to mitigate threats by monitoring security events on the instance. Enabling the flag and settings later in this recommendation will send these logs to Google Logs Explorer so that you can access them in a central location.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Enable `cloudsql.enable_pgaudit` and configure **pgAudit** to log required classes (e.g., `read`, `write`, `ddl`) under least privilege. Centralize logs, enforce retention and RBAC, and monitor with alerts. *Scope auditing to sensitive data to reduce noise and overhead, and review coverage regularly.*", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_enable_pgaudit_flag" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json index 95f4d094a0..8c7ea9968f 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_connections_flag/cloudsql_instance_postgres_log_connections_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_connections_flag", - "CheckTitle": "Ensure That the Log_connections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", + "CheckTitle": "Cloud SQL PostgreSQL instance has log_connections flag set to on", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Log_connections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", - "Risk": "Enabling the log_connections setting causes each attempted connection to the server to be logged, along with successful completion of client authentication. This parameter cannot be changed after the session starts.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances have the `log_connections` flag set to `on`, causing the server to record every connection attempt and the result of client authentication.", + "Risk": "Without connection logs, unauthorized access attempts can go unnoticed. Attackers may brute-force or reuse credentials without audit evidence, enabling stealthy data access (**confidentiality**), changes via compromised accounts (**integrity**), and connection floods that impact service (**availability**).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_connections=on", + "CLI": "gcloud sql instances patch --database-flags=log_connections=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_3#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open your PostgreSQL instance and click Edit\n3. In Flags, click Add item, select log_connections, set value to on\n4. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"POSTGRES_14\"\n region = \"\"\n\n settings {\n tier = \"db-f1-micro\"\n\n # Critical: enables connection logging to pass the check\n database_flags {\n name = \"log_connections\" # critical\n value = \"on\" # critical\n }\n }\n}\n```" }, "Recommendation": { - "Text": "PostgreSQL does not log attempted connections by default. Enabling the log_connections setting will create log entries for each attempted connection as well as successful completion of client authentication which can be useful in troubleshooting issues and to determine any unusual connection attempts to the server.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Enable `log_connections`=`on` for all PostgreSQL instances.\n- Apply **defense in depth**: also capture disconnects and audit events\n- Centralize logs, retain them, and alert on anomalies\n- Enforce **least privilege** and strong authentication to reduce exposure and improve detection", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_connections_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json index b80ebe79c5..b6fd6835b3 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_disconnections_flag/cloudsql_instance_postgres_log_disconnections_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_disconnections_flag", - "CheckTitle": "Ensure That the log_disconnections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", + "CheckTitle": "Cloud SQL PostgreSQL instance has log_disconnections flag set to on", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the log_disconnections Database Flag for Cloud SQL PostgreSQL Instance Is Set to On", - "Risk": "PostgreSQL does not log session details such as duration and session end by default. Enabling the log_disconnections setting will create log entries at the end of each session which can be useful in troubleshooting issues and determine any unusual activity across a time period.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances have the `log_disconnections` flag set to `on`, creating a record each time a client session ends, including its duration and status", + "Risk": "Without **disconnection logs**, session lifecycles lack visibility, obscuring **credential misuse**, **session hijacking**, and short-lived data exfiltration.\n\nWeak audit trails hinder correlation and forensics, undermining confidentiality and integrity and slowing incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_disconnections=on", + "CLI": "gcloud sql instances patch --database-flags=log_disconnections=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-log-connections-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_4#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Click your PostgreSQL instance\n3. Click Edit\n4. In Database flags, click Add item\n5. Select log_disconnections and set value to on\n6. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"\"\n\n # Critical: enable disconnect logging\n database_flags {\n name = \"log_disconnections\" # sets the required flag\n value = \"on\" # ensures the check passes\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Enabling the log_disconnections setting logs the end of each session, including the session duration.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Enable `log_disconnections=on` to ensure complete session auditing.\n- Pair with `log_connections` and a consistent `log_line_prefix`\n- Centralize and retain logs; alert on anomalies\n- Apply **defense in depth** with routine review of access and audit events", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_disconnections_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json index ceedd32501..4bc0693679 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_error_verbosity_flag/cloudsql_instance_postgres_log_error_verbosity_flag.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_error_verbosity_flag", - "CheckTitle": "Ensure Log_error_verbosity Database Flag for Cloud SQL PostgreSQL Instance Is Set to DEFAULT or Stricter", + "CheckTitle": "Cloud SQL PostgreSQL instance has log_error_verbosity flag set to default", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure Log_error_verbosity Database Flag for Cloud SQL PostgreSQL Instance Is Set to DEFAULT or Stricter", - "Risk": "The log_error_verbosity flag controls the verbosity/details of messages logged.TERSE excludes the logging of DETAIL, HINT, QUERY, and CONTEXT error information. VERBOSE output includes the SQLSTATE error code, source code file name, function name, and line number that generated the error. Ensure an appropriate value is set to 'DEFAULT' or stricter.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** evaluates the `log_error_verbosity` database flag and expects the value `default`.\n\nConfigurations using `terse` or `verbose` are flagged as deviations.", + "Risk": "With `verbose`, logs may reveal SQLSTATE, code paths, and function details, aiding recon and leaking metadata (**confidentiality**). With `terse`, missing DETAIL/HINT/CONTEXT hinders detection and forensics, reducing **integrity** of investigations and **availability** of operational insight.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_error_verbosity=default", + "CLI": "gcloud sql instances patch --database-flags=log_error_verbosity=default", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the PostgreSQL instance\n3. Click Edit\n4. In Database flags, set log_error_verbosity to default (or remove the custom value)\n5. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"main\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"\"\n\n database_flags {\n name = \"log_error_verbosity\"\n value = \"default\" # Critical: sets the flag to default to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Auditing helps in troubleshooting operational problems and also permits forensic analysis. If log_error_verbosity is not set to the correct value, too many details or too few details may be logged. This flag should be configured with a value of 'DEFAULT' or stricter. This recommendation is applicable to PostgreSQL database instances.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Set `log_error_verbosity` to `default` to balance **data minimization** and **observability**.\n\n- Avoid `verbose` in production; restrict log access (least privilege)\n- Avoid `terse` except briefly to curb noise\n- Centralize logs with retention and tamper protection for **defense in depth**", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_error_verbosity_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json index da6af1ee58..856f7e4163 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_duration_statement_flag/cloudsql_instance_postgres_log_min_duration_statement_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_min_duration_statement_flag", - "CheckTitle": "Ensure that the Log_min_duration_statement Flag for a Cloud SQL PostgreSQL Instance Is Set to -1", + "CheckTitle": "Cloud SQL PostgreSQL instance has the log_min_duration_statement flag set to -1", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the Log_min_duration_statement Flag for a Cloud SQL PostgreSQL Instance Is Set to -1", - "Risk": "The log_min_duration_statement flag defines the minimum amount of execution time of a statement in milliseconds where the total duration of the statement is logged. Ensure that log_min_duration_statement is disabled, i.e., a value of -1 is set.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** evaluates whether `log_min_duration_statement` is set to `-1`, disabling **statement duration logging**.", + "Risk": "When duration-based statement logging is enabled, logs can capture full SQL with literals, exposing **confidential data** in log stores. Adversaries or over-privileged users could harvest secrets/PII, profile schemas, and support **lateral movement**. Heavy logging can also raise costs and impact availability under load.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_min_duration_statement=-1", + "CLI": "gcloud sql instances patch --database-flags=log_min_duration_statement=-1", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances\n2. Select your PostgreSQL instance\n3. Click Edit\n4. In Database flags, add or edit log_min_duration_statement and set it to -1\n5. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"\"\n # Critical: set to -1 to pass the check\n database_flags {\n name = \"log_min_duration_statement\" # Critical\n value = \"-1\" # Critical\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Logging SQL statements may include sensitive information that should not be recorded in logs. This recommendation is applicable to PostgreSQL database instances.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Keep `log_min_duration_statement` at `-1` in production to avoid writing sensitive query text to logs. Apply **least privilege** to log access, enforce **data minimization** with redaction and short retention. *If troubleshooting is required*, enable narrowly and temporarily, prefer non-prod, and monitor access.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_min_duration_statement_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json index e70bfb9c9f..cf57b9250e 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_error_statement_flag/cloudsql_instance_postgres_log_min_error_statement_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_min_error_statement_flag", - "CheckTitle": "Ensure that the Log_min_error_statement Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", + "CheckTitle": "Cloud SQL for PostgreSQL instance has log_min_error_statement set to error", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the Log_min_error_statement Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", - "Risk": "The log_min_error_statement flag defines the minimum message severity level that are considered as an error statement. Messages for error statements are logged with the SQL statement. Valid values include DEBUG5, DEBUG4, DEBUG3, DEBUG2, DEBUG1, INFO, NOTICE, WARNING, ERROR, LOG, FATAL, and PANIC. Each severity level includes the subsequent levels mentioned above. Ensure a value of ERROR or stricter is set.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** uses the `log_min_error_statement` flag and expects it set to `error`, the severity threshold that controls when SQL text is logged with error messages.", + "Risk": "An incorrect threshold skews visibility and exposure:\n- Lower than `error`: logs excessive SQL, risking **confidentiality** loss and alert noise (monitoring availability).\n- Higher than `error`: omits query context for real errors, weakening audit trail **integrity** and incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_min_error_statement=error", + "CLI": "gcloud sql instances patch --database-flags=log_min_error_statement=error", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/configure-log-min-error-statement-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances and open your PostgreSQL instance\n2. Click Edit\n3. In Database flags, click Add item, select log_min_error_statement, set value to error\n4. Click Save (the instance will restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"POSTGRES_14\"\n region = \"us-central1\"\n\n settings {\n tier = \"db-custom-2-7680\"\n database_flags {\n name = \"log_min_error_statement\" # critical: requires minimum 'error' level\n value = \"error\" # sets the flag to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Auditing helps in troubleshooting operational problems and also permits forensic analysis. If log_min_error_statement is not set to the correct value, messages may not be classified as error messages appropriately. Considering general log messages as error messages would make is difficult to find actual errors and considering only stricter severity levels as error messages may skip actual errors to log their SQL statements. The log_min_error_statement flag should be set to ERROR or stricter.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Set `log_min_error_statement` to `error` to balance insight and exposure. Enforce a **logging policy** that limits sensitive data in queries and supports **defense in depth**. Periodically review severity and retention to match workload and compliance needs and maintain reliable forensic readiness.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_min_error_statement_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json index b54b9ee8c3..acedebebd9 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_min_messages_flag/cloudsql_instance_postgres_log_min_messages_flag.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_min_messages_flag", - "CheckTitle": "Ensure that the Log_min_messages Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", + "CheckTitle": "Cloud SQL PostgreSQL instance has the log_min_messages flag set to WARNING or higher", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the Log_min_messages Flag for a Cloud SQL PostgreSQL Instance Is Set Appropriately", - "Risk": "Auditing helps in troubleshooting operational problems and also permits forensic analysis. If log_min_messages is not set to the correct value, messages may not be classified as error messages appropriately. An organization will need to decide their own threshold for logging log_min_messages flag.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances are evaluated for the `log_min_messages` flag being set to a sufficiently high severity. Instances with the flag missing or set below `ERROR` (e.g., `DEBUG*`, `INFO`, `NOTICE`) are identified.", + "Risk": "Insufficient `log_min_messages` severity degrades **audit log integrity**, causing real failures to be treated as non-errors or lack context. This delays detection and impairs **forensics**, enabling unnoticed data tampering or repeated faulty operations, impacting the **integrity** and **availability** of the service.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_min_messages=warning", + "CLI": "gcloud sql instances patch --database-flags=log_min_messages=warning", "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_4#terraform" + "Other": "1. In Google Cloud Console, go to SQL and select your PostgreSQL instance\n2. Click Edit\n3. In Database flags, click Add item\n4. Choose log_min_messages and set value to warning\n5. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-f1-micro\"\n\n # Critical: sets the minimum PostgreSQL log level to WARNING (or higher) to pass the check\n database_flags {\n name = \"log_min_messages\" # sets the flag\n value = \"warning\" # acceptable level (WARNING or higher)\n }\n }\n}\n```" }, "Recommendation": { - "Text": "The log_min_messages flag defines the minimum message severity level that is considered as an error statement. Messages for error statements are logged with the SQL statement. Valid values include DEBUG5, DEBUG4, DEBUG3, DEBUG2, DEBUG1, INFO, NOTICE, WARNING, ERROR, LOG, FATAL, and PANIC. Each severity level includes the subsequent levels mentioned above. ERROR is considered the best practice setting. Changes should only be made in accordance with the organization's logging policy.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Set `log_min_messages` to **ERROR or stricter** to ensure error statements are captured with context. Align with centralized logging, retention, and review processes. Prefer **defense in depth** by preserving actionable error telemetry, while balancing verbosity and cost per your logging policy.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_min_messages_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json index 9f178ba2d5..96b9f7de55 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_postgres_log_statement_flag/cloudsql_instance_postgres_log_statement_flag.metadata.json @@ -1,30 +1,34 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_postgres_log_statement_flag", - "CheckTitle": "Ensure That the Log_statement Database Flag for Cloud SQL PostgreSQL Instance Is Set Appropriately", + "CheckTitle": "Cloud SQL PostgreSQL instance has 'log_statement' flag set to 'ddl'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Log_statement Database Flag for Cloud SQL PostgreSQL Instance Is Set Appropriately", - "Risk": "Auditing helps in forensic analysis. If log_statement is not set to the correct value, too many statements may be logged leading to issues in finding the relevant information from the logs, or too few statements may be logged with relevant information missing from the logs.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for PostgreSQL** instances have `log_statement` set to `ddl`, recording only data definition statements in server logs", + "Risk": "Missing `ddl` logging leaves schema changes untracked, undermining **integrity** and hindering investigations.\n\nExcessive logging (e.g., `all`) can inflate volumes, impair **availability**, raise costs, and leak sensitive values, harming **confidentiality**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/postgres/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags log_statement=ddl", + "CLI": "gcloud sql instances patch --database-flags=log_statement=ddl", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances and open \n2. Click Edit\n3. In Database flags, click Add item\n4. Select log_statement and set value to ddl\n5. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-custom-2-7680\"\n\n # Critical: sets PostgreSQL 'log_statement' to 'ddl' to pass the check\n database_flags {\n name = \"log_statement\" # required flag name\n value = \"ddl\" # required value\n }\n }\n}\n```" }, "Recommendation": { - "Text": "The value ddl logs all data definition statements. A value of 'ddl' is recommended unless otherwise directed by your organization's logging policy.", - "Url": "https://cloud.google.com/sql/docs/postgres/flags" + "Text": "Configure `log_statement` to `ddl` to capture schema changes without excessive noise. Apply **defense in depth**: use targeted auditing for data access, restrict and monitor log access with **least privilege**, and enforce log retention and rotation to protect availability.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_postgres_log_statement_flag" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json index 0d2bab9aec..35aad807ec 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_private_ip_assignment/cloudsql_instance_private_ip_assignment.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_private_ip_assignment", - "CheckTitle": "Ensure Instance IP assignment is set to private", + "CheckTitle": "Cloud SQL instance has no public IP addresses", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure Instance IP assignment is set to private", - "Risk": "Instance addresses can be public IP or private IP. Public IP means that the instance is accessible through the public internet. In contrast, instances using only private IP are not accessible through the public internet, but are accessible through a Virtual Private Cloud (VPC). Limiting network access to your database will limit potential attacks.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "Cloud SQL instances are evaluated for IP assignment, highlighting instances that have any **public IP** instead of being restricted to **private IP** only.", + "Risk": "**Public database endpoints** expose services to Internet scanning, brute-force logins, and exploit attempts. A compromise can cause data exfiltration (**confidentiality**), unauthorized changes (**integrity**), and outages or DDoS impact (**availability**), while bypassing VPC isolation and enabling lateral movement.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/mysql/configure-private-ip", + "https://docs.cloud.google.com/sql/docs/sqlserver/recommender-disable-public-ip" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud beta sql instances patch --project= --network=projects//global/networks/ --no-assign-ip", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL and open \n2. Click Connections > Networking\n3. Uncheck Public IP\n4. Check Private IP and select your VPC network\n5. If prompted, click Set up connection to create private services access, then continue\n6. Click Save and wait for the instance to restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"POSTGRES_14\"\n\n settings {\n tier = \"db-f1-micro\"\n\n ip_configuration {\n ipv4_enabled = false # Critical: disables public IP\n private_network = \"projects//global/networks/\" # Critical: enables private IP on the specified VPC\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Setting databases access only to private will reduce attack surface.", - "Url": "https://cloud.google.com/sql/docs/mysql/configure-private-ip" + "Text": "Use **private IP-only** connectivity for databases. Remove public IPs, segment access to required VPCs/subnets, and enforce **least privilege** with strong auth and TLS. For external access, use secure private channels (VPN/Interconnect) or a hardened bastion. Monitor connections as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_private_ip_assignment" } }, - "Categories": [], + "Categories": [ + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json index c1d2963974..516042bb12 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_access/cloudsql_instance_public_access.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_public_access", - "CheckTitle": "Ensure That Cloud SQL Database Instances Do Not Implicitly Whitelist All Public IP Addresses ", + "CheckTitle": "Cloud SQL instance does not allow 0.0.0.0/0 in authorized networks", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That Cloud SQL Database Instances Do Not Implicitly Whitelist All Public IP Addresses ", - "Risk": "To minimize attack surface on a Database server instance, only trusted/known and required IP(s) should be white-listed to connect to it. An authorized network should not have IPs/networks configured to 0.0.0.0/0 which will allow access to the instance from anywhere in the world. Note that authorized networks apply only to instances with public IPs.", + "Severity": "critical", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL authorized networks** are checked for the open CIDR `0.0.0.0/0` on instances using a public IP.\n\nThe finding flags configurations where a catch-all entry exists instead of specific client ranges.", + "Risk": "Allowing `0.0.0.0/0` makes the database reachable from the Internet, degrading **confidentiality** and **availability**. Attackers can brute-force credentials, probe for vulnerable endpoints, exfiltrate data via unauthorized queries, and trigger resource exhaustion through automated scanning.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/publicly-accessible-cloud-sql-instances.html", + "https://cloud.google.com/sql/docs/mysql/connection-org-policy" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --authorized-networks=IP_ADDR1,IP_ADDR2...", + "CLI": "gcloud sql instances patch --authorized-networks=\"\"", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/publicly-accessible-cloud-sql-instances.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances and select your instance\n2. Open the Connections tab\n3. Under Authorized networks, delete the entry 0.0.0.0/0\n4. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"\"\n\n settings {\n tier = \"\"\n\n ip_configuration {\n authorized_networks {\n value = \"\" # Critical: remove 0.0.0.0/0; allow only specific CIDR to pass the check\n }\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Database Server should accept connections only from trusted Network(s)/IP(s) and restrict access from public IP addresses.", - "Url": "https://cloud.google.com/sql/docs/mysql/connection-org-policy" + "Text": "Enforce **least privilege** network access:\n- Remove `0.0.0.0/0`; allow only trusted, fixed IP ranges\n- Prefer **private IP** or **Private Service Connect** with VPC controls\n- Use proxied access (Cloud SQL Auth Proxy) over direct public connections\n- Apply org policies to prevent broad allowlists", + "Url": "https://hub.prowler.com/check/cloudsql_instance_public_access" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json index cfdf4e1198..e6b70e9737 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_public_ip/cloudsql_instance_public_ip.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_public_ip", - "CheckTitle": "Check for Cloud SQL Database Instances with Public IPs", + "CheckTitle": "Cloud SQL database instance does not have a public IP address", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Check for Cloud SQL Database Instances with Public IPs", - "Risk": "To lower the organization's attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/sql-database-instances-with-public-ips.html", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL instances** are evaluated for exposure via **public IP addresses** instead of `private IP` connectivity within a VPC.\n\nInstances with an externally routable database endpoint are surfaced.", + "Risk": "**Public DB endpoints** expand attack surface:\n- Credential brute force and SQL injection threaten **confidentiality** and **integrity**\n- Internet DDoS reduces **availability**\n- Exposure bypasses VPC controls, easing **lateral movement** and data exfiltration", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/sql/docs/mysql/configure-private-ip", + "https://cloud.google.com/sql/docs/mysql/recommender-disable-public-ip" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --project --network= --no-assign-ip", + "CLI": "gcloud beta sql instances patch --project= --network=projects//global/networks/ --no-assign-ip", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_11", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_11#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances and select \n2. Open Connections > Networking\n3. Check Private IP and select the VPC network; if prompted, click Set up connection to create the private service connection\n4. Uncheck Public IP\n5. Click Save", + "Terraform": "```hcl\n# Cloud SQL instance without public IP\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"MYSQL_8_0\"\n\n settings {\n tier = \"db-f1-micro\"\n ip_configuration {\n ipv4_enabled = false # Critical: disables public (IPv4) IP\n private_network = \"projects//global/networks/\" # Critical: ensures private IP via specified VPC\n }\n }\n}\n```" }, "Recommendation": { - "Text": "To lower the organization's attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", - "Url": "https://cloud.google.com/sql/docs/mysql/configure-private-ip" + "Text": "Prefer **private IP** and disable public endpoints. Access databases over VPC, VPN/Interconnect, or **Private Service Connect**. If `authorized networks` are required, restrict to specific sources-never `0.0.0.0/0`. Enforce **least privilege** IAM, use Cloud SQL connectors/proxy, and layer **defense in depth** with network controls and monitoring.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_public_ip" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json index 20b338e3cc..67a2316db0 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_contained_database_authentication_flag/cloudsql_instance_sqlserver_contained_database_authentication_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_contained_database_authentication_flag", - "CheckTitle": "Ensure that the 'contained database authentication' database flag for Cloud SQL on the SQL Server instance is set to 'off' ", + "CheckTitle": "Cloud SQL for SQL Server instance has 'contained database authentication' flag set to off", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the 'contained database authentication' database flag for Cloud SQL on the SQL Server instance is set to 'off' ", - "Risk": "A contained database includes all database settings and metadata required to define the database and has no configuration dependencies on the instance of the Database Engine where the database is installed. Users can connect to the database without authenticating a login at the Database Engine level. Isolating the database from the Database Engine makes it possible to easily move the database to another instance of SQL Server. Contained databases have some unique threats that should be understood and mitigated by SQL Server Database Engine administrators. Most of the threats are related to the USER WITH PASSWORD authentication process, which moves the authentication boundary from the Database Engine level to the database level, hence this is recommended to disable this flag. This recommendation is applicable to SQL Server database instances.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "Cloud SQL for SQL Server instances are evaluated for the **contained database authentication** setting. The check inspects the `contained database authentication` flag and expects its value to be `off`.", + "Risk": "Enabling contained authentication moves identity checks to the database, bypassing server-level logins and policies. This weakens centralized controls and auditing, enables password spraying on contained users, and can persist users across copies, increasing unauthorized data access and tampering risk to **confidentiality** and **integrity**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-contained-database-authentication-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --database-flags contained database authentication=off", + "CLI": "gcloud sql instances patch --database-flags=\"contained database authentication\"=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-contained-database-authentication-flag.html", - "Terraform": "https://docs.prowler.com/checks/gcp/cloud-sql-policies/bc_gcp_sql_10#terraform" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the SQL Server instance\n3. Click Edit\n4. In Database flags, add or edit: contained database authentication = Off\n5. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"\"\n database_flags {\n name = \"contained database authentication\" # critical: target flag\n value = \"off\" # critical: disable contained DB auth\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set contained database authentication database flag for Cloud SQL on the SQL Server instance to off.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Keep `contained database authentication` set to `off`. Centralize authentication and auditing at the server layer or via directory integration, and apply **least privilege**. Avoid `USER WITH PASSWORD` contained users. If containment is unavoidable, tightly scope usage, enforce strong credentials, and monitor login activity.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_contained_database_authentication_flag" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json index 2d341c6893..ea03e35241 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag", - "CheckTitle": "Ensure that the 'cross db ownership chaining' database flag for Cloud SQL SQL Server instance is set to 'off'", + "CheckTitle": "Cloud SQL SQL Server instance has 'cross db ownership chaining' flag set to off", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure that the 'cross db ownership chaining' database flag for Cloud SQL SQL Server instance is set to 'off'", - "Risk": "Use the cross db ownership for chaining option to configure cross-database ownership chaining for an instance of Microsoft SQL Server. This server option allows you to control cross-database ownership chaining at the database level or to allow cross- database ownership chaining for all databases. Enabling cross db ownership is not recommended unless all of the databases hosted by the instance of SQL Server must participate in cross-database ownership chaining and you are aware of the security implications of this setting.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL SQL Server** instances are evaluated for the `cross db ownership chaining` server flag. The finding identifies SQL Server instances where this flag isn't set to `off`, meaning cross-database ownership chaining is permitted.", + "Risk": "Allowing cross-database ownership chaining erodes database boundaries, impacting **confidentiality** and **integrity**. Users with privileges in one database can traverse ownership chains to access or modify objects in others, enabling **privilege escalation**, **lateral movement**, and unauthorized data exposure.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-cross-db-ownership-chaining-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags cross db ownership=off", + "CLI": "gcloud sql instances patch --database-flags '\"cross db ownership chaining\"=off'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-cross-db-ownership-chaining-flag.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to SQL > Instances\n2. Open the SQL Server instance () and click Edit\n3. Scroll to Flags and click Add item (or edit if present)\n4. Select cross db ownership chaining and set value to off\n5. Click Save and restart if prompted", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"\"\n\n # Critical: ensures the flag is OFF to pass the check\n database_flags {\n name = \"cross db ownership chaining\" # disables cross-database ownership chaining\n value = \"off\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set cross db ownership chaining database flag for Cloud SQL SQL Server instance to off.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Keep `cross db ownership chaining` set to `off` to maintain database isolation. Enforce **least privilege** with explicit per-database permissions and **separation of duties**. Prefer controlled execution patterns (e.g., signed modules) over implicit trusts, and periodically review flags and access. *This flag is deprecated-do not enable it.*", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag" } }, - "Categories": [], + "Categories": [ + "identity-access", + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json index c7c36aab66..119a287573 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_external_scripts_enabled_flag/cloudsql_instance_sqlserver_external_scripts_enabled_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_external_scripts_enabled_flag", - "CheckTitle": "Ensure 'external scripts enabled' database flag for Cloud SQL SQL Server instance is set to 'off'", + "CheckTitle": "Cloud SQL SQL Server instance has 'external scripts enabled' flag set to 'off'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'external scripts enabled' database flag for Cloud SQL SQL Server instance is set to 'off'", - "Risk": "external scripts enabled enable the execution of scripts with certain remote language extensions. This property is OFF by default. When Advanced Analytics Services is installed, setup can optionally set this property to true. As the External Scripts Enabled feature allows scripts external to SQL such as files located in an R library to be executed, which could adversely affect the security of the system, hence this should be disabled.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances have the `external scripts enabled` database flag set to `off`", + "Risk": "Allowing **external scripts** lets SQL invoke language extensions (e.g., R/Python), enabling arbitrary code execution. This can cause data exfiltration (**confidentiality**), tampered query results (**integrity**), and resource exhaustion or service degradation (**availability**), and may facilitate lateral movement from the database layer.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-external-scripts-enabled-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags external scripts enabled=off", + "CLI": "gcloud sql instances patch --database-flags=\"external scripts enabled\"=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-external-scripts-enabled-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to SQL > Instances and select the SQL Server instance\n2. Click Edit\n3. In the Flags section, add or locate \"external scripts enabled\"\n4. Set its value to Off\n5. Click Save to apply (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"db-custom-2-7680\"\n\n # Critical: disables external scripts on the SQL Server instance\n database_flags {\n name = \"external scripts enabled\" # sets the flag\n value = \"off\" # required value to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set external scripts enabled database flag for Cloud SQL SQL Server instance to off", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Keep `external scripts enabled` set to `off`. Apply **least privilege** and **defense in depth** by disabling code-execution features in the database. If analytics are required, use isolated instances, restrict outbound network access, and enforce change control and auditing to prevent misuse.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_external_scripts_enabled_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json index 77477646b7..95b80be870 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_remote_access_flag/cloudsql_instance_sqlserver_remote_access_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_remote_access_flag", - "CheckTitle": "Ensure 'remote access' database flag for Cloud SQL SQL Server instance is set to 'off'", + "CheckTitle": "Cloud SQL SQL Server instance has 'remote access' database flag set to 'off'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'remote access' database flag for Cloud SQL SQL Server instance is set to 'off'", - "Risk": "The remote access option controls the execution of stored procedures from local or remote servers on which instances of SQL Server are running. This default value for this option is 1. This grants permission to run local stored procedures from remote servers or remote stored procedures from the local server. To prevent local stored procedures from being run from a remote server or remote stored procedures from being run on the local server, this must be disabled. The Remote Access option controls the execution of local stored procedures on remote servers or remote stored procedures on local server. 'Remote access' functionality can be abused to launch a Denial-of- Service (DoS) attack on remote servers by off-loading query processing to a target, hence this should be disabled. This recommendation is applicable to SQL Server database instances.", + "Severity": "medium", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances where the `remote access` database flag is `on`, allowing remote procedure calls between servers", + "Risk": "Enabling **remote procedure calls** expands exposure: untrusted servers can invoke stored procedures, leading to **data exfiltration** (confidentiality), unauthorized changes (**integrity**), and **DoS** via resource-heavy remote execution (**availability**). It can also enable lateral movement.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-remote-access-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud sql instances patch --database-flags=\"remote access\"=off", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-remote-access-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the SQL Server instance () and click Edit\n3. Scroll to Database flags and click Add item\n4. Select \"remote access\" and set value to off\n5. Click Save and confirm the restart when prompted\n6. Verify under Overview > Database flags that \"remote access\" = off", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n region = \"\"\n\n settings {\n tier = \"\"\n\n # Critical: disables SQL Server remote access to pass the check\n database_flags {\n name = \"remote access\"\n value = \"off\" # sets the flag to off\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set remote access database flag for Cloud SQL SQL Server instance to off.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Set `remote access` to `off` to reduce the attack surface. Apply **least privilege** and **defense in depth**: avoid remote stored procedures; if business-required, allow only trusted peers, enforce strong authentication, audit calls, and monitor for abuse.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_remote_access_flag" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json index 9df75263ba..4594aedad3 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_trace_flag/cloudsql_instance_sqlserver_trace_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_trace_flag", - "CheckTitle": "Ensure '3625 (trace flag)' database flag for all Cloud SQL Server instances is set to 'on' ", + "CheckTitle": "Cloud SQL for SQL Server instance has trace flag 3625 set to 'on'", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure '3625 (trace flag)' database flag for all Cloud SQL Server instances is set to 'on' ", - "Risk": "Microsoft SQL Trace Flags are frequently used to diagnose performance issues or to debug stored procedures or complex computer systems, but they may also be recommended by Microsoft Support to address behavior that is negatively impacting a specific workload. All documented trace flags and those recommended by Microsoft Support are fully supported in a production environment when used as directed. 3625(trace log) Limits the amount of information returned to users who are not members of the sysadmin fixed server role, by masking the parameters of some error messages using '******'. Setting this in a Google Cloud flag for the instance allows for security through obscurity and prevents the disclosure of sensitive information, hence this is recommended to set this flag globally to on to prevent the flag having been left off, or changed by bad actors. This recommendation is applicable to SQL Server database instances.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances have the `3625 (trace flag)` database flag set to `on`", + "Risk": "Without `3625` enabled, SQL errors can reveal parameters and object names to non-admins, weakening **confidentiality** and aiding targeted **injection**, account enumeration, and data discovery. Leaked context helps craft exploits and pivot attacks, ultimately risking data integrity and availability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/disable-3625-trace-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --database-flags 3625=on", + "CLI": "gcloud sql instances patch --database-flags=3625=on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/disable-3625-trace-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances and open \n2. Click Edit\n3. In Flags, click Add item\n4. Select 3625 (trace flag) and set value to on\n5. Click Save and confirm the restart", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"\"\n\n # Critical: enable SQL Server trace flag 3625\n # This sets the flag to 'on' so the check passes\n database_flags {\n name = \"3625\"\n value = \"on\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to set 3625 (trace flag) database flag for Cloud SQL SQL Server instance to on.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Set trace flag `3625` to `on` for all SQL Server instances in Cloud SQL to limit error details for non-admins. Apply **least privilege**, practice **defense in depth** with application-level error handling, and centralize diagnostics in logs rather than returning verbose messages to clients.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_trace_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json index cce517561a..4d45efcdd9 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_connections_flag/cloudsql_instance_sqlserver_user_connections_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_user_connections_flag", - "CheckTitle": "Ensure 'user Connections' Database Flag for Cloud Sql Sql Server Instance Is Set to a Non-limiting Value", + "CheckTitle": "Cloud SQL SQL Server instance has the 'user connections' database flag set to 0", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'user Connections' Database Flag for Cloud Sql Sql Server Instance Is Set to a Non-limiting Value", - "Risk": "The user connections option specifies the maximum number of simultaneous user connections that are allowed on an instance of SQL Server. The actual number of user connections allowed also depends on the version of SQL Server that you are using, and also the limits of your application or applications and hardware. SQL Server allows a maximum of 32,767 user connections. Because user connections is by default a self- configuring value, with SQL Server adjusting the maximum number of user connections automatically as needed, up to the maximum value allowable. For example, if only 10 users are logged in, 10 user connection objects are allocated. In most cases, you do not have to change the value for this option. The default is 0, which means that the maximum (32,767) user connections are allowed. However if there is a number defined here that limits connections, SQL Server will not allow anymore above this limit. If the connections are at the limit, any new requests will be dropped, potentially causing lost data or outages for those using the database.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances are evaluated to ensure the `user connections` database flag is set to `0` (unlimited), avoiding any artificial cap on concurrent user sessions", + "Risk": "A capped `user connections` value can exhaust available sessions, causing login failures, aborted transactions, and timeouts. This reduces **availability**, can delay administrative access, and may lead to **integrity** issues from failed or inconsistent retries under load.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/configure-user-connection-flag.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch INSTANCE_NAME --database-flags user connections=0", + "CLI": "gcloud sql instances patch --database-flags='\"user connections\"=0'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/configure-user-connection-flag.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Open the SQL Server instance and click Edit\n3. In Database flags, click Add item, select \"user connections\", set value to 0\n4. Click Save (the instance may restart)", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"db-custom-1-3840\"\n\n # Critical: ensure the 'user connections' flag is set to 0 to pass the check\n database_flags {\n name = \"user connections\" # Critical line: target flag\n value = \"0\" # Critical line: set to 0\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to check the user connections for a Cloud SQL SQL Server instance to ensure that it is not artificially limiting connections.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Set `user connections` to `0` to prevent artificial limits. Preserve **availability** with **connection pooling**, controlled retries, and **capacity planning** based on peak usage. *If a cap is required*, size it with ample headroom, monitor connection counts, and review regularly.", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_user_connections_flag" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json index 8b91366494..8027863415 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_sqlserver_user_options_flag/cloudsql_instance_sqlserver_user_options_flag.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_sqlserver_user_options_flag", - "CheckTitle": "Ensure 'user options' database flag for Cloud SQL SQL Server instance is not configured", + "CheckTitle": "Cloud SQL for SQL Server instance does not have the 'user options' flag configured", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure 'user options' database flag for Cloud SQL SQL Server instance is not configured", - "Risk": "The user options option specifies global defaults for all users. A list of default query processing options is established for the duration of a user's work session. The user options option allows you to change the default values of the SET options (if the server's default settings are not appropriate). A user can override these defaults by using the SET statement. You can configure user options dynamically for new logins. After you change the setting of user options, new login sessions use the new setting, current login sessions are not affected.", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "**Cloud SQL for SQL Server** instances are evaluated for the `user options` database flag configured with any value.\n\nThis flag sets global defaults for session `SET` behaviors; the check identifies instances where this global override is present.", + "Risk": "Global `user options` changes affect all sessions, impacting **data integrity** and **availability**. Disabling safe **ANSI behaviors** or enabling **implicit transactions** can alter NULL comparisons and error handling, leading to inconsistent results, lock contention, and application failures, reducing predictability and complicating auditing.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/user-options-flag-not-configured.html", + "https://cloud.google.com/sql/docs/sqlserver/flags" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud sql instances patch --clear-database-flags", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/user-options-flag-not-configured.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL and open your SQL Server instance\n2. Click Edit\n3. In Database flags, locate 'user options' and click the X to remove it\n4. Click Save\n5. Allow the instance to restart to apply the change", + "Terraform": "```hcl\n# Cloud SQL for SQL Server instance with no 'user options' flag set\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"SQLSERVER_2019_STANDARD\"\n\n settings {\n tier = \"db-custom-2-7680\"\n # Remediation: Do NOT set a database_flags block for 'user options'\n # This omission removes/unsets the 'user options' flag so the check passes.\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that, user options database flag for Cloud SQL SQL Server instance should not be configured.", - "Url": "https://cloud.google.com/sql/docs/sqlserver/flags" + "Text": "Leave `user options` unset at the instance level; keep default behavior. Control `SET` options explicitly at session or database scope.\n\n- Enforce **least privilege** for flag management\n- Use **change control** and testing before rollout\n- Monitor for configuration drift as part of **defense in depth**", + "Url": "https://hub.prowler.com/check/cloudsql_instance_sqlserver_user_options_flag" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json index bc1f402431..543081679e 100644 --- a/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json +++ b/prowler/providers/gcp/services/cloudsql/cloudsql_instance_ssl_connections/cloudsql_instance_ssl_connections.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "cloudsql_instance_ssl_connections", - "CheckTitle": "Ensure That the Cloud SQL Database Instance Requires All Incoming Connections To Use SSL", + "CheckTitle": "Cloud SQL database instance requires SSL for all incoming connections", "CheckType": [], "ServiceName": "cloudsql", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "DatabaseInstance", - "ResourceGroup": "database", - "Description": "Ensure That the Cloud SQL Database Instance Requires All Incoming Connections To Use SSL", - "Risk": "SQL database connections if successfully trapped (MITM), can reveal sensitive data like credentials, database queries, query outputs etc. For security, it is recommended to always use SSL encryption when connecting to your instance. This recommendation is applicable for Postgresql, MySql generation 1, MySql generation 2 and SQL Server 2017 instances.", + "Severity": "high", + "ResourceType": "sqladmin.googleapis.com/Instance", + "Description": "Cloud SQL instances enforce **SSL/TLS-only connections**, rejecting plaintext traffic. The connection policy requires encryption for all clients (e.g., `ENCRYPTED_ONLY` or `TRUSTED_CLIENT_CERTIFICATE_REQUIRED`) instead of allowing both encrypted and unencrypted connections.", + "Risk": "Without enforced TLS, database traffic is exposed to interception.\n- **MITM** can read creds and query results (**confidentiality**)\n- Inject/alter statements to corrupt data (**integrity**)\n\nMixed modes cause accidental plaintext use on public or untrusted networks.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudSQL/enable-ssl-for-incoming-connections.html", + "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + ], "Remediation": { "Code": { - "CLI": "gcloud sql instances patch --require-ssl", + "CLI": "gcloud sql instances patch --ssl-mode=ENCRYPTED_ONLY", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudSQL/enable-ssl-for-incoming-connections.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Cloud SQL > Instances\n2. Click your instance name\n3. Open Connections > Security tab\n4. Select \"Allow only SSL connections\"\n5. Click Save", + "Terraform": "```hcl\nresource \"google_sql_database_instance\" \"\" {\n name = \"\"\n region = \"\"\n database_version = \"\"\n\n settings {\n tier = \"\"\n ip_configuration {\n ssl_mode = \"ENCRYPTED_ONLY\" # Critical: only allow SSL/TLS-encrypted connections\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to enforce all incoming connections to SQL database instance to use SSL.", - "Url": "https://cloud.google.com/sql/docs/postgres/configure-ssl-instance/" + "Text": "Require **TLS for all connections**. Prefer `TRUSTED_CLIENT_CERTIFICATE_REQUIRED` or use Cloud SQL Auth Proxy/Connectors for encrypted, authenticated channels.\n- Disallow mixed plaintext/SSL modes\n- Rotate and monitor certificates\n- Combine with **least privilege** and private access", + "Url": "https://hub.prowler.com/check/cloudsql_instance_ssl_connections" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json b/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json index 3ca83f22e6..4aefa91e90 100644 --- a/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_firewall_rdp_access_from_the_internet_allowed/compute_firewall_rdp_access_from_the_internet_allowed.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "compute_firewall_rdp_access_from_the_internet_allowed", - "CheckTitle": "Ensure That RDP Access Is Restricted From the Internet", + "CheckTitle": "Firewall rule does not allow ingress from 0.0.0.0/0 to TCP port 3389 (RDP)", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "FirewallRule", - "ResourceGroup": "network", - "Description": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.", - "Risk": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.", + "ResourceType": "compute.googleapis.com/Firewall", + "Description": "**VPC firewall rules** permitting inbound **RDP** (`TCP 3389`) from `0.0.0.0/0` are flagged, including ingress rules that allow all TCP ports or `all` protocols", + "Risk": "Exposed **RDP** enables Internet-wide scanning and **brute force**. Exploits can yield **remote code execution**, followed by **lateral movement** and data theft.\n\nThis endangers **confidentiality**, **integrity**, and **availability** (e.g., ransomware, service disruption).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/vpc/docs/using-firewalls", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute firewall-rules delete default-allow-rdp", + "CLI": "gcloud compute firewall-rules delete ", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > Firewall.\n2. Find the ingress rule that allows TCP port 3389 with Source IPv4 ranges set to 0.0.0.0/0.\n3. Select the rule and click Delete, then confirm.", + "Terraform": "```hcl\nresource \"google_compute_firewall\" \"\" {\n name = \"\"\n network = \"\"\n\n allow {\n protocol = \"tcp\"\n ports = [\"3389\"]\n }\n\n source_ranges = [\"10.0.0.0/8\"] # CRITICAL: removes 0.0.0.0/0 so RDP is not exposed to the Internet\n}\n```" }, "Recommendation": { - "Text": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.", - "Url": "https://cloud.google.com/vpc/docs/using-firewalls" + "Text": "Restrict **RDP** to trusted IP ranges or a hardened **bastion/IAP** proxy; prefer private access with no public IPs. Apply **least privilege** and network segmentation, use just-in-time access and strong authentication, and monitor logs. Aim for **defense in depth** to minimize exposure.", + "Url": "https://hub.prowler.com/check/compute_firewall_rdp_access_from_the_internet_allowed" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json b/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json index 52fbcf652e..115c6df402 100644 --- a/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_firewall_ssh_access_from_the_internet_allowed/compute_firewall_ssh_access_from_the_internet_allowed.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "compute_firewall_ssh_access_from_the_internet_allowed", - "CheckTitle": "Ensure That SSH Access Is Restricted From the Internet", + "CheckTitle": "Firewall does not expose TCP port 22 (SSH) to the Internet", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "critical", - "ResourceType": "FirewallRule", - "ResourceGroup": "network", - "Description": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow the user to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, only an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the internet to VPC or VM instance using `SSH` on `Port 22` can be avoided.", - "Risk": "Exposing Secure Shell (SSH) port 22 to the Internet can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and brute-force attacks.", + "ResourceType": "compute.googleapis.com/Firewall", + "Description": "**VPC firewall rules** allowing Internet-sourced **ingress** (`0.0.0.0/0`) to `TCP port 22 (SSH)` are identified, including rules using protocol `all` or `tcp` whose ports or ranges include `22`.", + "Risk": "Exposed **SSH (22)** enables Internet-wide scanning, **brute force** and **credential stuffing**. Compromise can yield shell access for **data exfiltration**, command execution, and **lateral movement**, undermining **confidentiality** and **integrity**, and risking **availability** through abuse or lockouts.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/vpc/docs/using-firewalls", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/unrestricted-ssh-access.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute firewall-rules delete default-allow-ssh", + "CLI": "gcloud compute firewall-rules update --source-ranges=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-ssh-access.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_1#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > Firewall\n2. Locate the INGRESS rule that allows tcp:22 with Source IPv4 ranges set to 0.0.0.0/0 and open it\n3. Click Edit\n4. Replace Source IPv4 ranges from 0.0.0.0/0 to your trusted CIDR (e.g., )\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_firewall\" \"\" {\n name = \"\"\n network = \"\"\n\n source_ranges = [\"\"] # Critical: removes 0.0.0.0/0 to stop exposing SSH to the Internet\n\n allow {\n protocol = \"tcp\" # Critical: limit to SSH only\n ports = [\"22\"]\n }\n}\n```" }, "Recommendation": { - "Text": "Check your Google Cloud Virtual Private Cloud (VPC) firewall rules for inbound rules that allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 22 and restrict the access to trusted IP addresses or IP ranges only in order to implement the principle of least privilege and reduce the attack surface. TCP port 22 is used for secure remote login by connecting an SSH client application with an SSH server. It is strongly recommended to configure your Google Cloud VPC firewall rules to limit inbound traffic on TCP port 22 to known IP addresses only.", - "Url": "https://cloud.google.com/vpc/docs/using-firewalls" + "Text": "Restrict **SSH** to trusted sources; avoid `0.0.0.0/0`. Prefer **bastion hosts** or **IAP TCP forwarding**, or use **VPN/peering**. Enforce **least privilege** and **defense in depth**: limit to required CIDRs, use **key-based auth**, disable `PasswordAuthentication`, and monitor/alert on access attempts.", + "Url": "https://hub.prowler.com/check/compute_firewall_ssh_access_from_the_internet_allowed" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json index 513938f322..a24d47ec73 100644 --- a/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_image_not_publicly_shared/compute_image_not_publicly_shared.metadata.json @@ -13,8 +13,7 @@ "Risk": "Publicly shared disk images can expose **sensitive data** and application configurations to unauthorized users.\n\n- Any authenticated GCP user can access the image content\n- Could lead to **data breaches** if images contain secrets or proprietary code\n- Attackers may use exposed images to understand application architecture", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/images/managing-access-custom-images", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/publicly-shared-disk-images.html" + "https://cloud.google.com/compute/docs/images/managing-access-custom-images" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json index 64c1ffe547..d3d075f85a 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_automatic_restart_enabled/compute_instance_automatic_restart_enabled.metadata.json @@ -13,7 +13,6 @@ "Risk": "VM instances without Automatic Restart enabled will not recover automatically from host maintenance events or unexpected failures, potentially leading to prolonged service downtime and requiring manual intervention to restore services.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-automatic-restart.html", "https://cloud.google.com/compute/docs/instances/setting-instance-scheduling-options" ], "Remediation": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json index c9e4259157..1e9ebee74a 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_block_project_wide_ssh_keys_disabled/compute_instance_block_project_wide_ssh_keys_disabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "compute_instance_block_project_wide_ssh_keys_disabled", - "CheckTitle": "Ensure “Block Project-Wide SSH Keys” Is Enabled for VM Instances", + "CheckTitle": "VM instance has Block project-wide SSH keys enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "It is recommended to use Instance specific SSH key(s) instead of using common/shared project-wide SSH key(s) to access Instances.", - "Risk": "Project-wide SSH keys are stored in Compute/Project-meta-data. Project wide SSH keys can be used to login into all the instances within project. Using project-wide SSH keys eases the SSH key management but if compromised, poses the security risk which can impact all the instances within project.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VMs** are evaluated for the metadata key `block-project-ssh-keys` set to `true`, indicating **project-wide SSH keys** are blocked and only instance-level or OS Login credentials are honored.", + "Risk": "Allowing **project-wide SSH keys** lets a single compromised key reach many VMs, amplifying blast radius. This endangers **confidentiality** (data exposure) and **integrity** (unauthorized changes) and enables **lateral movement**. Per-instance revocation and accountability are weakened.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-block-project-wide-ssh-keys.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances add-metadata --metadata block-projectssh-keys=TRUE", + "CLI": "gcloud compute instances add-metadata --zone --metadata=block-project-ssh-keys=true", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-block-project-wide-ssh-keys.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_8#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the target VM and then click Edit\n3. Under Custom metadata, click Add item\n4. Key: block-project-ssh-keys, Value: true\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"vm\" {\n name = \"\"\n zone = \"\"\n machine_type = \"e2-micro\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-12\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n\n metadata = {\n block-project-ssh-keys = \"true\" # Critical: blocks project-wide SSH keys for this VM\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended to use Instance specific SSH keys which can limit the attack surface if the SSH keys are compromised.", - "Url": "https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys" + "Text": "Set `block-project-ssh-keys=true` to prevent shared key inheritance. Prefer **OS Login** or instance-specific keys, enforce **least privilege** and **separation of duties** for metadata changes, use **short-lived credentials** with rotation, limit direct SSH, and monitor access for anomalies.", + "Url": "https://hub.prowler.com/check/compute_instance_block_project_wide_ssh_keys_disabled" } }, - "Categories": [], + "Categories": [ + "identity-access", + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json index fb999ed9d6..c8f7f080a5 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_confidential_computing_enabled/compute_instance_confidential_computing_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_confidential_computing_enabled", - "CheckTitle": "Ensure Compute Instances Have Confidential Computing Enabled", + "CheckTitle": "Compute instance has Confidential Computing enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Ensure that the Confidential Computing security feature is enabled for your Google Cloud virtual machine (VM) instances in order to add protection to your sensitive data in use by keeping it encrypted in memory and using encryption keys that Google doesn't have access to. Confidential Computing is a breakthrough technology which encrypts data while it is being processed. This technology keeps data encrypted in memory, outside the CPU.", - "Risk": "Confidential Computing keeps your sensitive data encrypted while it is used, indexed, queried, or trained on, and does not allow Google to access the encryption keys (these keys are generated in hardware, per VM instance, and can't be exported). In this way, the Confidential Computing feature can help alleviate concerns about risk related to either dependency on Google Cloud infrastructure or Google insiders' access to your data in the clear.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Google Compute Engine VMs** configured as **Confidential VMs** encrypt data in use with hardware-based memory protection and per-instance keys.\n\nThis assessment identifies whether **Confidential Computing** is enabled on each VM instance.", + "Risk": "Absent **Confidential Computing**, plaintext data in RAM can be exposed via host introspection, hypervisor compromise, or cold-boot/DMA attacks, undermining **confidentiality** and enabling **in-memory tampering** that impacts **integrity** of computations, models, and secrets.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/confidential-computing.html", + "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/confidential-computing.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Select and click Stop\n3. Click Edit\n4. Under Confidential VM service, check Enable Confidential VM service\n5. If the option is unavailable, change Machine series to a supported one (e.g., N2D) and select a type\n6. Click Save, then click Start to power on the instance", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"n2d-standard-2\" # Supported for Confidential VM\n zone = \"\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-12\"\n }\n }\n\n network_interface {}\n\n # Critical: Enables Confidential Computing on the VM\n confidential_instance_config {\n enable_confidential_compute = true # Turns on Confidential VM\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that the Confidential Computing security feature is enabled for your Google Cloud virtual machine (VM) instances in order to add protection to your sensitive data in use by keeping it encrypted in memory and using encryption keys that Google doesn't have access to. Confidential Computing is a breakthrough technology which encrypts data while it is being processed. This technology keeps data encrypted in memory, outside the CPU.", - "Url": "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + "Text": "Enable **Confidential VMs** for workloads processing sensitive data to protect data-in-use. Apply **defense in depth**: enforce **least privilege** on administrative access, use disk encryption with `CMEK`, and require workload attestation/trusted images. *If unsupported*, isolate or refactor workloads to compatible options.", + "Url": "https://hub.prowler.com/check/compute_instance_confidential_computing_enabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json index 5d803c5964..9f6b0a4c77 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use/compute_instance_default_service_account_in_use.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_default_service_account_in_use", - "CheckTitle": "Ensure That Instances Are Not Configured To Use the Default Service Account", + "CheckTitle": "Compute Engine instance does not use the default service account", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "It is recommended to configure your instance to not use the default Compute Engine service account because it has the Editor role on the project.", - "Risk": "The default Compute Engine service account has the Editor role on the project, which allows read and write access to most Google Cloud Services. This can lead to a privilege escalations if your VM is compromised allowing an attacker gaining access to all of your project", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/default-service-accounts-in-use.html", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VMs** are evaluated for use of the **default service account** (`[PROJECT_NUMBER]-compute@developer.gserviceaccount.com`). The finding highlights instances configured with that account rather than a workload-specific service account. *GKE node VMs are ignored.*", + "Risk": "Using the default service account often grants project-wide rights (e.g., `roles/editor`). If a VM is compromised, metadata tokens can be abused to read/modify resources, exfiltrate data, and pivot across services, impacting **confidentiality** and **integrity**, and potentially **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/iam/docs/granting-changing-revoking-access", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/default-service-accounts-in-use.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances set-service-account --service-account=", + "CLI": "", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_1", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_1#terraform" + "Other": "1. In Google Cloud console, go to Compute Engine > VM instances\n2. Click the VM, then click Stop and wait until it is stopped\n3. Click Edit\n4. Under Service account, select a non-default service account (not ending with \"-compute@developer.gserviceaccount.com\")\n5. Click Save, then click Start to power the VM back on\n6. If no suitable service account exists: IAM & Admin > Service Accounts > Create service account, grant only required roles, then repeat steps 2-5", + "Terraform": "```hcl\n# Create a non-default service account\nresource \"google_service_account\" \"\" {\n account_id = \"\" # CRITICAL: custom SA to avoid default \"-compute@developer.gserviceaccount.com\"\n}\n\n# Attach the non-default service account to the VM\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"\"\n\n boot_disk { initialize_params { image = \"debian-cloud/debian-12\" } }\n network_interface { network = \"default\" }\n\n service_account {\n email = google_service_account..email # CRITICAL: use non-default SA so the check passes\n }\n}\n```" }, "Recommendation": { - "Text": "To defend against privilege escalations if your VM is compromised and prevent an attacker from gaining access to all of your project, it is recommended to not use the default Compute Engine service account. Instead, you should create a new service account and assigning only the permissions needed by your instance. The default Compute Engine service account is named `[PROJECT_NUMBER]-compute@developer.gserviceaccount.com`.", - "Url": "https://cloud.google.com/iam/docs/granting-changing-revoking-access" + "Text": "Avoid the default service account. Create per-workload service accounts and grant only required roles under **least privilege** and **separation of duties**. Remove broad roles like `roles/editor`. Prefer short-lived credentials and monitor service account usage to enforce **defense in depth**.", + "Url": "https://hub.prowler.com/check/compute_instance_default_service_account_in_use" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json index f115c276cd..6d14d73c6f 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_default_service_account_in_use_with_full_api_access/compute_instance_default_service_account_in_use_with_full_api_access.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_default_service_account_in_use_with_full_api_access", - "CheckTitle": "Ensure That Instances Are Not Configured To Use the Default Service Account With Full Access to All Cloud APIs", + "CheckTitle": "Compute Engine instance does not use the default service account with full access to all Cloud APIs", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "To support principle of least privileges and prevent potential privilege escalation it is recommended that instances are not assigned to default service account `Compute Engine default service account` with Scope `Allow full access to all Cloud APIs`.", - "Risk": "When an instance is configured with `Compute Engine default service account` with Scope `Allow full access to all Cloud APIs`, based on IAM roles assigned to the user(s) accessing Instance, it may allow user to perform cloud operations/API calls that user is not supposed to perform leading to successful privilege escalation.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instances** using the **default service account** with the `cloud-platform` scope (`Allow full access to all Cloud APIs`) are identified. *GKE nodes are excluded.*", + "Risk": "With full API scope, any code on the VM can obtain tokens and, combined with the service account's roles, call broad Google Cloud APIs. This enables **privilege escalation**, **data exfiltration**, unauthorized config changes, and service disruption, impacting **confidentiality, integrity, and availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/iam/docs/granting-changing-revoking-access", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/default-service-accounts-with-full-access-in-use.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances set-service-account --service-account= --scopes [,,...]", + "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/default-service-accounts-with-full-access-in-use.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_2#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the affected VM\n3. Click Stop and confirm\n4. Click Edit\n5. Under Service account, select a non-default service account (not -compute@developer.gserviceaccount.com) OR change Cloud API access scopes to not use \"Allow full access to all Cloud APIs\" (use Default access or select specific APIs)\n6. Click Save\n7. Click Start to restart the VM", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"us-central1-a\"\n\n boot_disk { initialize_params { image = \"debian-cloud/debian-12\" } }\n network_interface { network = \"default\" }\n\n service_account {\n email = \"\" # FIX: use a non-default service account to avoid the default SA\n scopes = [\"https://www.googleapis.com/auth/devstorage.read_only\"] # FIX: avoid cloud-platform (full API access)\n }\n}\n```" }, "Recommendation": { - "Text": "To enforce the principle of least privileges and prevent potential privilege escalation, ensure that your Google Compute Engine instances are not configured to use the default service account with the Cloud API access scope set to \"Allow full access to all Cloud APIs\". The principle of least privilege (POLP), also known as the principle of least authority, is the security concept of giving the user/system/service the minimal set of permissions required to successfully perform its tasks.", - "Url": "https://cloud.google.com/iam/docs/granting-changing-revoking-access" + "Text": "Use a **custom, least-privileged service account** per VM and avoid the default account. Restrict Cloud API scopes-prefer minimal or per-API scopes, not `Allow full access to all Cloud APIs`. Enforce **least privilege** and **separation of duties**, and regularly review roles to remove excessive permissions.", + "Url": "https://hub.prowler.com/check/compute_instance_default_service_account_in_use_with_full_api_access" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json index 7a862d1383..440986a5a8 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_deletion_protection_enabled/compute_instance_deletion_protection_enabled.metadata.json @@ -13,8 +13,7 @@ "Risk": "Without deletion protection enabled, VM instances are vulnerable to **accidental deletion** by users with sufficient permissions.\n\nThis could result in:\n- **Service disruption** and downtime for critical applications\n- **Data loss** if persistent disks are also deleted\n- **Recovery delays** while recreating instances and restoring configurations", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/instances/preventing-accidental-vm-deletion", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-deletion-protection.html" + "https://cloud.google.com/compute/docs/instances/preventing-accidental-vm-deletion" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json index 62e9eba637..be993c23e0 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_disk_auto_delete_disabled/compute_instance_disk_auto_delete_disabled.metadata.json @@ -12,8 +12,7 @@ "Risk": "With auto-delete enabled, persistent disks are automatically deleted when the associated VM instance is terminated.\n\nThis could result in:\n- **Permanent data loss** if the instance is accidentally or intentionally deleted\n- **Recovery challenges** for mission-critical workloads\n- **Compliance violations** where data retention is required", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/disks/add-persistent-disk", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-auto-delete.html" + "https://cloud.google.com/compute/docs/disks/add-persistent-disk" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json index 29334d675c..14fae6dac7 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_encryption_with_csek_enabled/compute_instance_encryption_with_csek_enabled.metadata.json @@ -1,27 +1,30 @@ { "Provider": "gcp", "CheckID": "compute_instance_encryption_with_csek_enabled", - "CheckTitle": "Ensure VM Disks for Critical VMs Are Encrypted With Customer-Supplied Encryption Keys (CSEK)", + "CheckTitle": "VM instance has all disks encrypted with Customer-Supplied Encryption Keys (CSEK)", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Disks", - "ResourceGroup": "storage", - "Description": "Customer-Supplied Encryption Keys (CSEK) are a feature in Google Cloud Storage and Google Compute Engine. If you supply your own encryption keys, Google uses your key to protect the Google-generated keys used to encrypt and decrypt your data. By default, Google Compute Engine encrypts all data at rest. Compute Engine handles and manages this encryption for you without any additional actions on your part. However, if you wanted to control and manage this encryption yourself, you can provide your own encryption keys.", - "Risk": "By default, Compute Engine service encrypts all data at rest. The cloud service manages this type of encryption without any additional actions from you and your application. However, if you want to fully control and manage instance disk encryption, you can provide your own encryption keys.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "Compute Engine VM disks use **Customer-Supplied Encryption Keys** (`CSEK`) rather than provider-managed keys. The finding flags instances where any attached disk is not protected with the customer-provided key.", + "Risk": "Without **CSEK**, encryption depends on provider-managed keys, reducing control over key lifecycle and access. This weakens confidentiality, impedes separation of duties, and can delay key revocation, increasing exposure to unauthorized data access and regulatory gaps.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/storage/docs/encryption/using-customer-supplied-keys", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-encryption-with-csek.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute disks create --size= --type= --zone= --source-snapshot= --csek-key-file=", + "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-encryption-with-csek.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/bc_gcp_general_x#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click Create instance (you must recreate VMs to use CSEK on boot disks)\n3. In Boot disk, click Change\n4. Expand Encryption and select Customer-supplied key\n5. Paste your base64-encoded 256-bit key and click Select\n6. If adding additional disks: in Additional disks, add a disk and set Encryption to Customer-supplied key with the same key\n7. Click Create to launch the VM with all disks encrypted using CSEK\n8. Migrate workload from the old VM and delete it when done", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-medium\"\n zone = \"\"\n\n boot_disk {\n initialize_params { image = \"debian-cloud/debian-12\" }\n # Critical: enables Customer-Supplied Encryption Key (CSEK) for the boot disk\n disk_encryption_key { raw_key = \"\" } # base64-encoded AES-256 key\n }\n\n network_interface { network = \"default\" }\n}\n```" }, "Recommendation": { - "Text": "Ensure that the disks attached to your production Google Compute Engine instances are encrypted with Customer-Supplied Encryption Keys (CSEKs) in order to have complete control over the data-at-rest encryption and decryption process, and meet strict compliance requirements. These custom keys, also known as Customer-Supplied Encryption Keys (CSEKs), are used by Google Compute Engine to protect the Google-generated keys used to encrypt and decrypt your instance data. Compute Engine service does not store your CSEKs on its servers and cannot access your protected data unless you provide the required key.", - "Url": "https://cloud.google.com/storage/docs/encryption/using-customer-supplied-keys" + "Text": "Use **CSEK** for VM disks that require full control over data-at-rest keys. Apply **least privilege** to key custodians, store keys in hardened vaults/HSMs, enforce rotation and rapid revocation, and document recovery procedures. Combine with **defense in depth** (network and IAM controls) to limit blast radius.", + "Url": "https://hub.prowler.com/check/compute_instance_encryption_with_csek_enabled" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json index 4805252f1c..f364573e2e 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_group_autohealing_enabled/compute_instance_group_autohealing_enabled.metadata.json @@ -13,8 +13,7 @@ "Risk": "Without autohealing, MIGs cannot detect application-level failures such as crashes, freezes, or memory issues. Instances that are technically running but experiencing problems will remain undetected and unreplaced, leading to:\n\n- **Service degradation** from unhealthy instances\n- **Extended downtime** during application failures\n- **Manual intervention** required to detect and replace failed instances", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/instance-groups/autohealing-instances-in-migs", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-instance-group-autohealing.html" + "https://cloud.google.com/compute/docs/instance-groups/autohealing-instances-in-migs" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json index 46068fb81b..b1f651538a 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_group_load_balancer_attached/compute_instance_group_load_balancer_attached.metadata.json @@ -14,8 +14,7 @@ "RelatedUrl": "", "AdditionalURLs": [ "https://cloud.google.com/compute/docs/instance-groups", - "https://cloud.google.com/load-balancing/docs/backend-service", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/mig-load-balancer-check.html" + "https://cloud.google.com/load-balancing/docs/backend-service" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json index 764f81757e..e9f39a18a4 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_ip_forwarding_is_enabled/compute_instance_ip_forwarding_is_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_ip_forwarding_is_enabled", - "CheckTitle": "Ensure That IP Forwarding Is Not Enabled on Instances", + "CheckTitle": "Compute Engine VM instance has IP forwarding disabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Compute Engine instance cannot forward a packet unless the source IP address of the packet matches the IP address of the instance. Similarly, GCP won't deliver a packet whose destination IP address is different than the IP address of the instance receiving the packet. However, both capabilities are required if you want to use instances to help route packets. Forwarding of data packets should be disabled to prevent data loss or information disclosure.", - "Risk": "When the IP Forwarding feature is enabled on a virtual machine's network interface (NIC), it allows the VM to act as a router and receive traffic addressed to other destinations. ", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-ip-forwarding.html", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instances** with `canIpForward` enabled are identified. This setting allows a VM to process packets not addressed to its own IP.\n\nInstances created by GKE (`gke-` prefix) are excluded from this evaluation.", + "Risk": "With **IP forwarding** a VM can route traffic for other addresses. If compromised, it can:\n- Spoof or tamper flows (**integrity**)\n- Intercept/redirect internal traffic (**confidentiality**)\n- Mask egress for exfiltration and enable lateral movement, degrading **availability**", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/create-start-instance", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/disable-ip-forwarding.html" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute instances update-from-file --zone --source= --most-disruptive-allowed-action=RESTART", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_12", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_12#terraform" + "Other": "1. In Google Cloud console, go to Compute Engine > VM instances and select the VM (exclude names starting with gke-)\n2. Click Delete to remove the instance with IP forwarding enabled\n3. Click Create instance\n4. Expand Networking > Network interfaces > Edit and ensure IP forwarding is Off (default)\n5. Click Create", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"\"\n\n can_ip_forward = false # Critical: disables IP forwarding to pass the check\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-12\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that IP Forwarding feature is not enabled at the Google Compute Engine instance level for security and compliance reasons, as instances with IP Forwarding enabled act as routers/packet forwarders. Because IP forwarding is rarely required, except when the virtual machine (VM) is used as a network virtual appliance, each Google Cloud VM instance should be reviewed in order to decide whether the IP forwarding is really needed for the verified instance. IP Forwarding is enabled at the VM instance level and applies to all network interfaces (NICs) attached to the instance. In addition, Instances created by GKE should be excluded from this recommendation because they need to have IP forwarding enabled and cannot be changed. Instances created by GKE have names that start with \"gke- \".", - "Url": "https://cloud.google.com/compute/docs/instances/create-start-instance" + "Text": "Disable **IP forwarding** on general-purpose VMs and allow it only for vetted **network appliances**, following **least privilege**.\n\nEnforce **network segmentation**, restrict routes, review exceptions regularly, and monitor egress to uphold **defense in depth**. *Exclude platform-managed nodes that require forwarding.*", + "Url": "https://hub.prowler.com/check/compute_instance_ip_forwarding_is_enabled" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json index fe8d76dac0..bd580349f4 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_on_host_maintenance_migrate/compute_instance_on_host_maintenance_migrate.metadata.json @@ -13,7 +13,6 @@ "Risk": "VM instances configured with On Host Maintenance set to `TERMINATE` will be shut down during host maintenance events, causing **service interruptions** and **unplanned downtime**. This can impact application availability and may require manual intervention to restart services.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/configure-maintenance-behavior.html", "https://cloud.google.com/compute/docs/instances/setting-instance-scheduling-options" ], "Remediation": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json index 5e8b62c6ce..7513e346da 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_preemptible_vm_disabled/compute_instance_preemptible_vm_disabled.metadata.json @@ -14,8 +14,7 @@ "RelatedUrl": "", "AdditionalURLs": [ "https://cloud.google.com/compute/docs/instances/preemptible", - "https://cloud.google.com/compute/docs/instances/spot", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-preemptibility.html" + "https://cloud.google.com/compute/docs/instances/spot" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json index 469772e43a..4fed0003e7 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_public_ip/compute_instance_public_ip.metadata.json @@ -1,27 +1,29 @@ { "Provider": "gcp", "CheckID": "compute_instance_public_ip", - "CheckTitle": "Check for Virtual Machine Instances with Public IP Addresses", + "CheckTitle": "VM instance does not have a public IP address", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Check for Virtual Machine Instances with Public IP Addresses", - "Risk": "To reduce your attack surface, Compute instances should not have public IP addresses. Instead, instances should be configured behind load balancers, to minimize the instance's exposure to the internet.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instances** with an assigned **external (public) IP address** on any network interface are identified.\n\nInstances without an external IP are considered internal-only.", + "Risk": "**Internet-exposed VMs** face automated scanning, **brute force**, and **remote exploit** attempts.\n\nCompromise can enable **data exfiltration**, **service account abuse**, and **lateral movement** within the VPC, while public endpoints invite **DDoS**, degrading availability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/connecting-to-instance" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute instances delete-access-config --access-config-name=\"External NAT\" --zone=", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-public-policies/bc_gcp_public_2", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-public-policies/bc_gcp_public_2#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the VM name\n3. Click Edit\n4. Under Network interfaces, set External IP to None\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"\"\n\n boot_disk {\n initialize_params { image = \"debian-cloud/debian-11\" }\n }\n\n network_interface {\n network = \"default\" # Critical: no access_config block -> no public IP\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your Google Compute Engine instances are not configured to have external IP addresses in order to minimize their exposure to the Internet.", - "Url": "https://cloud.google.com/compute/docs/instances/connecting-to-instance" + "Text": "Adopt **private-only VMs** and remove external IPs.\n- Place workloads behind **load balancers** or **reverse proxies**\n- Use **Cloud NAT** for egress; admin access via **IAP**, **VPN**, or a hardened **bastion**\n- Apply **least privilege** firewall rules and network segmentation for **defense in depth**", + "Url": "https://hub.prowler.com/check/compute_instance_public_ip" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json index e4e206fd9e..6633b7de87 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_serial_ports_in_use/compute_instance_serial_ports_in_use.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_serial_ports_in_use", - "CheckTitle": "Ensure ‘Enable Connecting to Serial Ports’ Is Not Enabled for VM Instance", + "CheckTitle": "VM instance has 'Enable Connecting to Serial Ports' disabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "Interacting with a serial port is often referred to as the serial console, which is similar to using a terminal window, in that input and output is entirely in text mode and there is no graphical interface or mouse support. If you enable the interactive serial console on an instance, clients can attempt to connect to that instance from any IP address. Therefore interactive serial console support should be disabled.", - "Risk": "If you enable the interactive serial console on your VM instance, clients can attempt to connect to your instance from any IP address and this allows anybody to access the instance if they know the user name, the SSH key, the project ID, and the instance name and zone.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "**Compute Engine VM instance** with the **interactive serial console** enabled via metadata `serial-port-enable` (`1`/`true`). Instances with this flag disabled do not allow interactive serial console connections.", + "Risk": "Enabling the **serial console** creates **out-of-band access** that can bypass network controls. Abuse can grant low-level OS interaction, expose sensitive boot logs, alter configuration, or disrupt services, degrading **confidentiality**, **integrity**, and **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/disable-interactive-serial-console-support.html" + ], "Remediation": { "Code": { "CLI": "gcloud compute instances add-metadata --zone= --metadata=serial-port-enable=false", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/disable-interactive-serial-console-support.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_11#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the target VM name, then click Edit\n3. Uncheck \"Enable connecting to serial ports\"\n4. Click Save", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n zone = \"us-central1-a\"\n\n boot_disk {\n initialize_params { image = \"debian-cloud/debian-12\" }\n }\n\n network_interface { network = \"default\" }\n\n metadata = {\n serial-port-enable = \"false\" # Critical: disables connecting to serial ports to pass the check\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that \"Enable connecting to serial ports\" configuration setting is disabled for all your production Google Compute Engine instances. A Google Cloud virtual machine (VM) instance has 4 virtual serial ports. On your VM instances, the operating system (OS), BIOS, and other system-level entities write often output data to the serial ports and can accept input, such as commands or answers, to prompts. Usually, these system-level entities use the first serial port (Port 1) and Serial Port 1 is often referred to as the interactive serial console. This interactive serial console does not support IP-based access restrictions such as IP address whitelists. To adhere to cloud security best practices and reduce the risk of unauthorized access, interactive serial console support should be disabled for all instances used in production.", - "Url": "https://cloud.google.com/compute" + "Text": "Disable the **interactive serial console** on production VMs (`serial-port-enable=false`). Use it only for *break-glass* cases. Enforce **least privilege** for console roles, prefer controlled access paths (IAP/SSH or session tools), and monitor access. Apply **defense in depth** to reduce alternate entry points.", + "Url": "https://hub.prowler.com/check/compute_instance_serial_ports_in_use" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json index d7d6c30ce8..8b4d09acc0 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_shielded_vm_enabled/compute_instance_shielded_vm_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_instance_shielded_vm_enabled", - "CheckTitle": "Ensure Compute Instances Are Launched With Shielded VM Enabled", + "CheckTitle": "Compute instance has vTPM and Integrity Monitoring enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "VMInstance", - "ResourceGroup": "compute", - "Description": "To defend against advanced threats and ensure that the boot loader and firmware on your VMs are signed and untampered, it is recommended that Compute instances are launched with Shielded VM enabled.", - "Risk": "Whithout shielded VM enabled is not possible to defend against advanced threats and ensure that the boot loader and firmware on your Google Compute Engine instances are signed and untampered.", + "ResourceType": "compute.googleapis.com/Instance", + "Description": "Compute Engine VM instances have **vTPM** and **Integrity Monitoring** enabled as part of Shielded VM configuration.", + "Risk": "Without **vTPM** or **Integrity Monitoring**, boot integrity isn't verified. Attackers can persist **bootkits/rootkits**, alter firmware, and evade attestation, enabling covert control and data theft.\n- Integrity: compromised boot chain\n- Confidentiality: secrets bound to TPM exposed\n- Availability: malicious boot code can brick VMs", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/compute/docs/instances/modifying-shielded-vm", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-shielded-vm.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute instances update --shielded-vtpm --shielded-vmintegrity-monitoring", + "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-shielded-vm.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/bc_gcp_general_y#terraform" + "Other": "1. In Google Cloud Console, go to Compute Engine > VM instances\n2. Click the VM name\n3. Click Stop and wait for the VM to stop\n4. Click Edit\n5. In Shielded VM, enable vTPM and enable Integrity monitoring\n6. Click Save\n7. Click Start to start the VM", + "Terraform": "```hcl\nresource \"google_compute_instance\" \"\" {\n name = \"\"\n machine_type = \"e2-micro\"\n\n boot_disk {\n initialize_params {\n image = \"debian-cloud/debian-11\"\n }\n }\n\n network_interface {\n network = \"default\"\n }\n\n shielded_instance_config {\n enable_vtpm = true # Critical: enable vTPM\n enable_integrity_monitoring = true # Critical: enable Integrity Monitoring\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your Google Compute Engine instances are configured to use Shielded VM security feature for protection against rootkits and bootkits.Google Compute Engine service can enable 3 advanced security components for Shielded VM instances: 1. Virtual Trusted Platform Module (vTPM) - this component validates the guest virtual machine (VM) pre-boot and boot integrity, and provides key generation and protection. 2. Integrity Monitoring - lets you monitor and verify the runtime boot integrity of your shielded VM instances using Google Cloud Operations reports (also known as Stackdriver reports). 3. Secure boot helps - this security component protects your VM instances against boot-level and kernel-level malware and rootkits. To defend against advanced threats and ensure that the boot loader and firmware on your Google Compute Engine instances are signed and untampered, it is strongly recommended that your production instances are launched with Shielded VM enabled.", - "Url": "https://cloud.google.com/compute/docs/instances/modifying-shielded-vm" + "Text": "Enable **Shielded VM** with `vTPM` and **Integrity Monitoring** set to `enabled` on all VMs. Prefer **Secure Boot** where compatible. Enforce via hardened images/templates, apply **least privilege** to shielded settings, and monitor integrity results-supporting **defense in depth** and trusted boot.", + "Url": "https://hub.prowler.com/check/compute_instance_shielded_vm_enabled" } }, - "Categories": [], + "Categories": [ + "node-security" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json index 8c6069c6b6..8b19b1dab5 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_single_network_interface/compute_instance_single_network_interface.metadata.json @@ -13,7 +13,6 @@ "Risk": "Multiple network interfaces on a VM instance can:\n\n- **Expand attack surface** by providing additional entry points for unauthorized access\n- **Create unintended network paths** that bypass security controls\n- **Increase management complexity** leading to potential misconfigurations", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/vms-with-multiple-enis.html", "https://cloud.google.com/vpc/docs/multiple-interfaces-concepts" ], "Remediation": { diff --git a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json index d90b06b906..3e65d6f844 100644 --- a/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_instance_suspended_without_persistent_disks/compute_instance_suspended_without_persistent_disks.metadata.json @@ -13,8 +13,7 @@ "Risk": "Persistent disks on suspended VM instances remain accessible through the GCP API and may contain **sensitive data**, creating potential security risks:\n\n- **Unauthorized data access** if credentials are compromised or permissions are misconfigured\n- **Data exposure** from forgotten infrastructure that is no longer actively monitored\n- **Security blind spots** where suspended resources are overlooked during security reviews and audits", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/icompute/docs/instances/suspend-resume-instance", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/persistent-disks-attached-to-suspended-vms.html" + "https://cloud.google.com/icompute/docs/instances/suspend-resume-instance" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json index 09f4c593a4..d5c6b9e3bc 100644 --- a/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_loadbalancer_logging_enabled/compute_loadbalancer_logging_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "compute_loadbalancer_logging_enabled", - "CheckTitle": "Ensure Logging is enabled for HTTP(S) Load Balancer", + "CheckTitle": "HTTP(S) load balancer has logging enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "LoadBalancer", - "ResourceGroup": "network", - "Description": "Logging enabled on a HTTPS Load Balancer will show all network traffic and its destination.", - "Risk": "HTTP(S) load balancing log entries contain information useful for monitoring and debugging web traffic. Google Cloud exports this logging data to Cloud Monitoring service so that monitoring metrics can be created to evaluate a load balancer's configuration, usage, and performance, troubleshoot problems, and improve resource utilization and user experience.", + "Severity": "high", + "ResourceType": "compute.googleapis.com/BackendService", + "Description": "**Application Load Balancer** (HTTP/S) backend services have **Cloud Logging for requests** enabled at the backend service level.\n\n*Only load balancers with a backend service support this setting.*", + "Risk": "Without **request logs**, visibility into HTTP(S) traffic is reduced, hindering detection of credential stuffing, path traversal, WAF bypass, and data exfiltration. This impacts **confidentiality** and **integrity**, and delays incident response; availability issues (surges in `5xx`) may go unnoticed.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/load-balancing/docs/https/https-logging-monitoring#gcloud:-global-mode", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLoadBalancing/https-load-balancer-logging-enabled.html", + "https://cloud.google.com/load-balancing/docs/l7-internal/monitoring" + ], "Remediation": { "Code": { - "CLI": "gcloud compute backend-services update --region=REGION --enable-logging --logging-sample-rate=", + "CLI": "gcloud compute backend-services update --global --enable-logging", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLoadBalancing/enableLoad-balancing-backend-service-logging.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Networking > Load balancing\n2. Click your HTTP(S) load balancer, then click Edit\n3. Open Backend configuration and click Edit next to the backend service\n4. Check Enable logging\n5. Click Update (backend service), then Update (load balancer)\n6. Verify logs appear in Logs Explorer under Cloud HTTP Load Balancer", + "Terraform": "```hcl\nresource \"google_compute_backend_service\" \"\" {\n name = \"\"\n health_checks = [\"\"]\n\n log_config {\n enable = true # Critical: enables logging on the backend service\n }\n}\n```" }, "Recommendation": { - "Text": "Logging will allow you to view HTTPS network traffic to your web applications.", - "Url": "https://cloud.google.com/load-balancing/docs/https/https-logging-monitoring#gcloud:-global-mode" + "Text": "Enable **request logging** on backend services with a risk-appropriate `sampleRate`; include key optional fields when needed. Export logs to monitoring for alerts and dashboards, enforce retention and integrity controls, and restrict access using **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/compute_loadbalancer_logging_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json b/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json index a2c7210558..720639e153 100644 --- a/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_network_default_in_use/compute_network_default_in_use.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_network_default_in_use", - "CheckTitle": "Ensure that the default network does not exist", + "CheckTitle": "Project does not have a default VPC network", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Network", - "ResourceGroup": "network", - "Description": "Ensure that the default network does not exist", - "Risk": "The default network has a preconfigured network configuration and automatically generates insecure firewall rules.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/default-vpc-in-use.html", + "Severity": "medium", + "ResourceType": "compute.googleapis.com/Network", + "Description": "Projects are assessed for a **VPC network** named `default` (the pre-created, auto-mode network).", + "Risk": "Using the **default VPC** can weaken segmentation and expose services via **permissive firewall rules** (e.g., broad internal trust or public admin ports). This increases likelihood of **unauthorized access**, **lateral movement**, and data exfiltration, impacting **confidentiality** and **integrity**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/default-vpc-in-use.html", + "https://cloud.google.com/vpc/docs/using-vpc" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute networks delete default --quiet", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_7", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_7#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > VPC networks\n2. Select the network named \"default\"\n3. Click Delete VPC network and confirm\n4. If deletion is blocked, remove or migrate any resources using the \"default\" network, then retry Delete", + "Terraform": "```hcl\n# Deletes the default VPC network to pass the check\nresource \"google_project_default_network\" \"\" {} # Ensures the 'default' network is removed\n```" }, "Recommendation": { - "Text": "When an organization deletes the default network, it may need to migrate or service onto a new network.", - "Url": "https://cloud.google.com/vpc/docs/using-vpc" + "Text": "Prefer **custom VPCs** over `default`. Remove unused default networks and apply **least privilege** with explicit firewall rules, private connectivity, and workload-based segmentation. Enforce creation controls (e.g., org policy to skip default network) and use **defense in depth** with logging and monitoring.", + "Url": "https://hub.prowler.com/check/compute_network_default_in_use" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json index 6a11377f1a..1a4b5afc7a 100644 --- a/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_network_dns_logging_enabled/compute_network_dns_logging_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "compute_network_dns_logging_enabled", - "CheckTitle": "Enable Cloud DNS Logging for VPC Networks", + "CheckTitle": "VPC network has Cloud DNS logging enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Network", - "ResourceGroup": "network", - "Description": "Ensure that Cloud DNS logging is enabled for all your Virtual Private Cloud (VPC) networks using DNS server policies. Cloud DNS logging records queries that the name servers resolve for your Google Cloud VPC networks, as well as queries from external entities directly to a public DNS zone. Recorded queries can come from virtual machine (VM) instances, GKE containers running in the same VPC network, peering zones, or other Google Cloud resources provisioned within your VPC.", - "Risk": "Cloud DNS logging is disabled by default on each Google Cloud VPC network. By enabling monitoring of Cloud DNS logs, you can increase visibility into the DNS names requested by the clients within your VPC network. Cloud DNS logs can be monitored for anomalous domain names and evaluated against threat intelligence.", + "ResourceType": "compute.googleapis.com/Network", + "Description": "**VPC networks** are assessed for a **DNS policy** that enables **Cloud DNS query logging**. When present, resolvers record queries for the network from VMs, GKE, peering, and inbound forwarding, with entries written to Cloud Logging.", + "Risk": "Without **DNS query logs**, suspicious lookups (C2, DGA, DNS exfiltration) go unseen, reducing **confidentiality** and hindering **incident response**. Visibility gaps also hide misconfigurations and elevated `NXDOMAIN` rates that can impact the **availability** of name resolution.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/dns/docs/monitoring", + "https://docs.cloud.google.com/compute/docs/networking/monitor-dns-failures", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/dns-logging-for-vpcs.html" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/dns-logging-for-vpcs.html", - "Terraform": "" + "Other": "1. In the Google Cloud console, go to Cloud DNS > Policies\n2. If the VPC already has a policy: select the policy, click Edit, check Enable logging, click Save\n3. If there is no policy for the VPC: click Create policy, enter a name, check Enable logging, add the target VPC network, click Create", + "Terraform": "```hcl\nresource \"google_dns_policy\" \"\" {\n name = \"\"\n enable_logging = true # CRITICAL: turns on DNS query logging for the policy\n\n networks {\n network_url = \"projects//global/networks/\" # Attach to the target VPC\n }\n}\n```" }, "Recommendation": { - "Text": "Cloud DNS logging records the queries from the name servers within your VPC to Stackdriver. Logged queries can come from Compute Engine VMs, GKE containers, or other GCP resources provisioned within the VPC.", - "Url": "https://cloud.google.com/dns/docs/monitoring" + "Text": "Enable **Cloud DNS query logging** for all VPC networks via **DNS policies** and route logs to centralized analysis. Enforce **least privilege** on log access, set retention and sampling to manage cost, and add detections for malicious domains. Apply **defense in depth** with DNS response policies and egress controls.", + "Url": "https://hub.prowler.com/check/compute_network_dns_logging_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json b/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json index 1a1e15cc84..3b0406c2a5 100644 --- a/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_network_not_legacy/compute_network_not_legacy.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "compute_network_not_legacy", - "CheckTitle": "Ensure Legacy Networks Do Not Exist", + "CheckTitle": "VPC network is not legacy", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Network", - "ResourceGroup": "network", - "Description": "In order to prevent use of legacy networks, a project should not have a legacy network configured. As of now, Legacy Networks are gradually being phased out, and you can no longer create projects with them. This recommendation is to check older projects to ensure that they are not using Legacy Networks.", - "Risk": "Google Cloud legacy networks have a single global IPv4 range which cannot be divided into subnets, and a single gateway IP address for the whole network. Legacy networks do not support several Google Cloud networking features such as subnets, alias IP ranges, multiple network interfaces, Cloud NAT (Network Address Translation), Virtual Private Cloud (VPC) Peering, and private access options for GCP services. Legacy networks are not recommended for high network traffic projects and are subject to a single point of contention or failure.", + "ResourceType": "compute.googleapis.com/Network", + "Description": "**Google Cloud networks** are evaluated for **legacy mode** (`subnet_mode: legacy`). The finding highlights networks using the older, non-subnetted design instead of **VPC with regional subnets**.", + "Risk": "Legacy networks lack subnets, peering, and private access. This reduces isolation and forces public IP paths, weakening **confidentiality** and enabling lateral movement/data exfiltration. Coarse controls and routing limits threaten **integrity**. A single global range and gateway create contention that can degrade **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/legacy-vpc-in-use.html", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/legacy-vpc-in-use.html#", + "https://cloud.google.com/vpc/docs/using-legacy#deleting_a_legacy_network" + ], "Remediation": { "Code": { - "CLI": "gcloud compute networks delete ", + "CLI": "gcloud beta compute networks update --switch-to-custom-subnet-mode", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/legacy-vpc-in-use.html#", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/ensure-legacy-networks-do-not-exist-for-a-project#terraform" + "Other": "1. In Google Cloud Console, go to Networking > VPC network > VPC networks\n2. Find the network with Subnet creation mode showing Legacy\n3. Select it and click Delete VPC network\n4. Type the network name to confirm and click Delete", + "Terraform": "" }, "Recommendation": { - "Text": "Ensure that your Google Cloud Platform (GCP) projects are not using legacy networks as this type of network is no longer recommended for production environments because it does not support advanced networking features. Instead, it is strongly recommended to use Virtual Private Cloud (VPC) networks for existing and future GCP projects.", - "Url": "https://cloud.google.com/vpc/docs/using-legacy#deleting_a_legacy_network" + "Text": "Decommission legacy networks. Migrate to **custom-mode VPCs** with regional subnets and granular firewall policies. Apply **least privilege** segmentation, enable private access and **Cloud NAT** to avoid public exposure, and use peering or private connectivity for dependencies. *Plan and test migration to limit downtime*.", + "Url": "https://hub.prowler.com/check/compute_network_not_legacy" } }, - "Categories": [], + "Categories": [ + "trust-boundaries" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json index bec06a7838..d41fd0fe00 100644 --- a/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_project_os_login_2fa_enabled/compute_project_os_login_2fa_enabled.metadata.json @@ -13,8 +13,7 @@ "Risk": "Without 2FA enforcement, compromised credentials (stolen SSH keys or passwords) grant immediate access to VM instances. Attackers could:\n\n- Gain unauthorized shell access to production systems\n- Exfiltrate sensitive data or deploy malware\n- Move laterally within the infrastructure\n\nThis single point of failure significantly increases the attack surface.", "RelatedUrl": "", "AdditionalURLs": [ - "https://cloud.google.com/compute/docs/oslogin/set-up-oslogin", - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-os-login-with-2fa-authentication.html" + "https://cloud.google.com/compute/docs/oslogin/set-up-oslogin" ], "Remediation": { "Code": { diff --git a/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json index 0a29243bb8..b650c6effd 100644 --- a/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "compute_project_os_login_enabled", - "CheckTitle": "Ensure Os Login Is Enabled for a Project", + "CheckTitle": "Project has OS Login enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "GCPProject", - "ResourceGroup": "governance", - "Description": "Ensure that the OS Login feature is enabled at the Google Cloud Platform (GCP) project level in order to provide you with centralized and automated SSH key pair management.", - "Risk": "Enabling OS Login feature ensures that the SSH keys used to connect to VM instances are mapped with Google Cloud IAM users. Revoking access to corresponding IAM users will revoke all the SSH keys associated with these users, therefore it facilitates centralized SSH key pair management, which is extremely useful in handling compromised or stolen SSH key pairs and/or revocation of external/third-party/vendor users.", + "ResourceType": "compute.googleapis.com/Project", + "Description": "Project metadata has **OS Login** enabled (`enable-oslogin`), so VM SSH access uses IAM-linked Linux identities instead of static project or instance keys.", + "Risk": "Without **OS Login**, SSH relies on static metadata keys that are hard to rotate and revoke. Leaked or orphaned keys can retain VM access, enabling unauthorized commands, data exfiltration, and lateral movement-impacting **confidentiality** and **integrity** and weakening accountability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/enable-os-login.html", + "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + ], "Remediation": { "Code": { "CLI": "gcloud compute project-info add-metadata --metadata enable-oslogin=TRUE", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-os-login.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_9#terraform" + "Other": "1. In Google Cloud Console, select your project\n2. Go to Compute Engine > Metadata\n3. Click Edit > Add item\n4. Set Key to enable-oslogin and Value to TRUE\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_project_metadata_item\" \"\" {\n # Critical: this key/value enables OS Login at the project level\n key = \"enable-oslogin\"\n value = \"TRUE\"\n}\n```" }, "Recommendation": { - "Text": "Ensure that the OS Login feature is enabled at the Google Cloud Platform (GCP) project level in order to provide you with centralized and automated SSH key pair management.", - "Url": "https://cloud.google.com/compute/confidential-vm/docs/creating-cvm-instance:https://cloud.google.com/compute/confidential-vm/docs/about-cvm:https://cloud.google.com/confidential-computing:https://cloud.google.com/blog/products/identity-security/introducing-google-cloud-confidential-computing-with-confidential-vms" + "Text": "Enable **OS Login** at the project level to centralize SSH through **IAM**.\n- Apply **least privilege** to OS Login roles\n- Remove metadata SSH keys\n- Enforce MFA and short-lived credentials\n- Monitor login activity and add network restrictions for **defense in depth**", + "Url": "https://hub.prowler.com/check/compute_project_os_login_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json b/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json index 3839f5a668..d7435f0438 100644 --- a/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_public_address_shodan/compute_public_address_shodan.metadata.json @@ -1,29 +1,27 @@ { "Provider": "gcp", "CheckID": "compute_public_address_shodan", - "CheckTitle": "Check if any of the Public Addresses are in Shodan (requires Shodan API KEY).", - "CheckType": [ - "Infrastructure Security" - ], + "CheckTitle": "Public IP address is not listed in Shodan", + "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "GCPComputeAddress", - "ResourceGroup": "network", - "Description": "Check if any of the Public Addresses are in Shodan (requires Shodan API KEY).", - "Risk": "Sites like Shodan index exposed systems and further expose them to wider audiences as a quick way to find exploitable systems.", + "Severity": "medium", + "ResourceType": "compute.googleapis.com/Address", + "Description": "**Compute Engine** public IP addresses are cross-checked with **Shodan** to identify Internet-exposed hosts that have been indexed, including observed open ports and metadata.\n\n*Only `EXTERNAL` addresses are evaluated.*", + "Risk": "Being listed in **Shodan** indicates an Internet-reachable host with identifiable services. Adversaries can quickly enumerate ports, run brute-force or exploit scans, and weaponize misconfigurations, leading to data exposure (C), service tampering (I), and outages from abuse or DDoS (A).", "RelatedUrl": "", + "AdditionalURLs": [], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud compute addresses delete --region ", "NativeIaC": "", - "Other": "", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to: VPC network > IP addresses > External\n2. Find the public IP shown in the finding\n3. If it is attached to a VM: go to the VM > Edit > Network interfaces > set External IP to None > Save\n4. Return to External IP addresses and click Release to delete the public IP", + "Terraform": "```hcl\n# Reserve an internal address instead of a public one\nresource \"google_compute_address\" \"\" {\n name = \"\"\n region = \"\"\n subnetwork = \"\"\n address_type = \"INTERNAL\" # FIX: use INTERNAL to avoid a public (EXTERNAL) IP listed by Shodan\n}\n```" }, "Recommendation": { - "Text": "Check Identified IPs, consider changing them to private ones and delete them from Shodan.", - "Url": "https://www.shodan.io/" + "Text": "Minimize Internet exposure:\n- Remove unused public IPs; prefer private addressing with controlled egress\n- Avoid `0.0.0.0/0`; restrict by allowlists and firewall policies\n- Place services behind proxies/VPN/bastions; close unused ports\n\nApply **least privilege** and **defense in depth**; continuously monitor external footprint.", + "Url": "https://hub.prowler.com/check/compute_public_address_shodan" } }, "Categories": [ diff --git a/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json b/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json index ddf5920900..2efc613161 100644 --- a/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_snapshot_not_outdated/compute_snapshot_not_outdated.metadata.json @@ -13,7 +13,6 @@ "Risk": "Outdated snapshots containing **sensitive data** expand the **attack surface** and risk data exposure if compromised.\n\nStale snapshots may violate compliance requirements, complicate disaster recovery efforts, and introduce configuration drift that affects system **integrity**.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/remove-old-disk-snapshots.html", "https://cloud.google.com/compute/docs/disks/create-snapshots", "https://cloud.google.com/compute/docs/disks/snapshot-best-practices" ], diff --git a/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json index e056d32ee3..3f9d1e0938 100644 --- a/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_subnet_flow_logs_enabled/compute_subnet_flow_logs_enabled.metadata.json @@ -1,30 +1,41 @@ { "Provider": "gcp", "CheckID": "compute_subnet_flow_logs_enabled", - "CheckTitle": "Enable VPC Flow Logs for VPC Subnets", + "CheckTitle": "Subnet has VPC Flow Logs enabled", "CheckType": [], "ServiceName": "compute", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Subnet", - "ResourceGroup": "network", - "Description": "Ensure that VPC Flow Logs is enabled for every subnet created within your production Virtual Private Cloud (VPC) network. Flow Logs is a logging feature that enables users to capture information about the IP traffic (accepted, rejected, or all traffic) going to and from the network interfaces (ENIs) available within your VPC subnets.", - "Risk": "By default, the VPC Flow Logs feature is disabled when a new VPC network subnet is created. Once enabled, VPC Flow Logs will start collecting network traffic data to and from your Virtual Private Cloud (VPC) subnets, logging data that can be useful for understanding network usage, network traffic expense optimization, network forensics, and real-time security analysis. To enhance Google Cloud VPC network visibility and security it is strongly recommended to enable Flow Logs for every business-critical or production VPC subnet.", + "ResourceType": "compute.googleapis.com/Subnetwork", + "Description": "**GCP VPC subnets** have **VPC Flow Logs** enabled at the subnet scope to capture connection metadata for traffic to and from VM interfaces.", + "Risk": "Without **VPC Flow Logs**, network activity lacks visibility, weakening **detection and response**. Blind spots enable covert **data exfiltration** (C), undetected **lateral movement** and policy bypass (I), and hinder containment and recovery (A). Forensics and cost insights are degraded.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/vpc/docs/using-flow-logs#enabling_vpc_flow_logging", + "https://docs.cloud.google.com/vpc/docs/flow-logs", + "https://docs.cloud.google.com/vpc/docs/org-policy-flow-logs", + "https://docs.cloud.google.com/vpc/docs/access-flow-logs", + "https://cloud.google.com/blog/products/networking/how-to-use-vpc-flow-logs-in-gcp-for-network-traffic-analysis", + "https://docs.cloud.google.com/vpc/docs/using-flow-logs", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudVPC/enable-vpc-flow-logs.html" + ], "Remediation": { "Code": { - "CLI": "gcloud compute networks subnets update [SUBNET_NAME] --region [REGION] --enable-flow-logs", + "CLI": "gcloud compute networks subnets update --region --enable-flow-logs", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/enable-vpc-flow-logs.html", - "Terraform": "https://docs.prowler.com/checks/gcp/logging-policies-1/bc_gcp_logging_1#terraform" + "Other": "1. In the Google Cloud console, go to Networking > VPC networks\n2. Open the Subnets tab and click the target subnet\n3. Click Edit\n4. Set Flow logs to On\n5. Click Save", + "Terraform": "```hcl\nresource \"google_compute_subnetwork\" \"\" {\n name = \"\"\n ip_cidr_range = \"10.0.0.0/24\"\n region = \"\"\n network = \"\"\n\n enable_flow_logs = true # Critical: enables VPC Flow Logs so the subnet passes the check\n}\n```" }, "Recommendation": { - "Text": "Ensure that VPC Flow Logs is enabled for every subnet created within your production Virtual Private Cloud (VPC) network. Flow Logs is a logging feature that enables users to capture information about the IP traffic (accepted, rejected, or all traffic) going to and from the network interfaces (ENIs) available within your VPC subnets.", - "Url": "https://cloud.google.com/vpc/docs/using-flow-logs#enabling_vpc_flow_logging" + "Text": "Enable **VPC Flow Logs** on all production subnets. Tune aggregation, sampling, and metadata to balance visibility and cost.\n\nExport to centralized logging for analytics and alerting, apply **least privilege** to log access, and use organization guardrails to enforce consistent coverage as part of **defense in depth**.", + "Url": "https://hub.prowler.com/check/compute_subnet_flow_logs_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json b/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json index ad781dec04..92dde729c1 100644 --- a/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json +++ b/prowler/providers/gcp/services/dataproc/dataproc_encrypted_with_cmks_disabled/dataproc_encrypted_with_cmks_disabled.metadata.json @@ -1,32 +1,34 @@ { "Provider": "gcp", "CheckID": "dataproc_encrypted_with_cmks_disabled", - "CheckTitle": "Ensure that Dataproc Cluster is encrypted using Customer-Managed Encryption Key", + "CheckTitle": "Dataproc cluster is encrypted with a customer-managed encryption key (CMEK)", "CheckType": [], "ServiceName": "dataproc", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "high", - "ResourceType": "Cluster", - "ResourceGroup": "container", - "Description": "When you use Dataproc, cluster and job data is stored on Persistent Disks (PDs) associated with the Compute Engine VMs in your cluster and in a Cloud Storage staging bucket. This PD and bucket data is encrypted using a Google-generated data encryption key (DEK) and key encryption key (KEK). The CMEK feature allows you to create, use, and revoke the key encryption key (KEK). Google still controls the data encryption key (DEK).", - "Risk": "The Dataproc cluster data is encrypted using a Google-generated Data Encryption Key (DEK) and a Key Encryption Key (KEK). If you need to control and manage your cluster data encryption yourself, you can use your own Customer-Managed Keys (CMKs). Cloud KMS Customer-Managed Keys can be implemented as an additional security layer on top of existing data encryption, and are often used in the enterprise world, where compliance and security controls are very strict.", + "Severity": "medium", + "ResourceType": "dataproc.googleapis.com/Cluster", + "Description": "Dataproc clusters use **Customer-Managed Encryption Keys** (`CMEK`) for VM **persistent disk** encryption. The finding determines whether a customer KMS key is configured for disk data instead of the default Google-managed keys.", + "Risk": "Without **CMEK** on Dataproc disks, keys remain provider-controlled, limiting **rotation**, **revocation**, and **location control**. This reduces containment if disks or snapshots are exposed and may block **data sovereignty** requirements, impacting **confidentiality** and incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/Dataproc/enable-encryption-with-cmks-for-dataproc-clusters.html", + "https://cloud.google.com/dataproc/docs/concepts/configuring-clusters/customer-managed-encryption" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/Dataproc/enable-encryption-with-cmks-for-dataproc-clusters.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/ensure-gcp-dataproc-cluster-is-encrypted-with-customer-supplied-encryption-keys-cseks#terraform" + "Other": "1. In Google Cloud Console, go to Dataproc > Clusters\n2. Click Create cluster\n3. In Cluster configuration, open Security (or Encryption)\n4. For Disk encryption key, select Customer-managed key and choose your Cloud KMS key\n5. Click Create\n6. Migrate workloads to the new cluster and delete the old non-CMEK cluster", + "Terraform": "```hcl\nresource \"google_dataproc_cluster\" \"\" {\n name = \"\"\n region = \"\"\n\n cluster_config {\n encryption_config {\n gce_pd_kms_key_name = \"projects//locations//keyRings//cryptoKeys/\" # FIX: Sets CMEK for persistent disks to pass the check\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that your Google Cloud Dataproc clusters on Compute Engine are encrypted with Customer-Managed Keys (CMKs) in order to control the cluster data encryption/decryption process. You can create and manage your own Customer-Managed Keys (CMKs) with Cloud Key Management Service (Cloud KMS). Cloud KMS provides secure and efficient encryption key management, controlled key rotation, and revocation mechanisms.", - "Url": "https://cloud.google.com/dataproc/docs/concepts/configuring-clusters/customer-managed-encryption" + "Text": "Enable **CMEK** for Dataproc disk, job-argument, and staging-bucket encryption.\n- Grant KMS access with **least privilege** to required service accounts\n- Enforce **regular rotation** and support **revocation/disable** procedures\n- Keep keys co-located with data and monitor KMS usage\n- Consider **Cloud EKM** for external key control", + "Url": "https://hub.prowler.com/check/dataproc_encrypted_with_cmks_disabled" } }, "Categories": [ - "encryption", - "gen-ai" + "encryption" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json b/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json index c0c0295695..81b7a96509 100644 --- a/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json +++ b/prowler/providers/gcp/services/dns/dns_dnssec_disabled/dns_dnssec_disabled.metadata.json @@ -1,30 +1,40 @@ { "Provider": "gcp", "CheckID": "dns_dnssec_disabled", - "CheckTitle": "Ensure That DNSSEC Is Enabled for Cloud DNS", + "CheckTitle": "Cloud DNS managed zone has DNSSEC enabled", "CheckType": [], "ServiceName": "dns", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DNS_Zone", - "ResourceGroup": "network", - "Description": "Cloud Domain Name System (DNS) is a fast, reliable and cost-effective domain name system that powers millions of domains on the internet. Domain Name System Security Extensions (DNSSEC) in Cloud DNS enables domain owners to take easy steps to protect their domains against DNS hijacking and man-in-the-middle and other attacks.", - "Risk": "Attackers can hijack the process of domain/IP lookup and redirect users to malicious web content through DNS hijacking and Man-In-The-Middle (MITM) attacks.", + "ResourceType": "dns.googleapis.com/ManagedZone", + "Description": "**Cloud DNS managed zones** are assessed for **DNSSEC** status. Zones with DNSSEC sign zone data and publish `DNSKEY`/`RRSIG`; zones without it remain unsigned and unauthenticated.", + "Risk": "Without **DNSSEC**, DNS responses lack authenticity, enabling cache poisoning, spoofed referrals, and domain hijacking. Victims may be redirected to attacker hosts, exposing credentials and data (confidentiality), enabling tampered content and records (integrity), and causing misrouting or outages (availability).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudDNS/enable-dns-sec.html", + "https://cloud.google.com/dns/docs/dnssec-config", + "https://cloud.google.com/sdk/gcloud/reference/dns/managed-zones/create?authuser=4", + "https://cloud.google.com/dns", + "https://docs.cloud.google.com/dns/docs/dnssec", + "https://cloud.google.com/dns/docs/dnssec-config?hl=vi", + "https://cloud.google.com/dns/docs/registrars?hl=Es" + ], "Remediation": { "Code": { "CLI": "gcloud dns managed-zones update --dnssec-state on", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudDNS/enable-dns-sec.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_5#terraform" + "Other": "1. In the Google Cloud Console, go to Cloud DNS\n2. Click the managed zone name\n3. Click Edit\n4. Under DNSSEC, select On\n5. Click Save", + "Terraform": "```hcl\nresource \"google_dns_managed_zone\" \"\" {\n name = \"\"\n dns_name = \"example.com.\"\n\n dnssec_config {\n state = \"on\" # Critical: enables DNSSEC for the managed zone\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that DNSSEC security feature is enabled for all your Google Cloud DNS managed zones in order to protect your domains against spoofing and cache poisoning attacks. By default, DNSSEC is not enabled for Google Cloud public DNS managed zones. DNSSEC security feature helps mitigate the risk of such attacks by encrypting signing DNS records. As a result, it prevents attackers from issuing fake DNS responses that may misdirect web clients to fake, fraudulent or scam websites.", - "Url": "https://cloud.google.com/dns/docs/dnssec-config" + "Text": "Enable **DNSSEC** on public zones and complete the chain of trust by publishing a `DS` record at your registrar. Use DNSSEC-validating resolvers, apply **least privilege** for DNS administration, and monitor key lifecycle events. *Private zones are not DNSSEC-signed.*", + "Url": "https://hub.prowler.com/check/dns_dnssec_disabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json index 85a739f425..64a75179d3 100644 --- a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json +++ b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_key_sign_in_dnssec/dns_rsasha1_in_use_to_key_sign_in_dnssec.metadata.json @@ -1,30 +1,38 @@ { "Provider": "gcp", "CheckID": "dns_rsasha1_in_use_to_key_sign_in_dnssec", - "CheckTitle": "Ensure That RSASHA1 Is Not Used for the Key-Signing Key in Cloud DNS DNSSEC", + "CheckTitle": "Cloud DNS managed zone DNSSEC key-signing key does not use RSASHA1", "CheckType": [], "ServiceName": "dns", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DNS_Zone", - "ResourceGroup": "network", - "Description": "NOTE: Currently, the SHA1 algorithm has been removed from general use by Google, and, if being used, needs to be whitelisted on a project basis by Google and will also, therefore, require a Google Cloud support contract. DNSSEC algorithm numbers in this registry may be used in CERT RRs. Zone signing (DNSSEC) and transaction security mechanisms (SIG(0) and TSIG) make use of particular subsets of these algorithms. The algorithm used for key signing should be a recommended one and it should be strong.", - "Risk": "SHA1 is considered weak and vulnerable to collision attacks.", + "ResourceType": "dns.googleapis.com/ManagedZone", + "Description": "**Cloud DNS zones** are assessed for DNSSEC **Key-Signing Key (KSK)** algorithms, specifically detecting use of `rsasha1`. Zones with KSKs on modern algorithms are distinguished from those still using `rsasha1`.", + "Risk": "Using `rsasha1` for KSK weakens DNSSEC. Collision-based forgeries can enable signed record spoofing, resulting in domain hijack, cache poisoning, and redirection-compromising **integrity** and **confidentiality**. Some validators reject SHA-1, causing resolution errors and reduced **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudDNS/dns-sec-key-signing-algorithm-in-use.html", + "https://cloud.google.com/dns/docs/dnssec-config", + "https://docs.cloud.google.com/dns/docs/dnssec-config", + "https://cloud.google.com/dns/docs/dnssec-advanced?hl=id", + "https://docs.cloud.google.com/dns/docs/dnssec-advanced" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud dns managed-zones update --dnssec-state on --ksk-algorithm RSASHA256 --ksk-key-length 2048 --zsk-algorithm RSASHA256 --zsk-key-length 1024", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudDNS/dns-sec-key-signing-algorithm-in-use.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_6#terraform" + "Other": "1. In Google Cloud Console, go to Networking > Cloud DNS and open the affected managed zone\n2. Click Edit\n3. If DNSSEC is enabled, set DNSSEC to Off and Save; then click Edit again\n4. Set DNSSEC to On, expand Advanced options\n5. Set Key-signing key (KSK) algorithm to RSASHA256 (not RSASHA1); set Zone-signing key (ZSK) algorithm to RSASHA256\n6. Click Save", + "Terraform": "```hcl\nresource \"google_dns_managed_zone\" \"\" {\n name = \"\"\n dns_name = \"example.com.\"\n\n dnssec_config {\n state = \"on\"\n\n default_key_specs {\n key_type = \"keySigning\"\n algorithm = \"rsasha256\" # FIX: use a non-RSASHA1 KSK algorithm to pass the check\n key_length = 2048\n }\n\n default_key_specs {\n key_type = \"zoneSigning\"\n algorithm = \"rsasha256\"\n key_length = 1024\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that Domain Name System Security Extensions (DNSSEC) feature is not using the deprecated RSASHA1 algorithm for the Key-Signing Key (KSK) associated with your DNS managed zone file. The algorithm used for DNSSEC signing should be a strong one, such as ECDSAP256SHA256 algorithm, as this is secure and widely deployed, and therefore it is a good choice for both DNSSEC validation and signing.", - "Url": "https://cloud.google.com/dns/docs/dnssec-config" + "Text": "Adopt **strong, supported DNSSEC algorithms** for KSKs (e.g., `ECDSAP256SHA256` or `RSASHA256`) and retire `rsasha1`. Rotate keys and validate changes before deployment. Keep KSK and ZSK algorithms consistent, document key-rotation policy, and enforce **least privilege** for DNS/DNSSEC administration.", + "Url": "https://hub.prowler.com/check/dns_rsasha1_in_use_to_key_sign_in_dnssec" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json index 7b8f0cb59d..356869799d 100644 --- a/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json +++ b/prowler/providers/gcp/services/dns/dns_rsasha1_in_use_to_zone_sign_in_dnssec/dns_rsasha1_in_use_to_zone_sign_in_dnssec.metadata.json @@ -1,30 +1,39 @@ { "Provider": "gcp", "CheckID": "dns_rsasha1_in_use_to_zone_sign_in_dnssec", - "CheckTitle": "Ensure That RSASHA1 Is Not Used for the Zone-Signing Key in Cloud DNS DNSSEC", + "CheckTitle": "Cloud DNS managed zone does not use the RSASHA1 algorithm for the DNSSEC zone-signing key", "CheckType": [], "ServiceName": "dns", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "DNS_Zone", - "ResourceGroup": "network", - "Description": "NOTE: Currently, the SHA1 algorithm has been removed from general use by Google, and, if being used, needs to be whitelisted on a project basis by Google and will also, therefore, require a Google Cloud support contract. DNSSEC algorithm numbers in this registry may be used in CERT RRs. Zone signing (DNSSEC) and transaction security mechanisms (SIG(0) and TSIG) make use of particular subsets of these algorithms. The algorithm used for key signing should be a recommended one and it should be strong.", - "Risk": "SHA1 is considered weak and vulnerable to collision attacks.", + "ResourceType": "dns.googleapis.com/ManagedZone", + "Description": "**Cloud DNS** DNSSEC settings are inspected for the **zone-signing key algorithm**. Zones that use `rsasha1` for zone signing are identified.", + "Risk": "Using **RSASHA1 for DNSSEC zone signing** weakens record integrity due to known **collision attacks**. Some validating resolvers no longer accept `SHA-1`, causing **resolution failures**. Adversaries may forge `RRSIGs`, enabling **DNS hijacking** or cache poisoning and redirecting traffic.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudDNS/dns-sec-zone-signing-algorithm-in-use.html", + "https://cloud.google.com/dns/docs/dnssec-config", + "https://cloud-kb.sentinelone.com/dns-security-rsa-sha1-enabled", + "https://datatracker.ietf.org/doc/html/rfc9905", + "https://stackoverflow.com/questions/68968312/terraform-errors-deploying-google-dns-managed-zone-with-rsasha1", + "https://docs.datadoghq.com/security/default_rules/def-000-jud/" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudDNS/dns-sec-zone-signing-algorithm-in-use.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_6#terraform" + "Other": "1. In Google Cloud Console, go to Networking > Network services > Cloud DNS\n2. Click the managed zone name, then click Edit\n3. If DNSSEC is On and the Zone signing algorithm is RSASHA1: set DNSSEC to Off and Save\n4. Click Edit again, set DNSSEC to On\n5. Open Advanced settings, set Zone signing algorithm to ECDSAP256SHA256 (or RSASHA256)\n6. Click Save", + "Terraform": "```hcl\nresource \"google_dns_managed_zone\" \"\" {\n name = \"\"\n dns_name = \"example.com.\"\n\n dnssec_config {\n state = \"on\"\n\n default_key_specs {\n algorithm = \"ecdsap256sha256\" # FIX: use a secure algorithm for zone signing\n key_type = \"zoneSigning\" # Ensures the zone-signing key is not RSASHA1\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that Domain Name System Security Extensions (DNSSEC) feature is not using the deprecated RSASHA1 algorithm for the Zone-Signing Key (ZSK) associated with your public DNS managed zone. The algorithm used for DNSSEC signing should be a strong one, such as RSASHA256, as this algorithm is secure and widely deployed, and therefore it is a good candidate for both DNSSEC validation and signing.", - "Url": "https://cloud.google.com/dns/docs/dnssec-config" + "Text": "Use **strong DNSSEC algorithms** for zone signing (e.g., `rsasha256`, `ecdsa-p256-sha256`, `ed25519`) and avoid `rsasha1`. Practice **crypto agility**: standardize secure defaults, rotate keys, and periodically validate signatures with modern resolvers. Apply **defense in depth** by monitoring DNSSEC health and limiting who can change settings.", + "Url": "https://hub.prowler.com/check/dns_rsasha1_in_use_to_zone_sign_in_dnssec" } }, - "Categories": [], + "Categories": [ + "vulnerabilities" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json b/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json index 52a71e20bc..4d05047a1c 100644 --- a/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json +++ b/prowler/providers/gcp/services/gcr/gcr_container_scanning_enabled/gcr_container_scanning_enabled.metadata.json @@ -1,33 +1,38 @@ { "Provider": "gcp", "CheckID": "gcr_container_scanning_enabled", - "CheckTitle": "Ensure Image Vulnerability Scanning using GCR Container Scanning or a third-party provider", - "CheckType": [ - "Security", - "Configuration" - ], + "CheckTitle": "Project has GCR Container Scanning API enabled", + "CheckType": [], "ServiceName": "gcr", - "SubServiceName": "Container Scanning", + "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Service", - "ResourceGroup": "container", - "Description": "Scan images stored in Google Container Registry (GCR) for vulnerabilities using GCR Container Scanning or a third-party provider. This helps identify and mitigate security risks associated with known vulnerabilities in container images.", - "Risk": "Without image vulnerability scanning, container images stored in GCR may contain known vulnerabilities, increasing the risk of exploitation by malicious actors.", - "RelatedUrl": "https://cloud.google.com/container-registry/docs/container-analysis", + "ResourceType": "serviceusage.googleapis.com/Service", + "Description": "**Google Cloud projects** with `containerscanning.googleapis.com` enabled perform **automatic vulnerability scanning** for images in Container Registry and Artifact Registry.\n\nThe finding indicates whether that service is active to generate and refresh vulnerability metadata for your container images.", + "Risk": "Without **image scanning**, vulnerable packages can reach production unchecked, enabling:\n- **Remote code execution** or **privilege escalation** (integrity/availability)\n- **Data exfiltration** from compromised workloads (confidentiality)\n- **Supply chain compromise** via unvetted base images", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ArtifactRegistry/enable-vulnerability-analysis.html", + "https://cloud.google.com/container-registry/docs/container-analysis", + "https://docs.cloud.google.com/artifact-analysis/docs/enable-automatic-scanning", + "https://cloud.google.com/container-registry/docs/container-best-practices" + ], "Remediation": { "Code": { "CLI": "gcloud services enable containerscanning.googleapis.com", "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/ensure-gcp-gcr-container-vulnerability-scanning-is-enabled#terraform" + "Other": "1. In the Google Cloud console, go to APIs & Services > Library\n2. Search for \"Container Scanning API\"\n3. Click the result and then click \"Enable\"", + "Terraform": "```hcl\nresource \"google_project_service\" \"\" {\n project = \"\"\n service = \"containerscanning.googleapis.com\" # Critical: enables Container Scanning API to pass the check\n}\n```" }, "Recommendation": { - "Text": "Enable vulnerability scanning for images stored in GCR using GCR Container Scanning or a third-party provider.", - "Url": "https://cloud.google.com/container-registry/docs/container-best-practices" + "Text": "Enable `containerscanning.googleapis.com` and integrate results into CI/CD gates. Apply **defense in depth**:\n- Use **Binary Authorization** to block noncompliant images\n- Enforce **least privilege** over who can disable scanning\n- Rebuild and patch frequently; prefer trusted, signed base images", + "Url": "https://hub.prowler.com/check/gcr_container_scanning_enabled" } }, - "Categories": [], + "Categories": [ + "vulnerabilities", + "container-security" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, GCR Container Scanning is disabled." diff --git a/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json b/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json index 7dafb56df8..9afae126c1 100644 --- a/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json +++ b/prowler/providers/gcp/services/gke/gke_cluster_no_default_service_account/gke_cluster_no_default_service_account.metadata.json @@ -1,33 +1,34 @@ { "Provider": "gcp", "CheckID": "gke_cluster_no_default_service_account", - "CheckTitle": "Ensure GKE clusters are not running using the Compute Engine default service account", - "CheckType": [ - "Security", - "Configuration" - ], + "CheckTitle": "GKE cluster does not use the Compute Engine default service account", + "CheckType": [], "ServiceName": "gke", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Service", - "ResourceGroup": "container", - "Description": "Ensure GKE clusters are not running using the Compute Engine default service account. Create and use minimally privileged service accounts for GKE cluster nodes instead of using the Compute Engine default service account to minimize unnecessary permissions.", - "Risk": "Using the Compute Engine default service account for GKE cluster nodes may grant excessive permissions, increasing the risk of unauthorized access or compromise if a node is compromised.", - "RelatedUrl": "https://cloud.google.com/compute/docs/access/service-accounts#default_service_account", + "ResourceType": "container.googleapis.com/Cluster", + "Description": "**GKE clusters** are evaluated for use of the **Compute Engine default service account** (`default`) as the node identity. The expectation is that clusters and node pools run with dedicated, minimally privileged IAM service accounts instead of the project-wide default.", + "Risk": "**Default node service accounts** often have broad project access. If a node is compromised, its credentials can read secrets, modify resources, or delete infrastructure, enabling lateral movement and data exfiltration. This harms **confidentiality**, **integrity**, and **availability** across the environment.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/GKE/ensure-service-account-is-not-the-default-compute-engine-service-account.html" + ], "Remediation": { "Code": { - "CLI": "gcloud container node-pools create [NODE_POOL] --service-account=[SA_NAME]@[PROJECT_ID].iam.gserviceaccount.com --cluster=[CLUSTER_NAME] --zone [COMPUTE_ZONE]", + "CLI": "gcloud container node-pools create --cluster= --location --service-account=@.iam.gserviceaccount.com", "NativeIaC": "", - "Other": "", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-kubernetes-policies/ensure-gke-clusters-are-not-running-using-the-compute-engine-default-service-account#terraform" + "Other": "1. In Google Cloud Console, go to Kubernetes Engine > Clusters and open your cluster\n2. Click Add node pool\n3. In Security > Service account, select your non-default service account and click Create\n4. In Nodes > Node Pools, delete the node pool(s) that show Service account = default", + "Terraform": "```hcl\nresource \"google_container_node_pool\" \"\" {\n name = \"\"\n cluster = \"\"\n location = \"\"\n\n node_config {\n service_account = \"@.iam.gserviceaccount.com\" # critical: use a custom SA, not the Compute Engine default\n }\n}\n```" }, "Recommendation": { - "Text": "Create and use minimally privileged service accounts for GKE cluster nodes instead of using the Compute Engine default service account.", - "Url": "https://cloud.google.com/compute/docs/access/service-accounts#default_service_account" + "Text": "Assign a **custom, least-privileged IAM service account** to each node pool instead of `default`.\n- Grant only permissions required for node logging/monitoring and operations\n- Enforce **separation of duties** and restrict impersonation\n- Periodically review roles and audit usage for **defense in depth**", + "Url": "https://hub.prowler.com/check/gke_cluster_no_default_service_account" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "By default, nodes use the Compute Engine default service account when you create a new cluster." diff --git a/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json b/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json index 61de3144f1..2f9136fe18 100644 --- a/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_account_access_approval_enabled", - "CheckTitle": "Ensure Access Approval is Enabled in your account", + "CheckTitle": "Project has Access Approval enabled", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Account", - "ResourceGroup": "governance", - "Description": "Ensure that Access Approval is enabled within your Google Cloud Platform (GCP) account in order to allow you to require your explicit approval whenever Google personnel need to access your GCP projects. Once the Access Approval feature is enabled, you can delegate users within your organization who can approve the access requests by giving them a security role in Identity and Access Management (IAM). These requests show the requester name/ID in an email or Pub/Sub message that you can choose to approve. This creates a new control and logging layer that reveals who in your organization approved/denied access requests to your projects.", - "Risk": "Controlling access to your Google Cloud data is crucial when working with business-critical and sensitive data. With Access Approval, you can be certain that your cloud information is accessed by approved Google personnel only. The Access Approval feature ensures that a cryptographically-signed approval is available for Google Cloud support and engineering teams when they need to access your cloud data (certain exceptions apply). By default, Access Approval and its dependency of Access Transparency are not enabled.", + "ResourceType": "accessapproval.googleapis.com/AccessApprovalSettings", + "Description": "**GCP project** has **Access Approval** configured at the project level, requiring explicit customer authorization before Google personnel can access project data. The evaluation looks for Access Approval settings associated with the project.", + "Risk": "Without Access Approval, Google support or engineering may access Customer Data without prior consent, weakening **confidentiality** and **accountability**. Reduced visibility hinders incident response and raises exposure for sensitive or regulated workloads.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/enable-access-approval.html", + "https://cloud.google.com/cloud-provider-access-management/access-approval/docs" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud access-approval settings update --project= --enrolled-services=all", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/enable-access-approval.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to Security > Access Approval (or search \"Access Approval\")\n2. Select the project \n3. Click Enable (or Edit settings if already open)\n4. Set Enrolled services to All Google Cloud services\n5. Click Save (enable the API if prompted)", + "Terraform": "```hcl\nresource \"google_access_approval_settings\" \"\" {\n project = \"\"\n\n enrolled_services {\n cloud_product = \"all\" # Critical: enroll all services to enable Access Approval for the project\n enrollment_level = \"BLOCK_ALL\" # Critical: require approval for all applicable access requests\n }\n}\n```" }, "Recommendation": { - "Text": "Ensure that Access Approval is enabled within your Google Cloud Platform (GCP) account in order to allow you to require your explicit approval whenever Google personnel need to access your GCP projects. Once the Access Approval feature is enabled, you can delegate users within your organization who can approve the access requests by giving them a security role in Identity and Access Management (IAM). These requests show the requester name/ID in an email or Pub/Sub message that you can choose to approve. This creates a new control and logging layer that reveals who in your organization approved/denied access requests to your projects.", - "Url": "https://cloud.google.com/cloud-provider-access-management/access-approval/docs" + "Text": "Enable **Access Approval** for projects and *where feasible* at higher hierarchy for consistency. Assign **least-privilege approvers** with **separation of duties**, integrate timely notifications, and monitor **Access Transparency** records to maintain **defense in depth**.", + "Url": "https://hub.prowler.com/check/iam_account_access_approval_enabled" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json b/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json index 25eb5242ec..70fd5d619c 100644 --- a/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "iam_audit_logs_enabled", - "CheckTitle": "Configure Google Cloud Audit Logs to Track All Activities", + "CheckTitle": "GCP project has Cloud Audit Logs enabled", "CheckType": [], "ServiceName": "iam", - "SubServiceName": "Audit Logs", + "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "GCPProject", - "ResourceGroup": "governance", - "Description": "Ensure that Google Cloud Audit Logs feature is configured to track Data Access logs for all Google Cloud Platform (GCP) services and users, in order to enhance overall access security and meet compliance requirements. Once configured, the feature can record all admin related activities, as well as all the read and write access requests to user data.", - "Risk": "In order to maintain an effective Google Cloud audit configuration for your project, folder, and organization, all 3 types of Data Access logs (ADMIN_READ, DATA_READ and DATA_WRITE) must be enabled for all supported GCP services. Also, Data Access logs should be captured for all IAM users, without exempting any of them. Exemptions let you control which users generate audit logs. When you add an exempted user to your log configuration, audit logs are not created for that user, for the selected log type(s). Data Access audit logs are disabled by default and must be explicitly enabled based on your business requirements.", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "**GCP project** has **Cloud Audit Logs** configured to capture administrative operations and data access events for services and principals (*per IAM Audit Logs*, including `ADMIN_READ`, `DATA_READ`, `DATA_WRITE`).", + "Risk": "Absent or partial audit logging reduces visibility into who accessed data or changed configurations, hindering detection and forensics.\n\nMisused identities can alter IAM to persist access, exfiltrate data, or delete resources, impacting **confidentiality**, **integrity**, and **availability**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/record-all-activities.html", + "https://cloud.google.com/logging/docs/audit/", + "https://docs.cloud.google.com/logging/docs/audit/configure-data-access" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/record-all-activities.html", - "Terraform": "https://docs.prowler.com/checks/gcp/logging-policies-1/ensure-that-cloud-audit-logging-is-configured-properly-across-all-services-and-all-users-from-a-project#terraform" + "Other": "1. In the Google Cloud console, go to IAM & Admin > Audit Logs\n2. Click Set default configuration\n3. Under Permission types, check Admin Read, Data Read, and Data Write\n4. Click Save", + "Terraform": "```hcl\n# Enable Cloud Audit Logs (Data Access) for all services\nresource \"google_project_iam_audit_config\" \"all\" {\n project = \"\"\n service = \"allServices\" # Critical: apply to all services\n\n # Critical: enable Data Access audit log types to pass the check\n audit_log_config { log_type = \"ADMIN_READ\" } # metadata/config reads\n audit_log_config { log_type = \"DATA_READ\" } # data reads\n audit_log_config { log_type = \"DATA_WRITE\" } # data writes\n}\n```" }, "Recommendation": { - "Text": "It is recommended that Cloud Audit Logging is configured to track all admin activities and read, write access to user data.", - "Url": "https://cloud.google.com/logging/docs/audit/" + "Text": "Enable comprehensive **Cloud Audit Logs** for all services and principals, including `ADMIN_READ`, `DATA_READ`, `DATA_WRITE`. *Avoid exemptions.* Set org/folder defaults, centralize and retain logs, enforce least privilege on log access, protect logs from alteration, and alert on anomalous access.", + "Url": "https://hub.prowler.com/check/iam_audit_logs_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json b/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json index 7ca70567d5..e2a47eefc9 100644 --- a/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_cloud_asset_inventory_enabled/iam_cloud_asset_inventory_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_cloud_asset_inventory_enabled", - "CheckTitle": "Ensure Cloud Asset Inventory Is Enabled", + "CheckTitle": "Project has Cloud Asset Inventory API enabled", "CheckType": [], "ServiceName": "iam", - "SubServiceName": "Asset Inventory", + "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "Service", - "ResourceGroup": "governance", - "Description": "GCP Cloud Asset Inventory is services that provides a historical view of GCP resources and IAM policies through a time-series database. The information recorded includes metadata on Google Cloud resources, metadata on policies set on Google Cloud projects or resources, and runtime information gathered within a Google Cloud resource.", - "Risk": "Gaining insight into Google Cloud resources and policies is vital for tasks such as DevOps, security analytics, multi-cluster and fleet management, auditing, and governance. With Cloud Asset Inventory you can discover, monitor, and analyze all GCP assets in one place, achieving a better understanding of all your cloud assets across projects and services.", + "ResourceType": "serviceusage.googleapis.com/Service", + "Description": "**Project service usage** includes the **Cloud Asset Inventory** API (`cloudasset.googleapis.com`), enabling resource and IAM policy inventory with time-series metadata and change history.", + "Risk": "Without **Cloud Asset Inventory**, gaps in asset and IAM visibility hinder detection of drift and unauthorized changes, weakening access control integrity and risking data confidentiality. Shadow assets and silent privilege escalation can persist, delaying incident response.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudAPI/enabled-cloud-asset-inventory.html", + "https://cloud.google.com/asset-inventory/docs" + ], "Remediation": { "Code": { - "CLI": "gcloud services enable cloudasset.googleapis.com", + "CLI": "gcloud services enable cloudasset.googleapis.com --project ", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudAPI/enabled-cloud-asset-inventory.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, select the project from the project picker.\n2. Go to APIs & Services > Library.\n3. Search for \"Cloud Asset Inventory API\" and select it.\n4. Click Enable.\n5. Verify it appears under APIs & Services > Enabled APIs & services.", + "Terraform": "```hcl\nresource \"google_project_service\" \"\" {\n project = \"\"\n service = \"cloudasset.googleapis.com\" # Enables Cloud Asset Inventory API to pass the check\n}\n```" }, "Recommendation": { - "Text": "Ensure that Cloud Asset Inventory is enabled for all your GCP projects in order to efficiently manage the history and the inventory of your cloud resources. Google Cloud Asset Inventory is a fully managed metadata inventory service that allows you to view, monitor, analyze, and gain insights for your Google Cloud and Anthos assets. Cloud Asset Inventory is disabled by default in each GCP project.", - "Url": "https://cloud.google.com/asset-inventory/docs" + "Text": "Enable **Cloud Asset Inventory** across all projects *and, if applicable, at the organization level* to maintain authoritative asset and IAM histories. Centralize analysis, retain records per policy, and use the data to enforce **least privilege** and **defense in depth**.", + "Url": "https://hub.prowler.com/check/iam_cloud_asset_inventory_enabled" } }, - "Categories": [], + "Categories": [ + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json b/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json index a9bbfb1b9f..9e6ba83ff9 100644 --- a/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_no_service_roles_at_project_level", - "CheckTitle": "Ensure That IAM Users Are Not Assigned the Service Account User or Service Account Token Creator Roles at Project Level", + "CheckTitle": "Project has no IAM users assigned the Service Account User or Service Account Token Creator roles at project level", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "IAM Policy", - "ResourceGroup": "IAM", - "Description": "It is recommended to assign the `Service Account User (iam.serviceAccountUser)` and `Service Account Token Creator (iam.serviceAccountTokenCreator)` roles to a user for a specific service account rather than assigning the role to a user at project level.", - "Risk": "The Service Account User (iam.serviceAccountUser) role allows an IAM user to attach a service account to a long-running job service such as an App Engine App or Dataflow Job, whereas the Service Account Token Creator (iam.serviceAccountTokenCreator) role allows a user to directly impersonate the identity of a service account.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/check-for-iam-users-with-service-roles.html", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "**Google Cloud IAM policies** are inspected for **project-level grants** of `roles/iam.serviceAccountUser` and `roles/iam.serviceAccountTokenCreator` to principals. The focus is on bindings that enable attaching or impersonating service accounts at the project scope rather than on individual service accounts.", + "Risk": "**Project-wide impersonation rights** enable **privilege escalation** and **lateral movement**. Holders can act as any service account, access data across services, modify resources, and persist access. New service accounts inherit exposure, undermining confidentiality and integrity.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/check-for-iam-users-with-service-roles.html", + "https://cloud.google.com/iam/docs/granting-changing-revoking-access", + "https://cloud.google.com/iam/docs/best-practices-service-accounts?ref=alphasec.io" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_3", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_3#terraform" + "Other": "1. In Google Cloud Console, go to IAM & Admin > IAM\n2. Use the filter to find Role: Service Account User\n3. Remove all project-level bindings for this role and click Save\n4. Repeat steps 2-3 for Role: Service Account Token Creator\n5. Do not add these roles at the project level; if needed, grant them on specific service accounts only (IAM & Admin > Service Accounts > select account > Permissions > Grant access)", + "Terraform": "```hcl\n# Grant required access at the service account level instead of the project level\nresource \"google_service_account_iam_member\" \"\" {\n service_account_id = \"projects//serviceAccounts/@.iam.gserviceaccount.com\" # CRITICAL: scope grant to a specific service account, not the project\n role = \"roles/iam.serviceAccountUser\" # CRITICAL: this role is granted only at the service account level\n member = \"user:@example.com\"\n}\n```" }, "Recommendation": { - "Text": "Ensure that the Service Account User and Service Account Token Creator roles are assigned to a user for a specific GCP service account rather than to a user at the GCP project level, in order to implement the principle of least privilege (POLP). The principle of least privilege (also known as the principle of minimal privilege) is the practice of providing every user the minimal amount of access required to perform its tasks. Google Cloud Platform (GCP) IAM users should not have assigned the Service Account User or Service Account Token Creator roles at the GCP project level. Instead, these roles should be allocated to a user associated with a specific service account, providing that user access to the service account only.", - "Url": "https://cloud.google.com/iam/docs/granting-changing-revoking-access" + "Text": "Assign `roles/iam.serviceAccountUser` and `roles/iam.serviceAccountTokenCreator` only on the specific service account, not at project scope. Enforce **least privilege** and **separation of duties** with per-SA grants, conditional bindings, and time-bound access. Prefer **short-lived impersonation**; review grants regularly.", + "Url": "https://hub.prowler.com/check/iam_no_service_roles_at_project_level" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json b/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json index d52ddff8d8..7a8156131d 100644 --- a/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_organization_essential_contacts_configured", - "CheckTitle": "Ensure Essential Contacts is Configured for Organization", + "CheckTitle": "Organization has Essential Contacts configured", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Organization", - "ResourceGroup": "governance", - "Description": "It is recommended that Essential Contacts is configured to designate email addresses for Google Cloud services to notify of important technical or security information.", - "Risk": "Google Cloud Platform (GCP) services, such as Cloud Billing, send out billing notifications to share important information with the cloud platform users. By default, these types of notifications are sent to members with certain Identity and Access Management (IAM) roles such as 'roles/owner' and 'roles/billing.admin'. With Essential Contacts, you can specify exactly who receives important notifications by providing your own list of contacts (i.e. email addresses).", + "ResourceType": "cloudresourcemanager.googleapis.com/Organization", + "Description": "Google Cloud organization has **Essential Contacts** defined at the organization level for categories such as `SECURITY`, `BILLING`, `LEGAL`, `SUSPENSION`, `TECHNICAL`, or `PRODUCT_UPDATES`.\n\nEvaluates whether at least one contact is configured.", + "Risk": "Missing **Essential Contacts** means security, abuse, and billing notices can go unnoticed or to inappropriate recipients, slowing response.\n\nConsequences: data exposure via unaddressed alerts (C), unauthorized changes persisting (I), and suspensions/outages from unresolved issues (A).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/essential-contacts.html", + "https://docs.cloud.google.com/resource-manager/docs/manage-essential-contacts?hl=es", + "https://cloud.google.com/resource-manager/docs/managing-notification-contacts" + ], "Remediation": { "Code": { - "CLI": "gcloud essential-contacts create --email= --notification-categories= --organization=", + "CLI": "gcloud essential-contacts create --email= --notification-categories=all --organization=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/essential-contacts.html", - "Terraform": "" + "Other": "1. In the Google Cloud console, go to Essential Contacts\n2. In the resource selector, choose your Organization\n3. Click Add contact\n4. Enter the contact email and select category All\n5. Click Save", + "Terraform": "```hcl\nresource \"google_essential_contacts_contact\" \"\" {\n parent = \"organizations/\" # Critical: set at org level to satisfy the check\n email = \"\" # Critical: creates the essential contact\n notification_category_subscriptions = [\"ALL\"] # Critical: required; ensures the contact is created\n}\n```" }, "Recommendation": { - "Text": "It is recommended that Essential Contacts is configured to designate email addresses for Google Cloud services to notify of important technical or security information.", - "Url": "https://cloud.google.com/resource-manager/docs/managing-notification-contacts" + "Text": "Configure **Essential Contacts** at the organization (and inherit to folders/projects) with group aliases for `SECURITY`, `BILLING`, `LEGAL`, `SUSPENSION`, `TECHNICAL`, and `PRODUCT_UPDATES`.\n\nApply **least privilege** and **separation of duties**. Review quarterly, verify delivery, and restrict contacts to approved domains.", + "Url": "https://hub.prowler.com/check/iam_organization_essential_contacts_configured" } }, - "Categories": [], + "Categories": [ + "resilience" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json b/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json index 9836c05d33..cf9ae4ba2b 100644 --- a/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_role_kms_enforce_separation_of_duties", - "CheckTitle": "Enforce Separation of Duties for KMS-Related Roles", + "CheckTitle": "Project members are not assigned both Cloud KMS Admin and CryptoKey Encrypter/Decrypter roles", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "IAMRole", - "ResourceGroup": "IAM", - "Description": "Ensure that separation of duties is enforced for all Cloud Key Management Service (KMS) related roles. The principle of separation of duties (also known as segregation of duties) has as its primary objective the prevention of fraud and human error. This objective is achieved by dismantling the tasks and the associated privileges for a specific business process among multiple users/identities. Google Cloud provides predefined roles that can be used to implement the principle of separation of duties, where it is needed. The predefined Cloud KMS Admin role is meant for users to manage KMS keys but not to use them. The Cloud KMS CryptoKey Encrypter/Decrypter roles are meant for services who can use keys to encrypt and decrypt data, but not to manage them. To adhere to cloud security best practices, your IAM users should not have the Admin role and any of the CryptoKey Encrypter/Decrypter roles assigned at the same time.", - "Risk": "The principle of separation of duties can be enforced in order to eliminate the need for the IAM user/identity that has all the permissions needed to perform unwanted actions, such as using a cryptographic key to access and decrypt data which the user should not normally have access to.", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "Project IAM assignments are analyzed for **Cloud KMS** separation of duties: principals simultaneously granted `roles/cloudkms.admin` and any of `roles/cloudkms.cryptoKeyEncrypterDecrypter`, `roles/cloudkms.cryptoKeyEncrypter`, or `roles/cloudkms.cryptoKeyDecrypter`.", + "Risk": "Combining key management and key usage undermines **confidentiality**, **integrity**, and **availability**:\n- Unauthorized decryption of sensitive data\n- Tampering with policies or rotation to conceal access\n- Disabling or destroying keys, causing outages\n\nThis concentration of power reduces oversight and auditability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-kms-related-roles.html", + "https://cloud.google.com/kms/docs/separation-of-duties" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud projects remove-iam-policy-binding --member= --role=roles/cloudkms.admin", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-kms-related-roles.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to IAM & Admin > IAM\n2. Locate the principal listed in the finding and click Edit principal\n3. Remove either \"Cloud KMS Admin\" or any of the \"Cloud KMS CryptoKey Encrypter/Decrypter\" roles from the project\n4. Click Save", + "Terraform": "```hcl\nresource \"google_project_iam_binding\" \"\" {\n project = \"\"\n role = \"roles/cloudkms.admin\" # Critical: ensure the offending principal is NOT bound as KMS Admin\n members = [\n \"user:\" # Critical: exclude any member who also has CryptoKey* roles to enforce separation of duties\n ]\n}\n```" }, "Recommendation": { - "Text": "It is recommended that the principle of 'Separation of Duties' is enforced while assigning KMS related roles to users.", - "Url": "https://cloud.google.com/kms/docs/separation-of-duties" + "Text": "Apply **least privilege** and **separation of duties**:\n- Never combine `roles/cloudkms.admin` with any `roles/cloudkms.cryptoKey*`\n- Isolate key management and usage in dedicated projects\n- Require approvals, log all key access, and monitor\n- Avoid broad `roles/owner` on key scopes", + "Url": "https://hub.prowler.com/check/iam_role_kms_enforce_separation_of_duties" } }, - "Categories": [], + "Categories": [ + "identity-access", + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json b/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json index ce5395abc8..8ad11f2a5f 100644 --- a/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_role_sa_enforce_separation_of_duties", - "CheckTitle": "Enforce Separation of Duties for Service-Account Related Roles", + "CheckTitle": "Project enforces separation of duties for Service Account Admin and Service Account User roles", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "IAMRole", - "ResourceGroup": "IAM", - "Description": "Ensure that separation of duties (also known as segregation of duties - SoD) is enforced for all Google Cloud Platform (GCP) service-account related roles. The security principle of separation of duties has as its primary objective the prevention of fraud and human error. This objective is achieved by disbanding the tasks and associated privileges for a specific business process among multiple users/members. To follow security best practices, your GCP service accounts should not have the Service Account Admin and Service Account User roles assigned at the same time.", - "Risk": "The principle of separation of duties should be enforced in order to eliminate the need for high-privileged IAM members, as the permissions granted to these members can allow them to perform malicious or unwanted actions.", + "ResourceType": "cloudresourcemanager.googleapis.com/Project", + "Description": "Google Cloud IAM policies are evaluated to find principals granted both `roles/iam.serviceAccountAdmin` and `roles/iam.serviceAccountUser` within a project. **Service-account related roles** are expected to be segregated so that service account lifecycle management is distinct from their use or impersonation.", + "Risk": "With both roles, a principal can create or modify service accounts and then use or attach them to workloads, enabling unchecked impersonation. This endangers confidentiality (expanded data access), integrity (policy/workload changes), and availability (persistence or sabotage via privileged automation).", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-service-account-roles.html", + "https://docs.cloud.google.com/iam/docs/service-account-overview", + "https://cloud.google.com/iam/docs/understanding-roles" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/enforce-separation-of-duties-for-service-account-roles.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_10#terraform" + "Other": "1. In Google Cloud Console, go to IAM & Admin > IAM\n2. Click the View by Role tab\n3. Select the role Service Account Admin (roles/iam.serviceAccountAdmin)\n4. Remove all listed principals from this role and click Save\n5. Select the role Service Account User (roles/iam.serviceAccountUser)\n6. Remove all listed principals from this role and click Save", + "Terraform": "```hcl\n# Remove all project-level principals from Service Account User\nresource \"google_project_iam_binding\" \"sa_user_none\" {\n project = \"\"\n role = \"roles/iam.serviceAccountUser\" # critical: target role to clear at project level\n members = [] # critical: empty list removes the binding (no members)\n}\n\n# Remove all project-level principals from Service Account Admin\nresource \"google_project_iam_binding\" \"sa_admin_none\" {\n project = \"\"\n role = \"roles/iam.serviceAccountAdmin\" # critical: target role to clear at project level\n members = [] # critical: empty list removes the binding (no members)\n}\n```" }, "Recommendation": { - "Text": "Ensure that separation of duties (also known as segregation of duties - SoD) is enforced for all Google Cloud Platform (GCP) service-account related roles. The security principle of separation of duties has as its primary objective the prevention of fraud and human error. This objective is achieved by disbanding the tasks and associated privileges for a specific business process among multiple users/members. To follow security best practices, your GCP service accounts should not have the Service Account Admin and Service Account User roles assigned at the same time.", - "Url": "https://cloud.google.com/iam/docs/understanding-roles" + "Text": "Enforce separation of duties: assign `roles/iam.serviceAccountAdmin` for lifecycle tasks and `roles/iam.serviceAccountUser` for attach/impersonate, never both to one principal.\n- Apply **least privilege** with narrow scope and conditions\n- Use temporary elevation/approvals\n- Regularly audit IAM bindings and logs", + "Url": "https://hub.prowler.com/check/iam_role_sa_enforce_separation_of_duties" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json index 467da6f96e..3de2ce9cd0 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_no_administrative_privileges/iam_sa_no_administrative_privileges.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_sa_no_administrative_privileges", - "CheckTitle": "Ensure Service Account does not have admin privileges", + "CheckTitle": "Service account has no administrative privileges", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "ServiceAccount", - "ResourceGroup": "IAM", - "Description": "Ensure Service Account does not have admin privileges", - "Risk": "Enrolling ServiceAccount with Admin rights gives full access to an assigned application or a VM. A ServiceAccount Access holder can perform critical actions, such as delete and update change settings, without user intervention.", - "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/restrict-admin-access-for-service-accounts.html", + "ResourceType": "iam.googleapis.com/ServiceAccount", + "Description": "Google Cloud service accounts with **high-privilege IAM roles** are identified, including `roles/owner`, `roles/editor`, or any role containing `admin`. The evaluation looks for service accounts bound to these roles in IAM policies across the project hierarchy.", + "Risk": "Over-privileged service accounts jeopardize the CIA triad:\n- Confidentiality: data can be read and exfiltrated\n- Integrity: configs, IAM, and code can be altered\n- Availability: resources can be deleted or halted\n\nCompromise via key theft or impersonation enables lateral movement and persistence.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/restrict-admin-access-for-service-accounts.html", + "https://cloud.google.com/iam/docs/manage-access-service-accounts" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud projects remove-iam-policy-binding --member=serviceAccount: --role=", "NativeIaC": "", - "Other": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_4", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-iam-policies/bc_gcp_iam_4#terraform" + "Other": "1. In the Google Cloud console, go to IAM & Admin > IAM\n2. Select the project (or switch to the folder/organization) where the role is granted\n3. Find the service account by email and click Edit principal\n4. Remove roles: Owner, Editor, and any role with \"Admin\" in the name\n5. Click Save\n6. Repeat at folder/organization level if the role was inherited", + "Terraform": "" }, "Recommendation": { - "Text": "Ensure that your Google Cloud user-managed service accounts are not using privileged (administrator) roles, in order to implement the principle of least privilege and prevent any accidental or intentional modifications that may lead to data leaks and/or data loss.", - "Url": "https://cloud.google.com/iam/docs/manage-access-service-accounts" + "Text": "Apply **least privilege**: replace `roles/owner`, `roles/editor`, and roles containing `admin` with narrowly scoped predefined or custom roles. Use **separation of duties**, **temporary elevation**, and **IAM Conditions** to limit scope and time. Prefer **impersonation** over long-lived keys and monitor SA usage.", + "Url": "https://hub.prowler.com/check/iam_sa_no_administrative_privileges" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json index 009a58b5fe..601f797806 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_no_user_managed_keys/iam_sa_no_user_managed_keys.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "iam_sa_no_user_managed_keys", - "CheckTitle": "Ensure That There Are Only GCP-Managed Service Account Keys for Each Service Account", + "CheckTitle": "Service account has no user-managed keys", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "ServiceAccountKey", - "ResourceGroup": "IAM", - "Description": "Ensure That There Are Only GCP-Managed Service Account Keys for Each Service Account", - "Risk": "Anyone who has access to the keys will be able to access resources through the service account. GCP-managed keys are used by Cloud Platform services such as App Engine and Compute Engine. These keys cannot be downloaded. Google will keep the keys and automatically rotate them on an approximately weekly basis. User-managed keys are created, downloadable, and managed by users.", + "ResourceType": "iam.googleapis.com/ServiceAccount", + "Description": "**IAM service accounts** do not have keys of type `USER_MANAGED`; only Google-managed keys (or no keys) are present.", + "Risk": "**User-managed keys** are downloadable and long-lived, increasing theft and reuse risk. An attacker with a key can impersonate the service account, perform unauthorized API calls, exfiltrate data, and alter resources, impacting **confidentiality** and **integrity**, and potentially **availability**. Copies in repos or logs can evade centralized rotation and revocation.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/delete-user-managed-service-account-keys.html", + "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud iam service-accounts keys delete --iam-account=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-user-managed-service-account-keys.html", + "Other": "1. In the Google Cloud console, go to IAM & Admin > Service Accounts\n2. Select your project and click the affected service account\n3. Open the Keys tab\n4. For each key with Type \"User-managed\", click Delete and confirm\n5. Verify no User-managed keys remain for that service account\n6. Repeat for any other affected service accounts", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended to prevent user-managed service account keys.", - "Url": "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + "Text": "Avoid **user-managed keys**. Use **service account impersonation** or **Workload Identity Federation** for short-lived credentials and **least privilege**. Enforce `iam.disableServiceAccountKeyCreation`, restrict who can create keys, and monitor usage. *If exceptions are unavoidable*, tightly scope, rotate aggressively, and store keys securely.", + "Url": "https://hub.prowler.com/check/iam_sa_no_user_managed_keys" } }, - "Categories": [], + "Categories": [ + "secrets", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json index 15fd25b66a..f322d86ff7 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_rotate_90_days/iam_sa_user_managed_key_rotate_90_days.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_sa_user_managed_key_rotate_90_days", - "CheckTitle": "Ensure User-Managed/External Keys for Service Accounts Are Rotated Every 90 Days", + "CheckTitle": "Service account user-managed key has been rotated within the last 90 days", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "ServiceAccountKey", - "ResourceGroup": "IAM", - "Description": "Ensure User-Managed/External Keys for Service Accounts Are Rotated Every 90 Days", - "Risk": "Service Account keys should be rotated to ensure that data cannot be accessed with an old key that might have been lost, cracked, or stolen.", + "ResourceType": "iam.googleapis.com/ServiceAccountKey", + "Description": "**GCP IAM service account user-managed keys** are evaluated by last rotation. Keys of type `USER_MANAGED` older than `90` days are identified; those rotated within `90` days align with the expected rotation cadence.", + "Risk": "**Stale service account keys** extend exposure of **long-lived credentials**. A leaked or retained key can grant persistent API access, enabling **data exfiltration**, tampering, and lateral movement. Weak rotation reduces revocation effectiveness and erodes **confidentiality** and **integrity**.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/rotate-service-account-user-managed-keys.html", + "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud iam service-accounts keys delete --iam-account=@.iam.gserviceaccount.com", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/rotate-service-account-user-managed-keys.html", + "Other": "1. In Google Cloud Console, go to IAM & Admin > Service Accounts\n2. Open the service account, then go to the Keys tab\n3. If the workload still needs a key: click Add key > Create new key (JSON), download it, and update the workload to use it\n4. Delete the user-managed key(s) older than 90 days by clicking Delete next to each\n5. Re-run the check to confirm only recent keys remain", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended that all Service Account keys are regularly rotated.", - "Url": "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + "Text": "Rotate **user-managed keys** at least every `90` days and prefer **Workload Identity Federation** or other short-lived credentials over static keys.\n- Minimize key count; remove unused keys\n- Enforce **least privilege** on service accounts\n- Automate rotation and alert on aged keys\n- Set key expiry as **defense in depth**", + "Url": "https://hub.prowler.com/check/iam_sa_user_managed_key_rotate_90_days" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json index 8d05b4bde5..6c659baf36 100644 --- a/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_sa_user_managed_key_unused/iam_sa_user_managed_key_unused.metadata.json @@ -1,30 +1,37 @@ { "Provider": "gcp", "CheckID": "iam_sa_user_managed_key_unused", - "CheckTitle": "Ensure That There Are No Unused Service Account Keys for Each Service Account", + "CheckTitle": "User-managed service account key was used within the allowed inactivity period", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "ServiceAccountKey", - "ResourceGroup": "IAM", - "Description": "Ensure That There Are No Unused Service Account Keys for Each Service Account.", - "Risk": "Anyone who has access to the keys will be able to access resources through the service account. GCP-managed keys are used by Cloud Platform services such as App Engine and Compute Engine. These keys cannot be downloaded. Google will keep the keys and automatically rotate them on an approximately weekly basis. User-managed keys are created, downloadable, and managed by users.", - "RelatedUrl": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused", + "ResourceType": "iam.googleapis.com/ServiceAccountKey", + "Description": "**User-managed service account keys** with no recorded activity during the last `max_unused_account_days` are identified using key-usage metrics per service account.", + "Risk": "**Stale user-managed keys** expand exposure of long-lived credentials. If leaked, an attacker can authenticate as the service account off-platform, bypass network controls, access data, alter resources, and persist-compromising confidentiality and integrity, and risking availability via destructive changes.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudIAM/delete-user-managed-service-account-keys.html", + "https://cloud.google.com/iam/docs/creating-managing-service-account-keys", + "https://docs.cloud.google.com/iam/docs/samples/iam-delete-key", + "https://cloud.google.com/iam/docs/service-account-overview#identify-unused" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud iam service-accounts keys delete --iam-account=", "NativeIaC": "", - "Other": "", + "Other": "1. In Google Cloud Console, go to IAM & Admin > Service Accounts\n2. Select your project and click the service account with the unused user-managed key\n3. Open the Keys tab\n4. Find the unused key (Type: User-managed), click Delete, and confirm", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended to prevent user-managed service account keys.", - "Url": "https://cloud.google.com/iam/docs/creating-managing-service-account-keys" + "Text": "Prefer **managed workload identities** and **service account impersonation** over user-managed keys. Enforce `iam.disableServiceAccountKeyCreation`, remove unused keys, and use short key lifetimes with rotation when unavoidable. Apply **least privilege**, monitor key usage, and enforce **separation of duties** to limit blast radius.", + "Url": "https://hub.prowler.com/check/iam_sa_user_managed_key_unused" } }, - "Categories": [], + "Categories": [ + "secrets" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json index 1d7c4aabd2..03ccf30274 100644 --- a/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_service_account_unused/iam_service_account_unused.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "iam_service_account_unused", - "CheckTitle": "Ensure That There Are No Unused Service Accounts", + "CheckTitle": "Service account was used within the configured maximum unused period", "CheckType": [], "ServiceName": "iam", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "ServiceAccount", - "ResourceGroup": "IAM", - "Description": "Ensure That There Are No Unused Service Accounts.", - "Risk": "A malicious actor could make use of privilege escalation or impersonation to access an unused Service Account that is over-privileged.", - "RelatedUrl": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused", + "ResourceType": "iam.googleapis.com/ServiceAccount", + "Description": "Google Cloud service accounts are evaluated for **recent usage** within a configurable window (default `180` days) using usage telemetry.\n\nIt highlights which accounts show activity versus those with **no observed use** in that period.", + "Risk": "Dormant but permissioned service accounts threaten **confidentiality** and **integrity** via:\n- **Impersonation/privilege escalation** through stale roles or leaked keys\n- **Lateral movement** and persistent access\nThey also weaken **accountability**, obscuring audit trails when reactivated unnoticed.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://cloud.google.com/iam/docs/best-practices-service-accounts?ref=alphasec.io", + "https://cloud.google.com/iam/docs/service-account-overview#identify-unused" + ], "Remediation": { "Code": { - "CLI": "", + "CLI": "gcloud auth print-access-token --impersonate-service-account=", "NativeIaC": "", - "Other": "", + "Other": "1. In the Google Cloud console, open the IAM Service Account Credentials API reference for \"GenerateAccessToken\" and click \"Try this method\" (APIs Explorer)\n2. Set name to: projects/-/serviceAccounts/\n3. Add scope: https://www.googleapis.com/auth/cloud-platform\n4. Click Execute (use an identity with roles/iam.serviceAccountTokenCreator on the service account)\n5. The generated token records recent usage for the service account, changing the finding to PASS", "Terraform": "" }, "Recommendation": { - "Text": "It is recommended to disable or remove unused Service Accounts.", - "Url": "https://cloud.google.com/iam/docs/service-account-overview#identify-unused" + "Text": "Apply **least privilege** and **reduce attack surface**:\n- Verify inactivity, then *disable* and later delete unused accounts\n- Revoke role bindings and keys; favor short-lived impersonation over keys\n- Avoid powerful defaults; enforce separation of duties\n- Continuously monitor usage and alert on dormancy", + "Url": "https://hub.prowler.com/check/iam_service_account_unused" } }, - "Categories": [], + "Categories": [ + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json b/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json index 03adbd16ef..d67cb1e8c5 100644 --- a/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json +++ b/prowler/providers/gcp/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.metadata.json @@ -1,31 +1,35 @@ { "Provider": "gcp", "CheckID": "kms_key_not_publicly_accessible", - "CheckTitle": "Check for Publicly Accessible Cloud KMS Keys", + "CheckTitle": "Cloud KMS key has no public IAM access", "CheckType": [], "ServiceName": "kms", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "CryptoKey", - "ResourceGroup": "security", - "Description": "Check for Publicly Accessible Cloud KMS Keys", - "Risk": "Ensure that the IAM policy associated with your Cloud Key Management Service (KMS) keys is restricting anonymous and/or public access", + "ResourceType": "cloudkms.googleapis.com/CryptoKey", + "Description": "**Cloud KMS crypto keys** are evaluated for **public principals** in their IAM bindings, specifically `allUsers` and `allAuthenticatedUsers`.\n\nThe finding reflects whether these memberships are present on a key.", + "Risk": "Granting **public principals** access lets anyone on the Internet or any Google account use permissions on the key.\n- **Confidentiality** loss via unauthorized `decrypt`\n- **Integrity** compromise via illicit `sign`\n- **Availability** impact from disable, rotation, or destruction", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudKMS/publicly-accessible-kms-cryptokeys.html", + "https://cloud.google.com/kms/docs/iam" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudKMS/publicly-accessible-kms-cryptokeys.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/ensure-that-cloud-kms-cryptokeys-are-not-anonymously-or-publicly-accessible#terraform" + "Other": "1. In Google Cloud Console, go to Security > Key Management > Key rings\n2. Open the key ring, then select the affected key\n3. Click the Permissions tab\n4. Remove principals \"allUsers\" and \"allAuthenticatedUsers\" from all roles\n5. Click Save", + "Terraform": "```hcl\n# Replace the IAM policy on the KMS key to remove any public members\nresource \"google_kms_crypto_key_iam_policy\" \"\" {\n crypto_key_id = \"\"\n \n policy_data = jsonencode({\n bindings = [] # Critical: empty bindings remove all IAM principals, eliminating allUsers/allAuthenticatedUsers\n })\n}\n```" }, "Recommendation": { - "Text": "To deny access from anonymous and public users, remove the bindings for 'allUsers' and 'allAuthenticatedUsers' members from the KMS key's IAM policy.", - "Url": "https://cloud.google.com/kms/docs/iam" + "Text": "Remove `allUsers` and `allAuthenticatedUsers` from key IAM. Grant access only to specific groups or service accounts with **least privilege** at the key scope. Enforce **separation of duties** between admins and users, and regularly review inherited bindings and audit logs.", + "Url": "https://hub.prowler.com/check/kms_key_not_publicly_accessible" } }, "Categories": [ - "internet-exposed" + "internet-exposed", + "identity-access" ], "DependsOn": [], "RelatedTo": [], diff --git a/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json b/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json index b497709ad2..5efe894b04 100644 --- a/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json +++ b/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "kms_key_rotation_enabled", - "CheckTitle": "Ensure KMS keys are rotated within a period of 90 days", + "CheckTitle": "KMS key is rotated at least annually", "CheckType": [], "ServiceName": "kms", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "CryptoKey", - "ResourceGroup": "security", - "Description": "Ensure KMS keys are rotated within a period of 90 days", - "Risk": "Ensure that all your Cloud Key Management Service (KMS) keys are rotated within a period of 90 days in order to meet security and compliance requirements", + "ResourceType": "cloudkms.googleapis.com/CryptoKey", + "Description": "Google Cloud KMS customer-managed keys have **automatic rotation** enabled or a rotation interval `365` days.\n\nThe evaluation reviews each key's rotation settings to confirm periodic creation of new key versions.", + "Risk": "Without timely rotation, a stolen key can decrypt an expanding volume of data, eroding **confidentiality**. Prolonged key lifetimes widen windows for misuse, impact **integrity** of protected workloads, and make emergency rollover harder, risking **availability** disruptions.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudKMS/rotate-kms-encryption-keys.html", + "https://cloud.google.com/iam/docs/manage-access-service-accounts" + ], "Remediation": { "Code": { - "CLI": "gcloud kms keys update new --keyring= --location= --nextrotation-time= --rotation-period=", + "CLI": "gcloud kms keys update --keyring= --location= --rotation-period=365d --next-rotation-time=", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudKMS/rotate-kms-encryption-keys.html", - "Terraform": "https://docs.prowler.com/checks/gcp/google-cloud-general-policies/bc_gcp_general_4#terraform" + "Other": "1. In Google Cloud Console, go to Security > Key Management > Key rings\n2. Open the key ring and select the key\n3. Click Edit rotation schedule (or Set rotation schedule)\n4. Set Rotation period to 365 days or less\n5. Set Next rotation date/time\n6. Click Save", + "Terraform": "```hcl\nresource \"google_kms_crypto_key\" \"\" {\n name = \"\"\n key_ring = \"\"\n purpose = \"ENCRYPT_DECRYPT\"\n\n rotation_period = \"31536000s\" # Critical: sets automatic rotation to 365 days (<= 365 ensures PASS)\n}\n```" }, "Recommendation": { - "Text": "After a successful key rotation, the older key version is required in order to decrypt the data encrypted by that previous key version.", - "Url": "https://cloud.google.com/iam/docs/manage-access-service-accounts" + "Text": "Enable **auto-rotation** for customer-managed keys with an interval `365` days.\n\nAdopt a **key lifecycle** policy: enforce **least privilege** on key usage, apply **separation of duties** between key admins and users, monitor key access, and rehearse emergency rotation to minimize blast radius.", + "Url": "https://hub.prowler.com/check/kms_key_rotation_enabled" } }, - "Categories": [], + "Categories": [ + "encryption" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json index 1d87991ba3..a8da3973fd 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for Audit Configuration Changes.", + "CheckTitle": "Log metric filter for audit configuration changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for Audit Configuration Changes.", - "Risk": "Admin Activity audit logs and Data Access audit logs produced by the Google Cloud Audit Logs service can be extremely useful for security analysis, resource change tracking, and compliance auditing.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** log-based metrics capture **audit configuration changes** (e.g., `SetIamPolicy` with `auditConfigDeltas`), and an associated **Cloud Monitoring alert policy** notifies when such log entries occur.", + "Risk": "Unmonitored **Audit Config** changes can reduce or disable **Admin Activity/Data Access** logging, creating blind spots. Adversaries could suppress evidence, evade detection, and alter permissions unnoticed, degrading **confidentiality**, **integrity**, and the **availability** of forensic telemetry.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-audit-configuration-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-audit-configuration-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Cloud Console, go to Logging > Logs-based metrics and click Create metric\n2. Set Metric type to Counter, Name to , and Filter to:\n protoPayload.methodName=\"SetIamPolicy\" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*\n3. Save the metric\n4. Go to Monitoring > Alerting > Create policy > Add condition\n5. Select Metric as the condition type, then choose metric: logging.googleapis.com/user/\n6. Set condition to is above 0 for 1 minute and click Done\n7. Name the policy and click Create (notification channels are optional for this check)", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"metric\" {\n name = \"\"\n # Critical: this filter captures audit config changes via SetIamPolicy\n filter = \"protoPayload.methodName=\\\"SetIamPolicy\\\" AND protoPayload.serviceData.policyDelta.auditConfigDeltas:*\"\n}\n\nresource \"google_monitoring_alert_policy\" \"alert\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n display_name = \"\"\n condition_threshold {\n # Critical: associates the alert with the log-based metric created above\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"60s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "By using Google Cloud alerting policies to detect audit configuration changes, you make sure that the recommended state of audit configuration is well maintained so that all the activities performed within your GCP project are available for security analysis and auditing at any point in time.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Create a log-based metric for **audit configuration changes** and pair it with a **log-based alert policy** that notifies responders.\n- Enforce **least privilege** on logging/IAM changes\n- Apply **change control** and **separation of duties**\n- Route alerts to durable channels and include response runbooks for **defense in depth**", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json index e2188d51f6..552f0fc538 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for Cloud Storage IAM Permission Changes.", + "CheckTitle": "Log metric filter for Cloud Storage IAM permission changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for Cloud Storage IAM Permission Changes.", - "Risk": "Monitoring changes to cloud storage bucket permissions may reduce the time needed to detect and correct permissions on sensitive cloud storage buckets and objects inside the bucket.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** defines a log-based metric for **Cloud Storage IAM changes** using filter `resource.type=\"gcs_bucket\" AND protoPayload.methodName=\"storage.setIamPermissions\"`, and a **Cloud Monitoring alert policy** that references that metric.", + "Risk": "Lack of alerting on bucket IAM changes degrades **confidentiality and integrity**. Adversaries or misconfigurations can:\n- grant broad/public access\n- persist access by adding roles\n- read, alter, or delete data\nDelays in detection enable **data exfiltration**, tampering, and disruptive actions.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-bucket-permission-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-bucket-permission-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud console, go to Logging > Logs-based metrics\n2. Click Create metric\n3. Name: \n4. In Filter, paste: resource.type=\"gcs_bucket\" AND protoPayload.methodName=\"storage.setIamPermissions\"\n5. Click Create\n6. In the metrics list, click the three dots for the new metric and select Create alert from metric\n7. Keep condition as Count > 0 for Most recent value and click Save", + "Terraform": "```hcl\n# Create a logs-based metric for GCS IAM permission changes\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n filter = \"resource.type=\\\"gcs_bucket\\\" AND protoPayload.methodName=\\\"storage.setIamPermissions\\\"\" # CRITICAL: matches required filter for detection\n}\n\n# Alert policy referencing the above metric\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n combiner = \"OR\"\n conditions {\n display_name = \"\"\n condition_threshold {\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric..name}\\\"\" # CRITICAL: ties alert to the metric so check passes\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Cloud Storage Bucket IAM changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Establish a **log-based metric** for bucket IAM permission changes with filter `resource.type=\"gcs_bucket\" AND protoPayload.methodName=\"storage.setIamPermissions\"` and link a **log-based alert policy** with clear notifications. Enforce **least privilege** and **separation of duties**, and routinely review alerts and audit logs to prevent and contain unauthorized access.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json index 9cdbbc51e8..d2eb0c8ef3 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled.metadata.json @@ -13,7 +13,7 @@ "Risk": "Without monitoring for Compute Engine configuration changes, **unauthorized modifications** to compute resources may go undetected. Attackers can establish **persistence** through instance modifications, escalate privileges via IAM policy changes, disable security controls, or pivot to other resources. This compromises **confidentiality**, **integrity**, and **availability** of workloads and may enable **data exfiltration** or **lateral movement**.", "RelatedUrl": "", "AdditionalURLs": [ - "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/gcp-compute-engine-configuration-changes.html", + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/ComputeEngine/gcp-compute-engine-configuration-changes.html", "https://cloud.google.com/logging/docs/audit", "https://cloud.google.com/monitoring/alerts" ], diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json index e6237e21de..e86597895a 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for Custom Role Changes.", + "CheckTitle": "Log metric filter for IAM custom role changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for Custom Role Changes.", - "Risk": "Google Cloud IAM provides predefined roles that give granular access to specific Google Cloud Platform resources and prevent unwanted access to other resources.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud projects are assessed for log-based metrics that filter `resource.type=\"iam_role\"` and the methods `CreateRole`, `DeleteRole`, `UpdateRole`, and for an associated Cloud Monitoring **alert policy** that references those metrics.", + "Risk": "Without alerts on custom role changes, privilege modifications can go unnoticed, enabling **privilege escalation**, unauthorized data access (confidentiality), permission tampering (integrity), and accidental revocations that disrupt services (availability). Insider misuse or compromised admins can silently reshape access across projects.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-custom-role-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-custom-role-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Logs-based metrics\n2. Click Create metric (Counter), set Name to \n3. In Filter, paste exactly: resource.type=\"iam_role\" AND (protoPayload.methodName=\"google.iam.admin.v1.CreateRole\" OR protoPayload.methodName=\"google.iam.admin.v1.DeleteRole\" OR protoPayload.methodName=\"google.iam.admin.v1.UpdateRole\") and Save\n4. Go to Monitoring > Alerting > Create policy\n5. Add Condition > Metric\n6. For Metric, search and select logging.googleapis.com/user/\n7. Set threshold > 0 and duration 0 min (or 0s), then Save the policy", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # Critical: log-based metric for IAM custom role create/update/delete\n filter = \"resource.type=\\\"iam_role\\\" AND (protoPayload.methodName=\\\"google.iam.admin.v1.CreateRole\\\" OR protoPayload.methodName=\\\"google.iam.admin.v1.DeleteRole\\\" OR protoPayload.methodName=\\\"google.iam.admin.v1.UpdateRole\\\")\"\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n conditions {\n condition_threshold {\n # Critical: alert policy targets the user log-based metric by name\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for changes to Identity and Access Management (IAM) role creation, deletion and updating activities.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Define log-based metrics capturing `resource.type=\"iam_role\"` events for `CreateRole`, `DeleteRole`, and `UpdateRole`, and attach **alert policies** to notify responders.\n\nEnforce **least privilege**, **separation of duties**, and **change control** for role management, and retain **audit logs** for investigation.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json index c537ee2302..eaa437aa3c 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", - "CheckTitle": "Ensure Log Metric Filter and Alerts Exist for Project Ownership Assignments/Changes.", + "CheckTitle": "Log metric filter for project ownership assignments/changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure Log Metric Filter and Alerts Exist for Project Ownership Assignments/Changes.", - "Risk": "Project ownership has the highest level of privileges on a GCP project. These privileges include viewer permissions on all GCP services inside the project, permission to modify the state of all GCP services within the project, set up billing and manage roles and permissions for the project and all the resources inside the project.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud Logging contains a **log-based metric** targeting project ownership changes in Cloud Resource Manager events, and Cloud Monitoring has an **alerting policy** tied to that metric. It detects metrics matching `roles/owner` additions/removals or ownership invites, and whether an alert references that metric.", + "Risk": "Lack of alerts on ownership changes enables **privilege escalation** and **project takeover**. Attackers can add/remove `roles/owner`, causing unauthorized data access (confidentiality), unauthorized config/billing changes (integrity), and resource deletion or lockout (availability) without timely detection.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-ownership-assignments-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-ownership-assignments-monitoring.html", - "Terraform": "" + "Other": "1. In the Google Cloud console, go to Logging > Logs-based metrics and click Create metric\n2. Set Name to \n3. In Filter, paste:\n (protoPayload.serviceName=\"cloudresourcemanager.googleapis.com\") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"REMOVE\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"ADD\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\")\n4. Click Create\n5. Go to Monitoring > Alerting > Create policy > Add condition\n6. Choose Metric, then select metric type logging.googleapis.com/user/\n7. Set condition to Greater than 0 with a duration of 0 minutes, then click Add\n8. Click Create policy (notification channels optional)\n9. Verify the alert condition filter contains ", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # CRITICAL: Detects project ownership assignments/changes\n filter = <<-EOT\n(protoPayload.serviceName=\"cloudresourcemanager.googleapis.com\") AND (ProjectOwnership OR projectOwnerInvitee) OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"REMOVE\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\") OR (protoPayload.serviceData.policyDelta.bindingDeltas.action=\"ADD\" AND protoPayload.serviceData.policyDelta.bindingDeltas.role=\"roles/owner\")\nEOT\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n condition_threshold {\n # CRITICAL: References the log-based metric so an alert is associated with it\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric..name}\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Using Google Cloud alerting policies to detect ownership assignments/changes will help you maintain the right access permissions for each IAM member created within your project, follow the security principle of least privilege, and prevent any accidental or intentional changes that may lead to unauthorized actions.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Create a log-based metric for ownership assignment/removal events and link it to an alerting policy that notifies a monitored channel. Minimize use of `roles/owner` per **least privilege**, require approvals and separation of duties, and apply **defense in depth** with centralized monitoring of IAM changes across projects.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging", + "identity-access" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json index 98a4eaa3c7..261dc25eb0 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for SQL Instance Configuration Changes.", + "CheckTitle": "Log metric filter for Cloud SQL instance configuration changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for SQL Instance Configuration Changes.", - "Risk": "Monitoring changes to SQL instance configuration changes may reduce the time needed to detect and correct misconfigurations done on the SQL server.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** has a log-based metric matching Cloud SQL instance updates (`protoPayload.methodName=\"cloudsql.instances.update\"`) and a **Cloud Monitoring** alert policy references that metric to notify on configuration changes.", + "Risk": "Without this visibility, **unauthorized or accidental Cloud SQL configuration changes** can persist undetected. Attackers or insiders might open public access, relax TLS, alter authorized networks, or disable backups, degrading **confidentiality**, **integrity**, and **availability** and delaying containment.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud console, go to Logging > Logs-based metrics and click Create metric\n2. Set Name to and Filter to: protoPayload.methodName=\"cloudsql.instances.update\" then click Create\n3. Go to Monitoring > Alerting > Create policy\n4. Click Add condition > Metric\n5. For Metric, select logging.googleapis.com/user/\n6. Set threshold to Greater than 0 over 0 minutes and click Add\n7. Click Save policy", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"sql_config_changes\" {\n name = \"\"\n # Critical: captures Cloud SQL instance configuration updates\n filter = \"protoPayload.methodName=\\\"cloudsql.instances.update\\\"\"\n}\n\nresource \"google_monitoring_alert_policy\" \"sql_config_change_alert\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n condition_threshold {\n # Critical: reference the logs-based metric so the alert is associated\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric.sql_config_changes.name}\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for SQL instance configuration changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Implement a **log-based metric** for Cloud SQL update events and attach a **Monitoring alert policy** that routes timely notifications. Apply **least privilege** for admin actions, enforce **change management** and **separation of duties**, and integrate alerts with on-call workflows to speed triage and prevent misconfigurations.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json index 9214f1dfb9..e97042a2d3 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Firewall Rule Changes.", + "CheckTitle": "Log metric filter for VPC network firewall rule changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Firewall Rule Changes.", - "Risk": "Monitoring for Create or Update Firewall rule events gives insight to network access changes and may reduce the time it takes to detect suspicious activity.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud Logging has a log-based metric for **VPC firewall rule** changes, matching `resource.type=\"gce_firewall_rule\"` and `protoPayload.methodName` of `compute.firewalls.insert`, `compute.firewalls.patch`, or `compute.firewalls.delete`, and Cloud Monitoring includes an alerting policy that references this metric.", + "Risk": "Without alerts on firewall rule changes, unauthorized or accidental modifications can go unnoticed, exposing services or blocking critical traffic.\n\nConfidentiality suffers (opened ports), integrity is reduced (tampered controls), and availability can be impacted (outages), enabling lateral movement and data exfiltration.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-firewall-rule-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-firewall-rule-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Logs-based metrics\n2. Click Create metric, set Name to \n3. In Filter, paste: resource.type=\"gce_firewall_rule\" AND (protoPayload.methodName:\"compute.firewalls.patch\" OR protoPayload.methodName:\"compute.firewalls.insert\" OR protoPayload.methodName:\"compute.firewalls.delete\")\n4. Save the metric\n5. Go to Monitoring > Alerting > Create policy\n6. Add Condition > Metric threshold\n7. For Metric, select User-defined and choose logging.googleapis.com/user/\n8. Set condition to > 0 with duration 0 minutes (or the minimum allowed)\n9. Create the policy (notification channels are optional)", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # CRITICAL: this filter captures VPC firewall rule changes\n filter = \"resource.type=\\\"gce_firewall_rule\\\" AND (protoPayload.methodName:\\\"compute.firewalls.patch\\\" OR protoPayload.methodName:\\\"compute.firewalls.insert\\\" OR protoPayload.methodName:\\\"compute.firewalls.delete\\\")\"\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n combiner = \"OR\"\n\n conditions {\n display_name = \"\"\n condition_threshold {\n # CRITICAL: reference the user log metric so the alert policy is associated\n # This makes the policy filter contain the metric name, satisfying the check\n filter = \"metric.type=\\\"logging.googleapis.com/user/${google_logging_metric..name}\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Virtual Private Cloud (VPC) Network Firewall rule changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Establish a log-based metric for `gce_firewall_rule` insert/patch/delete events and tie it to an alerting policy that notifies responders.\n\nEnforce **least privilege** and change control on firewall updates, apply **separation of duties**, and monitor all projects for rapid, auditable detection of network control changes.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json index 70c76b966f..4c89bdcb7e 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Changes.", + "CheckTitle": "Log metric filter for VPC network changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Changes.", - "Risk": "Monitoring changes to a VPC will help ensure VPC traffic flow is not getting impacted.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "Cloud projects are evaluated for a **log-based metric** with a linked **Cloud Monitoring alert** that targets **VPC network changes** on `gce_network` audit events: `compute.networks.insert`, `patch`, `delete`, `addPeering`, `removePeering`.\n\nIt checks that these changes are captured and generate notifications.", + "Risk": "Missing alerts on VPC changes lets **unauthorized or accidental modifications** go unnoticed, risking:\n- Data exposure via unintended peering or new networks\n- Segmentation bypass through routing/subnet edits\n- Outages from deletions or misconfigurations\n\nThis affects confidentiality, integrity, and availability.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-vpc-network-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-vpc-network-changes-monitoring.html", - "Terraform": "" + "Other": "1. In the Google Cloud Console, go to Logging > Logs-based metrics > + Create metric\n2. Name: \n3. In Filter, paste exactly:\n resource.type=\"gce_network\" AND (protoPayload.methodName:\"compute.networks.insert\" OR protoPayload.methodName:\"compute.networks.patch\" OR protoPayload.methodName:\"compute.networks.delete\" OR protoPayload.methodName:\"compute.networks.removePeering\" OR protoPayload.methodName:\"compute.networks.addPeering\")\n4. Click Create metric\n5. Go to Monitoring > Alerting > + Create policy > Add condition\n6. Select the metric logging.googleapis.com/user/\n7. Condition: Greater than, Threshold: 0, For: 0 minutes; click Add\n8. Name the policy and click Create policy", + "Terraform": "```hcl\n# Create a logs-based metric for VPC network changes\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n # CRITICAL: Filter capturing VPC network create/modify/delete/peering changes\n filter = \"resource.type=\\\"gce_network\\\" AND (protoPayload.methodName:\\\"compute.networks.insert\\\" OR protoPayload.methodName:\\\"compute.networks.patch\\\" OR protoPayload.methodName:\\\"compute.networks.delete\\\" OR protoPayload.methodName:\\\"compute.networks.removePeering\\\" OR protoPayload.methodName:\\\"compute.networks.addPeering\\\")\"\n}\n\n# Alert policy associated with the logs-based metric\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n\n conditions {\n condition_threshold {\n # CRITICAL: Reference the logs-based metric so the policy is associated with it\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\"\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Virtual Private Cloud (VPC) network changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Implement a **log-based metric** for VPC change audit events and attach an **alerting policy** that notifies accountable teams.\n\nApply **least privilege** for network admins, enforce **change approval**, and adopt **defense in depth** to prevent and quickly detect unintended network changes.", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json index 2fa5010aed..8e0dad31f9 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.metadata.json @@ -1,30 +1,35 @@ { "Provider": "gcp", "CheckID": "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", - "CheckTitle": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Route Changes.", + "CheckTitle": "Log metric filter for VPC network route changes has an associated alert policy", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "MetricFilter", - "ResourceGroup": "monitoring", - "Description": "Ensure That the Log Metric Filter and Alerts Exist for VPC Network Route Changes.", - "Risk": "Monitoring changes to route tables will help ensure that all VPC traffic flows through an expected path.", + "ResourceType": "logging.googleapis.com/LogMetric", + "Description": "**Cloud Logging** includes a **log-based metric** for **VPC route modifications** and a linked **Cloud Monitoring alert**.\n\nIt targets `gce_route` entries for `compute.routes.insert` and `compute.routes.delete` so route creations or deletions generate alertable signals.", + "Risk": "Without visibility into **route changes**, attackers or mistakes can:\n- Reroute traffic to bypass inspection **data exfiltration** (confidentiality)\n- Alter paths enabling **lateral movement** (integrity)\n- Blackhole networks causing **outages** (availability)", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", + "https://cloud.google.com/monitoring/alerts" + ], "Remediation": { "Code": { "CLI": "", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/enable-network-route-changes-monitoring.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Logs-based metrics and click Create metric\n2. Set Name to \n3. In Filter, paste: resource.type=\"gce_route\" AND (protoPayload.methodName:\"compute.routes.delete\" OR protoPayload.methodName:\"compute.routes.insert\")\n4. Click Create metric\n5. Go to Monitoring > Alerting > Create policy\n6. Add Condition: Metric threshold; Select metric logging.googleapis.com/user/; Set condition to > 0 for 0 minutes\n7. Click Create policy (skip notification channels if not needed)\n", + "Terraform": "```hcl\nresource \"google_logging_metric\" \"\" {\n name = \"\"\n filter = \"resource.type=\\\"gce_route\\\" AND (protoPayload.methodName:\\\"compute.routes.delete\\\" OR protoPayload.methodName:\\\"compute.routes.insert\\\")\" # Critical: matches VPC route insert/delete events\n}\n\nresource \"google_monitoring_alert_policy\" \"\" {\n display_name = \"\"\n\n conditions {\n condition_threshold {\n filter = \"metric.type=\\\"logging.googleapis.com/user/\\\"\" # Critical: alert evaluates the log-based metric by name\n comparison = \"COMPARISON_GT\"\n threshold_value = 0\n duration = \"0s\"\n }\n }\n}\n```" }, "Recommendation": { - "Text": "It is recommended that a metric filter and alarm be established for Virtual Private Cloud (VPC) network route changes.", - "Url": "https://cloud.google.com/monitoring/alerts" + "Text": "Create a **log-based metric** for `compute.routes.insert` and `compute.routes.delete`, and attach a **log-based alert** with reliable notifications.\n\nApply **least privilege** to route management, require **change approval**, and use **defense in depth** (egress filtering, private routing, durable audit logs).", + "Url": "https://hub.prowler.com/check/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled" } }, - "Categories": [], + "Categories": [ + "logging" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json b/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json index beac1b934a..be7ce98c8f 100644 --- a/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json +++ b/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.metadata.json @@ -1,30 +1,36 @@ { "Provider": "gcp", "CheckID": "logging_sink_created", - "CheckTitle": "Ensure there is at least one sink used to export copies of all the log entries.", + "CheckTitle": "Project has at least one logging sink exporting copies of all log entries", "CheckType": [], "ServiceName": "logging", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "Sink", - "ResourceGroup": "monitoring", - "Description": "Ensure there is at least one sink used to export copies of all the log entries.", - "Risk": "If sinks are not created, logs would be deleted after the configured retention period, and would not be backed up.", + "ResourceType": "logging.googleapis.com/LogSink", + "Description": "**Cloud Logging** project contains at least one **sink** that exports a copy of **all log entries** to a destination for centralized retention or processing", + "Risk": "Without exporting all logs, audit evidence can expire or be altered in-project, reducing the **availability** and **integrity** of telemetry. This hinders threat detection and forensics, prevents cross-project correlation, and can let attacker actions evade scrutiny after log rotation or deletion.", "RelatedUrl": "", + "AdditionalURLs": [ + "https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/gcp/CloudLogging/export-all-log-entries.html", + "https://cloud.google.com/logging/docs/export" + ], "Remediation": { "Code": { - "CLI": "gcloud logging sinks create ", + "CLI": "gcloud logging sinks create logging.googleapis.com/projects/", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudLogging/export-all-log-entries.html", - "Terraform": "" + "Other": "1. In Google Cloud Console, go to Logging > Log Router\n2. Click Create sink\n3. Set Sink name to \n4. For Sink destination, select Google Cloud project and choose \n5. Leave the inclusion filter empty (exports all logs)\n6. Click Create", + "Terraform": "```hcl\n# Create a project-level logging sink exporting all logs\nresource \"google_logging_project_sink\" \"sink\" {\n name = \"\" # critical: creates the sink required by the check\n destination = \"logging.googleapis.com/projects/\" # critical: required destination\n # No filter set -> exports all log entries (fixes the finding)\n}\n```" }, "Recommendation": { - "Text": "It is recommended to create a sink that will export copies of all the log entries. This can help aggregate logs from multiple projects and export them to a Security Information and Event Management (SIEM).", - "Url": "https://cloud.google.com/logging/docs/export" + "Text": "Create a **centralized export sink** that routes all logs to a secured, durable, preferably **immutable** destination with extended retention. Apply **least privilege** to sink identities, separate duties by isolating destinations, use **defense in depth** (encryption, access controls), and monitor sink health for continuity.", + "Url": "https://hub.prowler.com/check/logging_sink_created" } }, - "Categories": [], + "Categories": [ + "logging", + "forensics-ready" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/github/services/organization/organization_service.py b/prowler/providers/github/services/organization/organization_service.py index 2f7e9bc4ea..187da72f68 100644 --- a/prowler/providers/github/services/organization/organization_service.py +++ b/prowler/providers/github/services/organization/organization_service.py @@ -76,6 +76,7 @@ class Organization(GithubService): id=user.id, name=user.login, mfa_required=None, # Users don't have MFA requirements like orgs + is_verified=None, ) logger.info( f"Added user '{user.login}' as organization for checks" @@ -195,7 +196,7 @@ class Organization(GithubService): if isinstance(base_permission_raw, str) else None ) - + is_verified = _extract_flag("is_verified", bool) organizations[org.id] = Org( id=org.id, name=org.login, @@ -216,6 +217,7 @@ class Organization(GithubService): "members_allowed_repository_creation_type" ], base_permission=base_permission, + is_verified=is_verified, ) @@ -231,3 +233,4 @@ class Org(BaseModel): members_can_create_internal_repositories: Optional[bool] = None members_allowed_repository_creation_type: Optional[str] = None base_permission: Optional[str] = None + is_verified: Optional[bool] = None diff --git a/prowler/providers/github/services/organization/organization_verified_badge/__init__.py b/prowler/providers/github/services/organization/organization_verified_badge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.metadata.json b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.metadata.json new file mode 100644 index 0000000000..83bcb7a9dd --- /dev/null +++ b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "github", + "CheckID": "organization_verified_badge", + "CheckTitle": "Ensure GitHub organization has a verified badge", + "CheckType": [], + "ServiceName": "organization", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "GitHubOrganization", + "ResourceGroup": "governance", + "Description": "Checks whether a GitHub organization has a verified badge.", + "Risk": "Unverified organizations may be easier to impersonate, increasing the risk of phishing or trust abuse.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.github.com/en/organizations/managing-organization-settings/verifying-or-approving-a-domain-for-your-organization" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Verify the organization identity by completing GitHub organization verification.", + "Url": "https://hub.prowler.com/check/organization_verified_badge" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check uses the GitHub API field is_verified from organization metadata." +} diff --git a/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.py b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.py new file mode 100644 index 0000000000..948c543fd0 --- /dev/null +++ b/prowler/providers/github/services/organization/organization_verified_badge/organization_verified_badge.py @@ -0,0 +1,31 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportGithub +from prowler.providers.github.services.organization.organization_client import ( + organization_client, +) + + +class organization_verified_badge(Check): + """Check if GitHub organizations are verified.""" + + def execute(self) -> List[CheckReportGithub]: + findings: List[CheckReportGithub] = [] + + for org in organization_client.organizations.values(): + report = CheckReportGithub(metadata=self.metadata(), resource=org) + + if org.is_verified: + report.status = "PASS" + report.status_extended = ( + f"Organization {org.name} is verified on GitHub." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"Organization {org.name} is not verified on GitHub." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.metadata.json new file mode 100644 index 0000000000..98142e24d1 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_config_drive_enabled", + "CheckTitle": "Compute instances have config drive enabled", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **config drive** is enabled. Config drive provides metadata and user data via a virtual CD-ROM device instead of the metadata service (169.254.169.254). This improves security by eliminating network-based metadata access, which can be vulnerable to SSRF attacks and metadata service exploitation.", + "Risk": "Instances without config drive rely on the metadata service (169.254.169.254), vulnerable to SSRF attacks that extract credentials and SSH keys. Metadata service is vulnerable to spoofing in compromised networks and can become unavailable. Config drive eliminates this attack surface by providing metadata via virtual CD-ROM, removing dependency on network-accessible metadata.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/nova/queens/user/config-drive.html", + "https://cloudinit.readthedocs.io/en/latest/reference/datasources/configdrive.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server show -c config_drive\nopenstack server image create --name backup-snapshot\nopenstack server rebuild --image --config-drive true", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances > Launch Instance**\n2. In **Configuration** or **Advanced Options** tab, enable **Config Drive**\n3. Complete instance launch\n\nNote: Config drive cannot be added to existing instances without rebuild", + "Terraform": "```hcl\n# Terraform: enable config drive for compute instance\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"secure-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"default\"]\n # Enable config drive for secure metadata injection\n config_drive = true\n\n network {\n name = var.network_name\n }\n\n # Optional: cloud-init will automatically use config drive if available\n user_data = <<-EOF\n #cloud-config\n datasource_list: [ConfigDrive, OpenStack]\n EOF\n}\n```" + }, + "Recommendation": { + "Text": "Enable config drive on all instances to eliminate metadata service attack surface. Config drive provides metadata via virtual CD-ROM instead of network-accessible service (169.254.169.254), preventing SSRF attacks. Combine with firewall rules blocking metadata service access from applications. Use config drive in air-gapped environments where metadata service is unavailable.", + "Url": "https://hub.prowler.com/check/compute_instance_config_drive_enabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Config drive must be enabled at instance creation time and cannot be added to existing instances without rebuild. Some OpenStack deployments may not support config drive (e.g., bare metal provisioning). Config drive is read-only from the guest OS perspective." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.py b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.py new file mode 100644 index 0000000000..f15936f6b6 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled.py @@ -0,0 +1,24 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_config_drive_enabled(Check): + """Ensure compute instances have config drive enabled for secure metadata injection.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.has_config_drive: + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) has config drive enabled for secure metadata injection." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have config drive enabled (relies on metadata service)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.metadata.json new file mode 100644 index 0000000000..c9334f01f0 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_isolated_private_network", + "CheckTitle": "Compute instances are isolated in private networks", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify **network isolation** by ensuring they have private IP addresses without mixed public/private exposure. Proper network segmentation requires instances to be deployed in private networks and accessed via controlled entry points (bastion hosts, VPN, load balancers) rather than direct public exposure.", + "Risk": "Instances with mixed public/private exposure or only public IPs lack network isolation, allowing unauthorized internet access that bypasses segmentation controls. Attackers can pivot from compromised public instances to internal infrastructure for lateral movement. Flat topology exposes internal services to internet attacks including DDoS and exploit attempts.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/neutron/latest/admin/intro-os-networking.html", + "https://docs.openstack.org/security-guide/networking/architecture.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server remove floating ip ", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance with public IP\n3. Click **Actions > Disassociate Floating IP**\n4. Confirm disassociation\n5. Access instance via bastion host or VPN instead", + "Terraform": "```hcl\n# Terraform: deploy instance in isolated private network\n\n# Private network (no external gateway)\nresource \"openstack_networking_network_v2\" \"private\" {\n name = \"private-network\"\n admin_state_up = true\n}\n\nresource \"openstack_networking_subnet_v2\" \"private_subnet\" {\n name = \"private-subnet\"\n network_id = openstack_networking_network_v2.private.id\n cidr = \"10.0.1.0/24\"\n ip_version = 4\n # No gateway_ip means no external routing\n}\n\n# Instance in private network (no public IP)\nresource \"openstack_compute_instance_v2\" \"app_server\" {\n name = \"app-server\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"app-tier-sg\"]\n\n # Attach to private network ONLY\n network {\n uuid = openstack_networking_network_v2.private.id\n }\n\n # Do NOT associate floating IP\n}\n\n# Security group for app tier (least privilege)\nresource \"openstack_networking_secgroup_v2\" \"app_tier_sg\" {\n name = \"app-tier-sg\"\n description = \"App tier security group - private network only\"\n}\n\n# Allow SSH from bastion host only (not internet)\nresource \"openstack_networking_secgroup_rule_v2\" \"app_ssh_from_bastion\" {\n direction = \"ingress\"\n ethertype = \"IPv4\"\n protocol = \"tcp\"\n port_range_min = 22\n port_range_max = 22\n remote_ip_prefix = var.bastion_private_ip # e.g., 10.0.0.5/32\n security_group_id = openstack_networking_secgroup_v2.app_tier_sg.id\n}\n\n# Allow app traffic from web tier only\nresource \"openstack_networking_secgroup_rule_v2\" \"app_backend\" {\n direction = \"ingress\"\n ethertype = \"IPv4\"\n protocol = \"tcp\"\n port_range_min = 8080\n port_range_max = 8080\n remote_ip_prefix = var.web_tier_cidr # e.g., 10.0.2.0/24\n security_group_id = openstack_networking_secgroup_v2.app_tier_sg.id\n}\n```" + }, + "Recommendation": { + "Text": "Deploy instances in private networks (RFC1918) with tiered architecture: web tier with public IPs, app/database tiers private only. Never mix public and private IPs on same instance. Use bastion hosts or VPN for operator access, security groups for least-privilege policies, and NAT gateways for outbound access. Enable flow logs to detect lateral movement attempts.", + "Url": "https://hub.prowler.com/check/compute_instance_isolated_private_network" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check flags instances with mixed public/private IPs or only public IPs as not properly isolated. Some architectures legitimately require dual-homed instances (e.g., NAT gateways, VPN endpoints). Review findings in context. Instances without any IPs are also flagged as they lack network configuration." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.py b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.py new file mode 100644 index 0000000000..6d8b1a6500 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network.py @@ -0,0 +1,63 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client +from prowler.providers.openstack.services.compute.lib.ip import is_public_ip + + +class compute_instance_isolated_private_network(Check): + """Ensure compute instances are isolated in private networks without mixed public/private exposure.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + + private_ip_list = [] + public_ip_list = [] + + # Classify IPs from networks dict using actual IP validation + for ip_list in instance.networks.values(): + for ip in ip_list: + if is_public_ip(ip): + public_ip_list.append(ip) + else: + private_ip_list.append(ip) + + # Also check SDK fields for IPs not present in networks + seen_ips = set(private_ip_list + public_ip_list) + for ip in [ + instance.public_v4, + instance.public_v6, + instance.access_ipv4, + instance.access_ipv6, + ]: + if ip and ip not in seen_ips and is_public_ip(ip): + public_ip_list.append(ip) + for ip in [instance.private_v4, instance.private_v6]: + if ip and ip not in seen_ips and not is_public_ip(ip): + private_ip_list.append(ip) + + has_private_ips = bool(private_ip_list) + has_public_ips = bool(public_ip_list) + + # Determine status based on IP classification + if has_private_ips and not has_public_ips: + report.status = "PASS" + ip_display = ", ".join(private_ip_list) + report.status_extended = f"Instance {instance.name} ({instance.id}) is properly isolated in private network with private IPs ({ip_display}) and no public exposure." + elif has_public_ips and has_private_ips: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) has mixed public and private network exposure (not properly isolated)." + elif has_public_ips and not has_private_ips: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) has only public IP addresses (no private network isolation)." + else: + # No IPs at all (edge case) + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) has no network configuration (no IPs assigned)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.metadata.json new file mode 100644 index 0000000000..651676ee88 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_key_based_authentication", + "CheckTitle": "Compute instances use SSH key-based authentication", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **SSH key-based authentication** is configured by checking for an assigned keypair. Password-based authentication is vulnerable to brute-force attacks, credential stuffing, and phishing. SSH keys provide cryptographic authentication resistant to these attacks.", + "Risk": "Instances without SSH key-based authentication are vulnerable to brute-force password attacks, credential stuffing, and password reuse. Attackers can test common passwords, intercept credentials, or exploit leaked passwords from other breaches. Successful SSH access enables malware injection, lateral movement, privilege escalation, and data exfiltration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/security-guide/compute/hardening-the-virtualization-layers.html", + "https://docs.openstack.org/api-ref/compute/#keypairs-keypairs" + ], + "Remediation": { + "Code": { + "CLI": "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys\nsudo systemctl restart sshd\nssh -i ~/.ssh/private_key username@instance_ip", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances > Launch Instance**\n2. In **Key Pair** tab, select existing keypair or create new one\n3. Launch instance with keypair attached", + "Terraform": "```hcl\n# Terraform: ensure compute instance uses SSH key-based authentication\n\n# Create or import keypair\nresource \"openstack_compute_keypair_v2\" \"keypair\" {\n name = \"production-keypair\"\n public_key = file(\"~/.ssh/id_rsa.pub\")\n}\n\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"secure-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n # Critical: attach keypair for SSH key-based authentication\n key_pair = openstack_compute_keypair_v2.keypair.name\n security_groups = [\"default\"]\n\n network {\n name = var.network_name\n }\n\n # Optional: use cloud-init to enforce key-only authentication\n user_data = <<-EOF\n #cloud-config\n ssh_pwauth: false\n disable_root: true\n EOF\n}\n```" + }, + "Recommendation": { + "Text": "Always use SSH keys instead of passwords for instance authentication. Generate keys with strong algorithms (RSA 4096-bit, Ed25519). Protect private keys with passphrases and store securely. Disable password authentication in sshd_config. Rotate keypairs periodically and revoke old keys. Use bastion hosts or VPN for SSH access instead of direct internet exposure.", + "Url": "https://hub.prowler.com/check/compute_instance_key_based_authentication" + } + }, + "Categories": [ + "identity-access", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check verifies keypair assignment via OpenStack metadata. It does not validate authorized_keys file contents or SSH daemon configuration inside the instance. Manual verification may be needed for instances launched without keypairs but later configured with keys." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.py b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.py new file mode 100644 index 0000000000..10a1efb0a1 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication.py @@ -0,0 +1,24 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_key_based_authentication(Check): + """Ensure compute instances use SSH key-based authentication instead of passwords.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.key_name: + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) is configured with SSH key-based authentication (keypair: {instance.key_name})." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have SSH key-based authentication configured (no keypair assigned)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.metadata.json new file mode 100644 index 0000000000..89766f6214 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_locked_status_enabled", + "CheckTitle": "Compute instances have locked status enabled", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **locked status** is enabled. Locking an instance prevents unauthorized administrative operations (delete, resize, rebuild, etc.) without first unlocking it. This provides an additional layer of protection against accidental or malicious modifications.", + "Risk": "Instances without locked status can be subjected to unauthorized operations (deletion, resize, rebuild) by compromised accounts without additional barriers. Attackers can manipulate unlocked instances to destroy forensic evidence or disrupt production workloads. Accidental termination by operators also poses risk due to lack of change control barriers.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/api-ref/compute/#lock-server-lock", + "https://docs.openstack.org/security-guide/compute.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server lock --reason \"Production instance - requires approval\"\nopenstack server show ", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select the instance to protect\n3. Use CLI or API to lock the instance\n4. Verify locked status is enabled", + "Terraform": "```hcl\n# Note: Terraform openstack_compute_instance_v2 does not support locked status natively\n# Use null_resource with local-exec provisioner as workaround\n\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"production-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"default\"]\n\n network {\n name = var.network_name\n }\n}\n\nresource \"null_resource\" \"lock_instance\" {\n depends_on = [openstack_compute_instance_v2.instance]\n\n provisioner \"local-exec\" {\n command = \"openstack server lock ${openstack_compute_instance_v2.instance.id}\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Lock production instances to prevent accidental or unauthorized modifications. Use --reason parameter to document lock purpose. Implement approval workflows requiring consent before unlocking. Apply locks to critical infrastructure (databases, authentication, logging). Use RBAC policies to restrict who can lock/unlock. Audit lock/unlock operations via OpenStack logs.", + "Url": "https://hub.prowler.com/check/compute_instance_locked_status_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Locked status prevents administrative operations like delete, resize, rebuild, and shelve. It does not prevent start/stop/reboot operations or guest OS-level changes. Lock operations require specific Nova API permissions." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.py b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.py new file mode 100644 index 0000000000..b16dd4e4c2 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled.py @@ -0,0 +1,29 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_locked_status_enabled(Check): + """Ensure compute instances have locked status enabled to prevent unauthorized operations.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.is_locked: + report.status = "PASS" + reason = ( + f" (reason: {instance.locked_reason})" + if instance.locked_reason + else "" + ) + report.status_extended = f"Instance {instance.name} ({instance.id}) has locked status enabled{reason}." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have locked status enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.metadata.json new file mode 100644 index 0000000000..f5e3b95758 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_metadata_sensitive_data", + "CheckTitle": "Compute instance metadata does not contain sensitive data", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "critical", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instance metadata** is evaluated to detect **sensitive data** such as passwords, API keys, secrets, and private keys. Instance metadata is accessible via the metadata service (169.254.169.254) to any process inside the instance. Storing secrets in metadata exposes them to SSRF attacks, compromised applications, and unauthorized access.", + "Risk": "Instance metadata containing sensitive data exposes credentials through the metadata service (169.254.169.254), accessible to any process inside the instance. Attackers exploiting SSRF, compromised applications, or insider threats can extract passwords, API keys, and private keys. Stolen credentials enable unauthorized modifications, privilege escalation, resource deletion, and cryptomining.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/nova/latest/user/metadata.html", + "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server unset --property ", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance with sensitive metadata\n3. Remove sensitive metadata keys using CLI command\n4. Rotate exposed credentials immediately\n5. Store secrets in Barbican or external secrets manager instead", + "Terraform": "```hcl\n# Terraform: use Barbican for secrets instead of metadata\n\n# Store secret in Barbican\nresource \"openstack_keymanager_secret_v1\" \"db_password\" {\n name = \"database-password\"\n payload = random_password.db_password.result\n payload_content_type = \"text/plain\"\n secret_type = \"passphrase\"\n}\n\nresource \"random_password\" \"db_password\" {\n length = 32\n special = true\n}\n\n# Instance WITHOUT sensitive data in metadata\nresource \"openstack_compute_instance_v2\" \"secure_instance\" {\n name = \"app-server\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"app-sg\"]\n\n # Safe metadata (non-sensitive labels only)\n metadata = {\n environment = \"production\"\n application = \"web-app\"\n cost_center = \"engineering\"\n barbican_secret_id = openstack_keymanager_secret_v1.db_password.secret_ref\n }\n\n network {\n name = \"private-network\"\n }\n\n # Use cloud-init to retrieve secrets from Barbican\n user_data = <<-EOF\n #!/bin/bash\n # Retrieve database password from Barbican\n SECRET_REF=\"${openstack_keymanager_secret_v1.db_password.secret_ref}\"\n DB_PASSWORD=$(openstack secret get \"$SECRET_REF\" --payload -f value)\n \n # Configure application with retrieved secret\n echo \"DATABASE_URL=://user:$DB_PASSWORD@db-host/dbname\" > /etc/app/config.env\n chmod 600 /etc/app/config.env\n EOF\n}\n\n# ANTI-PATTERN: DO NOT DO THIS\n# resource \"openstack_compute_instance_v2\" \"insecure_instance\" {\n# metadata = {\n# db_password = \"hardcoded-secret-123\" # NEVER store secrets in metadata\n# api_key = \"sk-1234567890abcdef\" # Exposed via metadata service\n# }\n# }\n```" + }, + "Recommendation": { + "Text": "Never store secrets in metadata; use Barbican (OpenStack Key Manager), Vault, or external secrets management instead. Retrieve secrets at runtime via APIs. Implement least privilege access to secrets with RBAC. Enable secrets audit logging. Use envelope encryption for secrets at rest. Implement automatic rotation every 90 days. Scan metadata for hardcoded secrets using tools like TruffleHog.", + "Url": "https://hub.prowler.com/check/compute_instance_metadata_sensitive_data" + } + }, + "Categories": [ + "secrets", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check uses the detect-secrets library to scan for credentials. May produce false positives on metadata keys containing secret-like keywords. Findings should be reviewed manually. The audit_config allows configuring secrets_ignore_patterns to exclude specific patterns and detect_secrets_plugins to customize detection. Metadata is world-readable within instance via 169.254.169.254." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.py b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.py new file mode 100644 index 0000000000..5df151c939 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data.py @@ -0,0 +1,58 @@ +import json +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.lib.utils.utils import detect_secrets_scan +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_metadata_sensitive_data(Check): + """Ensure compute instance metadata does not contain sensitive data like passwords or API keys.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + secrets_ignore_patterns = compute_client.audit_config.get( + "secrets_ignore_patterns", [] + ) + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) metadata does not contain sensitive data." + + if instance.metadata: + # Build metadata dict and parallel list of keys (similar to AWS ECS pattern) + dump_metadata = {} + original_metadata_keys = [] + for key, value in instance.metadata.items(): + dump_metadata[key] = value + original_metadata_keys.append(key) + + # Convert metadata dict to JSON string for detect-secrets scanning + metadata_json = json.dumps(dump_metadata, indent=2) + detect_secrets_output = detect_secrets_scan( + data=metadata_json, + excluded_secrets=secrets_ignore_patterns, + detect_secrets_plugins=compute_client.audit_config.get( + "detect_secrets_plugins" + ), + ) + + if detect_secrets_output: + # Map line numbers back to metadata keys using the parallel list + # Line numbering: line 1 = "{", line 2 = first key-value, etc. + secrets_string = ", ".join( + [ + f"{secret['type']} in metadata key '{original_metadata_keys[secret['line_number'] - 2]}'" + for secret in detect_secrets_output + if secret["line_number"] - 2 < len(original_metadata_keys) + ] + ) + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) metadata contains potential secrets -> {secrets_string}." + else: + report.status_extended = f"Instance {instance.name} ({instance.id}) has no metadata (no sensitive data exposure risk)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.metadata.json new file mode 100644 index 0000000000..c6eca07451 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_public_ip_exposed", + "CheckTitle": "Compute instances are not exposed to the internet", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** are evaluated to verify they are **not exposed to the internet** via public IPs (floating IPs or access IPs). Instances with public IPs are directly reachable from the internet, increasing attack surface. Best practices recommend using **bastion hosts**, **VPN gateways**, or **load balancers** instead.", + "Risk": "Instances with public IPs are directly reachable from the internet, enabling reconnaissance, port scanning, and vulnerability exploitation. Attackers can target instances for brute-force attacks, credential stuffing, and malware injection. Public exposure bypasses network segmentation and defense-in-depth. Compromised public instances become pivot points for lateral movement.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/neutron/latest/admin/intro-nat.html", + "https://docs.openstack.org/security-guide/networking/architecture.html", + "https://docs.openstack.org/neutron/latest/admin/intro-os-networking.html" + ], + "Remediation": { + "Code": { + "CLI": "openstack server remove floating ip \nssh -J bastion private-instance", + "NativeIaC": "", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance with public IP\n3. Click **Actions > Disassociate Floating IP**\n4. Confirm disassociation\n5. Access via bastion host or VPN instead", + "Terraform": "```hcl\n# Terraform: deploy instance without public IP (private network only)\n\nresource \"openstack_compute_instance_v2\" \"private_instance\" {\n name = \"private-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"internal-sg\"]\n\n # Attach to private network ONLY (no floating IP)\n network {\n name = \"private-network\"\n }\n\n # Do NOT create floating IP association\n}\n\n# Bastion host for SSH access (only instance with public IP)\nresource \"openstack_compute_instance_v2\" \"bastion\" {\n name = \"bastion-host\"\n image_id = var.bastion_image_id\n flavor_id = \"small\"\n key_pair = var.bastion_keypair\n security_groups = [\"bastion-sg\"] # Restrict SSH to corporate IPs only\n\n network {\n name = \"dmz-network\"\n }\n}\n\nresource \"openstack_networking_floatingip_v2\" \"bastion_fip\" {\n pool = \"public\"\n}\n\nresource \"openstack_compute_floatingip_associate_v2\" \"bastion_fip_assoc\" {\n floating_ip = openstack_networking_floatingip_v2.bastion_fip.address\n instance_id = openstack_compute_instance_v2.bastion.id\n}\n\n# Security group for bastion (least privilege)\nresource \"openstack_networking_secgroup_v2\" \"bastion_sg\" {\n name = \"bastion-sg\"\n description = \"Allow SSH from corporate IPs only\"\n}\n\nresource \"openstack_networking_secgroup_rule_v2\" \"bastion_ssh\" {\n direction = \"ingress\"\n ethertype = \"IPv4\"\n protocol = \"tcp\"\n port_range_min = 22\n port_range_max = 22\n remote_ip_prefix = var.corporate_cidr # e.g., 203.0.113.0/24\n security_group_id = openstack_networking_secgroup_v2.bastion_sg.id\n}\n```" + }, + "Recommendation": { + "Text": "Avoid public IPs on application/database instances; use private networks only. Deploy bastion hosts or VPN gateways for operator access. Use load balancers (Octavia, HAProxy) with public IPs for web traffic instead of direct instance exposure. Apply least-privilege security groups. Enable flow logs. Use cloud NAT gateways for outbound access without inbound exposure.", + "Url": "https://hub.prowler.com/check/compute_instance_public_ip_exposed" + } + }, + "Categories": [ + "internet-exposed", + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check flags instances with any public IP (floating IP, access IP v4/v6). Some workloads legitimately require public IPs (bastion hosts, NAT gateways, public-facing load balancers). Review findings in context of architecture requirements. Private RFC1918 IPs (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) are not flagged." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.py b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.py new file mode 100644 index 0000000000..dfc37075cf --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed.py @@ -0,0 +1,62 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client +from prowler.providers.openstack.services.compute.lib.ip import is_public_ip + + +class compute_instance_public_ip_exposed(Check): + """Ensure compute instances are not exposed to the internet via public IP addresses.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + # Collect all potential public IP indicators + public_ips = [] + # Check SDK computed properties + if instance.public_v4: + public_ips.append(f"Public IPv4: {instance.public_v4}") + if instance.public_v6: + public_ips.append(f"Public IPv6: {instance.public_v6}") + if instance.access_ipv4: + public_ips.append(f"Access IPv4: {instance.access_ipv4}") + if instance.access_ipv6: + public_ips.append(f"Access IPv6: {instance.access_ipv6}") + + # Check networks for any additional public IPs (beyond first one captured in SDK attributes) + # This handles cases where instances have multiple public IPs on different networks + sdk_ips = { + instance.public_v4, + instance.public_v6, + instance.access_ipv4, + instance.access_ipv6, + } + for network_name, ip_list in instance.networks.items(): + for ip in ip_list: + # Check if IP is public and not already captured in SDK attributes + if is_public_ip(ip) and ip not in sdk_ips: + public_ips.append( + f"{network_name} network: {ip} (public range)" + ) + + # Remove duplicates while preserving order + seen = set() + unique_public_ips = [] + for ip in public_ips: + if ip not in seen: + seen.add(ip) + unique_public_ips.append(ip) + + if not unique_public_ips: + report.status = "PASS" + report.status_extended = f"Instance {instance.name} ({instance.id}) is not exposed to the internet (no public IP addresses or external network attachments detected)." + else: + report.status = "FAIL" + ip_list = ", ".join(unique_public_ips) + report.status_extended = f"Instance {instance.name} ({instance.id}) is exposed to the internet with public IP addresses: {ip_list}." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json index 1301c26b1d..95ba7d46fd 100644 --- a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json +++ b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.metadata.json @@ -10,23 +10,21 @@ "ResourceType": "OpenStackInstance", "ResourceGroup": "compute", "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that at least one **security group** is attached. Security groups act as virtual firewalls, controlling ingress and egress traffic. Instances without security groups may have **unrestricted network access**, violating defense-in-depth principles.", - "Risk": "Compute instances without security groups are exposed to unrestricted network traffic, enabling:\n- **Confidentiality**: unauthorized access to services and data exfiltration\n- **Integrity**: injection attacks, tampering, and lateral movement across workloads\n- **Availability**: DDoS, port scanning, and resource exhaustion\nAttackers can probe open ports, exploit vulnerable services, and establish persistence without firewall barriers.", + "Risk": "Instances without security groups are exposed to unrestricted network traffic from any source. Attackers can probe open ports, exploit vulnerable services, conduct injection attacks, and tamper with data without firewall barriers. Lack of network access controls enables unauthorized access, data exfiltration, lateral movement, DDoS attacks, and resource exhaustion.", "RelatedUrl": "", "AdditionalURLs": [ "https://docs.openstack.org/nova/latest/user/security-groups.html", - "https://docs.openstack.org/api-ref/compute/", - "https://docs.openstack.org/neutron/latest/admin/intro-os-networking.html", "https://docs.openstack.org/security-guide/networking/architecture.html" ], "Remediation": { "Code": { "CLI": "openstack server add security group ", "NativeIaC": "", - "Other": "1. Access the Horizon dashboard\n2. Navigate to **Compute > Instances**\n3. Locate instances without security groups (check the Security Groups column)\n4. Select the instance, then **Actions > Edit Security Groups**\n5. Add at least one security group (e.g., `default` or a custom group with least-privilege rules)\n6. Click **Save** and verify the attachment\n7. Test connectivity to ensure proper ingress/egress rules", + "Other": "1. Navigate to **Compute > Instances**\n2. Select instance without security groups\n3. Click **Actions > Edit Security Groups**\n4. Add at least one security group\n5. Click **Save**", "Terraform": "```hcl\n# Terraform: ensure compute instance has security groups attached\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"example-instance\"\n image_id = var.image_id\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n # Critical: attach at least one security group\n security_groups = [\"default\", \"web-sg\", \"app-sg\"]\n\n network {\n name = var.network_name\n }\n}\n```" }, "Recommendation": { - "Text": "Apply **defense-in-depth** for network security:\n- Attach **security groups** to all instances; never deploy without firewall rules\n- Follow **least privilege**: allow only required ports and sources; deny all by default\n- Use **separate security groups** per tier (web, app, database) with explicit rules\n- Avoid overly permissive rules like `0.0.0.0/0` ingress on sensitive ports\n- Implement **network segmentation** with isolated networks or VLANs\n- Enable **flow logs** for traffic analysis and anomaly detection\n- Regularly **audit** security group rules and remove stale or overly broad permissions", + "Text": "Attach security groups to all instances following least privilege principle: allow only required ports and sources, deny all by default. Use separate security groups per tier (web, app, database) with explicit rules. Avoid overly permissive rules like 0.0.0.0/0 ingress on sensitive ports. Implement network segmentation with isolated networks. Regularly audit and remove stale rules.", "Url": "https://hub.prowler.com/check/compute_instance_security_groups_attached" } }, diff --git a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py index aa353883e4..70a4a0768f 100644 --- a/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py +++ b/prowler/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached.py @@ -12,23 +12,13 @@ class compute_instance_security_groups_attached(Check): for instance in compute_client.instances: report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) - report.resource_id = instance.id - report.resource_name = instance.name - report.region = instance.region - - if instance.security_groups and len(instance.security_groups) > 0: + if instance.security_groups: report.status = "PASS" sg_names = ", ".join(instance.security_groups) - report.status_extended = ( - f"Instance {instance.name} ({instance.id}) has security groups " - f"attached: {sg_names}." - ) + report.status_extended = f"Instance {instance.name} ({instance.id}) has security groups attached: {sg_names}." else: report.status = "FAIL" - report.status_extended = ( - f"Instance {instance.name} ({instance.id}) does not have any " - f"security groups attached." - ) + report.status_extended = f"Instance {instance.name} ({instance.id}) does not have any security groups attached." findings.append(report) diff --git a/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/__init__.py b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.metadata.json b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.metadata.json new file mode 100644 index 0000000000..360dbdc759 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.metadata.json @@ -0,0 +1,39 @@ +{ + "Provider": "openstack", + "CheckID": "compute_instance_trusted_image_certificates", + "CheckTitle": "Compute instances use trusted image certificates", + "CheckType": [], + "ServiceName": "compute", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "OpenStackInstance", + "ResourceGroup": "compute", + "Description": "**OpenStack compute instances** (VMs) are evaluated to verify that **trusted image certificates** are configured. Trusted image certificates enable cryptographic validation of image signatures using Glance image signing (OpenStack Image Signature Verification). This ensures instances are launched from verified, untampered images signed by trusted authorities.", + "Risk": "Instances without trusted certificates can be launched from tampered images containing backdoors, rootkits, or malware. Attackers can inject malicious code into unsigned images, and without signature verification, Nova launches compromised images. Malicious images enable persistence, lateral movement, data exfiltration, service disruption, and cryptomining.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.openstack.org/glance/latest/user/signature.html", + "https://docs.openstack.org/nova/latest/admin/secure-boot.html", + "https://docs.openstack.org/api-ref/image/v2/index.html#image-signature-verification" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Enable Glance image signature verification in glance-api.conf\n2. Install Barbican for certificate storage\n3. Sign images using glance-manage or GPG tooling\n4. Upload certificates to Barbican\n5. Launch instances with trusted-image-certificate-id parameter\n\nNote: This requires administrator privileges and Glance/Nova configuration", + "Terraform": "```hcl\n# Terraform: launch instance with trusted image certificates\n\n# Note: Requires Glance image with signature metadata and Barbican certificate\n\nresource \"openstack_compute_instance_v2\" \"instance\" {\n name = \"secure-instance\"\n image_id = var.signed_image_id # Must be signed image\n flavor_id = var.flavor_id\n key_pair = var.key_pair_name\n security_groups = [\"default\"]\n\n # Specify trusted certificate IDs for signature validation\n # Note: This requires Nova microversion 2.63+ support\n trusted_image_certificates = [\n var.image_signing_cert_id,\n var.backup_cert_id\n ]\n\n network {\n name = var.network_name\n }\n}\n\n# Ensure image has signature metadata\ndata \"openstack_images_image_v2\" \"signed_image\" {\n name = \"ubuntu-22.04-signed\"\n most_recent = true\n\n # Verify signature properties exist\n properties = {\n img_signature = \"required\"\n img_signature_hash_method = \"SHA-256\"\n img_signature_key_type = \"RSA-PSS\"\n img_signature_certificate_uuid = \"required\"\n }\n}\n```" + }, + "Recommendation": { + "Text": "Sign all images using GPG or X.509 certificates before uploading to Glance. Use Barbican to store signing certificates securely. Enforce trusted_image_certificates for production instances via policy. Enable signature verification in Nova (verify_glance_signatures=True). Restrict image upload permissions to authorized CI/CD pipelines. Implement certificate rotation every 12 months.", + "Url": "https://hub.prowler.com/check/compute_instance_trusted_image_certificates" + } + }, + "Categories": [ + "trust-boundaries", + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Trusted image certificates require OpenStack Glance image signature verification (microversion 2.63+), Barbican for certificate storage, and Nova configured to verify signatures. Many OpenStack deployments do not enable this feature by default. This check may produce false positives for clouds not using image signing." +} diff --git a/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.py b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.py new file mode 100644 index 0000000000..60dab86a24 --- /dev/null +++ b/prowler/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates.py @@ -0,0 +1,25 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportOpenStack +from prowler.providers.openstack.services.compute.compute_client import compute_client + + +class compute_instance_trusted_image_certificates(Check): + """Ensure compute instances use trusted image certificates for image signature validation.""" + + def execute(self) -> List[CheckReportOpenStack]: + findings: List[CheckReportOpenStack] = [] + + for instance in compute_client.instances: + report = CheckReportOpenStack(metadata=self.metadata(), resource=instance) + if instance.trusted_image_certificates: + report.status = "PASS" + cert_ids = ", ".join(instance.trusted_image_certificates) + report.status_extended = f"Instance {instance.name} ({instance.id}) uses trusted image certificates: {cert_ids}." + else: + report.status = "FAIL" + report.status_extended = f"Instance {instance.name} ({instance.id}) does not use trusted image certificates (image signature validation not enforced)." + + findings.append(report) + + return findings diff --git a/prowler/providers/openstack/services/compute/compute_service.py b/prowler/providers/openstack/services/compute/compute_service.py index 8a372151ef..65233930b4 100644 --- a/prowler/providers/openstack/services/compute/compute_service.py +++ b/prowler/providers/openstack/services/compute/compute_service.py @@ -1,7 +1,8 @@ from __future__ import annotations +import ipaddress from dataclasses import dataclass -from typing import List +from typing import Dict, List from openstack import exceptions as openstack_exceptions @@ -27,8 +28,72 @@ class Compute(OpenStackService): sg_list = getattr(server, "security_groups", None) or [] security_groups = [sg.get("name", "") for sg in sg_list] + # Extract network information from addresses + networks_dict = {} + addresses_attr = getattr(server, "addresses", None) + if addresses_attr: + for net_name, addr_list in addresses_attr.items(): + # addr_list is a list of dicts like: + # [{'version': 4, 'addr': '57.128.163.151', 'OS-EXT-IPS:type': 'fixed'}] + ip_list = [] + if isinstance(addr_list, list): + for addr_dict in addr_list: + if isinstance(addr_dict, dict) and "addr" in addr_dict: + ip_list.append(addr_dict["addr"]) + elif isinstance(addr_dict, str): + # Fallback: if it's just a string IP + ip_list.append(addr_dict) + elif isinstance(addr_list, str): + # Fallback: single string IP + ip_list = [addr_list] + networks_dict[net_name] = ip_list + + # Extract trusted image certificates + trusted_certs = ( + getattr(server, "trusted_image_certificates", None) or [] + ) + + # Get SDK computed properties + public_v4 = getattr(server, "public_v4", "") + public_v6 = getattr(server, "public_v6", "") + private_v4 = getattr(server, "private_v4", "") + private_v6 = getattr(server, "private_v6", "") + + # Fallback: If SDK attributes are not populated, classify IPs from networks + # This handles clouds where SDK computed properties are not available + if ( + not (public_v4 or public_v6 or private_v4 or private_v6) + and networks_dict + ): + for network_name, ip_list in networks_dict.items(): + for ip_str in ip_list: + try: + ip_obj = ipaddress.ip_address(ip_str) + # Classify as private or public + if ip_obj.is_private: + # Assign first private IP found to appropriate field + if ip_obj.version == 4 and not private_v4: + private_v4 = ip_str + elif ip_obj.version == 6 and not private_v6: + private_v6 = ip_str + elif not ( + ip_obj.is_loopback + or ip_obj.is_link_local + or ip_obj.is_reserved + or ip_obj.is_multicast + ): + # Assign first public IP found to appropriate field + if ip_obj.version == 4 and not public_v4: + public_v4 = ip_str + elif ip_obj.version == 6 and not public_v6: + public_v6 = ip_str + except ValueError: + # Invalid IP address, skip + continue + self.instances.append( ComputeInstance( + # Basic instance information id=getattr(server, "id", ""), name=getattr(server, "name", ""), status=getattr(server, "status", ""), @@ -36,6 +101,27 @@ class Compute(OpenStackService): security_groups=security_groups, region=self.region, project_id=self.project_id, + # Access Control & Authentication + is_locked=getattr(server, "is_locked", False), + locked_reason=getattr(server, "locked_reason", ""), + key_name=getattr(server, "key_name", ""), + user_id=getattr(server, "user_id", ""), + # Network Exposure + access_ipv4=getattr(server, "access_ipv4", ""), + access_ipv6=getattr(server, "access_ipv6", ""), + public_v4=public_v4, + public_v6=public_v6, + private_v4=private_v4, + private_v6=private_v6, + networks=networks_dict, + # Configuration Security + has_config_drive=getattr(server, "has_config_drive", False), + metadata=getattr(server, "metadata", {}), + user_data=getattr(server, "user_data", ""), + # Image Trust + trusted_image_certificates=( + trusted_certs if isinstance(trusted_certs, list) else [] + ), ) ) except openstack_exceptions.SDKException as error: @@ -54,6 +140,7 @@ class Compute(OpenStackService): class ComputeInstance: """Represents an OpenStack compute instance (VM).""" + # Basic instance information id: str name: str status: str @@ -61,3 +148,26 @@ class ComputeInstance: security_groups: List[str] region: str project_id: str + + # Access Control & Authentication + is_locked: bool + locked_reason: str + key_name: str + user_id: str + + # Network Exposure + access_ipv4: str + access_ipv6: str + public_v4: str + public_v6: str + private_v4: str + private_v6: str + networks: Dict[str, List[str]] + + # Configuration Security + has_config_drive: bool + metadata: Dict[str, str] + user_data: str + + # Image Trust + trusted_image_certificates: List[str] diff --git a/prowler/providers/openstack/services/compute/lib/__init__.py b/prowler/providers/openstack/services/compute/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/openstack/services/compute/lib/ip.py b/prowler/providers/openstack/services/compute/lib/ip.py new file mode 100644 index 0000000000..5f73466284 --- /dev/null +++ b/prowler/providers/openstack/services/compute/lib/ip.py @@ -0,0 +1,10 @@ +import ipaddress + + +def is_public_ip(ip_str: str) -> bool: + """Check if an IP address is public (globally routable, non-multicast).""" + try: + ip = ipaddress.ip_address(ip_str) + return ip.is_global and not ip.is_multicast + except ValueError: + return False diff --git a/prowler/providers/oraclecloud/config.py b/prowler/providers/oraclecloud/config.py index 95392e6b8e..902227e77c 100644 --- a/prowler/providers/oraclecloud/config.py +++ b/prowler/providers/oraclecloud/config.py @@ -13,42 +13,49 @@ OCI_USER_AGENT = "Prowler" # OCI Regions - Commercial Regions OCI_COMMERCIAL_REGIONS = { - "ap-chuncheon-1": "South Korea Central (Chuncheon)", - "ap-hyderabad-1": "India West (Hyderabad)", - "ap-melbourne-1": "Australia Southeast (Melbourne)", - "ap-mumbai-1": "India West (Mumbai)", - "ap-osaka-1": "Japan Central (Osaka)", - "ap-seoul-1": "South Korea North (Seoul)", - "ap-singapore-1": "Singapore (Singapore)", - "ap-sydney-1": "Australia East (Sydney)", - "ap-tokyo-1": "Japan East (Tokyo)", - "ca-montreal-1": "Canada Southeast (Montreal)", - "ca-toronto-1": "Canada Southeast (Toronto)", - "eu-amsterdam-1": "Netherlands Northwest (Amsterdam)", - "eu-frankfurt-1": "Germany Central (Frankfurt)", - "eu-madrid-1": "Spain Central (Madrid)", - "eu-marseille-1": "France South (Marseille)", - "eu-milan-1": "Italy Northwest (Milan)", - "eu-paris-1": "France Central (Paris)", - "eu-stockholm-1": "Sweden Central (Stockholm)", - "eu-zurich-1": "Switzerland North (Zurich)", - "il-jerusalem-1": "Israel Central (Jerusalem)", - "me-abudhabi-1": "UAE East (Abu Dhabi)", - "me-dubai-1": "UAE East (Dubai)", - "me-jeddah-1": "Saudi Arabia West (Jeddah)", - "mx-monterrey-1": "Mexico Northeast (Monterrey)", - "mx-queretaro-1": "Mexico Central (Queretaro)", - "sa-bogota-1": "Colombia (Bogota)", - "sa-santiago-1": "Chile (Santiago)", - "sa-saopaulo-1": "Brazil East (Sao Paulo)", - "sa-valparaiso-1": "Chile West (Valparaiso)", - "sa-vinhedo-1": "Brazil Southeast (Vinhedo)", - "uk-cardiff-1": "UK West (Cardiff)", - "uk-london-1": "UK South (London)", - "us-ashburn-1": "US East (Ashburn)", - "us-chicago-1": "US East (Chicago)", - "us-phoenix-1": "US West (Phoenix)", - "us-sanjose-1": "US West (San Jose)", + "af-johannesburg-1": "af-johannesburg-1", + "ap-batam-1": "ap-batam-1", + "ap-chuncheon-1": "ap-chuncheon-1", + "ap-hyderabad-1": "ap-hyderabad-1", + "ap-kulai-2": "ap-kulai-2", + "ap-melbourne-1": "ap-melbourne-1", + "ap-mumbai-1": "ap-mumbai-1", + "ap-osaka-1": "ap-osaka-1", + "ap-seoul-1": "ap-seoul-1", + "ap-singapore-1": "ap-singapore-1", + "ap-singapore-2": "ap-singapore-2", + "ap-sydney-1": "ap-sydney-1", + "ap-tokyo-1": "ap-tokyo-1", + "ca-montreal-1": "ca-montreal-1", + "ca-toronto-1": "ca-toronto-1", + "eu-amsterdam-1": "eu-amsterdam-1", + "eu-frankfurt-1": "eu-frankfurt-1", + "eu-madrid-1": "eu-madrid-1", + "eu-madrid-3": "eu-madrid-3", + "eu-marseille-1": "eu-marseille-1", + "eu-milan-1": "eu-milan-1", + "eu-paris-1": "eu-paris-1", + "eu-stockholm-1": "eu-stockholm-1", + "eu-turin-1": "eu-turin-1", + "eu-zurich-1": "eu-zurich-1", + "il-jerusalem-1": "il-jerusalem-1", + "me-abudhabi-1": "me-abudhabi-1", + "me-dubai-1": "me-dubai-1", + "me-jeddah-1": "me-jeddah-1", + "me-riyadh-1": "me-riyadh-1", + "mx-monterrey-1": "mx-monterrey-1", + "mx-queretaro-1": "mx-queretaro-1", + "sa-bogota-1": "sa-bogota-1", + "sa-santiago-1": "sa-santiago-1", + "sa-saopaulo-1": "sa-saopaulo-1", + "sa-valparaiso-1": "sa-valparaiso-1", + "sa-vinhedo-1": "sa-vinhedo-1", + "uk-cardiff-1": "uk-cardiff-1", + "uk-london-1": "uk-london-1", + "us-ashburn-1": "us-ashburn-1", + "us-chicago-1": "us-chicago-1", + "us-phoenix-1": "us-phoenix-1", + "us-sanjose-1": "us-sanjose-1", } # OCI Government Regions diff --git a/skills/prowler-readme-table/SKILL.md b/skills/prowler-readme-table/SKILL.md new file mode 100644 index 0000000000..d6f9c6e780 --- /dev/null +++ b/skills/prowler-readme-table/SKILL.md @@ -0,0 +1,110 @@ +--- +name: prowler-readme-table +description: > + Updates the "Prowler at a Glance" table in README.md with accurate provider statistics. + Trigger: When updating README.md provider stats, checks count, services count, compliance frameworks, or categories. +license: Apache-2.0 +metadata: + author: prowler-cloud + version: "1.0" + scope: [root] + auto_invoke: + - "Updating README.md provider statistics table" + - "Updating checks, services, compliance, or categories count in README.md" +allowed-tools: Read, Edit, Bash, Glob, Grep +--- + +## When to Use + +Use this skill when updating the **Prowler at a Glance** table in the root `README.md`. This table tracks the number of checks, services, compliance frameworks, and categories for each supported provider. + +## Procedure + +### Step 1: Collect Stats via CLI + +Run the following command for **each provider** and **each metric**: + +```bash +python3 prowler-cli.py --list- +``` + +**Providers:** `aws`, `azure`, `gcp`, `kubernetes`, `github`, `m365`, `oraclecloud`, `alibabacloud`, `cloudflare`, `mongodbatlas`, `openstack`, `nhn` + +**Metrics:** `checks`, `services`, `compliance`, `categories` + +The CLI output ends with a summary line like: + +``` +There are 572 available checks. +There is 1 available Compliance Framework. +``` + +Extract the number from the summary line. Note that singular results use "There is" instead of "There are". + +### Step 2: Batch Extraction + +Use this one-liner to collect all stats at once (handles both singular and plural output): + +```bash +for provider in aws azure gcp kubernetes github m365 oraclecloud alibabacloud cloudflare mongodbatlas openstack nhn; do + for metric in checks services compliance categories; do + result=$(python3 prowler-cli.py $provider --list-$metric 2>&1 | sed -n 's/.*There \(are\|is\) .*\x1b\[33m\([0-9]*\)\x1b\[0m.*/\2/p') + echo "$provider $metric: $result" + done +done +``` + +### Step 3: Update the Table + +Edit the table in `README.md` (located in the `# Prowler at a Glance` section) with the collected numbers. + +**Table format:** + +```markdown +| Provider | Checks | Services | [Compliance Frameworks](...) | [Categories](...) | Support | Interface | +|---|---|---|---|---|---|---| +| AWS | 572 | 83 | 41 | 17 | Official | UI, API, CLI | +``` + +### Provider Name Mapping + +| CLI Provider | Table Display Name | +|---|---| +| `aws` | AWS | +| `azure` | Azure | +| `gcp` | GCP | +| `kubernetes` | Kubernetes | +| `github` | GitHub | +| `m365` | M365 | +| `oraclecloud` | OCI | +| `alibabacloud` | Alibaba Cloud | +| `cloudflare` | Cloudflare | +| `mongodbatlas` | MongoDB Atlas | +| `openstack` | OpenStack | +| `nhn` | NHN | + +### Special Rows (No CLI stats) + +These providers delegate to external tools and do NOT use CLI stats: + +| Provider | Checks Column | Services | Compliance | Categories | +|---|---|---|---|---| +| IaC | `[See trivy docs.](https://trivy.dev/latest/docs/coverage/iac/)` | N/A | N/A | N/A | +| LLM | `[See promptfoo docs.](https://www.promptfoo.dev/docs/red-team/plugins/)` | N/A | N/A | N/A | + +### Support and Interface Columns + +- **Support**: `Official` for all providers except `NHN` which is `Unofficial` +- **Interface**: Most providers use `UI, API, CLI`. Exceptions with `CLI` only: `Cloudflare`, `OpenStack`, `NHN`, `LLM` + +## Rules + +- **ALWAYS** use the CLI (`python3 prowler-cli.py`) to obtain numbers. Do NOT count files manually. +- **NEVER** commit changes unless explicitly asked. +- **NEVER** modify the IaC or LLM rows (they link to external docs). +- Verify the CLI is working by running one provider first before batch-processing all. + +## Resources + +- **CLI entry point**: `prowler-cli.py` in the repository root +- **Table location**: `README.md`, section `# Prowler at a Glance` (around line 100) diff --git a/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py b/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py index c8abdaaf5d..1637d1344e 100644 --- a/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible_test.py @@ -129,6 +129,116 @@ class Test_kms_key_not_publicly_accessible: assert result[0].resource_id == key["KeyId"] assert result[0].resource_arn == key["Arn"] + @mock_aws + def test_kms_key_public_accessible_with_describe_key(self): + # Generate KMS Client + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + # Create KMS key with public policy allowing kms:DescribeKey + key = kms_client.create_key( + MultiRegion=False, + Policy=json.dumps( + { + "Version": "2012-10-17", + "Id": "key-default-1", + "Statement": [ + { + "Sid": "AllowDescribeKeyPermissionForClusterOperator", + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "kms:DescribeKey", + "Resource": "*", + } + ], + } + ), + )["KeyMetadata"] + + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible.kms_client", + new=KMS(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible import ( + kms_key_not_publicly_accessible, + ) + + check = kms_key_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"KMS key {key['KeyId']} may be publicly accessible." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] + + @mock_aws + def test_kms_key_public_accessible_with_decrypt(self): + # Generate KMS Client + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + # Create KMS key with public policy allowing kms:Decrypt + key = kms_client.create_key( + MultiRegion=False, + Policy=json.dumps( + { + "Version": "2012-10-17", + "Id": "key-default-1", + "Statement": [ + { + "Sid": "AllowDecryptPermissionPublicly", + "Effect": "Allow", + "Principal": "*", + "Action": "kms:Decrypt", + "Resource": "*", + } + ], + } + ), + )["KeyMetadata"] + + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible.kms_client", + new=KMS(aws_provider), + ), + ): + # Test Check + from prowler.providers.aws.services.kms.kms_key_not_publicly_accessible.kms_key_not_publicly_accessible import ( + kms_key_not_publicly_accessible, + ) + + check = kms_key_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"KMS key {key['KeyId']} may be publicly accessible." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] + @mock_aws def test_kms_key_empty_principal(self): # Generate KMS Client diff --git a/tests/providers/github/services/organization/organization_verified_badge/organization_verified_badge_test.py b/tests/providers/github/services/organization/organization_verified_badge/organization_verified_badge_test.py new file mode 100644 index 0000000000..5bea0aa034 --- /dev/null +++ b/tests/providers/github/services/organization/organization_verified_badge/organization_verified_badge_test.py @@ -0,0 +1,137 @@ +from unittest import mock + +from prowler.providers.github.services.organization.organization_service import Org +from tests.providers.github.github_fixtures import set_mocked_github_provider + + +class Test_organization_verified_badge: + def test_no_organizations(self): + organization_client = mock.MagicMock + organization_client.organizations = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 0 + + def test_organization_is_verified_true_pass(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + is_verified=True, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == org_name + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Organization {org_name} is verified on GitHub." + ) + + def test_organization_is_verified_false_fail(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + is_verified=False, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == org_name + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Organization {org_name} is not verified on GitHub." + ) + + def test_organization_is_verified_none_edge_case(self): + organization_client = mock.MagicMock + org_name = "test-organization" + organization_client.organizations = { + 1: Org( + id=1, + name=org_name, + is_verified=None, + ), + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_github_provider(), + ), + mock.patch( + "prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge.organization_client", + new=organization_client, + ), + ): + from prowler.providers.github.services.organization.organization_verified_badge.organization_verified_badge import ( + organization_verified_badge, + ) + + check = organization_verified_badge() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_id == 1 + assert result[0].resource_name == org_name + # Treat none like not verified (false) + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Organization {org_name} is not verified on GitHub." + ) diff --git a/tests/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled_test.py b/tests/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled_test.py new file mode 100644 index 0000000000..3f94d40c18 --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_config_drive_enabled/compute_instance_config_drive_enabled_test.py @@ -0,0 +1,229 @@ +"""Tests for compute_instance_config_drive_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_config_drive_enabled: + """Test suite for compute_instance_config_drive_enabled check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_instance_with_config_drive(self): + """Test instance with config drive enabled (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="ConfigDrive Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=True, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance ConfigDrive Instance (instance-1) has config drive enabled for secure metadata injection." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "ConfigDrive Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_without_config_drive(self): + """Test instance without config drive (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="No ConfigDrive", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance No ConfigDrive (instance-2) does not have config drive enabled (relies on metadata service)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "No ConfigDrive" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed config drive status.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Pass", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=True, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-fail", + name="Fail", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_config_drive_enabled.compute_instance_config_drive_enabled import ( + compute_instance_config_drive_enabled, + ) + + check = compute_instance_config_drive_enabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network_test.py b/tests/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network_test.py new file mode 100644 index 0000000000..89831080ed --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_isolated_private_network/compute_instance_isolated_private_network_test.py @@ -0,0 +1,601 @@ +"""Tests for compute_instance_isolated_private_network check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_isolated_private_network: + """Test suite for compute_instance_isolated_private_network check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 0 + + def test_instance_private_only(self): + """Test instance with private IP only (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Isolated Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Isolated Instance (instance-1) is properly isolated in private network with private IPs (10.0.0.5) and no public exposure." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Isolated Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_mixed_public_private(self): + """Test instance with both public and private IPs (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Mixed Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.4.4", + public_v6="", + private_v4="10.0.0.10", + private_v6="", + networks={"public": ["8.8.4.4"], "private": ["10.0.0.10"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Mixed Instance (instance-2) has mixed public and private network exposure (not properly isolated)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Mixed Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_public_only(self): + """Test instance with only public IP (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Public Only", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="1.1.1.1", + public_v6="", + private_v4="", + private_v6="", + networks={"public": ["1.1.1.1"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Public Only (instance-3) has only public IP addresses (no private network isolation)." + ) + assert result[0].resource_id == "instance-3" + assert result[0].resource_name == "Public Only" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_no_ips(self): + """Test instance with no IPs (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-4", + name="No IPs", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance No IPs (instance-4) has no network configuration (no IPs assigned)." + ) + assert result[0].resource_id == "instance-4" + assert result[0].resource_name == "No IPs" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_private_ipv6_only(self): + """Test instance with private IPv6 only (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-5", + name="IPv6 Private", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="fd00::1", + networks={"private": ["fd00::1"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance IPv6 Private (instance-5) is properly isolated in private network with private IPs (fd00::1) and no public exposure." + ) + assert result[0].resource_id == "instance-5" + assert result[0].resource_name == "IPv6 Private" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_fallback_private_only_networks_dict(self): + """Test fallback logic: instance with private IP populated by service from networks dict.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-fallback-1", + name="Private Fallback", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", # Empty + access_ipv6="", # Empty + public_v4="", # Empty + public_v6="", # Empty + private_v4="10.99.1.207", # Populated by service fallback + private_v6="", # Empty + networks={"test-private-net": ["10.99.1.207"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "10.99.1.207" in result[0].status_extended + assert "properly isolated in private network" in result[0].status_extended + assert result[0].resource_id == "instance-fallback-1" + + def test_instance_fallback_public_only_networks_dict(self): + """Test fallback logic: instance with public IP populated by service from networks dict.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-fallback-2", + name="Public Fallback", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # Populated by service fallback + public_v6="", # Empty + private_v4="", + private_v6="", + networks={"ext-net": ["8.8.8.8"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "only public IP addresses" in result[0].status_extended + or "no private network isolation" in result[0].status_extended + ) + assert result[0].resource_id == "instance-fallback-2" + + def test_instance_fallback_mixed_networks_dict(self): + """Test fallback logic: instance with mixed IPs populated by service from networks dict.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-fallback-3", + name="Mixed Fallback", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # Populated by service fallback + public_v6="", # Empty + private_v4="10.0.0.100", # Populated by service fallback + private_v6="", # Empty + networks={ + "private-net": ["10.0.0.100"], + "ext-net": ["8.8.8.8"], + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "mixed public and private network exposure" in result[0].status_extended + ) + assert result[0].resource_id == "instance-fallback-3" + + def test_instance_access_ipv4_private_treated_as_private(self): + """Test that access_ipv4 set to a private IP is not treated as public exposure.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-access-priv", + name="Access Private", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="10.0.0.50", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.50", + private_v6="", + networks={"private-net": ["10.0.0.50"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert "properly isolated in private network" in result[0].status_extended + assert result[0].resource_id == "instance-access-priv" + + def test_instance_network_ips_validated_as_public(self): + """Test that IPs from networks dict are validated as truly public.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-net-pub", + name="Network Public", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={ + "my-net": ["10.0.0.5", "8.8.8.8"], + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_isolated_private_network.compute_instance_isolated_private_network import ( + compute_instance_isolated_private_network, + ) + + check = compute_instance_isolated_private_network() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + "mixed public and private network exposure" in result[0].status_extended + ) + assert result[0].resource_id == "instance-net-pub" diff --git a/tests/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication_test.py b/tests/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication_test.py new file mode 100644 index 0000000000..b2e2c83edd --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_key_based_authentication/compute_instance_key_based_authentication_test.py @@ -0,0 +1,229 @@ +"""Tests for compute_instance_key_based_authentication check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_key_based_authentication: + """Test suite for compute_instance_key_based_authentication check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 0 + + def test_instance_with_keypair(self): + """Test instance with SSH keypair configured (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Secure Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="my-production-keypair", + user_id="user-123", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Secure Instance (instance-1) is configured with SSH key-based authentication (keypair: my-production-keypair)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Secure Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_without_keypair(self): + """Test instance without SSH keypair (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Insecure Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="user-456", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.10", + private_v6="", + networks={"private": ["10.0.0.10"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Insecure Instance (instance-2) does not have SSH key-based authentication configured (no keypair assigned)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Insecure Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed keypair configuration.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-secure", + name="With Key", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="prod-keypair", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-insecure", + name="Without Key", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_key_based_authentication.compute_instance_key_based_authentication import ( + compute_instance_key_based_authentication, + ) + + check = compute_instance_key_based_authentication() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled_test.py b/tests/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled_test.py new file mode 100644 index 0000000000..8f52eb761e --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_locked_status_enabled/compute_instance_locked_status_enabled_test.py @@ -0,0 +1,287 @@ +"""Tests for compute_instance_locked_status_enabled check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_locked_status_enabled: + """Test suite for compute_instance_locked_status_enabled check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_instance_locked_with_reason(self): + """Test instance with locked status enabled and reason (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Locked Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=True, + locked_reason="Production instance - do not modify", + key_name="my-keypair", + user_id="user-123", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Locked Instance (instance-1) has locked status enabled (reason: Production instance - do not modify)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Locked Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_locked_without_reason(self): + """Test instance with locked status enabled but no reason (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Locked No Reason", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=True, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Locked No Reason (instance-2) has locked status enabled." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Locked No Reason" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_not_locked(self): + """Test instance without locked status (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Unlocked Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Unlocked Instance (instance-3) does not have locked status enabled." + ) + assert result[0].resource_id == "instance-3" + assert result[0].resource_name == "Unlocked Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed locked status.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-locked", + name="Locked", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=True, + locked_reason="Protected", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-unlocked", + name="Unlocked", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_locked_status_enabled.compute_instance_locked_status_enabled import ( + compute_instance_locked_status_enabled, + ) + + check = compute_instance_locked_status_enabled() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data_test.py b/tests/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data_test.py new file mode 100644 index 0000000000..174a8ab83f --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_metadata_sensitive_data/compute_instance_metadata_sensitive_data_test.py @@ -0,0 +1,576 @@ +"""Tests for compute_instance_metadata_sensitive_data check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_metadata_sensitive_data: + """Test suite for compute_instance_metadata_sensitive_data check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + compute_client.audit_config = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 0 + + def test_instance_no_metadata(self): + """Test instance with no metadata (PASS).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="No Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance No Metadata (instance-1) has no metadata (no sensitive data exposure risk)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "No Metadata" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_safe_metadata(self): + """Test instance with safe metadata (PASS).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Safe Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"environment": "production", "application": "web-app"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Safe Metadata (instance-2) metadata does not contain sensitive data." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Safe Metadata" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_password_in_metadata(self): + """Test instance with password in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Password Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"db_password": "supersecret123"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "contains potential secrets" in result[0].status_extended + + def test_instance_api_key_in_metadata(self): + """Test instance with API key in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-4", + name="API Key Metadata", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"api_key": "sk-1234567890"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance API Key Metadata (instance-4) metadata contains potential secrets ->" + ) + assert result[0].resource_id == "instance-4" + assert result[0].resource_name == "API Key Metadata" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_connection_string_in_metadata(self): + """Test instance with database connection string in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-5", + name="Connection String", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"db_url": "mysql://admin:s3cret@dbhost:3306/appdb"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Connection String (instance-5) metadata contains potential secrets ->" + ) + assert result[0].resource_id == "instance-5" + assert result[0].resource_name == "Connection String" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_private_key_in_metadata(self): + """Test instance with private key in metadata (FAIL).""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-6", + name="Private Key", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"ssh_key": "-----BEGIN RSA PRIVATE KEY-----"}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Private Key (instance-6) metadata contains potential secrets ->" + ) + assert result[0].resource_id == "instance-6" + assert result[0].resource_name == "Private Key" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed metadata.""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Safe", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"tier": "web"}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-fail", + name="Unsafe", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={"admin_password": "secret123"}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 + + def test_instance_multiple_metadata_keys_correct_identification(self): + """Test that secrets are correctly attributed to the right metadata keys.""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-7", + name="Multiple Keys", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={ + "environment": "production", + "application": "web-app", + "db_password": "supersecret123", + "region": "us-east", + }, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Verify the secret is correctly attributed to 'db_password' key + assert "in metadata key 'db_password'" in result[0].status_extended + assert result[0].resource_id == "instance-7" + + def test_instance_metadata_key_ordering(self): + """Test that secret detection works with different key orderings.""" + compute_client = mock.MagicMock() + compute_client.audit_config = {} + compute_client.instances = [ + ComputeInstance( + id="instance-8", + name="Ordered Keys", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={ + "first_key": "safe_value", + "api_key": "sk-1234567890abcdef", + "third_key": "also_safe", + }, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_metadata_sensitive_data.compute_instance_metadata_sensitive_data import ( + compute_instance_metadata_sensitive_data, + ) + + check = compute_instance_metadata_sensitive_data() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Verify the secret is correctly attributed to 'api_key' key (second in order) + assert "in metadata key 'api_key'" in result[0].status_extended + assert result[0].resource_id == "instance-8" diff --git a/tests/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed_test.py b/tests/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed_test.py new file mode 100644 index 0000000000..583a665cfc --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_public_ip_exposed/compute_instance_public_ip_exposed_test.py @@ -0,0 +1,708 @@ +"""Tests for compute_instance_public_ip_exposed check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_public_ip_exposed: + """Test suite for compute_instance_public_ip_exposed check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 0 + + def test_instance_without_public_ip(self): + """Test instance without public IP (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Private Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={"private": ["10.0.0.5"]}, # Processed from addresses + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Private Instance (instance-1) is not exposed to the internet (no public IP addresses or external network attachments detected)." + ) + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Private Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_with_public_ipv4(self): + """Test instance with public IPv4 (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Public Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="203.0.113.10", + public_v6="", + private_v4="10.0.0.10", + private_v6="", + networks={"public": ["203.0.113.10"], "private": ["10.0.0.10"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Public Instance (instance-2) is exposed to the internet with public IP addresses:" + ) + assert "203.0.113.10" in result[0].status_extended + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Public Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_with_access_ipv4(self): + """Test instance with access IPv4 (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-3", + name="Access IP Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="198.51.100.5", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.15", + private_v6="", + networks={"private": ["10.0.0.15"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance Access IP Instance (instance-3) is exposed to the internet with public IP addresses:" + ) + assert "198.51.100.5" in result[0].status_extended + assert result[0].resource_id == "instance-3" + assert result[0].resource_name == "Access IP Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_with_ipv6(self): + """Test instance with public IPv6 (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-4", + name="IPv6 Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="2001:db8::1", + public_v4="", + public_v6="", + private_v4="", + private_v6="fd00::1", + networks={"private": ["fd00::1"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended.startswith( + "Instance IPv6 Instance (instance-4) is exposed to the internet with public IP addresses:" + ) + assert "2001:db8::1" in result[0].status_extended + assert result[0].resource_id == "instance-4" + assert result[0].resource_name == "IPv6 Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed public IP configuration.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Private", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.20", + private_v6="", + networks={"private": ["10.0.0.20"]}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ComputeInstance( + id="instance-fail", + name="Public", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="203.0.113.20", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 + + def test_instance_on_external_network(self): + """Test instance directly attached to external network (OVH-style).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-extnet", + name="ExtNet Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", # SDK might not populate this + public_v6="", + private_v4="", + private_v6="", + networks={ + "Ext-Net": ["57.128.163.151", "2001:41d0:801:1000::164b"] + }, # OVH external network + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "57.128.163.151" in result[0].status_extended + assert "Ext-Net" in result[0].status_extended + assert result[0].resource_id + assert result[0].resource_name + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_mixed_networks_private_and_external(self): + """Test instance with both private and external network attachments.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-mixed", + name="Mixed Networks", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={ + "private-net": ["10.0.0.5"], + "public-network": ["8.8.8.8"], # Real public IP (Google DNS) + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "8.8.8.8" in result[0].status_extended + assert "public-network" in result[0].status_extended + assert result[0].resource_id + assert result[0].resource_name + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_false_positive_network_names(self): + """Test that network names containing 'ext' as substring don't cause false positives.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-context", + name="Context Network Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="10.0.0.100", + private_v6="", + networks={ + "context-internal": [ + "10.0.0.100" + ], # Contains "ext" but should not match + "next-hop": ["10.0.0.101"], # Contains "ext" but should not match + "text-processing": [ + "10.0.0.102" + ], # Contains "ext" but should not match + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Context Network Instance (instance-context) is not exposed to the internet (no public IP addresses or external network attachments detected)." + ) + assert result[0].resource_id == "instance-context" + assert result[0].resource_name == "Context Network Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_word_boundary_ext_network(self): + """Test that 'ext' as a complete word is properly detected.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-ext-word", + name="Ext Word Boundary Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={ + "ext": ["8.8.8.8"], # Word boundary: "ext" alone + "ext-network": ["1.1.1.1"], # Word boundary: "ext" at start + "network-ext": ["9.9.9.9"], # Word boundary: "ext" at end + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Should detect at least one external network with public IP + assert "ext" in result[0].status_extended.lower() + assert result[0].resource_id == "instance-ext-word" + assert result[0].resource_name == "Ext Word Boundary Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_public_ip_generic_network_name(self): + """Test that public IPs are detected regardless of network name (e.g., 'hello').""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-hello", + name="Generic Network Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # Service populates this via fallback + public_v6="", + private_v4="", + private_v6="", + networks={"hello": ["8.8.8.8"]}, # Generic name, but public IP + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "8.8.8.8" in result[0].status_extended + assert result[0].resource_id == "instance-hello" + assert result[0].resource_name == "Generic Network Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_multiple_public_ips_on_different_networks(self): + """Test that multiple public IPs on different networks are all detected.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-multi-ip", + name="Multiple Public IPs", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="8.8.8.8", # First public IP captured by service + public_v6="", + private_v4="", + private_v6="", + networks={ + "network1": ["8.8.8.8"], # First public IP + "network2": ["1.1.1.1"], # Second public IP + "network3": ["9.9.9.9"], # Third public IP + }, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_public_ip_exposed.compute_instance_public_ip_exposed import ( + compute_instance_public_ip_exposed, + ) + + check = compute_instance_public_ip_exposed() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + # Should detect all three public IPs + assert "8.8.8.8" in result[0].status_extended + assert "1.1.1.1" in result[0].status_extended + assert "9.9.9.9" in result[0].status_extended + # Should show network names for additional IPs + assert "network2" in result[0].status_extended + assert "network3" in result[0].status_extended + assert result[0].resource_id == "instance-multi-ip" + assert result[0].resource_name == "Multiple Public IPs" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID diff --git a/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py b/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py index 35636f550e..2ef1f08e6d 100644 --- a/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py +++ b/tests/providers/openstack/services/compute/compute_instance_security_groups_attached/compute_instance_security_groups_attached_test.py @@ -49,6 +49,21 @@ class Test_compute_instance_security_groups_attached: security_groups=["default", "web"], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ) ] @@ -71,11 +86,14 @@ class Test_compute_instance_security_groups_attached: assert len(result) == 1 assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance Instance One (instance-1) has security groups attached: default, web." + ) assert result[0].resource_id == "instance-1" assert result[0].resource_name == "Instance One" assert result[0].region == OPENSTACK_REGION assert result[0].project_id == OPENSTACK_PROJECT_ID - assert "has security groups attached" in result[0].status_extended def test_instance_without_security_groups(self): """Test instance without security groups attached (FAIL).""" @@ -89,6 +107,21 @@ class Test_compute_instance_security_groups_attached: security_groups=[], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ) ] @@ -111,14 +144,14 @@ class Test_compute_instance_security_groups_attached: assert len(result) == 1 assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Instance Two (instance-2) does not have any security groups attached." + ) assert result[0].resource_id == "instance-2" assert result[0].resource_name == "Instance Two" assert result[0].region == OPENSTACK_REGION assert result[0].project_id == OPENSTACK_PROJECT_ID - assert ( - "does not have any security groups attached" - in result[0].status_extended - ) def test_multiple_instances_mixed(self): """Test multiple instances with mixed results.""" @@ -132,6 +165,21 @@ class Test_compute_instance_security_groups_attached: security_groups=["default"], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ), ComputeInstance( id="instance-fail", @@ -141,6 +189,21 @@ class Test_compute_instance_security_groups_attached: security_groups=[], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ), ] @@ -177,6 +240,21 @@ class Test_compute_instance_security_groups_attached: security_groups=["default"], region=OPENSTACK_REGION, project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], ) ] @@ -198,6 +276,12 @@ class Test_compute_instance_security_groups_attached: result = check.execute() assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Instance (instance-3) has security groups attached: default." + ) assert result[0].resource_id == "instance-3" assert result[0].resource_name == "" - assert "instance-3" in result[0].status_extended + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID diff --git a/tests/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates_test.py b/tests/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates_test.py new file mode 100644 index 0000000000..5d299027f8 --- /dev/null +++ b/tests/providers/openstack/services/compute/compute_instance_trusted_image_certificates/compute_instance_trusted_image_certificates_test.py @@ -0,0 +1,229 @@ +"""Tests for compute_instance_trusted_image_certificates check.""" + +from unittest import mock + +from prowler.providers.openstack.services.compute.compute_service import ComputeInstance +from tests.providers.openstack.openstack_fixtures import ( + OPENSTACK_PROJECT_ID, + OPENSTACK_REGION, + set_mocked_openstack_provider, +) + + +class Test_compute_instance_trusted_image_certificates: + """Test suite for compute_instance_trusted_image_certificates check.""" + + def test_no_instances(self): + """Test when no instances exist.""" + compute_client = mock.MagicMock() + compute_client.instances = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 0 + + def test_instance_with_trusted_certificates(self): + """Test instance with trusted image certificates (PASS).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-1", + name="Trusted Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=["cert-123", "cert-456"], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended.startswith( + "Instance Trusted Instance (instance-1) uses trusted image certificates:" + ) + assert "cert-123" in result[0].status_extended + assert result[0].resource_id == "instance-1" + assert result[0].resource_name == "Trusted Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_instance_without_trusted_certificates(self): + """Test instance without trusted image certificates (FAIL).""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-2", + name="Untrusted Instance", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=["default"], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ) + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Instance Untrusted Instance (instance-2) does not use trusted image certificates (image signature validation not enforced)." + ) + assert result[0].resource_id == "instance-2" + assert result[0].resource_name == "Untrusted Instance" + assert result[0].region == OPENSTACK_REGION + assert result[0].project_id == OPENSTACK_PROJECT_ID + + def test_multiple_instances_mixed(self): + """Test multiple instances with mixed certificate configuration.""" + compute_client = mock.MagicMock() + compute_client.instances = [ + ComputeInstance( + id="instance-pass", + name="Pass", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=["cert-789"], + ), + ComputeInstance( + id="instance-fail", + name="Fail", + status="ACTIVE", + flavor_id="flavor-1", + security_groups=[], + region=OPENSTACK_REGION, + project_id=OPENSTACK_PROJECT_ID, + is_locked=False, + locked_reason="", + key_name="", + user_id="", + access_ipv4="", + access_ipv6="", + public_v4="", + public_v6="", + private_v4="", + private_v6="", + networks={}, + has_config_drive=False, + metadata={}, + user_data="", + trusted_image_certificates=[], + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_openstack_provider(), + ), + mock.patch( + "prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates.compute_client", + new=compute_client, + ), + ): + from prowler.providers.openstack.services.compute.compute_instance_trusted_image_certificates.compute_instance_trusted_image_certificates import ( + compute_instance_trusted_image_certificates, + ) + + check = compute_instance_trusted_image_certificates() + result = check.execute() + + assert len(result) == 2 + assert len([r for r in result if r.status == "PASS"]) == 1 + assert len([r for r in result if r.status == "FAIL"]) == 1 diff --git a/tests/providers/openstack/services/compute/lib/ip_test.py b/tests/providers/openstack/services/compute/lib/ip_test.py new file mode 100644 index 0000000000..6905bc45db --- /dev/null +++ b/tests/providers/openstack/services/compute/lib/ip_test.py @@ -0,0 +1,53 @@ +"""Tests for the shared is_public_ip utility.""" + +from prowler.providers.openstack.services.compute.lib.ip import is_public_ip + + +class Test_is_public_ip: + def test_public_ipv4(self): + assert is_public_ip("8.8.8.8") + + def test_public_ipv4_other(self): + assert is_public_ip("1.1.1.1") + + def test_private_ipv4_10(self): + assert not is_public_ip("10.0.0.5") + + def test_private_ipv4_172(self): + assert not is_public_ip("172.16.0.1") + + def test_private_ipv4_192(self): + assert not is_public_ip("192.168.1.1") + + def test_loopback_ipv4(self): + assert not is_public_ip("127.0.0.1") + + def test_link_local_ipv4(self): + assert not is_public_ip("169.254.0.1") + + def test_multicast_ipv4(self): + assert not is_public_ip("224.0.0.1") + + def test_documentation_ipv4_not_global(self): + assert not is_public_ip("203.0.113.10") + + def test_public_ipv6(self): + assert is_public_ip("2001:41d0:801:1000::164b") + + def test_private_ipv6(self): + assert not is_public_ip("fd00::1") + + def test_loopback_ipv6(self): + assert not is_public_ip("::1") + + def test_link_local_ipv6(self): + assert not is_public_ip("fe80::1") + + def test_documentation_ipv6_not_global(self): + assert not is_public_ip("2001:db8::1") + + def test_invalid_ip(self): + assert not is_public_ip("not-an-ip") + + def test_empty_string(self): + assert not is_public_ip("") diff --git a/tests/providers/openstack/services/compute/openstack_compute_service_test.py b/tests/providers/openstack/services/compute/openstack_compute_service_test.py index d21ffbbb30..782572d6a1 100644 --- a/tests/providers/openstack/services/compute/openstack_compute_service_test.py +++ b/tests/providers/openstack/services/compute/openstack_compute_service_test.py @@ -44,6 +44,24 @@ class TestComputeService: mock_server1.status = "ACTIVE" mock_server1.flavor = {"id": "flavor-1"} mock_server1.security_groups = [{"name": "default"}] + mock_server1.is_locked = True + mock_server1.locked_reason = "maintenance" + mock_server1.key_name = "my-keypair" + mock_server1.user_id = "user-123" + mock_server1.access_ipv4 = "203.0.113.10" + mock_server1.access_ipv6 = "2001:db8::1" + mock_server1.public_v4 = "203.0.113.10" + mock_server1.public_v6 = "" + mock_server1.private_v4 = "10.0.0.5" + mock_server1.private_v6 = "" + mock_server1.addresses = { + "private": [{"version": 4, "addr": "10.0.0.5"}], + "public": [{"version": 4, "addr": "203.0.113.10"}], + } + mock_server1.has_config_drive = True + mock_server1.metadata = {"environment": "production"} + mock_server1.user_data = "#!/bin/bash\necho hello" + mock_server1.trusted_image_certificates = ["cert-123"] mock_server2 = MagicMock() mock_server2.id = "instance-2" @@ -51,6 +69,21 @@ class TestComputeService: mock_server2.status = "SHUTOFF" mock_server2.flavor = {"id": "flavor-2"} mock_server2.security_groups = [{"name": "web"}, {"name": "db"}] + mock_server2.is_locked = False + mock_server2.locked_reason = "" + mock_server2.key_name = "" + mock_server2.user_id = "user-456" + mock_server2.access_ipv4 = "" + mock_server2.access_ipv6 = "" + mock_server2.public_v4 = "" + mock_server2.public_v6 = "" + mock_server2.private_v4 = "10.0.0.10" + mock_server2.private_v6 = "" + mock_server2.addresses = {"private": [{"version": 4, "addr": "10.0.0.10"}]} + mock_server2.has_config_drive = False + mock_server2.metadata = {} + mock_server2.user_data = "" + mock_server2.trusted_image_certificates = [] provider.connection.compute.servers.return_value = [ mock_server1, @@ -68,8 +101,27 @@ class TestComputeService: assert compute.instances[0].security_groups == ["default"] assert compute.instances[0].region == OPENSTACK_REGION assert compute.instances[0].project_id == OPENSTACK_PROJECT_ID + assert compute.instances[0].is_locked is True + assert compute.instances[0].locked_reason == "maintenance" + assert compute.instances[0].key_name == "my-keypair" + assert compute.instances[0].user_id == "user-123" + assert compute.instances[0].access_ipv4 == "203.0.113.10" + assert compute.instances[0].access_ipv6 == "2001:db8::1" + assert compute.instances[0].public_v4 == "203.0.113.10" + assert compute.instances[0].private_v4 == "10.0.0.5" + assert compute.instances[0].networks == { + "private": ["10.0.0.5"], + "public": ["203.0.113.10"], + } + assert compute.instances[0].has_config_drive is True + assert compute.instances[0].metadata == {"environment": "production"} + assert compute.instances[0].user_data == "#!/bin/bash\necho hello" + assert compute.instances[0].trusted_image_certificates == ["cert-123"] assert compute.instances[1].security_groups == ["web", "db"] + assert compute.instances[1].is_locked is False + assert compute.instances[1].key_name == "" + assert compute.instances[1].trusted_image_certificates == [] def test_compute_list_instances_empty(self): """Test listing instances when none exist.""" @@ -90,6 +142,21 @@ class TestComputeService: del mock_server.status del mock_server.flavor del mock_server.security_groups + del mock_server.is_locked + del mock_server.locked_reason + del mock_server.key_name + del mock_server.user_id + del mock_server.access_ipv4 + del mock_server.access_ipv6 + del mock_server.public_v4 + del mock_server.public_v6 + del mock_server.private_v4 + del mock_server.private_v6 + del mock_server.addresses + del mock_server.has_config_drive + del mock_server.metadata + del mock_server.user_data + del mock_server.trusted_image_certificates provider.connection.compute.servers.return_value = [mock_server] @@ -101,6 +168,21 @@ class TestComputeService: assert compute.instances[0].status == "" assert compute.instances[0].flavor_id == "" assert compute.instances[0].security_groups == [] + assert compute.instances[0].is_locked is False + assert compute.instances[0].locked_reason == "" + assert compute.instances[0].key_name == "" + assert compute.instances[0].user_id == "" + assert compute.instances[0].access_ipv4 == "" + assert compute.instances[0].access_ipv6 == "" + assert compute.instances[0].public_v4 == "" + assert compute.instances[0].public_v6 == "" + assert compute.instances[0].private_v4 == "" + assert compute.instances[0].private_v6 == "" + assert compute.instances[0].networks == {} + assert compute.instances[0].has_config_drive is False + assert compute.instances[0].metadata == {} + assert compute.instances[0].user_data == "" + assert compute.instances[0].trusted_image_certificates == [] def test_compute_list_instances_sdk_exception(self): """Test handling SDKException when listing instances.""" @@ -133,6 +215,21 @@ class TestComputeService: mock_server.status = "ACTIVE" mock_server.flavor = {"id": "flavor-1"} mock_server.security_groups = [{"name": "default"}] + mock_server.is_locked = False + mock_server.locked_reason = "" + mock_server.key_name = "" + mock_server.user_id = "" + mock_server.access_ipv4 = "" + mock_server.access_ipv6 = "" + mock_server.public_v4 = "" + mock_server.public_v6 = "" + mock_server.private_v4 = "" + mock_server.private_v6 = "" + mock_server.addresses = {} + mock_server.has_config_drive = False + mock_server.metadata = {} + mock_server.user_data = "" + mock_server.trusted_image_certificates = [] yield mock_server raise Exception("Iterator failed") @@ -154,6 +251,23 @@ class TestComputeService: security_groups=["default"], region="RegionOne", project_id="project-1", + is_locked=True, + locked_reason="maintenance", + key_name="my-keypair", + user_id="user-123", + access_ipv4="203.0.113.10", + access_ipv6="2001:db8::1", + public_v4="203.0.113.10", + public_v6="", + private_v4="10.0.0.5", + private_v6="", + networks={ + "private": ["10.0.0.5"] + }, # Note: This is the processed dict, not addresses + has_config_drive=True, + metadata={"environment": "production"}, + user_data="#!/bin/bash\necho hello", + trusted_image_certificates=["cert-123"], ) assert instance.id == "instance-1" @@ -163,6 +277,21 @@ class TestComputeService: assert instance.security_groups == ["default"] assert instance.region == "RegionOne" assert instance.project_id == "project-1" + assert instance.is_locked is True + assert instance.locked_reason == "maintenance" + assert instance.key_name == "my-keypair" + assert instance.user_id == "user-123" + assert instance.access_ipv4 == "203.0.113.10" + assert instance.access_ipv6 == "2001:db8::1" + assert instance.public_v4 == "203.0.113.10" + assert instance.public_v6 == "" + assert instance.private_v4 == "10.0.0.5" + assert instance.private_v6 == "" + assert instance.networks == {"private": ["10.0.0.5"]} + assert instance.has_config_drive is True + assert instance.metadata == {"environment": "production"} + assert instance.user_data == "#!/bin/bash\necho hello" + assert instance.trusted_image_certificates == ["cert-123"] def test_compute_service_inherits_from_base(self): """Test Compute service inherits from OpenStackService.""" @@ -180,3 +309,37 @@ class TestComputeService: assert hasattr(compute, "identity") assert hasattr(compute, "audit_config") assert hasattr(compute, "fixer_config") + + def test_compute_list_instances_with_none_addresses(self): + """Test listing instances when addresses attribute is None.""" + provider = set_mocked_openstack_provider() + + mock_server = MagicMock() + mock_server.id = "instance-1" + mock_server.name = "Instance With None Addresses" + mock_server.status = "ACTIVE" + mock_server.flavor = {"id": "flavor-1"} + mock_server.security_groups = [{"name": "default"}] + mock_server.is_locked = False + mock_server.locked_reason = "" + mock_server.key_name = "test-key" + mock_server.user_id = "user-123" + mock_server.access_ipv4 = "" + mock_server.access_ipv6 = "" + mock_server.public_v4 = "" + mock_server.public_v6 = "" + mock_server.private_v4 = "" + mock_server.private_v6 = "" + mock_server.addresses = None # This is the key test case + mock_server.has_config_drive = False + mock_server.metadata = {} + mock_server.user_data = "" + mock_server.trusted_image_certificates = [] + + provider.connection.compute.servers.return_value = [mock_server] + + compute = Compute(provider) + + assert len(compute.instances) == 1 + assert compute.instances[0].id == "instance-1" + assert compute.instances[0].networks == {} # Should default to empty dict diff --git a/util/update_oci_regions.py b/util/update_oci_regions.py index 3dbfe07668..1d778b777a 100644 --- a/util/update_oci_regions.py +++ b/util/update_oci_regions.py @@ -50,14 +50,18 @@ def setup_oci_client(): user_ocid = os.getenv("OCI_CLI_USER") fingerprint = os.getenv("OCI_CLI_FINGERPRINT") tenancy_ocid = os.getenv("OCI_CLI_TENANCY") - key_content_b64 = os.getenv("OCI_CLI_KEY_CONTENT") + key_content_raw = os.getenv("OCI_CLI_KEY_CONTENT") region = os.getenv("OCI_CLI_REGION", "us-ashburn-1") - # Decode base64 private key - try: - key_content = base64.b64decode(key_content_b64).decode("utf-8") - except Exception as e: - raise ValueError(f"Failed to decode OCI_CLI_KEY_CONTENT: {e}") + # Decode private key: the secret is stored base64-encoded (same format + # the UI/API use). If it's already raw PEM, use it directly. + if key_content_raw.strip().startswith("-----BEGIN"): + key_content = key_content_raw + else: + try: + key_content = base64.b64decode(key_content_raw).decode("utf-8") + except Exception as e: + raise ValueError(f"Failed to decode OCI_CLI_KEY_CONTENT: {e}") # Create OCI config dictionary config = {