From fa722f1dc76891dcd1c4b8af8651da83f8d2b258 Mon Sep 17 00:00:00 2001 From: sumit-tft <70506234+sumit-tft@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:30:25 +0530 Subject: [PATCH 01/28] =?UTF-8?q?feat(ui):=20add=2032-character=20limit=20?= =?UTF-8?q?validation=20for=20scan=20name=20in=20create=20a=E2=80=A6=20(#8?= =?UTF-8?q?319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: alejandrobailo --- ui/CHANGELOG.md | 1 + .../launch-workflow/launch-scan-workflow-form.tsx | 12 ++++++++---- ui/types/formSchemas.ts | 5 ++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 0915f7aea4..647e668191 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Navigation link in Scans view to access Compliance Overview [(#8251)](https://github.com/prowler-cloud/prowler/pull/8251) - Status column for findings table in the Compliance Detail view [(#8244)](https://github.com/prowler-cloud/prowler/pull/8244) - Allow to restrict routes access based on user permissions [(#8287)](https://github.com/prowler-cloud/prowler/pull/8287) +- Max character limit validation for Scan label [(#8319)](https://github.com/prowler-cloud/prowler/pull/8319) ### Security diff --git a/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx b/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx index bca5c061b5..b44775c0b0 100644 --- a/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx +++ b/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx @@ -29,9 +29,13 @@ export const LaunchScanWorkflow = ({ const formSchema = z.object({ ...onDemandScanFormSchema().shape, scanName: z - .string() - .min(3, "Must have at least 3 characters") - .or(z.literal("")) + .union([ + z + .string() + .min(3, "Must be at least 3 characters") + .max(32, "Must not exceed 32 characters"), + z.literal(""), + ]) .optional(), }); @@ -101,7 +105,7 @@ export const LaunchScanWorkflow = ({ animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -50 }} transition={{ duration: 0.3 }} - className="min-w-48 self-end" + className="h-[3.4rem] min-w-[15.2rem] self-end" > scanName: z .string() .refine((val) => val === "" || val.length >= 3, { - message: "The alias must be empty or have at least 3 characters.", + message: "Must be empty or have at least 3 characters.", + }) + .refine((val) => val === "" || val.length <= 32, { + message: "Must not exceed 32 characters.", }) .refine((val) => val !== currentName, { message: "The new name must be different from the current one.", From dd713351dcefb994d8bbcb2050a8b8100d6d7c61 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:07:26 +0200 Subject: [PATCH 02/28] fix(defender): avoid duplicated findings in check `defender_domain_dkim_enabled` (#8334) --- prowler/CHANGELOG.md | 7 +++++++ .../defender_domain_dkim_enabled.py | 2 +- .../defender_domain_dkim_enabled_test.py | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 72c6742888..4c87cf1360 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the **Prowler SDK** are documented in this file. +## [v5.9.2] (Prowler v5.9.2) UNRELEASED + +### Fixed +- Use the correct resource name in `defender_domain_dkim_enabled` check [(#8334)](https://github.com/prowler-cloud/prowler/pull/8334) + +--- + ## [v5.9.0] (Prowler v5.9.0) ### Added diff --git a/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py index f6258744d0..1f14aa9a42 100644 --- a/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py +++ b/prowler/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled.py @@ -26,7 +26,7 @@ class defender_domain_dkim_enabled(Check): report = CheckReportM365( metadata=self.metadata(), resource=config, - resource_name="DKIM Configuration", + resource_name=config.id, resource_id=config.id, ) report.status = "FAIL" diff --git a/tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py b/tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py index bbbf82b51c..839c42ddc4 100644 --- a/tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py +++ b/tests/providers/m365/services/defender/defender_domain_dkim_enabled/defender_domain_dkim_enabled_test.py @@ -43,7 +43,7 @@ class Test_defender_domain_dkim_enabled: == "DKIM is enabled for domain with ID domain1." ) assert result[0].resource == defender_client.dkim_configurations[0].dict() - assert result[0].resource_name == "DKIM Configuration" + assert result[0].resource_name == "domain1" assert result[0].resource_id == "domain1" assert result[0].location == "global" @@ -86,7 +86,7 @@ class Test_defender_domain_dkim_enabled: == "DKIM is not enabled for domain with ID domain2." ) assert result[0].resource == defender_client.dkim_configurations[0].dict() - assert result[0].resource_name == "DKIM Configuration" + assert result[0].resource_name == "domain2" assert result[0].resource_id == "domain2" assert result[0].location == "global" From ab348d57521b77d6644c0a44d471dd14c1adf42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Mon, 21 Jul 2025 16:33:07 +0200 Subject: [PATCH 03/28] feat(resources): Optimize findings prefetching during resource views (#8336) --- api/CHANGELOG.md | 7 +++ api/pyproject.toml | 2 +- ...40_rfm_tenant_resource_index_partitions.py | 30 +++++++++++++ ...1_rfm_tenant_resource_parent_partitions.py | 17 ++++++++ .../0042_scan_scans_prov_ins_desc_idx.py | 23 ++++++++++ api/src/backend/api/models.py | 11 +++++ api/src/backend/api/specs/v1.yaml | 2 +- api/src/backend/api/v1/views.py | 43 +++++++++++++++---- 8 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py create mode 100644 api/src/backend/api/migrations/0041_rfm_tenant_resource_parent_partitions.py create mode 100644 api/src/backend/api/migrations/0042_scan_scans_prov_ins_desc_idx.py diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 87d82df6f5..6afe7abb1b 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the **Prowler API** are documented in this file. +## [1.10.2] (Prowler v5.9.2) + +### Changed +- Optimized queries for resources views [(#8336)](https://github.com/prowler-cloud/prowler/pull/8336) + +--- + ## [v1.10.1] (Prowler v5.9.1) ### Fixed diff --git a/api/pyproject.toml b/api/pyproject.toml index f7cd0d1808..dac2c9d8b1 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -38,7 +38,7 @@ name = "prowler-api" package-mode = false # Needed for the SDK compatibility requires-python = ">=3.11,<3.13" -version = "1.10.1" +version = "1.10.2" [project.scripts] celery = "src.backend.config.settings.celery" diff --git a/api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py b/api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py new file mode 100644 index 0000000000..431d656376 --- /dev/null +++ b/api/src/backend/api/migrations/0040_rfm_tenant_resource_index_partitions.py @@ -0,0 +1,30 @@ +from functools import partial + +from django.db import migrations + +from api.db_utils import create_index_on_partitions, drop_index_on_partitions + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0039_resource_resources_failed_findings_idx"), + ] + + operations = [ + migrations.RunPython( + partial( + create_index_on_partitions, + parent_table="resource_finding_mappings", + index_name="rfm_tenant_resource_idx", + columns="tenant_id, resource_id", + method="BTREE", + ), + reverse_code=partial( + drop_index_on_partitions, + parent_table="resource_finding_mappings", + index_name="rfm_tenant_resource_idx", + ), + ), + ] diff --git a/api/src/backend/api/migrations/0041_rfm_tenant_resource_parent_partitions.py b/api/src/backend/api/migrations/0041_rfm_tenant_resource_parent_partitions.py new file mode 100644 index 0000000000..cd4b54d61b --- /dev/null +++ b/api/src/backend/api/migrations/0041_rfm_tenant_resource_parent_partitions.py @@ -0,0 +1,17 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0040_rfm_tenant_resource_index_partitions"), + ] + + operations = [ + migrations.AddIndex( + model_name="resourcefindingmapping", + index=models.Index( + fields=["tenant_id", "resource_id"], + name="rfm_tenant_resource_idx", + ), + ), + ] diff --git a/api/src/backend/api/migrations/0042_scan_scans_prov_ins_desc_idx.py b/api/src/backend/api/migrations/0042_scan_scans_prov_ins_desc_idx.py new file mode 100644 index 0000000000..da2db905c6 --- /dev/null +++ b/api/src/backend/api/migrations/0042_scan_scans_prov_ins_desc_idx.py @@ -0,0 +1,23 @@ +from django.contrib.postgres.operations import AddIndexConcurrently +from django.db import migrations, models + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("api", "0041_rfm_tenant_resource_parent_partitions"), + ("django_celery_beat", "0019_alter_periodictasks_options"), + ] + + operations = [ + AddIndexConcurrently( + model_name="scan", + index=models.Index( + condition=models.Q(("state", "completed")), + fields=["tenant_id", "provider_id", "-inserted_at"], + include=("id",), + name="scans_prov_ins_desc_idx", + ), + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index f81ebafaf3..632e3b5dd3 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -476,6 +476,13 @@ class Scan(RowLevelSecurityProtectedModel): condition=Q(state=StateChoices.COMPLETED), name="scans_prov_state_ins_desc_idx", ), + # TODO This might replace `scans_prov_state_ins_desc_idx` completely. Review usage + models.Index( + fields=["tenant_id", "provider_id", "-inserted_at"], + condition=Q(state=StateChoices.COMPLETED), + include=["id"], + name="scans_prov_ins_desc_idx", + ), ] class JSONAPIMeta: @@ -860,6 +867,10 @@ class ResourceFindingMapping(PostgresPartitionedModel, RowLevelSecurityProtected fields=["tenant_id", "finding_id"], name="rfm_tenant_finding_idx", ), + models.Index( + fields=["tenant_id", "resource_id"], + name="rfm_tenant_resource_idx", + ), ] constraints = [ models.UniqueConstraint( diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 3b500422dc..3ea3610e29 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.10.1 + version: 1.10.2 description: |- Prowler API specification. diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 6219609717..e81808ae8f 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -22,7 +22,7 @@ from django.conf import settings as django_settings from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.search import SearchQuery from django.db import transaction -from django.db.models import Count, F, Prefetch, Q, Sum +from django.db.models import Count, F, Prefetch, Q, Subquery, Sum from django.db.models.functions import Coalesce from django.http import HttpResponse from django.shortcuts import redirect @@ -292,7 +292,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.10.1" + spectacular_settings.VERSION = "1.10.2" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -1994,6 +1994,21 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): ) ) + def _should_prefetch_findings(self) -> bool: + fields_param = self.request.query_params.get("fields[resources]", "") + include_param = self.request.query_params.get("include", "") + return ( + fields_param == "" + or "findings" in fields_param.split(",") + or "findings" in include_param.split(",") + ) + + def _get_findings_prefetch(self): + findings_queryset = Finding.all_objects.defer("scan", "resources").filter( + tenant_id=self.request.tenant_id + ) + return [Prefetch("findings", queryset=findings_queryset)] + def get_serializer_class(self): if self.action in ["metadata", "metadata_latest"]: return ResourceMetadataSerializer @@ -2017,7 +2032,11 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): filtered_queryset, manager=Resource.all_objects, select_related=["provider"], - prefetch_related=["findings"], + prefetch_related=( + self._get_findings_prefetch() + if self._should_prefetch_findings() + else [] + ), ) def retrieve(self, request, *args, **kwargs): @@ -2042,14 +2061,18 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): tenant_id = request.tenant_id filtered_queryset = self.filter_queryset(self.get_queryset()) - latest_scan_ids = ( - Scan.all_objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED) + latest_scans = ( + Scan.all_objects.filter( + tenant_id=tenant_id, + state=StateChoices.COMPLETED, + ) .order_by("provider_id", "-inserted_at") .distinct("provider_id") - .values_list("id", flat=True) + .values("provider_id") ) + filtered_queryset = filtered_queryset.filter( - tenant_id=tenant_id, provider__scan__in=latest_scan_ids + provider_id__in=Subquery(latest_scans) ) return self.paginate_by_pk( @@ -2057,7 +2080,11 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet): filtered_queryset, manager=Resource.all_objects, select_related=["provider"], - prefetch_related=["findings"], + prefetch_related=( + self._get_findings_prefetch() + if self._should_prefetch_findings() + else [] + ), ) @action(detail=False, methods=["get"], url_name="metadata") From 00c527ff79d9d5542c7780aa2434737894b401c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Tue, 22 Jul 2025 10:53:22 +0200 Subject: [PATCH 04/28] chore: update Prowler changelog for v5.9.2 (#8342) --- prowler/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 4c87cf1360..11c27a447b 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [v5.9.2] (Prowler v5.9.2) UNRELEASED +## [v5.9.2] (Prowler v5.9.2) ### Fixed - Use the correct resource name in `defender_domain_dkim_enabled` check [(#8334)](https://github.com/prowler-cloud/prowler/pull/8334) From 3b0cb3db856a3e8b8c3029a416a6a519c7873479 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Tue, 22 Jul 2025 11:23:24 +0200 Subject: [PATCH 05/28] chore(regions_update): Changes in regions for AWS services (#8333) Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com> --- .../providers/aws/aws_regions_by_service.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index c2e1068fd1..163c2f0ddb 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -1417,6 +1417,11 @@ "bedrock-data-automation": { "regions": { "aws": [ + "ap-south-1", + "ap-southeast-2", + "eu-central-1", + "eu-west-1", + "eu-west-2", "us-east-1", "us-west-2" ], @@ -2503,6 +2508,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -2544,6 +2550,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -2587,6 +2594,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -5075,6 +5083,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "ca-west-1", "eu-central-1", @@ -5088,6 +5097,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -5994,6 +6004,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -7396,6 +7407,8 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "eu-central-1", "eu-central-2", @@ -7492,6 +7505,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -8181,6 +8195,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -9540,7 +9555,9 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -10090,6 +10107,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", From 4f2a8b71bbb333cc93e9c85c59452b6b30d9561f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Tue, 22 Jul 2025 13:01:19 +0200 Subject: [PATCH 06/28] feat(performance): resources scenario (#8345) --- api/tests/performance/scenarios/resources.py | 234 +++++++++++++++++++ api/tests/performance/utils/config.py | 17 ++ api/tests/performance/utils/helpers.py | 22 +- 3 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 api/tests/performance/scenarios/resources.py diff --git a/api/tests/performance/scenarios/resources.py b/api/tests/performance/scenarios/resources.py new file mode 100644 index 0000000000..dabf99c3f2 --- /dev/null +++ b/api/tests/performance/scenarios/resources.py @@ -0,0 +1,234 @@ +from locust import events, task +from utils.config import ( + L_PROVIDER_NAME, + M_PROVIDER_NAME, + RESOURCES_UI_FIELDS, + S_PROVIDER_NAME, + TARGET_INSERTED_AT, +) +from utils.helpers import ( + APIUserBase, + get_api_token, + get_auth_headers, + get_dynamic_filters_pairs, + get_next_resource_filter, + get_scan_id_from_provider_name, +) + +GLOBAL = { + "token": None, + "scan_ids": {}, + "resource_filters": None, + "large_resource_filters": None, +} + + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + GLOBAL["token"] = get_api_token(environment.host) + + GLOBAL["scan_ids"]["small"] = get_scan_id_from_provider_name( + environment.host, GLOBAL["token"], S_PROVIDER_NAME + ) + GLOBAL["scan_ids"]["medium"] = get_scan_id_from_provider_name( + environment.host, GLOBAL["token"], M_PROVIDER_NAME + ) + GLOBAL["scan_ids"]["large"] = get_scan_id_from_provider_name( + environment.host, GLOBAL["token"], L_PROVIDER_NAME + ) + + GLOBAL["resource_filters"] = get_dynamic_filters_pairs( + environment.host, GLOBAL["token"], "resources" + ) + GLOBAL["large_resource_filters"] = get_dynamic_filters_pairs( + environment.host, GLOBAL["token"], "resources", GLOBAL["scan_ids"]["large"] + ) + + +class APIUser(APIUserBase): + def on_start(self): + self.token = GLOBAL["token"] + self.s_scan_id = GLOBAL["scan_ids"]["small"] + self.m_scan_id = GLOBAL["scan_ids"]["medium"] + self.l_scan_id = GLOBAL["scan_ids"]["large"] + self.available_resource_filters = GLOBAL["resource_filters"] + self.available_resource_filters_large_scan = GLOBAL["large_resource_filters"] + + @task + def resources_default(self): + name = "/resources" + page_number = self._next_page(name) + endpoint = ( + f"/resources?page[number]={page_number}" + f"&filter[updated_at]={TARGET_INSERTED_AT}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(3) + def resources_default_ui_fields(self): + name = "/resources?fields" + page_number = self._next_page(name) + endpoint = ( + f"/resources?page[number]={page_number}" + f"&fields[resources]={','.join(RESOURCES_UI_FIELDS)}" + f"&filter[updated_at]={TARGET_INSERTED_AT}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(3) + def resources_default_include(self): + name = "/resources?include" + page = self._next_page(name) + endpoint = ( + f"/resources?page[number]={page}" + f"&filter[updated_at]={TARGET_INSERTED_AT}" + f"&include=provider" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(3) + def resources_metadata(self): + name = "/resources/metadata" + endpoint = f"/resources/metadata?filter[updated_at]={TARGET_INSERTED_AT}" + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task + def resources_scan_small(self): + name = "/resources?filter[scan_id] - 50k" + page_number = self._next_page(name) + endpoint = ( + f"/resources?page[number]={page_number}" f"&filter[scan]={self.s_scan_id}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task + def resources_metadata_scan_small(self): + name = "/resources/metadata?filter[scan_id] - 50k" + endpoint = f"/resources/metadata?&filter[scan]={self.s_scan_id}" + self.client.get( + endpoint, + headers=get_auth_headers(self.token), + name=name, + ) + + @task(2) + def resources_scan_medium(self): + name = "/resources?filter[scan_id] - 250k" + page_number = self._next_page(name) + endpoint = ( + f"/resources?page[number]={page_number}" f"&filter[scan]={self.m_scan_id}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task + def resources_metadata_scan_medium(self): + name = "/resources/metadata?filter[scan_id] - 250k" + endpoint = f"/resources/metadata?&filter[scan]={self.m_scan_id}" + self.client.get( + endpoint, + headers=get_auth_headers(self.token), + name=name, + ) + + @task + def resources_scan_large(self): + name = "/resources?filter[scan_id] - 500k" + page_number = self._next_page(name) + endpoint = ( + f"/resources?page[number]={page_number}" f"&filter[scan]={self.l_scan_id}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task + def resources_scan_large_include(self): + name = "/resources?filter[scan_id]&include - 500k" + page_number = self._next_page(name) + endpoint = ( + f"/resources?page[number]={page_number}" + f"&filter[scan]={self.l_scan_id}" + f"&include=provider" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task + def resources_metadata_scan_large(self): + endpoint = f"/resources/metadata?&filter[scan]={self.l_scan_id}" + self.client.get( + endpoint, + headers=get_auth_headers(self.token), + name="/resources/metadata?filter[scan_id] - 500k", + ) + + @task(2) + def resources_filters(self): + name = "/resources?filter[resource_filter]&include" + filter_name, filter_value = get_next_resource_filter( + self.available_resource_filters + ) + + endpoint = ( + f"/resources?filter[{filter_name}]={filter_value}" + f"&filter[updated_at]={TARGET_INSERTED_AT}" + f"&include=provider" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(3) + def resources_metadata_filters(self): + name = "/resources/metadata?filter[resource_filter]" + filter_name, filter_value = get_next_resource_filter( + self.available_resource_filters + ) + + endpoint = ( + f"/resources/metadata?filter[{filter_name}]={filter_value}" + f"&filter[updated_at]={TARGET_INSERTED_AT}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(3) + def resources_metadata_filters_scan_large(self): + name = "/resources/metadata?filter[resource_filter]&filter[scan_id] - 500k" + filter_name, filter_value = get_next_resource_filter( + self.available_resource_filters + ) + + endpoint = ( + f"/resources/metadata?filter[{filter_name}]={filter_value}" + f"&filter[scan]={self.l_scan_id}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(2) + def resourcess_filter_large_scan_include(self): + name = "/resources?filter[resource_filter][scan]&include - 500k" + filter_name, filter_value = get_next_resource_filter( + self.available_resource_filters + ) + + endpoint = ( + f"/resources?filter[{filter_name}]={filter_value}" + f"&filter[scan]={self.l_scan_id}" + f"&include=provider" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(3) + def resources_latest_default_ui_fields(self): + name = "/resources/latest?fields" + page_number = self._next_page(name) + endpoint = ( + f"/resources/latest?page[number]={page_number}" + f"&fields[resources]={','.join(RESOURCES_UI_FIELDS)}" + ) + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) + + @task(3) + def resources_latest_metadata_filters(self): + name = "/resources/metadata/latest?filter[resource_filter]" + filter_name, filter_value = get_next_resource_filter( + self.available_resource_filters + ) + + endpoint = f"/resources/metadata/latest?filter[{filter_name}]={filter_value}" + self.client.get(endpoint, headers=get_auth_headers(self.token), name=name) diff --git a/api/tests/performance/utils/config.py b/api/tests/performance/utils/config.py index febc5e0a24..8cbb2c0646 100644 --- a/api/tests/performance/utils/config.py +++ b/api/tests/performance/utils/config.py @@ -13,6 +13,23 @@ FINDINGS_RESOURCE_METADATA = { "resource_types": "resource_type", "services": "service", } +RESOURCE_METADATA = { + "regions": "region", + "types": "type", + "services": "service", +} + +RESOURCES_UI_FIELDS = [ + "name", + "failed_findings_count", + "region", + "service", + "type", + "provider", + "inserted_at", + "updated_at", + "uid", +] S_PROVIDER_NAME = "provider-50k" M_PROVIDER_NAME = "provider-250k" diff --git a/api/tests/performance/utils/helpers.py b/api/tests/performance/utils/helpers.py index 999a2d55c8..08144a5c4b 100644 --- a/api/tests/performance/utils/helpers.py +++ b/api/tests/performance/utils/helpers.py @@ -7,6 +7,7 @@ from locust import HttpUser, between from utils.config import ( BASE_HEADERS, FINDINGS_RESOURCE_METADATA, + RESOURCE_METADATA, TARGET_INSERTED_AT, USER_EMAIL, USER_PASSWORD, @@ -121,13 +122,16 @@ def get_scan_id_from_provider_name(host: str, token: str, provider_name: str) -> return response.json()["data"][0]["id"] -def get_resource_filters_pairs(host: str, token: str, scan_id: str = "") -> dict: +def get_dynamic_filters_pairs( + host: str, token: str, endpoint: str, scan_id: str = "" +) -> dict: """ - Retrieves and maps resource metadata filter values from the findings endpoint. + Retrieves and maps metadata filter values from a given endpoint. Args: host (str): The host URL of the API. token (str): Bearer token for authentication. + endpoint (str): The API endpoint to query for metadata. scan_id (str, optional): Optional scan ID to filter metadata. Defaults to using inserted_at timestamp. Returns: @@ -136,22 +140,28 @@ def get_resource_filters_pairs(host: str, token: str, scan_id: str = "") -> dict Raises: AssertionError: If the request fails or does not return a 200 status code. """ + metadata_mapping = ( + FINDINGS_RESOURCE_METADATA if endpoint == "findings" else RESOURCE_METADATA + ) + date_filter = "inserted_at" if endpoint == "findings" else "updated_at" metadata_filters = ( f"filter[scan]={scan_id}" if scan_id - else f"filter[inserted_at]={TARGET_INSERTED_AT}" + else f"filter[{date_filter}]={TARGET_INSERTED_AT}" ) response = requests.get( - f"{host}/findings/metadata?{metadata_filters}", headers=get_auth_headers(token) + f"{host}/{endpoint}/metadata?{metadata_filters}", + headers=get_auth_headers(token), ) assert ( response.status_code == 200 ), f"Failed to get resource filters values: {response.text}" attributes = response.json()["data"]["attributes"] + return { - FINDINGS_RESOURCE_METADATA[key]: values + metadata_mapping[key]: values for key, values in attributes.items() - if key in FINDINGS_RESOURCE_METADATA.keys() + if key in metadata_mapping.keys() } From ca86aeb1d775ab429a88afc16609c6746059070d Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Tue, 22 Jul 2025 22:06:17 +0800 Subject: [PATCH 07/28] feat(aws): new check `bedrock_api_key_no_administrative_privileges` (#8321) --- prowler/CHANGELOG.md | 13 +- .../__init__.py | 0 ...no_administrative_privileges.metadata.json | 36 + ...ck_api_key_no_administrative_privileges.py | 57 ++ ...hed_policy_no_administrative_privileges.py | 2 +- ...hed_policy_no_administrative_privileges.py | 2 +- ...hed_policy_no_administrative_privileges.py | 2 +- ...line_policy_allows_privilege_escalation.py | 2 +- ...ine_policy_no_administrative_privileges.py | 2 +- ...ine_policy_no_full_access_to_cloudtrail.py | 2 +- ...iam_inline_policy_no_full_access_to_kms.py | 2 +- ...ustom_policy_permissive_role_assumption.py | 2 +- .../iam_policy_allows_privilege_escalation.py | 2 +- ...iam_policy_no_full_access_to_cloudtrail.py | 2 +- .../iam_policy_no_full_access_to_kms.py | 2 +- .../providers/aws/services/iam/iam_service.py | 139 ++-- ...i_key_no_administrative_privileges_test.py | 618 ++++++++++++++++++ .../aws/services/iam/iam_service_test.py | 10 +- 18 files changed, 826 insertions(+), 69 deletions(-) create mode 100644 prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/__init__.py create mode 100644 prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json create mode 100644 prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.py create mode 100644 tests/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 11c27a447b..8ed58cec41 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the **Prowler SDK** are documented in this file. +## [v5.10.0] (Prowler UNRELEASED) + +### Added +- Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) + +--- + ## [v5.9.2] (Prowler v5.9.2) ### Fixed @@ -39,12 +46,6 @@ All notable changes to the **Prowler SDK** are documented in this file. - Update `entra_users_mfa_capable` check to use the correct resource name and ID [(#8288)](https://github.com/prowler-cloud/prowler/pull/8288) - Handle multiple services and severities while listing checks [(#8302)](https://github.com/prowler-cloud/prowler/pull/8302) - Handle `tenant_id` for M365 Mutelist [(#8306)](https://github.com/prowler-cloud/prowler/pull/8306) - ---- - -## [v5.8.2] (Prowler 5.8.2) - -### Fixed - Fix error in Dashboard Overview page when reading CSV files [(#8257)](https://github.com/prowler-cloud/prowler/pull/8257) --- diff --git a/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/__init__.py b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json new file mode 100644 index 0000000000..3f5c25382a --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "aws", + "CheckID": "bedrock_api_key_no_administrative_privileges", + "CheckTitle": "Ensure Amazon Bedrock API keys do not have administrative privileges or privilege escalation", + "CheckType": [ + "Software and Configuration Checks", + "Industry and Regulatory Standards" + ], + "ServiceName": "bedrock", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:iam:region:account-id:user/{user-name}/credential/{api-key-id}", + "Severity": "high", + "ResourceType": "AwsIamServiceSpecificCredential", + "Description": "Ensure that Amazon Bedrock API keys do not have administrative privileges or privilege escalation capabilities. API keys with administrative privileges can perform any action on any resource in your AWS environment, while privilege escalation allows users to grant themselves additional permissions, both posing significant security risks.", + "Risk": "Amazon Bedrock API keys with administrative privileges can perform any action on any resource in your AWS environment. Privilege escalation capabilities allow users to grant themselves additional permissions beyond their intended scope. Both violations of the principle of least privilege can lead to security vulnerabilities, data leaks, data loss, or unexpected charges if the API key is compromised or misused.", + "RelatedUrl": "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html", + "Remediation": { + "Code": { + "CLI": "aws iam delete-service-specific-credential --user-name --service-specific-credential-id ", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Apply the principle of least privilege to Amazon Bedrock API keys. Instead of granting administrative privileges or privilege escalation capabilities, assign only the permissions necessary for specific tasks. Create custom IAM policies with minimal permissions based on the principle of least privilege. Regularly review and audit API key permissions to ensure they cannot be used for privilege escalation.", + "Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" + } + }, + "Categories": [ + "gen-ai", + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check verifies that Amazon Bedrock API keys do not have administrative privileges or privilege escalation capabilities through attached IAM policies or inline policies. It follows the principle of least privilege to ensure API keys only have the minimum necessary permissions and cannot be used to escalate privileges." +} diff --git a/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.py b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.py new file mode 100644 index 0000000000..e43ffa5256 --- /dev/null +++ b/prowler/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges.py @@ -0,0 +1,57 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import ( + check_admin_access, + check_full_service_access, +) +from prowler.providers.aws.services.iam.lib.privilege_escalation import ( + check_privilege_escalation, +) + + +class bedrock_api_key_no_administrative_privileges(Check): + def execute(self): + findings = [] + for api_key in iam_client.service_specific_credentials: + if api_key.service_name != "bedrock.amazonaws.com": + continue + report = Check_Report_AWS(metadata=self.metadata(), resource=api_key) + report.status = "PASS" + report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has no administrative privileges." + for policy in api_key.user.attached_policies: + policy_arn = policy["PolicyArn"] + if policy_arn in iam_client.policies: + policy_document = iam_client.policies[policy_arn].document + if policy_document: + if check_admin_access(policy_document): + report.status = "FAIL" + report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has administrative privileges through attached policy {policy['PolicyName']}." + break + elif check_privilege_escalation(policy_document): + report.status = "FAIL" + report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has privilege escalation through attached policy {policy['PolicyName']}." + break + elif check_full_service_access("bedrock", policy_document): + report.status = "FAIL" + report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has full service access through attached policy {policy['PolicyName']}." + break + for inline_policy_name in api_key.user.inline_policies: + inline_policy_arn = f"{api_key.user.arn}:policy/{inline_policy_name}" + if inline_policy_arn in iam_client.policies: + policy_document = iam_client.policies[inline_policy_arn].document + if policy_document: + if check_admin_access(policy_document): + report.status = "FAIL" + report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has administrative privileges through inline policy {inline_policy_name}." + break + elif check_privilege_escalation(policy_document): + report.status = "FAIL" + report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has privilege escalation through inline policy {inline_policy_name}." + break + elif check_full_service_access("bedrock", policy_document): + report.status = "FAIL" + report.status_extended = f"API key {api_key.id} in user {api_key.user.name} has full service access through inline policy {inline_policy_name}." + break + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py index 992ec909b8..7ba8adb2c1 100644 --- a/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py @@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_aws_attached_policy_no_administrative_privileges(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): # Check only for attached AWS policies if policy.attached and policy.type == "AWS": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) diff --git a/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py index 953b9dee04..56ac6c4b79 100644 --- a/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py @@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_customer_attached_policy_no_administrative_privileges(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): # Check only for attached custom policies if policy.attached and policy.type == "Custom": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) diff --git a/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py index 61f18ebaf9..d09318fa07 100644 --- a/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py @@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_customer_unattached_policy_no_administrative_privileges(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): # Check only for cutomer unattached policies if not policy.attached and policy.type == "Custom": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py b/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py index 826e9902c4..e8aa205d1c 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py @@ -9,7 +9,7 @@ class iam_inline_policy_allows_privilege_escalation(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): if policy.type == "Inline": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.resource_id = f"{policy.entity}/{policy.name}" diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py index f78546b100..83643b709a 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py @@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_inline_policy_no_administrative_privileges(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): if policy.type == "Inline": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py index bf03103640..125b18d895 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py @@ -9,7 +9,7 @@ class iam_inline_policy_no_full_access_to_cloudtrail(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): # Check only inline policies if policy.type == "Inline": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py index 33fc5fe6a5..b30e71849a 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py @@ -9,7 +9,7 @@ class iam_inline_policy_no_full_access_to_kms(Check): def execute(self): findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): if policy.type == "Inline": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region diff --git a/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py b/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py index 48142c7068..6e310d7ca9 100644 --- a/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py +++ b/prowler/providers/aws/services/iam/iam_no_custom_policy_permissive_role_assumption/iam_no_custom_policy_permissive_role_assumption.py @@ -13,7 +13,7 @@ class iam_no_custom_policy_permissive_role_assumption(Check): return any("*" in r for r in resource) return False - for policy in iam_client.policies: + for policy in iam_client.policies.values(): # Check only custom policies if policy.type == "Custom": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) diff --git a/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py b/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py index bb6292d00f..c867292b22 100644 --- a/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py +++ b/prowler/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py @@ -9,7 +9,7 @@ class iam_policy_allows_privilege_escalation(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): if policy.type == "Custom": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) report.region = iam_client.region diff --git a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py index 2c0161f0d0..4887bbcf6b 100644 --- a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py +++ b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_cloudtrail/iam_policy_no_full_access_to_cloudtrail.py @@ -8,7 +8,7 @@ critical_service = "cloudtrail" class iam_policy_no_full_access_to_cloudtrail(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): # Check only custom policies if policy.type == "Custom": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) diff --git a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py index 1cd7faf2c0..adad5d0d1d 100644 --- a/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py +++ b/prowler/providers/aws/services/iam/iam_policy_no_full_access_to_kms/iam_policy_no_full_access_to_kms.py @@ -8,7 +8,7 @@ critical_service = "kms" class iam_policy_no_full_access_to_kms(Check): def execute(self) -> Check_Report_AWS: findings = [] - for policy in iam_client.policies: + for policy in iam_client.policies.values(): # Check only custom policies if policy.type == "Custom": report = Check_Report_AWS(metadata=self.metadata(), resource=policy) diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index d56f6e446b..914483081b 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -77,13 +77,15 @@ class IAM(AWSService): cloudshell_admin_policy_arn ) # List both Customer (attached and unattached) and AWS Managed (only attached) policies - self.policies = [] - self.policies.extend(self._list_policies("AWS")) - self.policies.extend(self._list_policies("Local")) + self.policies = {} + self.policies.update(self._list_policies("AWS")) + self.policies.update(self._list_policies("Local")) self._list_policies_version(self.policies) self._list_inline_user_policies() self._list_inline_group_policies() self._list_inline_role_policies() + self.service_specific_credentials = [] + self._list_service_specific_credentials() self.saml_providers = self._list_saml_providers() self.server_certificates = self._list_server_certificates() self.access_keys_metadata = {} @@ -99,7 +101,7 @@ class IAM(AWSService): self.__threading_call__(self._list_tags, self.roles) self.__threading_call__( self._list_tags, - [policy for policy in self.policies if policy.type == "Custom"], + [policy for policy in self.policies.values() if policy.type == "Custom"], ) self.__threading_call__(self._list_tags, self.server_certificates) if self.saml_providers is not None: @@ -514,16 +516,15 @@ class IAM(AWSService): UserName=user.name, PolicyName=policy ) inline_user_policy_doc = inline_policy["PolicyDocument"] - self.policies.append( - Policy( - name=policy, - arn=user.arn, - entity=user.name, - type="Inline", - attached=True, - version_id="v1", - document=inline_user_policy_doc, - ) + inline_user_policy_arn = f"{user.arn}:policy/{policy}" + self.policies[inline_user_policy_arn] = Policy( + name=policy, + arn=user.arn, + entity=user.name, + type="Inline", + attached=True, + version_id="v1", + document=inline_user_policy_doc, ) except ClientError as error: if error.response["Error"]["Code"] == "NoSuchEntity": @@ -572,16 +573,15 @@ class IAM(AWSService): GroupName=group.name, PolicyName=policy ) inline_group_policy_doc = inline_policy["PolicyDocument"] - self.policies.append( - Policy( - name=policy, - arn=group.arn, - entity=group.name, - type="Inline", - attached=True, - version_id="v1", - document=inline_group_policy_doc, - ) + inline_group_policy_arn = f"{group.arn}:policy/{policy}" + self.policies[inline_group_policy_arn] = Policy( + name=policy, + arn=group.arn, + entity=group.name, + type="Inline", + attached=True, + version_id="v1", + document=inline_group_policy_doc, ) except ClientError as error: if error.response["Error"]["Code"] == "NoSuchEntity": @@ -633,16 +633,15 @@ class IAM(AWSService): RoleName=role.name, PolicyName=policy ) inline_role_policy_doc = inline_policy["PolicyDocument"] - self.policies.append( - Policy( - name=policy, - arn=role.arn, - entity=role.name, - type="Inline", - attached=True, - version_id="v1", - document=inline_role_policy_doc, - ) + inline_role_policy_arn = f"{role.arn}:policy/{policy}" + self.policies[inline_role_policy_arn] = Policy( + name=policy, + arn=role.arn, + entity=role.name, + type="Inline", + attached=True, + version_id="v1", + document=inline_role_policy_doc, ) except ClientError as error: if error.response["Error"]["Code"] == "NoSuchEntity": @@ -742,7 +741,7 @@ class IAM(AWSService): def _list_policies(self, scope): logger.info("IAM - List Policies...") try: - policies = [] + policies = {} list_policies_paginator = self.client.get_paginator("list_policies") for page in list_policies_paginator.paginate( Scope=scope, OnlyAttached=False if scope == "Local" else True @@ -751,17 +750,13 @@ class IAM(AWSService): if not self.audit_resources or ( is_resource_filtered(policy["Arn"], self.audit_resources) ): - policies.append( - Policy( - name=policy["PolicyName"], - arn=policy["Arn"], - entity=policy["PolicyId"], - version_id=policy["DefaultVersionId"], - type="Custom" if scope == "Local" else "AWS", - attached=( - True if policy["AttachmentCount"] > 0 else False - ), - ) + policies[policy["Arn"]] = Policy( + name=policy["PolicyName"], + arn=policy["Arn"], + entity=policy["PolicyId"], + version_id=policy["DefaultVersionId"], + type="Custom" if scope == "Local" else "AWS", + attached=(True if policy["AttachmentCount"] > 0 else False), ) except Exception as error: logger.error( @@ -773,7 +768,7 @@ class IAM(AWSService): def _list_policies_version(self, policies): logger.info("IAM - List Policies Version...") try: - for policy in policies: + for policy in policies.values(): try: policy_version = self.client.get_policy_version( PolicyArn=policy.arn, VersionId=policy.version_id @@ -1019,6 +1014,43 @@ class IAM(AWSService): f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_service_specific_credentials(self): + logger.info("IAM - List Service Specific Credentials...") + try: + for user in self.users: + service_specific_credentials = ( + self.client.list_service_specific_credentials(UserName=user.name) + ) + for credential in service_specific_credentials.get( + "ServiceSpecificCredentials", [] + ): + credential["Arn"] = ( + f"arn:{self.audited_partition}:iam:{self.region}:{self.audited_account}:user/{user.name}/credential/{credential['ServiceSpecificCredentialId']}" + ) + if not self.audit_resources or ( + is_resource_filtered(credential["Arn"], self.audit_resources) + ): + self.service_specific_credentials.append( + ServiceSpecificCredential( + arn=credential["Arn"], + user=user, + status=credential["Status"], + create_date=credential["CreateDate"], + service_user_name=credential.get("ServiceUserName"), + service_credential_alias=credential.get( + "ServiceCredentialAlias" + ), + expiration_date=credential.get("ExpirationDate"), + id=credential.get("ServiceSpecificCredentialId"), + service_name=credential.get("ServiceName"), + region=self.region, + ) + ) + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class MFADevice(BaseModel): serial_number: str @@ -1046,6 +1078,19 @@ class Role(BaseModel): tags: Optional[list] +class ServiceSpecificCredential(BaseModel): + arn: str + user: User + status: str + create_date: datetime + service_user_name: Optional[str] + service_credential_alias: Optional[str] + expiration_date: Optional[datetime] + id: str + service_name: str + region: str + + class Group(BaseModel): name: str arn: str diff --git a/tests/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges_test.py b/tests/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges_test.py new file mode 100644 index 0000000000..1324eede95 --- /dev/null +++ b/tests/providers/aws/services/bedrock/bedrock_api_key_no_administrative_privileges/bedrock_api_key_no_administrative_privileges_test.py @@ -0,0 +1,618 @@ +from datetime import timezone +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +# Test policy documents +ADMIN_POLICY = { + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "Action": ["*"], "Resource": "*"}], +} + +NON_ADMIN_POLICY = { + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "Action": ["bedrock:*"], "Resource": "*"}], +} + +PRIVILEGE_ESCALATION_POLICY = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:CreateAccessKey", + "iam:CreateUser", + "iam:AttachUserPolicy", + ], + "Resource": "*", + } + ], +} + + +class Test_bedrock_api_key_no_administrative_privileges: + @mock_aws + def test_no_bedrock_api_keys(self): + from prowler.providers.aws.services.iam.iam_service import IAM + + 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.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=IAM(aws_provider), + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_bedrock_api_key_with_admin_attached_policy(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + + # Create user + user_name = "test_user" + user_arn = iam_client.create_user(UserName=user_name)["User"]["Arn"] + + # Create admin policy + admin_policy_arn = iam_client.create_policy( + PolicyName="AdminPolicy", + PolicyDocument=dumps(ADMIN_POLICY), + Path="/", + )["Policy"]["Arn"] + + # Attach admin policy to user + iam_client.attach_user_policy(UserName=user_name, PolicyArn=admin_policy_arn) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + # Mock service-specific credentials + from datetime import datetime + + from prowler.providers.aws.services.iam.iam_service import ( + ServiceSpecificCredential, + User, + ) + + # Create a mock user with the attached policy + mock_user = User( + name=user_name, + arn=user_arn, + attached_policies=[ + {"PolicyArn": admin_policy_arn, "PolicyName": "AdminPolicy"} + ], + inline_policies=[], + ) + + # Create a mock service-specific credential + mock_credential = ServiceSpecificCredential( + arn=f"arn:aws:iam:{AWS_REGION_US_EAST_1}:123456789012:user/{user_name}/credential/test-credential-id", + user=mock_user, + status="Active", + create_date=datetime.now(timezone.utc), + service_user_name=None, + service_credential_alias=None, + expiration_date=None, + id="test-credential-id", + service_name="bedrock.amazonaws.com", + region=AWS_REGION_US_EAST_1, + ) + + iam.service_specific_credentials = [mock_credential] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=iam, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "API key test-credential-id in user test_user has administrative privileges through attached policy AdminPolicy." + ) + assert result[0].resource_id == "test-credential-id" + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bedrock_api_key_with_admin_inline_policy(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + + # Create user + user_name = "test_user" + user_arn = iam_client.create_user(UserName=user_name)["User"]["Arn"] + + # Create inline admin policy + iam_client.put_user_policy( + UserName=user_name, + PolicyName="AdminInlinePolicy", + PolicyDocument=dumps(ADMIN_POLICY), + ) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + # Mock service-specific credentials + from datetime import datetime + + from prowler.providers.aws.services.iam.iam_service import ( + ServiceSpecificCredential, + User, + ) + + # Create a mock user with the inline policy + mock_user = User( + name=user_name, + arn=user_arn, + attached_policies=[], + inline_policies=["AdminInlinePolicy"], + ) + + # Create a mock service-specific credential + mock_credential = ServiceSpecificCredential( + arn=f"arn:aws:iam:{AWS_REGION_US_EAST_1}:123456789012:user/{user_name}/credential/test-credential-id", + user=mock_user, + status="Active", + create_date=datetime.now(timezone.utc), + service_user_name=None, + service_credential_alias=None, + expiration_date=None, + id="test-credential-id", + service_name="bedrock.amazonaws.com", + region=AWS_REGION_US_EAST_1, + ) + + iam.service_specific_credentials = [mock_credential] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=iam, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "API key test-credential-id in user test_user has administrative privileges through inline policy AdminInlinePolicy." + ) + assert result[0].resource_id == "test-credential-id" + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bedrock_api_key_with_privilege_escalation_attached_policy(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + + # Create user + user_name = "test_user" + user_arn = iam_client.create_user(UserName=user_name)["User"]["Arn"] + + # Create privilege escalation policy + escalation_policy_arn = iam_client.create_policy( + PolicyName="EscalationPolicy", + PolicyDocument=dumps(PRIVILEGE_ESCALATION_POLICY), + Path="/", + )["Policy"]["Arn"] + + # Attach privilege escalation policy to user + iam_client.attach_user_policy( + UserName=user_name, PolicyArn=escalation_policy_arn + ) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + # Mock service-specific credentials + from datetime import datetime + + from prowler.providers.aws.services.iam.iam_service import ( + ServiceSpecificCredential, + User, + ) + + # Create a mock user with the attached policy + mock_user = User( + name=user_name, + arn=user_arn, + attached_policies=[ + {"PolicyArn": escalation_policy_arn, "PolicyName": "EscalationPolicy"} + ], + inline_policies=[], + ) + + # Create a mock service-specific credential + mock_credential = ServiceSpecificCredential( + arn=f"arn:aws:iam:{AWS_REGION_US_EAST_1}:123456789012:user/{user_name}/credential/test-credential-id", + user=mock_user, + status="Active", + create_date=datetime.now(timezone.utc), + service_user_name=None, + service_credential_alias=None, + expiration_date=None, + id="test-credential-id", + service_name="bedrock.amazonaws.com", + region=AWS_REGION_US_EAST_1, + ) + + iam.service_specific_credentials = [mock_credential] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=iam, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "API key test-credential-id in user test_user has privilege escalation through attached policy EscalationPolicy." + ) + assert result[0].resource_id == "test-credential-id" + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bedrock_api_key_with_privilege_escalation_inline_policy(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + + # Create user + user_name = "test_user" + user_arn = iam_client.create_user(UserName=user_name)["User"]["Arn"] + + # Create inline privilege escalation policy + iam_client.put_user_policy( + UserName=user_name, + PolicyName="EscalationInlinePolicy", + PolicyDocument=dumps(PRIVILEGE_ESCALATION_POLICY), + ) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + # Mock service-specific credentials + from datetime import datetime + + from prowler.providers.aws.services.iam.iam_service import ( + ServiceSpecificCredential, + User, + ) + + # Create a mock user with the inline policy + mock_user = User( + name=user_name, + arn=user_arn, + attached_policies=[], + inline_policies=["EscalationInlinePolicy"], + ) + + # Create a mock service-specific credential + mock_credential = ServiceSpecificCredential( + arn=f"arn:aws:iam:{AWS_REGION_US_EAST_1}:123456789012:user/{user_name}/credential/test-credential-id", + user=mock_user, + status="Active", + create_date=datetime.now(timezone.utc), + service_user_name=None, + service_credential_alias=None, + expiration_date=None, + id="test-credential-id", + service_name="bedrock.amazonaws.com", + region=AWS_REGION_US_EAST_1, + ) + + iam.service_specific_credentials = [mock_credential] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=iam, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "API key test-credential-id in user test_user has privilege escalation through inline policy EscalationInlinePolicy." + ) + assert result[0].resource_id == "test-credential-id" + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bedrock_api_key_with_non_admin_policy(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + + # Create user + user_name = "test_user" + user_arn = iam_client.create_user(UserName=user_name)["User"]["Arn"] + + # Create non-admin policy + non_admin_policy_arn = iam_client.create_policy( + PolicyName="NonAdminPolicy", + PolicyDocument=dumps(NON_ADMIN_POLICY), + Path="/", + )["Policy"]["Arn"] + + # Attach non-admin policy to user + iam_client.attach_user_policy( + UserName=user_name, PolicyArn=non_admin_policy_arn + ) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + # Mock service-specific credentials + from datetime import datetime + + from prowler.providers.aws.services.iam.iam_service import ( + ServiceSpecificCredential, + User, + ) + + # Create a mock user with the attached policy + mock_user = User( + name=user_name, + arn=user_arn, + attached_policies=[ + {"PolicyArn": non_admin_policy_arn, "PolicyName": "NonAdminPolicy"} + ], + inline_policies=[], + ) + + # Create a mock service-specific credential + mock_credential = ServiceSpecificCredential( + arn=f"arn:aws:iam:{AWS_REGION_US_EAST_1}:123456789012:user/{user_name}/credential/test-credential-id", + user=mock_user, + status="Active", + create_date=datetime.now(timezone.utc), + service_user_name=None, + service_credential_alias=None, + expiration_date=None, + id="test-credential-id", + service_name="bedrock.amazonaws.com", + region=AWS_REGION_US_EAST_1, + ) + + iam.service_specific_credentials = [mock_credential] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=iam, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "API key test-credential-id in user test_user has full service access through attached policy NonAdminPolicy." + ) + assert result[0].resource_id == "test-credential-id" + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bedrock_api_key_with_no_policies(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + + # Create user + user_name = "test_user" + user_arn = iam_client.create_user(UserName=user_name)["User"]["Arn"] + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + # Mock service-specific credentials + from datetime import datetime + + from prowler.providers.aws.services.iam.iam_service import ( + ServiceSpecificCredential, + User, + ) + + # Create a mock user with no policies + mock_user = User( + name=user_name, + arn=user_arn, + attached_policies=[], + inline_policies=[], + ) + + # Create a mock service-specific credential + mock_credential = ServiceSpecificCredential( + arn=f"arn:aws:iam:{AWS_REGION_US_EAST_1}:123456789012:user/{user_name}/credential/test-credential-id", + user=mock_user, + status="Active", + create_date=datetime.now(timezone.utc), + service_user_name=None, + service_credential_alias=None, + expiration_date=None, + id="test-credential-id", + service_name="bedrock.amazonaws.com", + region=AWS_REGION_US_EAST_1, + ) + + iam.service_specific_credentials = [mock_credential] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=iam, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "API key test-credential-id in user test_user has no administrative privileges." + ) + assert result[0].resource_id == "test-credential-id" + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_non_bedrock_api_key_ignored(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + + # Create user + user_name = "test_user" + user_arn = iam_client.create_user(UserName=user_name)["User"]["Arn"] + + # Create admin policy + admin_policy_arn = iam_client.create_policy( + PolicyName="AdminPolicy", + PolicyDocument=dumps(ADMIN_POLICY), + Path="/", + )["Policy"]["Arn"] + + # Attach admin policy to user + iam_client.attach_user_policy(UserName=user_name, PolicyArn=admin_policy_arn) + + from prowler.providers.aws.services.iam.iam_service import IAM + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + + # Mock service-specific credentials + from datetime import datetime + + from prowler.providers.aws.services.iam.iam_service import ( + ServiceSpecificCredential, + User, + ) + + # Create a mock user with the attached policy + mock_user = User( + name=user_name, + arn=user_arn, + attached_policies=[ + {"PolicyArn": admin_policy_arn, "PolicyName": "AdminPolicy"} + ], + inline_policies=[], + ) + + # Create a mock service-specific credential for a different service (not Bedrock) + mock_credential = ServiceSpecificCredential( + arn=f"arn:aws:iam:{AWS_REGION_US_EAST_1}:123456789012:user/{user_name}/credential/test-credential-id", + user=mock_user, + status="Active", + create_date=datetime.now(timezone.utc), + service_user_name=None, + service_credential_alias=None, + expiration_date=None, + id="test-credential-id", + service_name="codecommit.amazonaws.com", + region=AWS_REGION_US_EAST_1, + ) + + iam.service_specific_credentials = [mock_credential] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges.iam_client", + new=iam, + ), + ): + from prowler.providers.aws.services.bedrock.bedrock_api_key_no_administrative_privileges.bedrock_api_key_no_administrative_privileges import ( + bedrock_api_key_no_administrative_privileges, + ) + + check = bedrock_api_key_no_administrative_privileges() + result = check.execute() + + # Should return 0 results since the API key is not for Bedrock + assert len(result) == 0 diff --git a/tests/providers/aws/services/iam/iam_service_test.py b/tests/providers/aws/services/iam/iam_service_test.py index 70835f3d69..b65b8ea18c 100644 --- a/tests/providers/aws/services/iam/iam_service_test.py +++ b/tests/providers/aws/services/iam/iam_service_test.py @@ -760,7 +760,7 @@ class Test_IAM_Service: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) iam = IAM(aws_provider) custom_policies = 0 - for policy in iam.policies: + for policy in iam.policies.values(): if policy.type == "Custom": custom_policies += 1 assert policy.name == "policy1" @@ -786,7 +786,7 @@ class Test_IAM_Service: iam = IAM(aws_provider) custom_policies = 0 - for policy in iam.policies: + for policy in iam.policies.values(): if policy.type == "Custom": custom_policies += 1 assert policy.name == "policy2" @@ -872,7 +872,7 @@ nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS assert iam.users[0].tags == [] # TODO: Workaround until this gets fixed https://github.com/getmoto/moto/issues/6712 - for policy in iam.policies: + for policy in iam.policies.values(): if policy.name == policy_name: assert policy == Policy( name=policy_name, @@ -914,7 +914,7 @@ nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS assert iam.groups[0].users == [] # TODO: Workaround until this gets fixed https://github.com/getmoto/moto/issues/6712 - for policy in iam.policies: + for policy in iam.policies.values(): if policy.name == policy_name: assert policy == Policy( name=policy_name, @@ -960,7 +960,7 @@ nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS assert iam.roles[0].tags == [] # TODO: Workaround until this gets fixed https://github.com/getmoto/moto/issues/6712 - for policy in iam.policies: + for policy in iam.policies.values(): if policy.name == policy_name: assert policy == Policy( name=policy_name, From 1efd5668ceef62cb4d18dc6d9c441331e36f127d Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Tue, 22 Jul 2025 23:26:02 +0800 Subject: [PATCH 08/28] feat(api): add GitHub provider support (#8271) --- api/CHANGELOG.md | 7 + .../api/migrations/0043_github_provider.py | 33 ++++ api/src/backend/api/models.py | 11 ++ api/src/backend/api/specs/v1.yaml | 162 +++++++++++++++++- api/src/backend/api/tests/test_views.py | 61 +++++++ api/src/backend/api/utils.py | 27 ++- .../api/v1/serializer_utils/providers.py | 37 ++++ api/src/backend/api/v1/serializers.py | 12 ++ api/src/backend/tasks/jobs/export.py | 4 + prowler/CHANGELOG.md | 1 + prowler/lib/outputs/finding.py | 2 + prowler/providers/github/github_provider.py | 3 + 12 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 api/src/backend/api/migrations/0043_github_provider.py diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 6afe7abb1b..09d2fef7e4 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the **Prowler API** are documented in this file. +## [1.11.0] (Prowler UNRELEASED) + +### Added +- Github provider support [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) + +--- + ## [1.10.2] (Prowler v5.9.2) ### Changed diff --git a/api/src/backend/api/migrations/0043_github_provider.py b/api/src/backend/api/migrations/0043_github_provider.py new file mode 100644 index 0000000000..3607ce4af3 --- /dev/null +++ b/api/src/backend/api/migrations/0043_github_provider.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.7 on 2025-07-09 14:44 + +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0042_scan_scans_prov_ins_desc_idx"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="provider", + field=api.db_utils.ProviderEnumField( + choices=[ + ("aws", "AWS"), + ("azure", "Azure"), + ("gcp", "GCP"), + ("kubernetes", "Kubernetes"), + ("m365", "M365"), + ("github", "GitHub"), + ], + default="aws", + ), + ), + migrations.RunSQL( + "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'github';", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 632e3b5dd3..8e4614675d 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -205,6 +205,7 @@ class Provider(RowLevelSecurityProtectedModel): GCP = "gcp", _("GCP") KUBERNETES = "kubernetes", _("Kubernetes") M365 = "m365", _("M365") + GITHUB = "github", _("GitHub") @staticmethod def validate_aws_uid(value): @@ -265,6 +266,16 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_github_uid(value): + if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9-]{0,38}$", value): + raise ModelValidationError( + detail="GitHub provider ID must be a valid GitHub username or organization name (1-39 characters, " + "starting with alphanumeric, containing only alphanumeric characters and hyphens).", + code="github-uid", + pointer="/data/attributes/uid", + ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 3ea3610e29..f13cd95325 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.10.2 + version: 1.11.0 description: |- Prowler API specification. @@ -544,6 +544,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -552,6 +553,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -562,6 +564,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -572,6 +575,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -1061,6 +1065,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1069,6 +1074,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -1079,6 +1085,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1089,6 +1096,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -1486,6 +1494,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1494,6 +1503,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -1504,6 +1514,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1514,6 +1525,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -1909,6 +1921,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1917,6 +1930,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -1927,6 +1941,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1937,6 +1952,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -2320,6 +2336,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -2328,6 +2345,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -2338,6 +2356,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -2348,6 +2367,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -3121,6 +3141,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3129,6 +3150,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -3139,6 +3161,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3149,6 +3172,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -3282,6 +3306,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3290,6 +3315,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -3300,6 +3326,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3310,6 +3337,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -3459,6 +3487,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3467,6 +3496,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -3477,6 +3507,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3487,6 +3518,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -4165,6 +4197,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -4173,6 +4206,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider__in] schema: @@ -4746,6 +4780,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -4754,6 +4789,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -4764,6 +4800,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -4774,6 +4811,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -6457,6 +6495,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -6465,6 +6504,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -6475,6 +6515,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -6485,6 +6526,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -11130,6 +11172,34 @@ components: encoded as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret @@ -12035,6 +12105,7 @@ components: - gcp - kubernetes - m365 + - github type: string description: |- * `aws` - AWS @@ -12042,6 +12113,7 @@ components: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub uid: type: string title: Unique identifier for the provider, set by the provider @@ -12149,6 +12221,7 @@ components: - gcp - kubernetes - m365 + - github type: string description: |- * `aws` - AWS @@ -12156,6 +12229,7 @@ components: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub uid: type: string title: Unique identifier for the provider, set by the provider @@ -12194,6 +12268,7 @@ components: - gcp - kubernetes - m365 + - github type: string description: |- * `aws` - AWS @@ -12201,6 +12276,7 @@ components: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub uid: type: string minLength: 3 @@ -12852,6 +12928,34 @@ components: as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret_type @@ -13071,6 +13175,34 @@ components: encoded as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret_type @@ -13305,6 +13437,34 @@ components: as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 3071127228..c651e8fb81 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -966,6 +966,31 @@ class TestProviderViewSet: "uid": "subdomain1.subdomain2.subdomain3.subdomain4.domain.net", "alias": "test", }, + { + "provider": "github", + "uid": "test-user", + "alias": "test", + }, + { + "provider": "github", + "uid": "test-organization", + "alias": "GitHub Org", + }, + { + "provider": "github", + "uid": "prowler-cloud", + "alias": "Prowler", + }, + { + "provider": "github", + "uid": "microsoft", + "alias": "Microsoft", + }, + { + "provider": "github", + "uid": "a12345678901234567890123456789012345678", + "alias": "Long Username", + }, ] ), ) @@ -1079,6 +1104,42 @@ class TestProviderViewSet: "m365-uid", "uid", ), + ( + { + "provider": "github", + "uid": "-invalid-start", + "alias": "test", + }, + "github-uid", + "uid", + ), + ( + { + "provider": "github", + "uid": "invalid@username", + "alias": "test", + }, + "github-uid", + "uid", + ), + ( + { + "provider": "github", + "uid": "invalid_username", + "alias": "test", + }, + "github-uid", + "uid", + ), + ( + { + "provider": "github", + "uid": "a" * 40, + "alias": "test", + }, + "github-uid", + "uid", + ), ] ), ) diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 5a601ef7c9..0fe81a7ace 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -13,6 +13,7 @@ from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.common.models import Connection from prowler.providers.gcp.gcp_provider import GcpProvider +from prowler.providers.github.github_provider import GithubProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.m365.m365_provider import M365Provider @@ -55,14 +56,21 @@ def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict: def return_prowler_provider( provider: Provider, -) -> [AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider]: +) -> [ + AwsProvider + | AzureProvider + | GcpProvider + | GithubProvider + | KubernetesProvider + | M365Provider +]: """Return the Prowler provider class based on the given provider type. Args: provider (Provider): The provider object containing the provider type and associated secrets. Returns: - AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: The corresponding provider class. + AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: The corresponding provider class. Raises: ValueError: If the provider type specified in `provider.provider` is not supported. @@ -78,6 +86,8 @@ def return_prowler_provider( prowler_provider = KubernetesProvider case Provider.ProviderChoices.M365.value: prowler_provider = M365Provider + case Provider.ProviderChoices.GITHUB.value: + prowler_provider = GithubProvider case _: raise ValueError(f"Provider type {provider.provider} not supported") return prowler_provider @@ -120,7 +130,14 @@ def get_prowler_provider_kwargs( def initialize_prowler_provider( provider: Provider, mutelist_processor: Processor | None = None, -) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: +) -> ( + AwsProvider + | AzureProvider + | GcpProvider + | GithubProvider + | KubernetesProvider + | M365Provider +): """Initialize a Prowler provider instance based on the given provider type. Args: @@ -128,8 +145,8 @@ def initialize_prowler_provider( mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration. Returns: - AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class - (`AwsProvider`, `AzureProvider`, `GcpProvider`, `KubernetesProvider` or `M365Provider`) initialized with the + AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class + (`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `KubernetesProvider` or `M365Provider`) initialized with the provider's secrets. """ prowler_provider = return_prowler_provider(provider) diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 0a1e999fe2..76fa0b4911 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -176,6 +176,43 @@ from rest_framework_json_api import serializers }, "required": ["kubeconfig_content"], }, + { + "type": "object", + "title": "GitHub Personal Access Token", + "properties": { + "personal_access_token": { + "type": "string", + "description": "GitHub personal access token for authentication.", + } + }, + "required": ["personal_access_token"], + }, + { + "type": "object", + "title": "GitHub OAuth App Token", + "properties": { + "oauth_app_token": { + "type": "string", + "description": "GitHub OAuth App token for authentication.", + } + }, + "required": ["oauth_app_token"], + }, + { + "type": "object", + "title": "GitHub App Credentials", + "properties": { + "github_app_id": { + "type": "integer", + "description": "GitHub App ID for authentication.", + }, + "github_app_key": { + "type": "string", + "description": "Path to the GitHub App private key file.", + }, + }, + "required": ["github_app_id", "github_app_key"], + }, ] } ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index d19a68f61d..6061cf99fe 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1217,6 +1217,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): serializer = AzureProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.GCP.value: serializer = GCPProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.GITHUB.value: + serializer = GithubProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.KUBERNETES.value: serializer = KubernetesProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.M365.value: @@ -1296,6 +1298,16 @@ class KubernetesProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class GithubProviderSecret(serializers.Serializer): + personal_access_token = serializers.CharField(required=False) + oauth_app_token = serializers.CharField(required=False) + github_app_id = serializers.IntegerField(required=False) + github_app_key_content = serializers.CharField(required=False) + + class Meta: + resource_name = "provider-secrets" + + class AWSRoleAssumptionProviderSecret(serializers.Serializer): role_arn = serializers.CharField() external_id = serializers.CharField() diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index f4b389d8f0..78026a5875 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -20,6 +20,7 @@ from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected im from prowler.lib.outputs.compliance.cis.cis_aws import AWSCIS from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS +from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS @@ -93,6 +94,9 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "prowler_threatscore_m365", ProwlerThreatScoreM365), (lambda name: name.startswith("iso27001_"), M365ISO27001), ], + "github": [ + (lambda name: name.startswith("cis_"), GithubCIS), + ], } diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 8ed58cec41..1883fe67f9 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 - Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) +- Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) --- diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 9cabffec86..74baa752ec 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -353,6 +353,8 @@ class Finding(BaseModel): finding.region = resource.region # Azure, GCP specified field finding.location = resource.region + # GitHub specified field + finding.owner = resource.region # K8s specified field if provider.type == "kubernetes": finding.namespace = resource.region.removeprefix("namespace: ") diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py index df6c8ee046..30d3ed865a 100644 --- a/prowler/providers/github/github_provider.py +++ b/prowler/providers/github/github_provider.py @@ -99,6 +99,7 @@ class GithubProvider(Provider): personal_access_token: str = "", oauth_app_token: str = "", github_app_key: str = "", + github_app_key_content: str = "", github_app_id: int = 0, # Provider configuration config_path: str = None, @@ -114,6 +115,7 @@ class GithubProvider(Provider): personal_access_token (str): GitHub personal access token. oauth_app_token (str): GitHub OAuth App token. github_app_key (str): GitHub App key. + github_app_key_content (str): GitHub App key content. github_app_id (int): GitHub App ID. config_path (str): Path to the audit configuration file. config_content (dict): Audit configuration content. @@ -128,6 +130,7 @@ class GithubProvider(Provider): oauth_app_token, github_app_id, github_app_key, + github_app_key_content, ) # Set the authentication method From cbb5b21e6c4ff83d847882ba962e716684b3a65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:49:23 +0200 Subject: [PATCH 09/28] chore(gha): e2e tests pipeline with API services (#8338) --- .github/workflows/ui-pull-request.yml | 51 +++++++++++++++++-- .../backend/api/fixtures/dev/0_dev_users.json | 13 +++++ .../api/fixtures/dev/1_dev_tenants.json | 19 +++++++ .../backend/api/fixtures/dev/6_dev_rbac.json | 27 ++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ui-pull-request.yml b/.github/workflows/ui-pull-request.yml index e82b892f48..c67014dc73 100644 --- a/.github/workflows/ui-pull-request.yml +++ b/.github/workflows/ui-pull-request.yml @@ -52,11 +52,49 @@ jobs: AUTH_SECRET: 'fallback-ci-secret-for-testing' AUTH_TRUST_HOST: true NEXTAUTH_URL: http://localhost:3000 + PROWLER_API_PORT: 8080 + NEXT_PUBLIC_API_BASE_URL: http://localhost:8080/api/v1 steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: Start needed services with docker compose + run: | + docker compose up -d api worker worker-beat + - name: Wait for prowler-api to respond + run: | + echo "Waiting for prowler-api..." + for i in {1..30}; do + if curl -s http://localhost:${PROWLER_API_PORT}/api/v1/docs >/dev/null 2>&1; then + echo "Prowler API is ready!" + break + fi + echo "Waiting for prowler-api... (attempt $i/30)" + sleep 3 + done + - name: Run database migrations + run: | + echo "Running Django migrations..." + docker compose exec -T api sh -c ' + poetry run python manage.py migrate --database admin + ' + echo "Database migrations completed!" + - name: Copy local fixtures into API container + run: | + docker cp ./api/src/backend/api/fixtures/dev/. prowler-api-1:/home/prowler/backend/api/fixtures/dev + - name: Load database fixtures for e2e tests + run: | + docker compose exec -T api sh -c ' + echo "Loading all fixtures from api/fixtures/dev/..." + for fixture in api/fixtures/dev/*.json; do + if [ -f "$fixture" ]; then + echo "Loading $fixture" + poetry run python manage.py loaddata "$fixture" --database admin + fi + done + echo "All database fixtures loaded successfully!" + ' - name: Setup Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: @@ -66,6 +104,9 @@ jobs: - name: Install dependencies working-directory: ./ui run: npm ci + - name: Build the application + working-directory: ./ui + run: npm run build - name: Cache Playwright browsers uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: playwright-cache @@ -78,9 +119,6 @@ jobs: working-directory: ./ui if: steps.playwright-cache.outputs.cache-hit != 'true' run: npm run test:e2e:install - - name: Build the application - working-directory: ./ui - run: npm run build - name: Run Playwright tests working-directory: ./ui run: npm run test:e2e @@ -92,6 +130,13 @@ jobs: path: ui/playwright-report/ retention-days: 30 + - name: Cleanup services + if: always() + run: | + echo "Shutting down services..." + docker-compose down -v || true + echo "Cleanup completed" + test-container-build: runs-on: ubuntu-latest steps: diff --git a/api/src/backend/api/fixtures/dev/0_dev_users.json b/api/src/backend/api/fixtures/dev/0_dev_users.json index 61fb0bd883..bd61994982 100644 --- a/api/src/backend/api/fixtures/dev/0_dev_users.json +++ b/api/src/backend/api/fixtures/dev/0_dev_users.json @@ -24,5 +24,18 @@ "is_active": true, "date_joined": "2024-09-18T09:04:20.850Z" } + }, + { + "model": "api.user", + "pk": "6d4f8a91-3c2e-4b5a-8f7d-1e9c5b2a4d6f", + "fields": { + "password": "pbkdf2_sha256$870000$Z63pGJ7nre48hfcGbk5S0O$rQpKczAmijs96xa+gPVJifpT3Fetb8DOusl5Eq6gxac=", + "last_login": null, + "name": "E2E Test User", + "email": "e2e@prowler.com", + "company_name": "Prowler E2E Tests", + "is_active": true, + "date_joined": "2024-01-01T00:00:00.850Z" + } } ] diff --git a/api/src/backend/api/fixtures/dev/1_dev_tenants.json b/api/src/backend/api/fixtures/dev/1_dev_tenants.json index dd89b100d4..1b5cdfa314 100644 --- a/api/src/backend/api/fixtures/dev/1_dev_tenants.json +++ b/api/src/backend/api/fixtures/dev/1_dev_tenants.json @@ -46,5 +46,24 @@ "role": "member", "date_joined": "2024-09-19T11:03:59.712Z" } + }, + { + "model": "api.tenant", + "pk": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b", + "fields": { + "inserted_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "name": "E2E Test Tenant" + } + }, + { + "model": "api.membership", + "pk": "9b1a2c3d-4e5f-6789-abc1-23456789def0", + "fields": { + "user": "6d4f8a91-3c2e-4b5a-8f7d-1e9c5b2a4d6f", + "tenant": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b", + "role": "owner", + "date_joined": "2024-01-01T00:00:00.000Z" + } } ] diff --git a/api/src/backend/api/fixtures/dev/6_dev_rbac.json b/api/src/backend/api/fixtures/dev/6_dev_rbac.json index 84ce439fa5..ccd1816301 100644 --- a/api/src/backend/api/fixtures/dev/6_dev_rbac.json +++ b/api/src/backend/api/fixtures/dev/6_dev_rbac.json @@ -149,5 +149,32 @@ "user": "8b38e2eb-6689-4f1e-a4ba-95b275130200", "inserted_at": "2024-11-20T15:36:14.302Z" } + }, + { + "model": "api.role", + "pk": "a5b6c7d8-9e0f-1234-5678-90abcdef1234", + "fields": { + "tenant": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b", + "name": "e2e_admin", + "manage_users": true, + "manage_account": true, + "manage_billing": true, + "manage_providers": true, + "manage_integrations": true, + "manage_scans": true, + "unlimited_visibility": true, + "inserted_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-01T00:00:00.000Z" + } + }, + { + "model": "api.userrolerelationship", + "pk": "f1e2d3c4-b5a6-9876-5432-10fedcba9876", + "fields": { + "tenant": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b", + "role": "a5b6c7d8-9e0f-1234-5678-90abcdef1234", + "user": "6d4f8a91-3c2e-4b5a-8f7d-1e9c5b2a4d6f", + "inserted_at": "2024-01-01T00:00:00.000Z" + } } ] From ab2d57554a6ee41a54b99267c787870c3ee8f51d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:53:32 +0200 Subject: [PATCH 10/28] chore(deps): bump form-data from 4.0.3 to 4.0.4 in /ui (#8346) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 326bf8717a..611002a0b5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10075,9 +10075,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", From 3840e40870cd57d4de386c40b94715a5df463865 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:04:54 +0200 Subject: [PATCH 11/28] test(e2e): Sign-in (#8337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: César Arroba --- ui/next.config.js | 6 +- ui/playwright.config.ts | 11 +- ui/tests/auth-login.spec.ts | 207 ++++++++++++++++++++++++++++++++++++ ui/tests/example.spec.ts | 14 --- ui/tests/helpers.ts | 136 +++++++++++++++++++++++ 5 files changed, 356 insertions(+), 18 deletions(-) create mode 100644 ui/tests/auth-login.spec.ts delete mode 100644 ui/tests/example.spec.ts create mode 100644 ui/tests/helpers.ts diff --git a/ui/next.config.js b/ui/next.config.js index af64900d4c..9b001370c6 100644 --- a/ui/next.config.js +++ b/ui/next.config.js @@ -15,7 +15,11 @@ const cspHeader = ` module.exports = { poweredByHeader: false, - output: "standalone", + // Use standalone only in production deployments, not for CI/testing + ...(process.env.NODE_ENV === "production" && + !process.env.CI && { + output: "standalone", + }), async headers() { return [ { diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index eab99e3c63..a31621d537 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -24,11 +24,16 @@ export default defineConfig({ ], webServer: { - command: process.env.CI - ? "npm run build && npm run start:standalone" - : "npm run dev", + command: process.env.CI ? "npm run start" : "npm run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, timeout: 120 * 1000, + env: { + NEXT_PUBLIC_API_BASE_URL: + process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080/api/v1", + AUTH_SECRET: process.env.AUTH_SECRET || "fallback-ci-secret-for-testing", + AUTH_TRUST_HOST: process.env.AUTH_TRUST_HOST || "true", + NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000", + }, }, }); diff --git a/ui/tests/auth-login.spec.ts b/ui/tests/auth-login.spec.ts new file mode 100644 index 0000000000..c1764a3b28 --- /dev/null +++ b/ui/tests/auth-login.spec.ts @@ -0,0 +1,207 @@ +import { test, expect } from "@playwright/test"; +import { + goToLogin, + goToSignUp, + fillLoginForm, + submitLoginForm, + login, + verifySuccessfulLogin, + verifyLoginError, + verifyLoginFormElements, + verifyDashboardRoute, + toggleSamlMode, + verifySamlModeActive, + goBackFromSaml, + verifyNormalModeActive, + logout, + verifyLogoutSuccess, + waitForPageLoad, + TEST_CREDENTIALS, + ERROR_MESSAGES, + URLS, + verifyLoadingState, +} from "./helpers"; + +test.describe("Login Flow", () => { + test.beforeEach(async ({ page }) => { + await goToLogin(page); + }); + + test("should display login form elements", async ({ page }) => { + await verifyLoginFormElements(page); + }); + + test("should successfully login with valid credentials", async ({ page }) => { + await login(page, TEST_CREDENTIALS.VALID); + await verifySuccessfulLogin(page); + await verifyDashboardRoute(page); + }); + + test("should show error message with invalid credentials", async ({ + page, + }) => { + // Attempt login with invalid credentials + await login(page, TEST_CREDENTIALS.INVALID); + await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS); + }); + + test("should handle empty form submission", async ({ page }) => { + // Submit empty form + await submitLoginForm(page); + await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS); + // Verify we're still on login page + await expect(page).toHaveURL(URLS.LOGIN); + }); + + /* + TODO: This test is failing, need UI work before. + test("should validate email format", async ({ page }) => { + // Attempt login with invalid email format + await login(page, TEST_CREDENTIALS.INVALID_EMAIL_FORMAT); + // Verify error message (application shows generic error for invalid email format too) + await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS); + // Verify we're still on login page + await expect(page).toHaveURL(URLS.LOGIN); + }); + */ + + test("should toggle SAML SSO mode", async ({ page }) => { + // Toggle to SAML mode + await toggleSamlMode(page); + await verifySamlModeActive(page); + // Toggle back to normal mode + await goBackFromSaml(page); + await verifyNormalModeActive(page); + }); + + test("should show loading state during form submission", async ({ page }) => { + // Fill valid credentials + await fillLoginForm( + page, + TEST_CREDENTIALS.VALID.email, + TEST_CREDENTIALS.VALID.password, + ); + // Submit form and verify loading state + await submitLoginForm(page); + // Verify loading state + await verifyLoadingState(page); + }); + + test("should handle SAML authentication flow", async ({ page }) => { + // Enter email for SAML + const samlEmail = "user@saml-domain.com"; + // Toggle to SAML mode + await toggleSamlMode(page); + // Fill email (password should be hidden) + await page.getByLabel("Email").fill(samlEmail); + // Submit should trigger SAML redirect (we can't test the actual SAML flow in E2E) + // but we can verify the form submission + await submitLoginForm(page); + + // Note: In a real scenario, this would redirect to IdP + // For testing, we just verify the form was submitted + }); +}); + +test.describe("Session Persistence", () => { + test("should maintain session after browser refresh", async ({ page }) => { + // Login first + await goToLogin(page); + await login(page, TEST_CREDENTIALS.VALID); + await verifySuccessfulLogin(page); + // Refresh the page + await page.reload(); + await waitForPageLoad(page); + // Verify session is maintained + await expect(page).toHaveURL(URLS.DASHBOARD); + await verifyDashboardRoute(page); + // Verify user is not redirected back to login + await expect(page).not.toHaveURL(URLS.LOGIN); + }); + + test("should redirect to login when accessing protected route without session", async ({ + page, + }) => { + // Try to access protected route without login + await page.goto(URLS.DASHBOARD); + // Should be redirected to login page + await expect(page).toHaveURL(URLS.LOGIN); + await expect(page.getByText("Sign in", { exact: true })).toBeVisible(); + }); + + test("should logout successfully", async ({ page }) => { + // Login first + await goToLogin(page); + await login(page, TEST_CREDENTIALS.VALID); + await verifySuccessfulLogin(page); + // Logout + await logout(page); + await verifyLogoutSuccess(page); + // Verify cannot access protected route after logout + await page.goto(URLS.DASHBOARD); + await expect(page).toHaveURL(URLS.LOGIN); + }); + + test("should handle session timeout gracefully", async ({ page }) => { + // Login first + await goToLogin(page); + await login(page, TEST_CREDENTIALS.VALID); + await verifySuccessfulLogin(page); + // Simulate session timeout by clearing cookies + await page.context().clearCookies(); + // Try to navigate to a protected route + await page.goto(URLS.PROFILE); + // Should be redirected to login + await expect(page).toHaveURL(URLS.LOGIN); + }); +}); + +test.describe("Navigation", () => { + test("should navigate to sign up page", async ({ page }) => { + await goToLogin(page); + await page.getByRole("link", { name: "Sign up" }).click(); + await expect(page).toHaveURL(URLS.SIGNUP); + }); + + test("should navigate from sign up back to sign in", async ({ page }) => { + await goToSignUp(page); + await page.getByRole("link", { name: "Log in" }).click(); + await expect(page).toHaveURL(URLS.LOGIN); + await expect(page.getByText("Sign in", { exact: true })).toBeVisible(); + }); + + test("should handle browser back button correctly", async ({ page }) => { + await goToLogin(page); + await page.getByRole("link", { name: "Sign up" }).click(); + await expect(page).toHaveURL(URLS.SIGNUP); + await page.goBack(); + await expect(page).toHaveURL(URLS.LOGIN); + await expect(page.getByText("Sign in", { exact: true })).toBeVisible(); + }); +}); + +test.describe("Accessibility", () => { + test.beforeEach(async ({ page }) => { + await goToLogin(page); + }); + + test("should be navigable with keyboard", async ({ page }) => { + // Tab through form elements + await page.keyboard.press("Tab"); // Toggle theme + await page.keyboard.press("Tab"); // Email field + await expect(page.getByLabel("Email")).toBeFocused(); + + await page.keyboard.press("Tab"); // Password field + await expect(page.getByLabel("Password")).toBeFocused(); + + await page.keyboard.press("Tab"); // Show password button + await page.keyboard.press("Tab"); // Login button + await expect(page.getByRole("button", { name: "Log in" })).toBeFocused(); + }); + + test("should have proper ARIA labels", async ({ page }) => { + await expect(page.getByRole("textbox", { name: "Email" })).toBeVisible(); + await expect(page.getByRole("textbox", { name: "Password" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Log in" })).toBeVisible(); + }); +}); diff --git a/ui/tests/example.spec.ts b/ui/tests/example.spec.ts deleted file mode 100644 index 4b8d55810b..0000000000 --- a/ui/tests/example.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("has title", async ({ page }) => { - await page.goto("https://playwright.dev/"); - await expect(page).toHaveTitle(/Playwright/); -}); - -test("get started link", async ({ page }) => { - await page.goto("https://playwright.dev/"); - await page.getByRole("link", { name: "Get started" }).click(); - await expect( - page.getByRole("heading", { name: "Installation" }), - ).toBeVisible(); -}); diff --git a/ui/tests/helpers.ts b/ui/tests/helpers.ts new file mode 100644 index 0000000000..e807c595fd --- /dev/null +++ b/ui/tests/helpers.ts @@ -0,0 +1,136 @@ +import { Page, expect } from "@playwright/test"; + +export const ERROR_MESSAGES = { + INVALID_CREDENTIALS: "Invalid email or password", +} as const; + +export const URLS = { + LOGIN: "/sign-in", + SIGNUP: "/sign-up", + DASHBOARD: "/", + PROFILE: "/profile", +} as const; + +export const TEST_CREDENTIALS = { + VALID: { + email: "e2e@prowler.com", + password: "Thisisapassword123@", + }, + INVALID: { + email: "invalid@example.com", + password: "wrongPassword", + }, + INVALID_EMAIL_FORMAT: { + email: "invalid-email", + password: "somepassword", + }, +} as const; + +export async function goToLogin(page: Page) { + await page.goto("/sign-in"); +} + +export async function goToSignUp(page: Page) { + await page.goto("/sign-up"); +} + +export async function fillLoginForm( + page: Page, + email: string, + password: string, +) { + await page.getByLabel("Email").fill(email); + await page.getByLabel("Password").fill(password); +} + +export async function submitLoginForm(page: Page) { + await page.getByRole("button", { name: "Log in" }).click(); +} + +export async function login( + page: Page, + credentials: { email: string; password: string } = TEST_CREDENTIALS.VALID, +) { + await fillLoginForm(page, credentials.email, credentials.password); + await submitLoginForm(page); +} + +export async function verifySuccessfulLogin(page: Page) { + await expect(page).toHaveURL("/"); + await expect(page.locator("main")).toBeVisible(); + await expect( + page + .getByLabel("Breadcrumbs") + .getByRole("heading", { name: "Overview", exact: true }), + ).toBeVisible(); +} + +export async function verifyLoginError( + page: Page, + errorMessage = "Invalid email or password", +) { + await expect(page.getByText(errorMessage)).toBeVisible(); + await expect(page).toHaveURL("/sign-in"); +} + +export async function toggleSamlMode(page: Page) { + await page.getByText("Continue with SAML SSO").click(); +} + +export async function goBackFromSaml(page: Page) { + await page.getByText("Back").click(); +} + +export async function verifySamlModeActive(page: Page) { + await expect(page.getByText("Sign in with SAML SSO")).toBeVisible(); + await expect(page.getByLabel("Password")).not.toBeVisible(); + await expect(page.getByText("Back")).toBeVisible(); +} + +export async function verifyNormalModeActive(page: Page) { + await expect(page.getByText("Sign in", { exact: true })).toBeVisible(); + await expect(page.getByLabel("Password")).toBeVisible(); +} + +export async function logout(page: Page) { + await page.getByRole("button", { name: "Sign out" }).click(); +} + +export async function verifyLogoutSuccess(page: Page) { + await expect(page).toHaveURL("/sign-in"); + await expect(page.getByText("Sign in", { exact: true })).toBeVisible(); +} + +export async function verifyLoadingState(page: Page) { + const submitButton = page.getByRole("button", { name: "Log in" }); + await expect(submitButton).toHaveAttribute("aria-disabled", "true"); + await expect(page.getByText("Loading")).toBeVisible(); +} + +export async function verifyLoginFormElements(page: Page) { + await expect(page).toHaveTitle(/Prowler/); + await expect(page.locator('svg[width="300"]')).toBeVisible(); + + // Verify form elements + await expect(page.getByText("Sign in", { exact: true })).toBeVisible(); + await expect(page.getByLabel("Email")).toBeVisible(); + await expect(page.getByLabel("Password")).toBeVisible(); + await expect(page.getByRole("button", { name: "Log in" })).toBeVisible(); + + // Verify OAuth buttons + await expect(page.getByText("Continue with Google")).toBeVisible(); + await expect(page.getByText("Continue with Github")).toBeVisible(); + await expect(page.getByText("Continue with SAML SSO")).toBeVisible(); + + // Verify navigation links + await expect(page.getByText("Need to create an account?")).toBeVisible(); + await expect(page.getByRole("link", { name: "Sign up" })).toBeVisible(); +} + +export async function waitForPageLoad(page: Page) { + await page.waitForLoadState("networkidle"); +} + +export async function verifyDashboardRoute(page: Page) { + await expect(page).toHaveURL("/"); +} From 676cc44fe2d7cfe3c0274f8766ce13d43eeb9bbd Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:44:28 +0200 Subject: [PATCH 12/28] feat: env keys behavior updated (#8348) --- .github/workflows/ui-pull-request.yml | 11 +++++++++-- ui/playwright.config.ts | 2 ++ ui/tests/helpers.ts | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ui-pull-request.yml b/.github/workflows/ui-pull-request.yml index c67014dc73..ad22630e32 100644 --- a/.github/workflows/ui-pull-request.yml +++ b/.github/workflows/ui-pull-request.yml @@ -53,16 +53,20 @@ jobs: AUTH_TRUST_HOST: true NEXTAUTH_URL: http://localhost:3000 PROWLER_API_PORT: 8080 - NEXT_PUBLIC_API_BASE_URL: http://localhost:8080/api/v1 + NEXT_PUBLIC_API_BASE_URL: ${{ secrets.API_BASE_URL || 'http://localhost:8080/api/v1' }} + E2E_USER: ${{ secrets.E2E_USER }} + E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }} steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Start needed services with docker compose + if: github.repository == 'prowler-cloud/prowler' run: | docker compose up -d api worker worker-beat - name: Wait for prowler-api to respond + if: github.repository == 'prowler-cloud/prowler' run: | echo "Waiting for prowler-api..." for i in {1..30}; do @@ -74,6 +78,7 @@ jobs: sleep 3 done - name: Run database migrations + if: github.repository == 'prowler-cloud/prowler' run: | echo "Running Django migrations..." docker compose exec -T api sh -c ' @@ -81,9 +86,11 @@ jobs: ' echo "Database migrations completed!" - name: Copy local fixtures into API container + if: github.repository == 'prowler-cloud/prowler' run: | docker cp ./api/src/backend/api/fixtures/dev/. prowler-api-1:/home/prowler/backend/api/fixtures/dev - name: Load database fixtures for e2e tests + if: github.repository == 'prowler-cloud/prowler' run: | docker compose exec -T api sh -c ' echo "Loading all fixtures from api/fixtures/dev/..." @@ -131,7 +138,7 @@ jobs: retention-days: 30 - name: Cleanup services - if: always() + if: github.repository == 'prowler-cloud/prowler' run: | echo "Shutting down services..." docker-compose down -v || true diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index a31621d537..8fd6bc0dd4 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -34,6 +34,8 @@ export default defineConfig({ AUTH_SECRET: process.env.AUTH_SECRET || "fallback-ci-secret-for-testing", AUTH_TRUST_HOST: process.env.AUTH_TRUST_HOST || "true", NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000", + E2E_USER: process.env.E2E_USER || "e2e@prowler.com", + E2E_PASSWORD: process.env.E2E_PASSWORD || "Thisisapassword123@", }, }, }); diff --git a/ui/tests/helpers.ts b/ui/tests/helpers.ts index e807c595fd..cb2e8d2857 100644 --- a/ui/tests/helpers.ts +++ b/ui/tests/helpers.ts @@ -13,8 +13,8 @@ export const URLS = { export const TEST_CREDENTIALS = { VALID: { - email: "e2e@prowler.com", - password: "Thisisapassword123@", + email: process.env.E2E_USER || "e2e@prowler.com", + password: process.env.E2E_PASSWORD || "Thisisapassword123@", }, INVALID: { email: "invalid@example.com", From a69d0d16c0bfdd9898e3d404fa12ada229da145e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Wed, 23 Jul 2025 11:11:04 +0200 Subject: [PATCH 13/28] fix(azure/storage): handle when Azure API set values to None (#8325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pedro Martín Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 7 +++ .../azure/services/storage/storage_service.py | 43 +++++++++++++++---- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 1883fe67f9..86f91170bb 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -10,6 +10,13 @@ All notable changes to the **Prowler SDK** are documented in this file. --- +## [v5.9.3] (Prowler UNRELEASED) + +### Fixed +- Add more validations to Azure Storage models when some values are None to avoid serialization issues [(#8325)](https://github.com/prowler-cloud/prowler/pull/8325) + +--- + ## [v5.9.2] (Prowler v5.9.2) ### Fixed diff --git a/prowler/providers/azure/services/storage/storage_service.py b/prowler/providers/azure/services/storage/storage_service.py index 887fb80b7b..e93b920380 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -70,17 +70,44 @@ class Storage(AzureService): ], key_expiration_period_in_days=key_expiration_period_in_days, location=storage_account.location, - default_to_entra_authorization=getattr( - storage_account, - "default_to_o_auth_authentication", - False, + default_to_entra_authorization=( + False + if getattr( + storage_account, + "default_to_o_auth_authentication", + False, + ) + is None + else getattr( + storage_account, + "default_to_o_auth_authentication", + False, + ) ), replication_settings=replication_settings, - allow_cross_tenant_replication=getattr( - storage_account, "allow_cross_tenant_replication", True + allow_cross_tenant_replication=( + True + if getattr( + storage_account, + "allow_cross_tenant_replication", + True, + ) + is None + else getattr( + storage_account, + "allow_cross_tenant_replication", + True, + ) ), - allow_shared_key_access=getattr( - storage_account, "allow_shared_key_access", True + allow_shared_key_access=( + True + if getattr( + storage_account, "allow_shared_key_access", True + ) + is None + else getattr( + storage_account, "allow_shared_key_access", True + ) ), ) ) From 922f9d2f91eca4d86adfd84530686048574a4a2f Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 23 Jul 2025 17:43:42 +0800 Subject: [PATCH 14/28] docs(gcp): update GCP permissions (#8350) --- docs/getting-started/requirements.md | 3 +-- docs/tutorials/gcp/authentication.md | 5 ++--- docs/tutorials/gcp/organization.md | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md index d2be93c630..eb623f5f28 100644 --- a/docs/getting-started/requirements.md +++ b/docs/getting-started/requirements.md @@ -109,13 +109,12 @@ Prowler will follow the same credentials search as [Google authentication librar Prowler for Google Cloud needs the following permissions to be set: -- **Viewer (`roles/viewer`) IAM role**: granted at the project / folder / org level in order to scan the target projects +- **Reader (`roles/reader`) IAM role**: granted at the project / folder / org level in order to scan the target projects - **Project level settings**: you need to have at least one project with the below settings: - Identity and Access Management (IAM) API (`iam.googleapis.com`) enabled by either using the [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics) or by using the gcloud CLI `gcloud services enable iam.googleapis.com --project ` command - - Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`) IAM role - Set the quota project to be this project by either running `gcloud auth application-default set-quota-project ` or by setting an environment variable: `export GOOGLE_CLOUD_QUOTA_PROJECT=` diff --git a/docs/tutorials/gcp/authentication.md b/docs/tutorials/gcp/authentication.md index 971796a753..da4104041d 100644 --- a/docs/tutorials/gcp/authentication.md +++ b/docs/tutorials/gcp/authentication.md @@ -51,7 +51,7 @@ Prowler follows the same search order as [Google authentication libraries](https ???+ note The credentials must belong to a user or service account with the necessary permissions. - To ensure full access, assign the roles/viewer IAM role to the identity being used. + To ensure full access, assign the roles/reader IAM role to the identity being used. ???+ note Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks. @@ -63,13 +63,12 @@ Prowler follows the same search order as [Google authentication libraries](https Prowler for Google Cloud needs the following permissions to be set: -- **Viewer (`roles/viewer`) IAM role**: granted at the project / folder / org level in order to scan the target projects +- **Reader (`roles/reader`) IAM role**: granted at the project / folder / org level in order to scan the target projects - **Project level settings**: you need to have at least one project with the below settings: - Identity and Access Management (IAM) API (`iam.googleapis.com`) enabled by either using the [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics) or by using the gcloud CLI `gcloud services enable iam.googleapis.com --project ` command - - Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`) IAM role - Set the quota project to be this project by either running `gcloud auth application-default set-quota-project ` or by setting an environment variable: `export GOOGLE_CLOUD_QUOTA_PROJECT=` diff --git a/docs/tutorials/gcp/organization.md b/docs/tutorials/gcp/organization.md index 43a58aa7b9..272fd6b7c6 100644 --- a/docs/tutorials/gcp/organization.md +++ b/docs/tutorials/gcp/organization.md @@ -9,7 +9,7 @@ prowler gcp --organization-id organization-id ``` ???+ warning - Make sure that the used credentials have the role Cloud Asset Viewer (`roles/cloudasset.viewer`) or Cloud Asset Owner (`roles/cloudasset.owner`) on the organization level. + Make sure that the used credentials have a role with the `cloudasset.assets.listResource` permission on the organization level like `roles/cloudasset.viewer` (Cloud Asset Viewer) or `roles/cloudasset.owner` (Cloud Asset Owner). ???+ note With this option, Prowler retrieves all projects within the specified organization, including those organized in folders and nested subfolders. This ensures that every project under the organization’s hierarchy is scanned, providing full visibility across the entire organization. From a6c88c0d9ee281f7ce55b2a15ccc0ba453383d61 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:11:32 +0200 Subject: [PATCH 15/28] test: timeout updated for E2E (#8351) --- ui/playwright.config.ts | 3 +++ ui/tests/auth-login.spec.ts | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 8fd6bc0dd4..a680feabdb 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -8,6 +8,9 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: [["list"]], outputDir: "/tmp/playwright-tests", + expect: { + timeout: 20000, + }, use: { baseURL: "http://localhost:3000", diff --git a/ui/tests/auth-login.spec.ts b/ui/tests/auth-login.spec.ts index c1764a3b28..fc612a71a5 100644 --- a/ui/tests/auth-login.spec.ts +++ b/ui/tests/auth-login.spec.ts @@ -196,6 +196,11 @@ test.describe("Accessibility", () => { await page.keyboard.press("Tab"); // Show password button await page.keyboard.press("Tab"); // Login button + + if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") { + await page.keyboard.press("Tab"); // Forgot password + } + await expect(page.getByRole("button", { name: "Log in" })).toBeFocused(); }); From 83b328ea92ec3951f1e6120c6caf9d7c3f009148 Mon Sep 17 00:00:00 2001 From: Kay Agahd Date: Wed, 23 Jul 2025 15:03:02 +0200 Subject: [PATCH 16/28] fix(aws): avoid false positives in SQS encryption check for ephemeral queues (#8330) Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> --- prowler/CHANGELOG.md | 3 +++ prowler/providers/aws/services/sqs/sqs_service.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 86f91170bb..bbb3ed5515 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) - Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) +### Fixed +- False positives in SQS encryption check for ephemeral queues [(#8330)](https://github.com/prowler-cloud/prowler/pull/8330) + --- ## [v5.9.3] (Prowler UNRELEASED) diff --git a/prowler/providers/aws/services/sqs/sqs_service.py b/prowler/providers/aws/services/sqs/sqs_service.py index 598cd625a4..9e6bb0c230 100644 --- a/prowler/providers/aws/services/sqs/sqs_service.py +++ b/prowler/providers/aws/services/sqs/sqs_service.py @@ -51,6 +51,7 @@ class SQS(AWSService): def _get_queue_attributes(self): try: logger.info("SQS - describing queue attributes...") + valid_queues = [] for queue in self.queues: try: regional_client = self.regional_clients[queue.region] @@ -72,6 +73,7 @@ class SQS(AWSService): == "true" ): queue.kms_key_id = "SqsManagedSseEnabled" + valid_queues.append(queue) except ClientError as error: if ( error.response["Error"]["Code"] @@ -84,10 +86,13 @@ class SQS(AWSService): logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + valid_queues.append(queue) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + valid_queues.append(queue) + self.queues = valid_queues except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" From 5669a42039974136ff4bc0bbd0c4a07efb00602d Mon Sep 17 00:00:00 2001 From: Cole Murray Date: Wed, 23 Jul 2025 07:06:55 -0700 Subject: [PATCH 17/28] fix(wazuh): patch command injection vulnerability in prowler-wrapper.py (#8331) Co-authored-by: Test User Co-authored-by: MrCloudSec --- contrib/wazuh/prowler-wrapper.py | 9 +- .../wazuh/prowler_wrapper_security_test.py | 256 ++++++++++++++++++ 2 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 tests/contrib/wazuh/prowler_wrapper_security_test.py diff --git a/contrib/wazuh/prowler-wrapper.py b/contrib/wazuh/prowler-wrapper.py index a76344a00b..e7e7faa9af 100644 --- a/contrib/wazuh/prowler-wrapper.py +++ b/contrib/wazuh/prowler-wrapper.py @@ -23,6 +23,7 @@ import argparse import json import os import re +import shlex import signal import socket import subprocess @@ -145,11 +146,11 @@ def _get_script_arguments(): def _run_prowler(prowler_args): _debug("Running prowler with args: {0}".format(prowler_args), 1) - _prowler_command = "{prowler}/prowler {args}".format( - prowler=PATH_TO_PROWLER, args=prowler_args + _prowler_command = shlex.split( + "{prowler}/prowler {args}".format(prowler=PATH_TO_PROWLER, args=prowler_args) ) - _debug("Running command: {0}".format(_prowler_command), 2) - _process = subprocess.Popen(_prowler_command, stdout=subprocess.PIPE, shell=True) + _debug("Running command: {0}".format(" ".join(_prowler_command)), 2) + _process = subprocess.Popen(_prowler_command, stdout=subprocess.PIPE) _output, _error = _process.communicate() _debug("Raw prowler output: {0}".format(_output), 3) _debug("Raw prowler error: {0}".format(_error), 3) diff --git a/tests/contrib/wazuh/prowler_wrapper_security_test.py b/tests/contrib/wazuh/prowler_wrapper_security_test.py new file mode 100644 index 0000000000..0f10144168 --- /dev/null +++ b/tests/contrib/wazuh/prowler_wrapper_security_test.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python +""" +Security test for prowler-wrapper.py command injection vulnerability +This test demonstrates the command injection vulnerability and validates the fix +""" + +import os +import shutil +import sys +import tempfile +import unittest +from unittest.mock import MagicMock, patch + + +class TestProwlerWrapperSecurity(unittest.TestCase): + """Test cases for command injection vulnerability in prowler-wrapper.py""" + + def setUp(self): + """Set up test environment""" + # Create a temporary directory for testing + self.test_dir = tempfile.mkdtemp() + self.prowler_wrapper_path = os.path.join( + os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + ), + "contrib", + "wazuh", + "prowler-wrapper.py", + ) + + def tearDown(self): + """Clean up test environment""" + shutil.rmtree(self.test_dir, ignore_errors=True) + + def _import_prowler_wrapper(self): + """Helper to import prowler_wrapper with mocked WAZUH_PATH""" + sys.path.insert(0, os.path.dirname(self.prowler_wrapper_path)) + + # Mock the WAZUH_PATH that's read at module level + with patch("builtins.open", create=True) as mock_open: + mock_open.return_value.readline.return_value = 'DIRECTORY="/opt/wazuh"' + + import importlib.util + + spec = importlib.util.spec_from_file_location( + "prowler_wrapper", self.prowler_wrapper_path + ) + prowler_wrapper = importlib.util.module_from_spec(spec) + spec.loader.exec_module(prowler_wrapper) + return prowler_wrapper._run_prowler + + def test_command_injection_semicolon(self): + """Test command injection using semicolon""" + # Create a test file that should not be created if injection is prevented + test_file = os.path.join(self.test_dir, "pwned.txt") + + # Malicious profile that attempts to create a file + malicious_profile = f"test; touch {test_file}" + + # Mock the subprocess.Popen to capture the command + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the vulnerable function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Check that Popen was called + self.assertTrue(mock_popen.called) + + # Get the actual command that was passed to Popen + actual_command = mock_popen.call_args[0][0] + + # With the fix, the command should be a list (from shlex.split) + # and should NOT have shell=True + self.assertIsInstance( + actual_command, list, "Command should be a list after shlex.split" + ) + + # Check that shell=True is not in the call + call_kwargs = mock_popen.call_args[1] + self.assertNotIn( + "shell", + call_kwargs, + "shell parameter should not be present (defaults to False)", + ) + + def test_command_injection_ampersand(self): + """Test command injection using ampersand""" + # Create a test file that should not be created if injection is prevented + test_file = os.path.join(self.test_dir, "pwned2.txt") + + # Malicious profile that attempts to create a file + malicious_profile = f"test && touch {test_file}" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify it's a list (safe execution) + self.assertIsInstance(actual_command, list) + + # The malicious characters should be preserved as part of the argument + # not interpreted as shell commands + command_str = " ".join(actual_command) + self.assertIn( + "&&", + command_str, + "Shell metacharacters should be preserved as literals", + ) + + def test_command_injection_pipe(self): + """Test command injection using pipe""" + malicious_profile = 'test | echo "injected"' + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify safe execution + self.assertIsInstance(actual_command, list) + + # Pipe should be preserved as literal + command_str = " ".join(actual_command) + self.assertIn("|", command_str) + + def test_command_injection_backticks(self): + """Test command injection using backticks""" + malicious_profile = "test `echo injected`" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify safe execution + self.assertIsInstance(actual_command, list) + + # Backticks should be preserved as literals + command_str = " ".join(actual_command) + self.assertIn("`", command_str) + + def test_command_injection_dollar_parentheses(self): + """Test command injection using $() syntax""" + malicious_profile = "test $(echo injected)" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify safe execution + self.assertIsInstance(actual_command, list) + + # $() should be preserved as literals + command_str = " ".join(actual_command) + self.assertIn("$(", command_str) + + def test_legitimate_profile_name(self): + """Test that legitimate profile names still work correctly""" + legitimate_profile = "production-aws-profile" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with legitimate input + result = _run_prowler(f"-p {legitimate_profile} -V") + + # Verify the function returns output + self.assertEqual(result, b"test output") + + # Verify Popen was called correctly + actual_command = mock_popen.call_args[0][0] + self.assertIsInstance(actual_command, list) + + # Check the profile is passed correctly + command_str = " ".join(actual_command) + self.assertIn(legitimate_profile, command_str) + + def test_shlex_split_behavior(self): + """Test that shlex properly handles quoted arguments""" + profile_with_spaces = "my profile name" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with profile containing spaces + _run_prowler(f'-p "{profile_with_spaces}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify it's properly split + self.assertIsInstance(actual_command, list) + + # The profile name should be preserved as a single argument + # despite containing spaces + self.assertIn("my profile name", actual_command) + + +if __name__ == "__main__": + unittest.main() From ad0b8a42081e624e95e08f28e21b72be48135470 Mon Sep 17 00:00:00 2001 From: sumit-tft <70506234+sumit-tft@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:40:51 +0530 Subject: [PATCH 18/28] feat(ui): create CustomLink component and refactor links to use it (#8341) Co-authored-by: alejandrobailo --- ui/CHANGELOG.md | 1 + ui/app/(prowler)/error.tsx | 6 +-- ui/components/auth/oss/auth-form.tsx | 32 +++++------- .../aws-well-architected-details.tsx | 8 ++- .../compliance-custom-details/cis-details.tsx | 4 +- .../mitre-details.tsx | 6 +-- .../shared-components.tsx | 21 -------- .../findings/table/finding-detail.tsx | 16 +++--- .../integrations/forms/saml-config-form.tsx | 8 +-- .../integrations/saml-integration-card.tsx | 11 ++-- ui/components/lighthouse/chat.tsx | 10 ++-- .../forms/muted-findings-config-form.tsx | 10 ++-- .../workflow/forms/test-connection-form.tsx | 7 +-- .../via-credentials/m365-credentials-form.tsx | 18 +++---- .../workflow/provider-title-docs.tsx | 10 ++-- ui/components/ui/custom/custom-link.tsx | 51 +++++++++++++++++++ ui/components/ui/user-nav/user-nav.tsx | 13 +++-- 17 files changed, 124 insertions(+), 108 deletions(-) create mode 100644 ui/components/ui/custom/custom-link.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 647e668191..836f06c391 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Upgrade to Next.js 14.2.30 and lock TypeScript to 5.5.4 for ESLint compatibility [(#8189)](https://github.com/prowler-cloud/prowler/pull/8189) - Improved active step highlighting and updated step titles and descriptions in the Cloud Provider credentials update flow [(#8303)](https://github.com/prowler-cloud/prowler/pull/8303) +- Refactored all existing links across the app to use new custom-link component for consistent styling [(#8341)](https://github.com/prowler-cloud/prowler/pull/8341) ### 🐞 Fixed diff --git a/ui/app/(prowler)/error.tsx b/ui/app/(prowler)/error.tsx index d5083d9d33..f1813e0ede 100644 --- a/ui/app/(prowler)/error.tsx +++ b/ui/app/(prowler)/error.tsx @@ -1,10 +1,10 @@ "use client"; -import Link from "next/link"; import { useEffect } from "react"; import { RocketIcon } from "@/components/icons"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui"; +import { CustomLink } from "@/components/ui/custom/custom-link"; export default function Error({ error, @@ -27,9 +27,9 @@ export default function Error({ We're sorry for the inconvenience. Please try again or contact support if the problem persists. - + Go to the homepage - + ); } diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx index a965204dbc..401a691354 100644 --- a/ui/components/auth/oss/auth-form.tsx +++ b/ui/components/auth/oss/auth-form.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; -import { Button, Checkbox, Divider, Link, Tooltip } from "@nextui-org/react"; +import { Button, Checkbox, Divider, Tooltip } from "@nextui-org/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; @@ -15,6 +15,7 @@ import { NotificationIcon, ProwlerExtended } from "@/components/icons"; import { ThemeSwitch } from "@/components/ThemeSwitch"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form, FormControl, @@ -301,13 +302,12 @@ export const AuthForm = ({ onChange={(e) => field.onChange(e.target.checked)} > I agree with the  - Terms of Service - +  of Prowler @@ -359,13 +359,9 @@ export const AuthForm = ({ content={
Social Login with Google is not enabled.{" "} - + Read the docs - +
} placement="right-start" @@ -392,13 +388,9 @@ export const AuthForm = ({ content={
Social Login with Github is not enabled.{" "} - + Read the docs - +
} placement="right-start" @@ -451,12 +443,16 @@ export const AuthForm = ({ {type === "sign-in" ? (

Need to create an account?  - Sign up + + Sign up +

) : (

Already have an account?  - Log in + + Log in +

)} diff --git a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx index 9317c61485..96ed16a8f1 100644 --- a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx +++ b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx @@ -1,3 +1,4 @@ +import { CustomLink } from "@/components/ui/custom/custom-link"; import { SeverityBadge } from "@/components/ui/table"; import { Requirement } from "@/types/compliance"; @@ -7,7 +8,6 @@ import { ComplianceDetailContainer, ComplianceDetailSection, ComplianceDetailText, - ComplianceLink, } from "./shared-components"; export const AWSWellArchitectedCustomDetails = ({ @@ -75,11 +75,9 @@ export const AWSWellArchitectedCustomDetails = ({ {requirement.implementation_guidance_url && ( - + {requirement.implementation_guidance_url as string} - + )} diff --git a/ui/components/compliance/compliance-custom-details/cis-details.tsx b/ui/components/compliance/compliance-custom-details/cis-details.tsx index f804f8e7d0..e9244d6e57 100644 --- a/ui/components/compliance/compliance-custom-details/cis-details.tsx +++ b/ui/components/compliance/compliance-custom-details/cis-details.tsx @@ -1,5 +1,6 @@ import ReactMarkdown from "react-markdown"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Requirement } from "@/types/compliance"; import { @@ -8,7 +9,6 @@ import { ComplianceDetailContainer, ComplianceDetailSection, ComplianceDetailText, - ComplianceLink, } from "./shared-components"; interface CISDetailsProps { @@ -121,7 +121,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { {processReferences(requirement.references).map( (url: string, index: number) => (
- {url} + {url}
), )} diff --git a/ui/components/compliance/compliance-custom-details/mitre-details.tsx b/ui/components/compliance/compliance-custom-details/mitre-details.tsx index 71eca0d370..747531af3c 100644 --- a/ui/components/compliance/compliance-custom-details/mitre-details.tsx +++ b/ui/components/compliance/compliance-custom-details/mitre-details.tsx @@ -1,3 +1,4 @@ +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Requirement } from "@/types/compliance"; import { @@ -7,7 +8,6 @@ import { ComplianceDetailContainer, ComplianceDetailSection, ComplianceDetailText, - ComplianceLink, } from "./shared-components"; export const MITRECustomDetails = ({ @@ -63,9 +63,9 @@ export const MITRECustomDetails = ({ {requirement.technique_url && ( - + {requirement.technique_url as string} - + )} diff --git a/ui/components/compliance/compliance-custom-details/shared-components.tsx b/ui/components/compliance/compliance-custom-details/shared-components.tsx index 2d1344b19b..416df6cc9c 100644 --- a/ui/components/compliance/compliance-custom-details/shared-components.tsx +++ b/ui/components/compliance/compliance-custom-details/shared-components.tsx @@ -1,26 +1,5 @@ -import Link from "next/link"; - import { cn } from "@/lib/utils"; -export const ComplianceLink = ({ - href, - children, -}: { - href: string; - children: React.ReactNode; -}) => { - return ( - - {children} - - ); -}; - export const ComplianceDetailContainer = ({ children, }: { diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index 06d665658c..00c88e285f 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -1,10 +1,10 @@ "use client"; import { Snippet } from "@nextui-org/react"; -import Link from "next/link"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CustomSection } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { EntityInfoShort, InfoField } from "@/components/ui/entities"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; import { SeverityBadge } from "@/components/ui/table/severity-badge"; @@ -151,15 +151,14 @@ export const FindingDetail = ({ {attributes.check_metadata.remediation.recommendation.text}

{attributes.check_metadata.remediation.recommendation.url && ( - Learn more - + )} @@ -179,13 +178,12 @@ export const FindingDetail = ({ {/* Additional Resources section */} {attributes.check_metadata.remediation.code.other && ( - View documentation - + )} diff --git a/ui/components/integrations/forms/saml-config-form.tsx b/ui/components/integrations/forms/saml-config-form.tsx index ecea01343f..c9e71d628d 100644 --- a/ui/components/integrations/forms/saml-config-form.tsx +++ b/ui/components/integrations/forms/saml-config-form.tsx @@ -1,6 +1,5 @@ "use client"; -import Link from "next/link"; import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { useFormState } from "react-dom"; import { z } from "zod"; @@ -9,6 +8,7 @@ import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; import { AddIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton, CustomServerInput } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { SnippetChip } from "@/components/ui/entities"; import { FormButtons } from "@/components/ui/form"; import { apiBaseUrl } from "@/lib"; @@ -239,9 +239,9 @@ export const SamlConfigForm = ({ assign the user's role. If the role does not exist, one will be created with minimal permissions. You can assign permissions to roles on the{" "} - - Roles - {" "} + + Roles + {" "} page.

diff --git a/ui/components/integrations/saml-integration-card.tsx b/ui/components/integrations/saml-integration-card.tsx index ae3eb6f60d..6ad745aa85 100644 --- a/ui/components/integrations/saml-integration-card.tsx +++ b/ui/components/integrations/saml-integration-card.tsx @@ -1,13 +1,13 @@ "use client"; import { Card, CardBody, CardHeader } from "@nextui-org/react"; -import { Link } from "@nextui-org/react"; import { CheckIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; import { deleteSamlConfig } from "@/actions/integrations"; import { useToast } from "@/components/ui"; import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { SamlConfigForm } from "./forms"; @@ -73,14 +73,9 @@ export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => { ) : ( <> Configure SAML Single Sign-On for secure authentication.{" "} - + Read the docs - + )}

diff --git a/ui/components/lighthouse/chat.tsx b/ui/components/lighthouse/chat.tsx index c31c6a08b9..54ca5604e7 100644 --- a/ui/components/lighthouse/chat.tsx +++ b/ui/components/lighthouse/chat.tsx @@ -1,12 +1,12 @@ "use client"; import { useChat } from "@ai-sdk/react"; -import Link from "next/link"; import { useEffect, useRef } from "react"; import { useForm } from "react-hook-form"; import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown"; import { CustomButton, CustomTextarea } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form } from "@/components/ui/form"; interface SuggestedAction { @@ -138,12 +138,14 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => { ? "Please configure your OpenAI API key to use Lighthouse AI." : "OpenAI API key is invalid. Please update your key to use Lighthouse AI."}

- Configure API Key - + )} diff --git a/ui/components/providers/forms/muted-findings-config-form.tsx b/ui/components/providers/forms/muted-findings-config-form.tsx index 52c5e2c7df..1a5241b330 100644 --- a/ui/components/providers/forms/muted-findings-config-form.tsx +++ b/ui/components/providers/forms/muted-findings-config-form.tsx @@ -1,7 +1,6 @@ "use client"; import { Textarea } from "@nextui-org/react"; -import Link from "next/link"; import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { useFormState } from "react-dom"; @@ -14,6 +13,7 @@ import { import { DeleteIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { FormButtons } from "@/components/ui/form"; import { fontMono } from "@/config/fonts"; import { convertToYaml, parseYamlValidation } from "@/lib/yaml"; @@ -178,13 +178,9 @@ export const MutedFindingsConfigForm = ({
  • Learn more about configuring the Mutelist{" "} - + here - + .
  • diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx index 6fdc6e2215..0ac38c03a1 100644 --- a/ui/components/providers/workflow/forms/test-connection-form.tsx +++ b/ui/components/providers/workflow/forms/test-connection-form.tsx @@ -3,7 +3,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; import { Checkbox } from "@nextui-org/react"; -import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -18,6 +17,7 @@ import { getTask } from "@/actions/task/tasks"; import { CheckIcon, RocketIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form } from "@/components/ui/form"; import { checkTaskStatus } from "@/lib/helper"; import { ProviderType } from "@/types"; @@ -295,8 +295,9 @@ export const TestConnectionForm = ({
    {apiErrorMessage ? ( - Back to providers - + ) : connectionStatus?.error ? ( router.back() : onResetCredentials} diff --git a/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx index 5fa8f2f9a1..13eb61710d 100644 --- a/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx @@ -1,8 +1,8 @@ -import Link from "next/link"; import { Control } from "react-hook-form"; import { InfoIcon } from "@/components/icons"; import { CustomInput } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { M365Credentials } from "@/types"; export const M365CredentialsForm = ({ @@ -57,14 +57,12 @@ export const M365CredentialsForm = ({ {" "} User and password authentication is being deprecated due to Microsoft's on-going MFA enforcement across all tenants (see{" "} - Microsoft docs - + ).

    @@ -76,14 +74,12 @@ export const M365CredentialsForm = ({

    Due to that change, you must only{" "} - use application authentication - {" "} + {" "} to maintain all Prowler M365 scan capabilities.

    {getProviderHelpText(providerType as string).text}

    - Read the docs - + ); diff --git a/ui/components/ui/custom/custom-link.tsx b/ui/components/ui/custom/custom-link.tsx new file mode 100644 index 0000000000..d52fbeaca2 --- /dev/null +++ b/ui/components/ui/custom/custom-link.tsx @@ -0,0 +1,51 @@ +import Link from "next/link"; +import React from "react"; + +import { cn } from "@/lib"; + +interface CustomLinkProps + extends React.AnchorHTMLAttributes { + href: string; + target?: "_self" | "_blank" | string; + ariaLabel?: string; + className?: string; + children: React.ReactNode; + scroll?: boolean; + size?: string; +} + +export const CustomLink = React.forwardRef( + ( + { + href, + target = "_blank", + ariaLabel, + className, + children, + scroll = true, + size = "xs", + ...props + }, + ref, + ) => { + return ( + + {children} + + ); + }, +); + +CustomLink.displayName = "CustomLink"; diff --git a/ui/components/ui/user-nav/user-nav.tsx b/ui/components/ui/user-nav/user-nav.tsx index ff441f0ec8..f4734e6384 100644 --- a/ui/components/ui/user-nav/user-nav.tsx +++ b/ui/components/ui/user-nav/user-nav.tsx @@ -1,7 +1,6 @@ "use client"; import { LogOut, User } from "lucide-react"; -import Link from "next/link"; import { useSession } from "next-auth/react"; import { logOut } from "@/actions/auth"; @@ -10,6 +9,8 @@ import { AvatarFallback, AvatarImage, } from "@/components/ui/avatar/avatar"; +import { Button } from "@/components/ui/button/button"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { DropdownMenu, DropdownMenuContent, @@ -26,8 +27,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip/tooltip"; -import { Button } from "../button/button"; - export const UserNav = () => { const { data: session } = useSession(); @@ -80,10 +79,14 @@ export const UserNav = () => { - + Account - + From 95791a9909b20a414607ba1d97de637dd2681986 Mon Sep 17 00:00:00 2001 From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:34:45 +0200 Subject: [PATCH 19/28] chore(aws): replace known errors with warnings (#8347) Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 5 +++- .../elasticbeanstalk_service.py | 27 ++++++++++++++++--- .../providers/aws/services/iam/iam_service.py | 5 +++- .../secretsmanager/secretsmanager_service.py | 16 +++++++++++ .../providers/aws/services/ssm/ssm_service.py | 15 +++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index bbb3ed5515..e8250cf07f 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -5,9 +5,12 @@ All notable changes to the **Prowler SDK** are documented in this file. ## [v5.10.0] (Prowler UNRELEASED) ### Added -- Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) +- `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) - Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) +### Changed +- Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347) + ### Fixed - False positives in SQS encryption check for ephemeral queues [(#8330)](https://github.com/prowler-cloud/prowler/pull/8330) diff --git a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py index 0005177bdb..c8d821687d 100644 --- a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py +++ b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py @@ -1,5 +1,6 @@ from typing import Optional +from botocore.client import ClientError from pydantic.v1 import BaseModel from prowler.lib.logger import logger @@ -71,6 +72,17 @@ class ElasticBeanstalk(AWSService): and option["OptionName"] == "StreamLogs" ): environment.cloudwatch_stream_logs = option.get("Value", "false") + except ClientError as error: + if error.response["Error"]["Code"] in [ + "InvalidParameterValue", + ]: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -84,10 +96,17 @@ class ElasticBeanstalk(AWSService): "ResourceTags" ] resource.tags = response - except Exception as error: - logger.error( - f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + except ClientError as error: + if error.response["Error"]["Code"] in [ + "ResourceNotFoundException", + ]: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index 914483081b..1ff3dbd07a 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -865,7 +865,10 @@ class IAM(AWSService): SAMLProviderArn=resource.arn ).get("Tags", []) except Exception as error: - if error.response["Error"]["Code"] == "NoSuchEntityException": + if error.response["Error"]["Code"] in [ + "NoSuchEntity", + "NoSuchEntityException", + ]: logger.warning( f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) diff --git a/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py b/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py index 8a85d33502..695072c878 100644 --- a/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py +++ b/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py @@ -2,6 +2,7 @@ import json from datetime import datetime, timezone from typing import Dict, List, Optional +from botocore.client import ClientError from pydantic.v1 import BaseModel, Field from prowler.lib.logger import logger @@ -67,6 +68,21 @@ class SecretsManager(AWSService): ) if secret_policy.get("ResourcePolicy"): secret.policy = json.loads(secret_policy["ResourcePolicy"]) + except ClientError as error: + if error.response["Error"]["Code"] in [ + "ResourceNotFoundException", + ]: + logger.warning( + f"{self.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + else: + logger.error( + f"{self.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) except Exception as error: logger.error( f"{self.region} --" diff --git a/prowler/providers/aws/services/ssm/ssm_service.py b/prowler/providers/aws/services/ssm/ssm_service.py index 33f1187993..689047551c 100644 --- a/prowler/providers/aws/services/ssm/ssm_service.py +++ b/prowler/providers/aws/services/ssm/ssm_service.py @@ -99,6 +99,21 @@ class SSM(AWSService): "AccountIds" ] + except ClientError as error: + if error.response["Error"]["Code"] in [ + "InvalidDocumentOperation", + ]: + logger.warning( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + else: + logger.error( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) except Exception as error: logger.error( f"{regional_client.region} --" From 44d70f8467a555f6711169eef8b60c27591653c0 Mon Sep 17 00:00:00 2001 From: Chandrapal Badshah Date: Thu, 24 Jul 2025 14:20:36 +0530 Subject: [PATCH 20/28] fix(lighthouse): update prompt and tool schema for checks tool (#8265) Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com> --- ui/lib/lighthouse/prompts.ts | 4 ++-- ui/types/lighthouse/checks.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/lib/lighthouse/prompts.ts b/ui/lib/lighthouse/prompts.ts index 925640aa7f..3dfcc8c965 100644 --- a/ui/lib/lighthouse/prompts.ts +++ b/ui/lib/lighthouse/prompts.ts @@ -127,8 +127,8 @@ You operate in an agent loop, iterating through these steps: - Fetches information related to: - All findings data across providers. Supports filtering by severity, status, etc. - Unique metadata values from findings - - Remediation for checks - - Check IDs supported by different provider types + - Available checks for a specific provider (aws, gcp, azure, kubernetes, etc) + - Details of a specific check including details about severity, risk, remediation, compliances that are associated with the check, etc ### roles_agent diff --git a/ui/types/lighthouse/checks.ts b/ui/types/lighthouse/checks.ts index 186eb6cae6..5c0aaaa455 100644 --- a/ui/types/lighthouse/checks.ts +++ b/ui/types/lighthouse/checks.ts @@ -10,5 +10,5 @@ export const checkSchema = z.object({ }); export const checkDetailsSchema = z.object({ - id: z.string(), + checkId: z.string(), }); From 04749c1da1a4dbd4ecd5270e1a4f2aeb770ebb74 Mon Sep 17 00:00:00 2001 From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:03:30 +0200 Subject: [PATCH 21/28] fix(aws): `sns_topics_not_publicly_accessible` false positive with aws:SourceArn conditions (#8340) Co-authored-by: MrCloudSec --- prowler/CHANGELOG.md | 1 + .../providers/aws/services/iam/lib/policy.py | 252 +++++++++++++---- .../sns_topics_not_publicly_accessible.py | 58 ++-- ...ingress_from_internet_to_all_ports_test.py | 2 +- .../aws/services/iam/lib/policy_test.py | 265 ++++++++++++++++++ ...sns_topics_not_publicly_accessible_test.py | 250 ++++++++++++++++- 6 files changed, 729 insertions(+), 99 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index e8250cf07f..2bb9dac6e3 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Fixed - Add more validations to Azure Storage models when some values are None to avoid serialization issues [(#8325)](https://github.com/prowler-cloud/prowler/pull/8325) +- `sns_topics_not_publicly_accessible` false positive with `aws:SourceArn` conditions [(#8326)](https://github.com/prowler-cloud/prowler/issues/8326) --- diff --git a/prowler/providers/aws/services/iam/lib/policy.py b/prowler/providers/aws/services/iam/lib/policy.py index b560389feb..c3b6eb3dba 100644 --- a/prowler/providers/aws/services/iam/lib/policy.py +++ b/prowler/providers/aws/services/iam/lib/policy.py @@ -223,6 +223,108 @@ def check_full_service_access(service: str, policy: dict) -> bool: return all_target_service_actions.issubset(actions_allowed_on_all_resources) +def has_public_principal(statement: dict) -> bool: + """ + Check if a policy statement has a public principal. + + Args: + statement (dict): IAM policy statement + + Returns: + bool: True if the statement has a public principal, False otherwise + """ + principal = statement.get("Principal", "") + return ( + "*" in principal + or "arn:aws:iam::*:root" in principal + or ( + isinstance(principal, dict) + and ( + "*" in principal.get("AWS", "") + or "arn:aws:iam::*:root" in principal.get("AWS", "") + or ( + isinstance(principal.get("AWS"), list) + and ( + "*" in principal["AWS"] + or "arn:aws:iam::*:root" in principal["AWS"] + ) + ) + or "*" in principal.get("CanonicalUser", "") + or "arn:aws:iam::*:root" in principal.get("CanonicalUser", "") + ) + ) + ) + + +def has_restrictive_source_arn_condition( + statement: dict, source_account: str = "" +) -> bool: + """ + Check if a policy statement has a restrictive aws:SourceArn condition. + + A SourceArn condition is considered restrictive if: + 1. It doesn't contain overly permissive wildcards (like "*" or "arn:aws:s3:::*") + 2. When source_account is provided, the ARN either contains no account field (like S3 buckets) + or contains the source_account + + Args: + statement (dict): IAM policy statement + source_account (str): The account to check restrictions for (optional) + + Returns: + bool: True if the statement has a restrictive aws:SourceArn condition, False otherwise + """ + if "Condition" not in statement: + return False + + for condition_operator in statement["Condition"]: + for condition_key, condition_value in statement["Condition"][ + condition_operator + ].items(): + if condition_key.lower() == "aws:sourcearn": + arn_values = ( + condition_value + if isinstance(condition_value, list) + else [condition_value] + ) + + for arn_value in arn_values: + if ( + arn_value == "*" # Global wildcard + or arn_value.count("*") + >= 3 # Too many wildcards (e.g., arn:aws:*:*:*:*) + or ( + isinstance(arn_value, str) + and ( + arn_value.endswith( + ":::*" + ) # Service-wide wildcard (e.g., arn:aws:s3:::*) + or arn_value.endswith( + ":*" + ) # Resource wildcard (e.g., arn:aws:sns:us-east-1:123456789012:*) + ) + ) + ): + return False + + if source_account: + arn_parts = arn_value.split(":") + if len(arn_parts) > 4 and arn_parts[4] and arn_parts[4] != "*": + if arn_parts[4].isdigit(): + if source_account not in arn_value: + return False + else: + if arn_parts[4] != source_account: + return False + elif len(arn_parts) > 4 and arn_parts[4] == "*": + return False + # else: ARN doesn't contain account field (like S3 bucket), so it's restrictive + + return True + + return False + + def is_condition_restricting_from_private_ip(condition_statement: dict) -> bool: """Check if the policy condition is coming from a private IP address. @@ -303,61 +405,49 @@ def is_policy_public( for statement in policy.get("Statement", []): # Only check allow statements if statement["Effect"] == "Allow": + has_public_access = has_public_principal(statement) + principal = statement.get("Principal", "") - if ( - "*" in principal - or "arn:aws:iam::*:root" in principal - or ( - isinstance(principal, dict) - and ( - "*" in principal.get("AWS", "") - or "arn:aws:iam::*:root" in principal.get("AWS", "") - or ( - isinstance(principal.get("AWS"), str) - and source_account - and not is_cross_account_allowed - and source_account not in principal.get("AWS", "") - ) - or ( - isinstance(principal.get("AWS"), list) - and ( - "*" in principal["AWS"] - or "arn:aws:iam::*:root" in principal["AWS"] - or ( - source_account - and not is_cross_account_allowed - and not any( - source_account in principal_aws - for principal_aws in principal["AWS"] - ) - ) - ) - ) - or "*" in principal.get("CanonicalUser", "") - or "arn:aws:iam::*:root" - in principal.get("CanonicalUser", "") - or check_cross_service_confused_deputy - and ( - # Check if function can be invoked by other AWS services if check_cross_service_confused_deputy is True - ( - ".amazonaws.com" in principal.get("Service", "") - or ".amazon.com" in principal.get("Service", "") - or "*" in principal.get("Service", "") - ) - and ( - "secretsmanager.amazonaws.com" - not in principal.get( - "Service", "" - ) # AWS ensures that resources called by SecretsManager are executed in the same AWS account - or "eks.amazonaws.com" - not in principal.get( - "Service", "" - ) # AWS ensures that resources called by EKS are executed in the same AWS account - ) - ) + if not has_public_access and isinstance(principal, dict): + # Check for cross-account access when not allowed + if ( + isinstance(principal.get("AWS"), str) + and source_account + and not is_cross_account_allowed + and source_account not in principal.get("AWS", "") + ) or ( + isinstance(principal.get("AWS"), list) + and source_account + and not is_cross_account_allowed + and not any( + source_account in principal_aws + for principal_aws in principal["AWS"] ) - ) - ) and ( + ): + has_public_access = True + + # Check for cross-service confused deputy + if check_cross_service_confused_deputy and ( + # Check if function can be invoked by other AWS services if check_cross_service_confused_deputy is True + ( + ".amazonaws.com" in principal.get("Service", "") + or ".amazon.com" in principal.get("Service", "") + or "*" in principal.get("Service", "") + ) + and ( + "secretsmanager.amazonaws.com" + not in principal.get( + "Service", "" + ) # AWS ensures that resources called by SecretsManager are executed in the same AWS account + or "eks.amazonaws.com" + not in principal.get( + "Service", "" + ) # AWS ensures that resources called by EKS are executed in the same AWS account + ) + ): + has_public_access = True + + if has_public_access and ( not not_allowed_actions # If not_allowed_actions is empty, the function will not consider the actions in the policy or ( statement.get( @@ -498,9 +588,29 @@ def is_condition_block_restrictive( "aws:sourcevpc" != value and "aws:sourcevpce" != value ): - if source_account not in item: - is_condition_key_restrictive = False - break + if value == "aws:sourcearn": + # Use the specialized function to properly validate SourceArn restrictions + # Create a minimal statement to test with our function + test_statement = { + "Condition": { + condition_operator: { + value: condition_statement[ + condition_operator + ][value] + } + } + } + is_condition_key_restrictive = ( + has_restrictive_source_arn_condition( + test_statement, source_account + ) + ) + if not is_condition_key_restrictive: + break + else: + if source_account not in item: + is_condition_key_restrictive = False + break if is_condition_key_restrictive: is_condition_valid = True @@ -516,11 +626,31 @@ def is_condition_block_restrictive( if is_cross_account_allowed: is_condition_valid = True else: - if ( - source_account - in condition_statement[condition_operator][value] - ): - is_condition_valid = True + if value == "aws:sourcearn": + # Use the specialized function to properly validate SourceArn restrictions + # Create a minimal statement to test with our function + test_statement = { + "Condition": { + condition_operator: { + value: condition_statement[ + condition_operator + ][value] + } + } + } + is_condition_valid = ( + has_restrictive_source_arn_condition( + test_statement, source_account + ) + ) + else: + if ( + source_account + in condition_statement[condition_operator][ + value + ] + ): + is_condition_valid = True return is_condition_valid diff --git a/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py b/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py index 1dc0776b64..b6c66dc702 100644 --- a/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py +++ b/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py @@ -1,5 +1,7 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.iam.lib.policy import ( + has_public_principal, + has_restrictive_source_arn_condition, is_condition_block_restrictive, is_condition_block_restrictive_organization, is_condition_block_restrictive_sns_endpoint, @@ -16,46 +18,26 @@ class sns_topics_not_publicly_accessible(Check): report.status_extended = ( f"SNS topic {topic.name} is not publicly accessible." ) + if topic.policy: for statement in topic.policy["Statement"]: - # Only check allow statements - if statement["Effect"] == "Allow": - if ( - "*" in statement["Principal"] - or ( - "AWS" in statement["Principal"] - and "*" in statement["Principal"]["AWS"] + if statement["Effect"] == "Allow" and has_public_principal( + statement + ): + if has_restrictive_source_arn_condition(statement): + break + elif "Condition" in statement: + condition_account = is_condition_block_restrictive( + statement["Condition"], sns_client.audited_account ) - or ( - "CanonicalUser" in statement["Principal"] - and "*" in statement["Principal"]["CanonicalUser"] + condition_org = is_condition_block_restrictive_organization( + statement["Condition"] ) - ): - condition_account = False - condition_org = False - condition_endpoint = False - if ( - "Condition" in statement - and is_condition_block_restrictive( - statement["Condition"], - sns_client.audited_account, + condition_endpoint = ( + is_condition_block_restrictive_sns_endpoint( + statement["Condition"] ) - ): - condition_account = True - if ( - "Condition" in statement - and is_condition_block_restrictive_organization( - statement["Condition"], - ) - ): - condition_org = True - if ( - "Condition" in statement - and is_condition_block_restrictive_sns_endpoint( - statement["Condition"], - ) - ): - condition_endpoint = True + ) if condition_account and condition_org: report.status_extended = f"SNS topic {topic.name} is not public because its policy only allows access from the account {sns_client.audited_account} and an organization." @@ -69,7 +51,11 @@ class sns_topics_not_publicly_accessible(Check): report.status = "FAIL" report.status_extended = f"SNS topic {topic.name} is public because its policy allows public access." break + else: + # Public principal with no conditions = public + report.status = "FAIL" + report.status_extended = f"SNS topic {topic.name} is public because its policy allows public access." + break findings.append(report) - return findings diff --git a/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_all_ports/ec2_securitygroup_allow_ingress_from_internet_to_all_ports_test.py b/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_all_ports/ec2_securitygroup_allow_ingress_from_internet_to_all_ports_test.py index 98bfe0907e..f08d4cfc44 100644 --- a/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_all_ports/ec2_securitygroup_allow_ingress_from_internet_to_all_ports_test.py +++ b/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_all_ports/ec2_securitygroup_allow_ingress_from_internet_to_all_ports_test.py @@ -404,7 +404,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_all_ports: new=EC2(aws_provider), ), mock.patch( - "prowler.providers.aws.services.vpc.vpc_service.VPC", + "prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_all_ports.ec2_securitygroup_allow_ingress_from_internet_to_all_ports.vpc_client", new=VPC(aws_provider), ), mock.patch( diff --git a/tests/providers/aws/services/iam/lib/policy_test.py b/tests/providers/aws/services/iam/lib/policy_test.py index f8022d6f77..4687d38dab 100644 --- a/tests/providers/aws/services/iam/lib/policy_test.py +++ b/tests/providers/aws/services/iam/lib/policy_test.py @@ -6,6 +6,8 @@ from prowler.providers.aws.services.iam.lib.policy import ( check_full_service_access, get_effective_actions, has_codebuild_trusted_principal, + has_public_principal, + has_restrictive_source_arn_condition, is_codebuild_using_allowed_github_org, is_condition_block_restrictive, is_condition_block_restrictive_organization, @@ -2451,3 +2453,266 @@ def test_has_codebuild_trusted_principal_list(): ], } assert has_codebuild_trusted_principal(trust_policy) is True + + +class Test_has_public_principal: + """Tests for the has_public_principal function""" + + def test_has_public_principal_wildcard_string(self): + """Test public principal detection with wildcard string""" + statement = {"Principal": "*"} + assert has_public_principal(statement) is True + + def test_has_public_principal_root_arn_string(self): + """Test public principal detection with root ARN string""" + statement = {"Principal": "arn:aws:iam::*:root"} + assert has_public_principal(statement) is True + + def test_has_public_principal_aws_dict_wildcard(self): + """Test public principal detection with AWS dict containing wildcard""" + statement = {"Principal": {"AWS": "*"}} + assert has_public_principal(statement) is True + + def test_has_public_principal_aws_dict_root_arn(self): + """Test public principal detection with AWS dict containing root ARN""" + statement = {"Principal": {"AWS": "arn:aws:iam::*:root"}} + assert has_public_principal(statement) is True + + def test_has_public_principal_aws_list_wildcard(self): + """Test public principal detection with AWS list containing wildcard""" + statement = {"Principal": {"AWS": ["arn:aws:iam::123456789012:user/test", "*"]}} + assert has_public_principal(statement) is True + + def test_has_public_principal_aws_list_root_arn(self): + """Test public principal detection with AWS list containing root ARN""" + statement = { + "Principal": { + "AWS": ["arn:aws:iam::123456789012:user/test", "arn:aws:iam::*:root"] + } + } + assert has_public_principal(statement) is True + + def test_has_public_principal_canonical_user_wildcard(self): + """Test public principal detection with CanonicalUser wildcard""" + statement = {"Principal": {"CanonicalUser": "*"}} + assert has_public_principal(statement) is True + + def test_has_public_principal_canonical_user_root_arn(self): + """Test public principal detection with CanonicalUser root ARN""" + statement = {"Principal": {"CanonicalUser": "arn:aws:iam::*:root"}} + assert has_public_principal(statement) is True + + def test_has_public_principal_no_principal(self): + """Test with statement that has no Principal field""" + statement = {"Effect": "Allow", "Action": "s3:GetObject"} + assert has_public_principal(statement) is False + + def test_has_public_principal_empty_principal(self): + """Test with empty principal""" + statement = {"Principal": ""} + assert has_public_principal(statement) is False + + def test_has_public_principal_specific_account(self): + """Test with specific account principal (not public)""" + statement = {"Principal": {"AWS": "arn:aws:iam::123456789012:root"}} + assert has_public_principal(statement) is False + + def test_has_public_principal_service_principal(self): + """Test with service principal (not public)""" + statement = {"Principal": {"Service": "lambda.amazonaws.com"}} + assert has_public_principal(statement) is False + + def test_has_public_principal_mixed_principals(self): + """Test with mixed principals including public one""" + statement = { + "Principal": { + "AWS": ["arn:aws:iam::123456789012:user/test"], + "Service": "lambda.amazonaws.com", + "CanonicalUser": "*", + } + } + assert has_public_principal(statement) is True + + +class Test_has_restrictive_source_arn_condition: + """Tests for the has_restrictive_source_arn_condition function""" + + def test_no_condition_block(self): + """Test statement without Condition block""" + statement = {"Effect": "Allow", "Principal": "*", "Action": "s3:GetObject"} + assert has_restrictive_source_arn_condition(statement) is False + + def test_no_source_arn_condition(self): + """Test with condition block but no aws:SourceArn""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Condition": {"StringEquals": {"aws:SourceAccount": "123456789012"}}, + } + assert has_restrictive_source_arn_condition(statement) is False + + def test_restrictive_source_arn_s3_bucket(self): + """Test restrictive SourceArn condition with S3 bucket""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": {"ArnLike": {"aws:SourceArn": "arn:aws:s3:::my-bucket"}}, + } + assert has_restrictive_source_arn_condition(statement) is True + + def test_restrictive_source_arn_lambda_function(self): + """Test restrictive SourceArn condition with Lambda function""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": { + "ArnEquals": { + "aws:SourceArn": "arn:aws:lambda:us-east-1:123456789012:function:MyFunction" + } + }, + } + assert has_restrictive_source_arn_condition(statement) is True + + def test_non_restrictive_global_wildcard(self): + """Test non-restrictive SourceArn with global wildcard""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": {"ArnLike": {"aws:SourceArn": "*"}}, + } + assert has_restrictive_source_arn_condition(statement) is False + + def test_non_restrictive_service_wildcard(self): + """Test non-restrictive SourceArn with service wildcard""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": {"ArnLike": {"aws:SourceArn": "arn:aws:s3:::*"}}, + } + assert has_restrictive_source_arn_condition(statement) is False + + def test_non_restrictive_multi_wildcard(self): + """Test non-restrictive SourceArn with multiple wildcards""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": {"ArnLike": {"aws:SourceArn": "arn:aws:*:*:*:*"}}, + } + assert has_restrictive_source_arn_condition(statement) is False + + def test_non_restrictive_resource_wildcard(self): + """Test non-restrictive SourceArn with resource wildcard""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": { + "ArnLike": {"aws:SourceArn": "arn:aws:lambda:us-east-1:123456789012:*"} + }, + } + assert has_restrictive_source_arn_condition(statement) is False + + def test_source_arn_list_with_valid_arn(self): + """Test SourceArn condition with list containing valid ARN""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": ["arn:aws:s3:::bucket1", "arn:aws:s3:::bucket2"] + } + }, + } + assert has_restrictive_source_arn_condition(statement) is True + + def test_source_arn_list_with_wildcard(self): + """Test SourceArn condition with list containing wildcard""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": {"ArnLike": {"aws:SourceArn": ["arn:aws:s3:::bucket1", "*"]}}, + } + assert has_restrictive_source_arn_condition(statement) is False + + def test_source_arn_with_account_validation_match(self): + """Test SourceArn with account validation - matching account""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": "arn:aws:lambda:us-east-1:123456789012:function:MyFunction" + } + }, + } + assert has_restrictive_source_arn_condition(statement, "123456789012") is True + + def test_source_arn_with_account_validation_mismatch(self): + """Test SourceArn with account validation - non-matching account""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": "arn:aws:lambda:us-east-1:123456789012:function:MyFunction" + } + }, + } + assert has_restrictive_source_arn_condition(statement, "987654321098") is False + + def test_source_arn_with_account_wildcard(self): + """Test SourceArn with account wildcard""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": { + "ArnLike": { + "aws:SourceArn": "arn:aws:lambda:us-east-1:*:function:MyFunction" + } + }, + } + assert has_restrictive_source_arn_condition(statement, "123456789012") is False + + def test_source_arn_s3_bucket_no_account_field(self): + """Test SourceArn with S3 bucket (no account field) - should be restrictive""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": {"ArnLike": {"aws:SourceArn": "arn:aws:s3:::my-bucket"}}, + } + assert has_restrictive_source_arn_condition(statement, "123456789012") is True + + def test_source_arn_case_insensitive(self): + """Test SourceArn condition key is case insensitive""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": {"ArnLike": {"AWS:SourceArn": "arn:aws:s3:::my-bucket"}}, + } + assert has_restrictive_source_arn_condition(statement) is True + + def test_source_arn_mixed_operators(self): + """Test SourceArn with multiple condition operators""" + statement = { + "Effect": "Allow", + "Principal": "*", + "Action": "sns:Publish", + "Condition": { + "ArnLike": {"aws:SourceArn": "arn:aws:s3:::my-bucket"}, + "StringEquals": {"aws:SourceAccount": "123456789012"}, + }, + } + assert has_restrictive_source_arn_condition(statement) is True diff --git a/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py b/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py index 71aa6c4477..a868be5715 100644 --- a/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py @@ -2,9 +2,10 @@ from typing import Any, Dict from unittest import mock from uuid import uuid4 +import pytest + from prowler.providers.aws.services.sns.sns_service import Topic from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 -import pytest kms_key_id = str(uuid4()) topic_name = "test-topic" @@ -98,6 +99,73 @@ test_policy_restricted_principal_account_organization = { ] } +test_policy_restricted_source_arn = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "SNS:Publish", + "Resource": f"arn:aws:sns:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:{topic_name}", + "Condition": { + "ArnLike": {"aws:SourceArn": "arn:aws:s3:::test-bucket-name"} + }, + } + ], +} + +test_policy_invalid_source_arn = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "SNS:Publish", + "Resource": f"arn:aws:sns:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:{topic_name}", + "Condition": {"ArnLike": {"aws:SourceArn": "invalid-arn-format"}}, + } + ], +} + +test_policy_unrestricted_source_arn_wildcard = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "SNS:Publish", + "Resource": f"arn:aws:sns:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:{topic_name}", + "Condition": {"ArnLike": {"aws:SourceArn": "*"}}, + } + ], +} + +test_policy_unrestricted_source_arn_service_wildcard = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "SNS:Publish", + "Resource": f"arn:aws:sns:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:{topic_name}", + "Condition": {"ArnLike": {"aws:SourceArn": "arn:aws:s3:::*"}}, + } + ], +} + +test_policy_unrestricted_source_arn_multi_wildcard = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "SNS:Publish", + "Resource": f"arn:aws:sns:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:{topic_name}", + "Condition": {"ArnLike": {"aws:SourceArn": "arn:aws:*:*:*:*"}}, + } + ], +} + def generate_policy_restricted_on_sns_endpoint(endpoint: str) -> Dict[str, Any]: return { @@ -396,6 +464,78 @@ class Test_sns_topics_not_publicly_accessible: assert result[0].region == AWS_REGION_EU_WEST_1 assert result[0].resource_tags == [] + def test_topic_public_with_source_arn_restriction(self): + sns_client = mock.MagicMock + sns_client.audited_account = AWS_ACCOUNT_NUMBER + sns_client.topics = [] + sns_client.topics.append( + Topic( + arn=topic_arn, + name=topic_name, + policy=test_policy_restricted_source_arn, + region=AWS_REGION_EU_WEST_1, + ) + ) + sns_client.provider = mock.MagicMock() + sns_client.provider.organizations_metadata = mock.MagicMock() + sns_client.provider.organizations_metadata.organization_id = org_id + with mock.patch( + "prowler.providers.aws.services.sns.sns_service.SNS", + sns_client, + ): + from prowler.providers.aws.services.sns.sns_topics_not_publicly_accessible.sns_topics_not_publicly_accessible import ( + sns_topics_not_publicly_accessible, + ) + + check = sns_topics_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SNS topic {topic_name} is not publicly accessible." + ) + assert result[0].resource_id == topic_name + assert result[0].resource_arn == topic_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [] + + def test_topic_public_with_invalid_source_arn(self): + sns_client = mock.MagicMock + sns_client.audited_account = AWS_ACCOUNT_NUMBER + sns_client.topics = [] + sns_client.topics.append( + Topic( + arn=topic_arn, + name=topic_name, + policy=test_policy_invalid_source_arn, + region=AWS_REGION_EU_WEST_1, + ) + ) + sns_client.provider = mock.MagicMock() + sns_client.provider.organizations_metadata = mock.MagicMock() + sns_client.provider.organizations_metadata.organization_id = org_id + with mock.patch( + "prowler.providers.aws.services.sns.sns_service.SNS", + sns_client, + ): + from prowler.providers.aws.services.sns.sns_topics_not_publicly_accessible.sns_topics_not_publicly_accessible import ( + sns_topics_not_publicly_accessible, + ) + + check = sns_topics_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SNS topic {topic_name} is not publicly accessible." + ) + assert result[0].resource_id == topic_name + assert result[0].resource_arn == topic_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [] + @pytest.mark.parametrize( "endpoint", [ @@ -443,6 +583,114 @@ class Test_sns_topics_not_publicly_accessible: assert result[0].region == AWS_REGION_EU_WEST_1 assert result[0].resource_tags == [] + def test_topic_public_with_unrestricted_source_arn_wildcard(self): + sns_client = mock.MagicMock + sns_client.audited_account = AWS_ACCOUNT_NUMBER + sns_client.topics = [] + sns_client.topics.append( + Topic( + arn=topic_arn, + name=topic_name, + policy=test_policy_unrestricted_source_arn_wildcard, + region=AWS_REGION_EU_WEST_1, + ) + ) + sns_client.provider = mock.MagicMock() + sns_client.provider.organizations_metadata = mock.MagicMock() + sns_client.provider.organizations_metadata.organization_id = org_id + with mock.patch( + "prowler.providers.aws.services.sns.sns_service.SNS", + sns_client, + ): + from prowler.providers.aws.services.sns.sns_topics_not_publicly_accessible.sns_topics_not_publicly_accessible import ( + sns_topics_not_publicly_accessible, + ) + + check = sns_topics_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SNS topic {topic_name} is public because its policy allows public access." + ) + assert result[0].resource_id == topic_name + assert result[0].resource_arn == topic_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [] + + def test_topic_public_with_unrestricted_source_arn_service_wildcard(self): + sns_client = mock.MagicMock + sns_client.audited_account = AWS_ACCOUNT_NUMBER + sns_client.topics = [] + sns_client.topics.append( + Topic( + arn=topic_arn, + name=topic_name, + policy=test_policy_unrestricted_source_arn_service_wildcard, + region=AWS_REGION_EU_WEST_1, + ) + ) + sns_client.provider = mock.MagicMock() + sns_client.provider.organizations_metadata = mock.MagicMock() + sns_client.provider.organizations_metadata.organization_id = org_id + with mock.patch( + "prowler.providers.aws.services.sns.sns_service.SNS", + sns_client, + ): + from prowler.providers.aws.services.sns.sns_topics_not_publicly_accessible.sns_topics_not_publicly_accessible import ( + sns_topics_not_publicly_accessible, + ) + + check = sns_topics_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SNS topic {topic_name} is public because its policy allows public access." + ) + assert result[0].resource_id == topic_name + assert result[0].resource_arn == topic_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [] + + def test_topic_public_with_unrestricted_source_arn_multi_wildcard(self): + sns_client = mock.MagicMock + sns_client.audited_account = AWS_ACCOUNT_NUMBER + sns_client.topics = [] + sns_client.topics.append( + Topic( + arn=topic_arn, + name=topic_name, + policy=test_policy_unrestricted_source_arn_multi_wildcard, + region=AWS_REGION_EU_WEST_1, + ) + ) + sns_client.provider = mock.MagicMock() + sns_client.provider.organizations_metadata = mock.MagicMock() + sns_client.provider.organizations_metadata.organization_id = org_id + with mock.patch( + "prowler.providers.aws.services.sns.sns_service.SNS", + sns_client, + ): + from prowler.providers.aws.services.sns.sns_topics_not_publicly_accessible.sns_topics_not_publicly_accessible import ( + sns_topics_not_publicly_accessible, + ) + + check = sns_topics_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SNS topic {topic_name} is public because its policy allows public access." + ) + assert result[0].resource_id == topic_name + assert result[0].resource_arn == topic_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [] + @pytest.mark.parametrize( "endpoint", [ From b99dce6a43f0e546468431aea054c40a232f7977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 24 Jul 2025 16:29:46 +0200 Subject: [PATCH 22/28] feat(azure): add CIS 4.0 (#7782) --- README.md | 2 +- dashboard/compliance/cis_4_0_azure.py | 25 + prowler/CHANGELOG.md | 1 + prowler/compliance/azure/cis_4.0_azure.json | 3087 +++++++++++++++++++ 4 files changed, 3114 insertions(+), 1 deletion(-) create mode 100644 dashboard/compliance/cis_4_0_azure.py create mode 100644 prowler/compliance/azure/cis_4.0_azure.json diff --git a/README.md b/README.md index ea249d473d..cdf6fc29f4 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ prowler dashboard |---|---|---|---|---| | AWS | 567 | 82 | 36 | 10 | | GCP | 79 | 13 | 10 | 3 | -| Azure | 142 | 18 | 10 | 3 | +| Azure | 142 | 18 | 11 | 3 | | Kubernetes | 83 | 7 | 5 | 7 | | GitHub | 16 | 2 | 1 | 0 | | M365 | 69 | 7 | 3 | 2 | diff --git a/dashboard/compliance/cis_4_0_azure.py b/dashboard/compliance/cis_4_0_azure.py new file mode 100644 index 0000000000..9d33cc67a8 --- /dev/null +++ b/dashboard/compliance/cis_4_0_azure.py @@ -0,0 +1,25 @@ +import warnings + +from dashboard.common_methods import get_section_containers_cis + +warnings.filterwarnings("ignore") + + +def get_table(data): + + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_DESCRIPTION", + "REQUIREMENTS_ATTRIBUTES_SECTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_cis( + aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION" + ) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 2bb9dac6e3..12be1dda1c 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Added - `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) - Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) +- CIS 4.0 for the Azure provider [(#7782)](https://github.com/prowler-cloud/prowler/pull/7782) ### Changed - Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347) diff --git a/prowler/compliance/azure/cis_4.0_azure.json b/prowler/compliance/azure/cis_4.0_azure.json new file mode 100644 index 0000000000..ec6fae18eb --- /dev/null +++ b/prowler/compliance/azure/cis_4.0_azure.json @@ -0,0 +1,3087 @@ +{ + "Framework": "CIS", + "Version": "4.0", + "Provider": "Azure", + "Description": "The CIS Azure Foundations Benchmark provides prescriptive guidance for configuring security options for a subset of Azure with an emphasis on foundational, testable, and architecture agnostic settings.", + "Requirements": [ + { + "Id": "2.2.1.1", + "Description": "Ensure public network access is Disabled", + "Checks": [], + "Attributes": [ + { + "Section": "2 Common Reference Recommendations", + "SubSection": "2.2 Networking", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Disable public network access to prevent exposure to the internet and reduce the risk of unauthorized access. Use private endpoints to securely manage access within trusted networks.", + "RationaleStatement": "Disabling public network access improves security by ensuring that a service is not exposed on the public internet.", + "ImpactStatement": "Disabling public network access restricts access to the service. This enhances security but may require the configuration of private endpoints for any services or users needing access within trusted networks.", + "RemediationProcedure": "", + "AuditProcedure": "", + "AdditionalInformation": "This Common Reference Recommendation is referenced in the following Service Recommendations: - Storage Services > Storage Accounts > Networking > **Ensure that 'Public Network Access' is 'Disabled' for storage accounts**", + "References": "", + "DefaultValue": "" + } + ] + }, + { + "Id": "2.1.1.2.1", + "Description": "Ensure Critical Data is Encrypted with Customer Managed Keys (CMK)", + "Checks": [ + "storage_ensure_encryption_with_customer_managed_keys" + ], + "Attributes": [ + { + "Section": "2 Common Reference Recommendations", + "SubSection": "2.1 Secrets and Keys", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Customer Managed Keys introduce additional depth to security by providing a means to manage access control for encryption keys. Where compliance and security frameworks indicate the need, and organizational capacity allows, sensitive data at rest can be encrypted using Customer Managed Keys (CMK) rather than Microsoft Managed keys.", + "RationaleStatement": "By default in Azure, data at rest tends to be encrypted using Microsoft Managed Keys. If your organization wants to control and manage encryption keys for compliance and defense-in-depth, Customer Managed Keys can be established. While it is possible to automate the assessment of this recommendation, the assessment status for this recommendation remains 'Manual' due to ideally limited scope. The scope of application - which workloads CMK is applied to - should be carefully considered to account for organizational capacity and targeted to workloads with specific need for CMK.", + "ImpactStatement": "If the key expires due to setting the 'activation date' and 'expiration date', the key must be rotated manually. Using Customer Managed Keys may also incur additional man-hour requirements to create, store, manage, and protect the keys as needed.", + "RemediationProcedure": "", + "AuditProcedure": "", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-5-use-customer-managed-key-option-in-data-at-rest-encryption-when-required", + "DefaultValue": "By default, Encryption type is set to Microsoft Managed Keys." + } + ] + }, + { + "Id": "3.1.1", + "Description": "Ensure that Azure Databricks is deployed in a customer-managed virtual network (VNet)", + "Checks": [ + "databricks_workspace_vnet_injection_enabled" + ], + "Attributes": [ + { + "Section": "3 Analytics Services", + "SubSection": "3.1 Azure Databricks", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Networking for Azure Databricks can be set up in a few different ways. Using a customer-managed Virtual Network (VNet) (also known as VNet Injection) ensures that compute clusters and control planes are securely isolated within the organization’s network boundary. By default, Databricks creates a managed VNet, which provides limited control over network security policies, firewall configurations, and routing.", + "RationaleStatement": "Using a customer-managed VNet ensures better control over network security and aligns with zero-trust architecture principles. It allows for: - Restricted outbound internet access to prevent unauthorized data exfiltration. - Integration with on-premises networks via VPN or ExpressRoute for hybrid connectivity. - Fine-grained NSG policies to restrict access at the subnet level. - Private Link for secure API access, avoiding public internet exposure.", + "ImpactStatement": "- Requires additional configuration during Databricks workspace deployment. - Might increase operational overhead for network maintenance. - May impact connectivity if misconfigured (e.g., restrictive NSG rules or missing routes).", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Delete the existing Databricks workspace (migration required). 1. Create a new Databricks workspace with VNet Injection: 1. Go to Azure Portal → Create Databricks Workspace. 1. Select Advanced Networking. 1. Choose Deploy into your own Virtual Network. 1. Specify a customer-managed VNet and associated subnets. 1. Enable Private Link for secure API access. **Remediate from Azure CLI** Deploy a new Databricks workspace in a custom VNet: ``` az databricks workspace create --name \\ --resource-group \\ --location \\ --managed-resource-group \\ --enable-no-public-ip true \\ --network-security-group-rule NoAzureServices \\ --public-network-access Disabled \\ --custom-virtual-network-id /subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/ ``` Ensure NSG Rules are correctly configured: ``` az network nsg rule create --resource-group \\ --nsg-name \\ --name DenyAllOutbound \\ --direction Outbound \\ --access Deny \\ --priority 4096 ``` **Remediate from PowerShell** ``` New-AzDatabricksWorkspace -ResourceGroupName -Name -Location -ManagedResourceGroupName -CustomVirtualNetworkId /subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/ ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to Azure Portal → Search for Databricks Workspaces. 1. Select the Databricks Workspace to audit. 1. Under Networking, check if the workspace is deployed in a Customer-Managed VNet. 1. If the Virtual Network field shows Databricks-Managed VNet, it is non-compliant. 1. Verify NSG rules and Private Endpoints for fine-grained access control. **Audit from Azure CLI** Run the following command to check if Databricks is using a customer-managed VNet: ``` az network vnet show --resource-group --name ``` Ensure that Databricks subnets are present in the VNet configuration. Validate NSG rules attached to the Databricks subnets. **Audit from PowerShell** ``` Get-AzDatabricksWorkspace -ResourceGroupName -Name | Select-Object VirtualNetworkId ``` If VirtualNetworkId is null or shows a Databricks-Managed VNet, it is non-compliant. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [9c25c9e4-ee12-4882-afd2-11fb9d87893f](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%9c25c9e4-ee12-4882-afd2-11fb9d87893f) **- Name:** 'Azure Databricks Workspaces should be in a virtual network'", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "By default, Azure Databricks uses a Databricks-Managed VNet." + } + ] + }, + { + "Id": "2.2.1.2", + "Description": "Ensure Network Access Rules are set to Deny-by-default", + "Checks": [ + "storage_default_network_access_rule_is_denied" + ], + "Attributes": [ + { + "Section": "2 Common Reference Recommendations", + "SubSection": "2.2 Networking", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Restricting default network access provides a foundational level of security to networked resources. To limit access to selected networks, the default action must be changed.", + "RationaleStatement": "Resources using Virtual Network interfaces should be configured to deny-by-default all access from all networks (including internet traffic). Access can be granted to traffic from specific Azure Virtual networks, allowing a secure network boundary for specific applications to be built. If necessary, access can also be granted to public internet IP address ranges to enable connections from specific internet or on-premises clients. For all traffic inbound from- and outbound to- the internet, a NAT Gateway is recommended at minimum, and ideally all traffic flows through a security gateway device such as a firewall. Security gateway devices will provide an additional level of visibility to inbound and outbound traffic and usually perform advanced monitoring and response activity such as intrusion detection and prevention (IDP), and deep packet inspection (DPI) which help detect activity indicating vulnerabilities and threats.", + "ImpactStatement": "All allowed networks and protocols will need to be allow-listed which creates some administrative overhead. Implementing a deny-by-default rule may result in a loss of network connectivity. Careful planning and a scheduled implementation window allowing for downtime is highly recommended.", + "RemediationProcedure": "", + "AuditProcedure": "", + "AdditionalInformation": "This Common Reference Recommendation is referenced in the following Service Recommendations: - Storage Services > Storage Accounts > Networking > **Ensure default network access rule for storage accounts is set to deny**", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-network-security:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, interfaces attached to virtual networks will accept connections from clients on any network and have a default outbound access rule which allows access to the internet. The default outbound access rule is scheduled for retirement on September 30th, 2025: https://azure.microsoft.com/en-us/updates?id=default-outbound-access-for-vms-in-azure-will-be-retired-transition-to-a-new-method-of-internet-access" + } + ] + }, + { + "Id": "2.2.2.1", + "Description": "Ensure Private Endpoints are used to access {service}", + "Checks": [ + "storage_ensure_private_endpoints_in_storage_accounts" + ], + "Attributes": [ + { + "Section": "2 Common Reference Recommendations", + "SubSection": "2.2 Networking", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Use private endpoints to allow clients and services to securely access data located over a network via an encrypted Private Link. To do this, the private endpoint uses an IP address from the VNet for each service. Network traffic between disparate services securely traverses encrypted over the VNet. This VNet can also link addressing space, extending your network and accessing resources on it. Similarly, it can be a tunnel through public networks to connect remote infrastructures together. This creates further security through segmenting network traffic and preventing outside sources from accessing it.", + "RationaleStatement": "Securing traffic between services through encryption protects the data from easy interception and reading.", + "ImpactStatement": "If an Azure Virtual Network is not implemented correctly, this may result in the loss of critical network traffic. Private endpoints are charged per hour of use. Refer to https://azure.microsoft.com/en-us/pricing/details/private-link/ and https://azure.microsoft.com/en-us/pricing/calculator/ to estimate potential costs.", + "RemediationProcedure": "", + "AuditProcedure": "", + "AdditionalInformation": "A NAT gateway is the recommended solution for outbound internet access. This Common Reference Recommendation is referenced in the following Service Recommendations: - Storage Services > Storage Accounts > Networking > **Ensure Private Endpoints are used to access Storage Accounts**", + "References": "https://docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-overview:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-portal:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-cli?tabs=dynamic-ip:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-powershell?tabs=dynamic-ip:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, Private Endpoints are not created for services." + } + ] + }, + { + "Id": "3.1.8", + "Description": "Ensure that data at rest and in transit is encrypted in Azure Databricks using customer managed keys (CMK)", + "Checks": [ + "databricks_workspace_cmk_encryption_enabled" + ], + "Attributes": [ + { + "Section": "3 Analytics Services", + "SubSection": "3.1 Azure Databricks", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Azure Databricks encrypts data in transit using TLS 1.2+ to secure API, workspace, and cluster communications. By default, data at rest is encrypted using Microsoft-managed keys.", + "RationaleStatement": "Organizations with stricter needs for control of encryption keys should enable customer-managed keys (CMK) for greater control over data encryption, auditing, and regulatory compliance. Azure Key Vault should be used to store and manage CMKs. Enforcing encryption at rest and in transit in Azure Databricks: - Protects sensitive data from unauthorized access. - Ensures regulatory compliance (ISO 27001, GDPR, HIPAA, SOC 2). - Allows key revocation and rotation control with customer-managed keys (CMK). - Mitigates insider threats by preventing unauthorized access to raw storage.", + "ImpactStatement": "Enabling CMK encryption requires additional configuration. Key management introduces maintenance overhead (rotation, revocation, lifecycle management). Potential access issues will be encountered if keys are deleted or rotated incorrectly.", + "RemediationProcedure": "**NOTE:** These remediations assume that an Azure KeyVault already exists in the subscription. **Remediate from Azure CLI** 1. Create a dedicated key: ``` az keyvault key create --vault-name --name --protection ``` 2. Assign permissions to Databricks: ``` az keyvault set-policy --name --resource-group --spn --key-permissions get wrapKey unwrapKey ``` 3. Enable encryption with CMK: ``` az databricks workspace update --name --resource-group --key-source Microsoft.KeyVault --key-name --keyvault-uri ``` **Remediate from PowerShell** ``` $Key = Add-AzKeyVaultKey -VaultName -Name -Destination Set-AzDatabricksWorkspace -ResourceGroupName -WorkspaceName -EncryptionKeySource Microsoft.KeyVault -KeyVaultUri $Key.Id ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to Azure Portal → Databricks Workspaces. 1. Select a Databricks Workspace and go to Encryption settings. 1. Check if customer-managed keys (CMK) are enabled under Managed Disk Encryption. 1. If CMK is not enabled, the workspace is non-compliant. **Audit from Azure CLI** Run the following command to check encryption settings for Databricks workspace: ``` az databricks workspace show --name --resource-group --query encryption ``` Ensure that keySource is set to `Microsoft.KeyVault`. **Audit from PowerShell** ``` Get-AzDatabricksWorkspace -ResourceGroupName -Name | Select-Object Encryption ``` Verify that encryption is set to `Customer-Managed Keys (CMK)`. **Audit from Databricks CLI** ``` databricks workspace get-metadata --workspace-id ``` Ensure that encryption settings reflect a CMK setup.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "By default, Azure Databricks uses Microsoft-managed keys for encryption. Data in transit is always encrypted using TLS 1.2+. Customer-Managed Keys (CMK) must be manually enabled." + } + ] + }, + { + "Id": "6.3.3", + "Description": "Ensure that use of the 'User Access Administrator' role is restricted", + "Checks": [ + "iam_role_user_access_admin_restricted" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.3 Periodic Identity Reviews", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The User Access Administrator role grants the ability to view all resources and manage access assignments at any subscription or management group level within the tenant. Due to its high privilege level, this role assignment should be removed immediately after completing the necessary changes at the root scope to minimize security risks.", + "RationaleStatement": "The User Access Administrator role provides extensive access control privileges. Unnecessary assignments heighten the risk of privilege escalation and unauthorized access. Removing the role immediately after use minimizes security exposure.", + "ImpactStatement": "Increased administrative effort to manage and remove role assignments appropriately.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Look for the following banner at the top of the page: `Action required: X users have elevated access in your tenant. You should take immediate action and remove all role assignments with elevated access.` 1. Click `View role assignments`. 1. Click `Remove`. **Remediate from Azure CLI** Run the following command: ``` az role assignment delete --role User Access Administrator --scope / ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Look for the following banner at the top of the page: `Action required: X users have elevated access in your tenant. You should take immediate action and remove all role assignments with elevated access.` If the banner is displayed, the `User Access Administrator` is assigned. **Audit from Azure CLI** Run the following command: ``` az role assignment list --role User Access Administrator --scope / ``` Ensure that the command does not return any `User Access Administrator` role assignment information.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles:https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin", + "DefaultValue": "" + } + ] + }, + { + "Id": "7.1.2.11", + "Description": "Ensure that an Activity Log Alert exists for Service Health", + "Checks": [ + "monitor_alert_service_health_exists" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for Service Health.", + "RationaleStatement": "Monitoring for Service Health events provides insight into service issues, planned maintenance, security advisories, and other changes that may affect the Azure services and regions in use.", + "ImpactStatement": "There is no charge for creating activity log alert rules.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Monitor`. 1. Click `Alerts`. 1. Click `+ Create`. 1. Select `Alert rule` from the drop-down menu. 1. Choose a subscription. 1. Click `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Service health`. 1. Click `Apply`. 1. Open the drop-down menu next to `Event types`. 1. Check the box next to `Select all`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. 1. Repeat steps 1-19 for each subscription requiring remediation. **Remediate from Azure CLI** For each subscription requiring remediation, run the following command to create a `ServiceHealth` alert rule for a subscription: ``` az monitor activity-log alert create --subscription --resource-group --name --condition category=ServiceHealth and properties.incidentType=Incident --scope /subscriptions/ --action-group ``` **Remediate from PowerShell** Create the `Conditions` object: ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Field category -Equal ServiceHealth $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Field properties.incidentType -Equal Incident ``` Retrieve the `Action Group` information and store in a variable: ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name ``` Create the `Actions` object: ``` $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object: ``` $scope = /subscriptions/ ``` Create the activity log alert rule: ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ``` Repeat for each subscription requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Monitor`. 1. Click `Alerts`. 1. Click `Alert rules`. 1. Ensure an alert rule exists for a subscription with `Condition` set to `Service names=All, Event types=All` and `Target resource type` set to `Subscription`. 1. If an alert rule is found for step 4, click the name of the alert rule. 1. Ensure the `Actions` panel displays an action group configured to notify appropriate personnel. 1. Repeat steps 1-6 for each subscription. **Audit from Azure CLI** Run the following command to list activity log alerts: ``` az monitor activity-log alert list --subscription ``` For each activity log alert, run the following command: ``` az monitor activity-log alert show --subscription --resource-group --activity-log-alert-name ``` Ensure an alert exists for `ServiceHealth` with `scopes` set to a subscription ID. Repeat for each subscription. **Audit from PowerShell** Run the following command to locate `ServiceHealth` alert rules for a subscription: ``` Get-AzActivityLogAlert -SubscriptionId | where-object {$_.ConditionAllOf.Equal -match ServiceHealth} | select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` Ensure that at least one `ServiceHealth` alert rule is returned. Repeat for each subscription.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/service-health/overview:https://learn.microsoft.com/en-us/azure/service-health/alerts-activity-log-service-notifications-portal:https://azure.microsoft.com/en-gb/pricing/details/monitor/#faq:https://learn.microsoft.com/en-us/cli/azure/monitor/activity-log/alert:https://learn.microsoft.com/en-us/powershell/module/az.monitor/get-azactivitylogalert:https://learn.microsoft.com/en-us/powershell/module/az.monitor/new-azactivitylogalert", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "8.8", + "Description": "Ensure that virtual network flow log retention days is set to greater than or equal to 90", + "Checks": [ + "network_flow_log_more_than_90_days" + ], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Ensure that virtual network flow logs are retained for greater than or equal to 90 days.", + "RationaleStatement": "Virtual network flow logs provide critical visibility into traffic patterns. Logs can be used to check for anomalies and give insight into suspected breaches.", + "ImpactStatement": "* Virtual network flow logs are charged per gigabyte of network flow logs collected and come with a free tier of 5 GB/month per subscription. * If traffic analytics is enabled with virtual network flow logs, traffic analytics pricing applies at per gigabyte processing rates. * The storage of logs is charged separately, and the cost will depend on the amount of logs and the retention period.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Click the name of a virtual network flow log. 1. Under `Storage Account`, set `Retention days` to `0`, `90`, or a number greater than 90. If `Retention days` is set to `0`, the logs are retained indefinitely with no retention policy. 1. Repeat steps 7 and 8 for each virtual network flow log requiring remediation. **Remediate from Azure CLI** Run the following command update the retention policy for a flow log in a network watcher, setting `retention` to `0`, `90`, or a number greater than 90: ``` az network watcher flow-log update --location --name --retention ``` Repeat for each virtual network flow log requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Click the name of a virtual network flow log. 1. Under `Storage Account`, ensure that `Retention days` is set to `0`, `90`, or a number greater than 90. If `Retention days` is set to `0`, the logs are retained indefinitely with no retention policy. 1. Repeat steps 7 and 8 for each virtual network flow log. **Audit from Azure CLI** Run the following command to list network watchers: ``` az network watcher list ``` Run the following command to list the name and retention policy of flow logs in a network watcher: ``` az network watcher flow-log list --location --query [*].[name,retentionPolicy] ``` For each flow log, ensure that `days` is set to `0`, `90`, or a number greater than 90. If `days` is set to `0`, the logs are retained indefinitely with no retention policy. Repeat for each network watcher.", + "AdditionalInformation": "As network security group flow logs are on the retirement path, Azure recommends migrating to virtual network flow logs.", + "References": "https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-portal:https://learn.microsoft.com/en-us/cli/azure/network/watcher/flow-log", + "DefaultValue": "When a virtual network flow log is created using the Azure CLI, retention days is set to 0 by default. When creating via the Azure Portal, retention days must be specified by the creator." + } + ] + }, + { + "Id": "9.1.10", + "Description": "Ensure that Microsoft Defender for Cloud is configured to check VM operating systems for updates", + "Checks": [ + "defender_ensure_system_updates_are_applied" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that the latest OS patches for all virtual machines are applied.", + "RationaleStatement": "Windows and Linux virtual machines should be kept updated to: - Address a specific bug or flaw - Improve an OS or application’s general stability - Fix a security vulnerability Microsoft Defender for Cloud retrieves a list of available security and critical updates from Windows Update or Windows Server Update Services (WSUS), depending on which service is configured on a Windows VM. The security center also checks for the latest updates in Linux systems. If a VM is missing a system update, the security center will recommend system updates be applied.", + "ImpactStatement": "Running Microsoft Defender for Cloud incurs additional charges for each resource monitored. Please see attached reference for exact charges per hour.", + "RemediationProcedure": "Follow Microsoft Azure documentation to apply security patches from the security center. Alternatively, you can employ your own patch assessment and management tool to periodically assess, report, and install the required security patches for your OS.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Then the `Recommendations` blade 1. Ensure that there are no recommendations for `System updates should be installed on your machines (powered by Update Center)` Alternatively, you can employ your own patch assessment and management tool to periodically assess, report and install the required security patches for your OS. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [f85bf3e0-d513-442e-89c3-1784ad63382b](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ff85bf3e0-d513-442e-89c3-1784ad63382b) **- Name:** 'System updates should be installed on your machines (powered by Update Center)' - **Policy ID:** [bd876905-5b84-4f73-ab2d-2e7a7c4568d9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fbd876905-5b84-4f73-ab2d-2e7a7c4568d9) **- Name:** 'Machines should be configured to periodically check for missing system updates'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-posture-vulnerability-management#pv-6-rapidly-and-automatically-remediate-vulnerabilities:https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/:https://docs.microsoft.com/en-us/azure/defender-for-cloud/deploy-vulnerability-assessment-vm", + "DefaultValue": "By default, patches are not automatically deployed." + } + ] + }, + { + "Id": "9.1.15", + "Description": "Ensure that 'Notify about attack paths with the following risk level (or higher)' is enabled", + "Checks": [ + "defender_attack_path_notifications_properly_configured" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enables emailing attack paths to the subscription owner or other designated security contact.", + "RationaleStatement": "Enabling attack path emails ensures that attack path emails are sent by Microsoft. This ensures that the right people are aware of any potential security issues and can mitigate the risk.", + "ImpactStatement": "Enabling attack path emails can cause alert fatigue, increasing the risk of missing important alerts. Select an appropriate risk level to manage notifications. Azure aims to reduce alert fatigue by limiting the daily email volume per risk level. Learn more: https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications#email-frequency.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under Notification types, check the box next to `Notify about attack paths with the following risk level (or higher)`, and select an appropriate risk level from the drop-down menu. 1. Repeat steps 1-6 for each Subscription.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under Notification types, ensure that the box next to `Notify about attack paths with the following risk level (or higher)` is checked, and an appropriate risk level is selected. 1. Repeat steps 1-6 for each Subscription. **Audit from Azure CLI** Including a Subscription ID at the `$0` in `/subscriptions/$0/providers`, ensure the below command returns `sourceType: AttackPath`, and that `minimalRiskLevel` is set to an appropriate risk level: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview' | jq '.|.[]' ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications:https://learn.microsoft.com/en-us/azure/defender-for-cloud/how-to-manage-attack-path:https://learn.microsoft.com/en-us/azure/defender-for-cloud/concept-attack-path", + "DefaultValue": "" + } + ] + }, + { + "Id": "9.3.7", + "Description": "Ensure that Public Network Access when using Private Endpoint is disabled", + "Checks": [ + "keyvault_access_only_through_private_endpoints" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "When Private endpoint is configured on a Key Vault, connections from Azure resources within the same subnet will use its private IP address. However, network traffic from the public internet can still flow connect to the Key Vault’s public endpoint (mykeyvault.vault.azure.net) using its public IP address unless Public network access is set to “Disabled”. Setting the Public network access to “Disabled” with a Private Endpoint will remove the Vault’s public endpoint from Azure public DNS, reducing its exposure to the public internet. Network traffic will use the Vault private endpoint IP address for all requests (mykeyvault.vault.privatelink.azure.net).", + "RationaleStatement": "Removing a point of interconnection from the internet edge to your Key Vault can strengthen the network security boundary of your system and reduce the risk of exposing the control plane or vault objects to untrusted clients. Although Azure resources are never truly isolated from the public internet, disabling the public endpoint removes a line of sight from the public internet and increases the effort required for an attack.", + "ImpactStatement": "Implementation needs to be properly designed from the ground up, as this is a fundamental change to the network architecture of your system. It will increase the configuration effort and decrease the usability of the Key Vault, and is appropriate for workloads where security is the primary consideration.", + "RemediationProcedure": "**Remediate from Azure Portal** Key Vaults can be configured to use `Azure role-based access control` on creation. For existing Key Vaults: 1. From Azure Home open the Portal Menu in the top left corner 2. Select `Key Vaults` 3. Select a Key Vault to audit 4. Select `Networking` 5. NEXT **Remediate from Azure CLI** To disable Public network access for each Key Vault, run the following Azure CLI command: ``` az keyvault update --resource-group --name --public-network-access Disabled ``` **Remediate from PowerShell** To enable RBAC authorization on each Key Vault, run the following PowerShell command: ``` Update-AzKeyVault -ResourceGroupName -VaultName -PublicNetworkAccess Disabled ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal Menu in the top left corner 2. Select Key Vaults 3. Select a Key Vault to audit 4. Select Networking 5. Ensure that Public network access is Disabled. 6. Ensure that a Private endpoint is provisioned and connected. **Audit from Azure CLI** Run the following command for each Key Vault in each Resource Group: ``` az keyvault show --resource-group --name ``` Ensure the `publicNetworkSetting` setting is set to `Disabled` within the output of the above command. **Audit from PowerShell** Run the following PowerShell command: ``` Get-AzKeyVault -Vaultname -ResourceGroupName ``` Ensure the `Public network access` setting is set to `Disabled` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [405c5871-3e91-4644-8a63-58e19d68ff5b](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F405c5871-3e91-4644-8a63-58e19d68ff5b) **- Name:** 'Azure Key Vault should disable public network access'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/key-vault/general/network-security:https://learn.microsoft.com/en-us/azure/key-vault/general/private-link-service", + "DefaultValue": "The default value for Access control in Key Vaults is Vault Policy." + } + ] + }, + { + "Id": "10.1.1", + "Description": "Ensure soft delete for Azure File Shares is Enabled", + "Checks": [ + "storage_ensure_file_shares_soft_delete_is_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.1 Azure Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Azure Files offers soft delete for file shares, allowing you to easily recover your data when it is mistakenly deleted by an application or another storage account user.", + "RationaleStatement": "Important data could be accidentally deleted or removed by a malicious actor. With soft delete enabled, the data is retained for the defined retention period before permanent deletion, allowing for recovery of the data.", + "ImpactStatement": "When a file share is soft-deleted, the used portion of the storage is charged for the indicated soft-deleted period. All other meters are not charged unless the share is restored.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account with file shares, under `Data storage`, click `File shares`. 1. Under `File share settings`, click the value next to `Soft delete`. 1. Under `Soft delete for all file shares`, click the toggle to set it to `Enabled`. 1. Under `Retention policies`, set an appropriate number of days to retain soft deleted data between 1 and 365, inclusive. 1. Click `Save`. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable soft delete for file shares and set an appropriate number of days for deleted data to be retained, between 1 and 365, inclusive: ``` az storage account file-service-properties update --account-name --enable-delete-retention true --delete-retention-days ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to enable soft delete for file shares and set an appropriate number of days for deleted data to be retained, between 1 and 365, inclusive: ``` Update-AzStorageFileServiceProperty -ResourceGroupName -AccountName -EnableShareDeleteRetentionPolicy $true -ShareRetentionDays ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account with file shares, under `Data storage`, click on `File shares`. 1. Under `File share settings`, ensure the value for `Soft delete` shows a number of days between 1 and 365, inclusive. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` Run the following command to determine if a storage account has file shares: ``` az storage share list --account-name ``` For each storage account with file shares, run the following command: ``` az storage account file-service-properties show --resource-group --account-name ``` Ensure that under `shareDeleteRetentionPolicy`, `enabled` is set to `true`, and `days` is set to an appropriate value between 1 and 365, inclusive. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount -ResourceGroupName ``` With a storage account context set, run the following command to determine if a storage account has file shares: ``` Get-AzStorageShare ``` For each storage account with file shares, run the following command: ``` Get-AzStorageFileServiceProperty -ResourceGroupName -AccountName ``` Ensure that `ShareDeleteRetentionPolicy.Enabled` is set to `True` and `ShareDeleteRetentionPolicy.Days` is set to an appropriate value between 1 and 365, inclusive.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/files/storage-files-enable-soft-delete:https://learn.microsoft.com/en-us/cli/azure/storage/account/file-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/azure/storage/files/storage-files-prevent-file-share-deletion", + "DefaultValue": "Soft delete is enabled by default at the storage account file share setting level." + } + ] + }, + { + "Id": "4.1.1", + "Description": "Ensure only MFA enabled identities can access privileged Virtual Machine", + "Checks": [ + "entra_user_with_vm_access_has_mfa" + ], + "Attributes": [ + { + "Section": "4 Compute Services", + "SubSection": "4.1 Virtual Machines", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Verify identities without MFA that can log in to a privileged virtual machine using separate login credentials. An adversary can leverage the access to move laterally and perform actions with the virtual machine's managed identity. Make sure the virtual machine only has necessary permissions, and revoke the admin-level permissions according to the principle of least privilege.", + "RationaleStatement": "Integrating multi-factor authentication (MFA) as part of the organizational policy can greatly reduce the risk of an identity gaining control of valid credentials that may be used for additional tactics such as initial access, lateral movement, and collecting information. MFA can also be used to restrict access to cloud resources and APIs. An Adversary may log into accessible cloud services within a compromised environment using Valid Accounts that are synchronized to move laterally and perform actions with the virtual machine's managed identity. The adversary may then perform management actions or access cloud-hosted resources as the logged-on managed identity.", + "ImpactStatement": "This recommendation requires the Entra ID P2 license to implement. Ensure that identities provisioned to a virtual machine utilize an RBAC/ABAC group and are allocated a role using Azure PIM, and that the role settings require MFA or use another third-party PAM solution for accessing virtual machines.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Log in to the Azure portal. 2. This can be remediated by enabling MFA for user, Removing user access or Reducing access of managed identities attached to virtual machines. - Case I : Enable MFA for users having access on virtual machines. 1. Go to `Microsoft Entra ID`. 1. For `Per-user MFA`: 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA`. 1. For each user requiring remediation, check the box next to their name. 1. Click `Enable MFA`. 1. Click `Enable`. 1. For `Conditional Access`: 1. Under `Manage`, click `Security`. 1. Under `Protect`, click `Conditional Access`. 1. Update the Conditional Access policy requiring MFA for all users, removing each user requiring remediation from the `Exclude` list. - Case II : Removing user access on a virtual machine. 1. Select the `Subscription`, then click on `Access control (IAM)`. 2. Select `Role assignments` and search for `Virtual Machine Administrator Login` or `Virtual Machine User Login` or any role that provides access to log into virtual machines. 3. Click on `Role Name`, Select `Assignments`, and remove identities with no MFA configured. - Case III : Reducing access of managed identities attached to virtual machines. 1. Select the `Subscription`, then click on `Access control (IAM)`. 2. Select `Role Assignments` from the top menu and apply filters on `Assignment type` as `Privileged administrator roles` and `Type` as `Virtual Machines`. 3. Click on `Role Name`, Select `Assignments`, and remove identities access make sure this follows the least privileges principal.", + "AuditProcedure": "**Audit from Azure Portal** 1. Log in to the Azure portal. 1. Select the `Subscription`, then click on `Access control (IAM)`. 1. Click `Role : All` and click `All` to display the drop-down menu. 1. Type `Virtual Machine Administrator Login` and select `Virtual Machine Administrator Login`. 1. Review the list of identities that have been assigned the `Virtual Machine Administrator Login` role. 1. Go to `Microsoft Entra ID`. 1. For `Per-user MFA`: 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA`. 1. Ensure that none of the identities assigned the `Virtual Machine Administrator Login` role from step 4 have `Status` set to `disabled`. 1. For `Conditional Access`: 1. Under `Manage`, click `Security`. 1. Under `Protect`, click `Conditional Access`. 1. Ensure that none of the identities assigned the `Virtual Machine Administrator Login` role from step 4 are exempt from a Conditional Access policy requiring MFA for all users.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "" + } + ] + }, + { + "Id": "6.4", + "Description": "Ensure that 'Restrict non-admin users from creating tenants' is set to 'Yes'", + "Checks": [ + "entra_policy_ensure_default_user_cannot_create_tenants" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Require administrators or appropriately delegated users to create new tenants.", + "RationaleStatement": "It is recommended to only allow an administrator to create new tenants. This prevent users from creating new Microsoft Entra ID or Azure AD B2C tenants and ensures that only authorized users are able to do so.", + "ImpactStatement": "Enforcing this setting will ensure that only authorized users are able to create new tenants.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Set `Restrict non-admin users from creating tenants ` to `Yes` 1. Click `Save` **Remediate from PowerShell** ``` Import-Module Microsoft.Graph.Identity.SignIns Connect-MgGraph -Scopes 'Policy.ReadWrite.Authorization' Select-MgProfile -Name beta $params = @{ DefaultUserRolePermissions = @{ AllowedToCreateTenants = $false } } Update-MgPolicyAuthorizationPolicy -AuthorizationPolicyId -BodyParameter $params ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Ensure that `Restrict non-admin users from creating tenants` is set to `Yes` **Audit from PowerShell** ``` Import-Module Microsoft.Graph.Identity.SignIns Connect-MgGraph -Scopes 'Policy.ReadWrite.Authorization' Get-MgPolicyAuthorizationPolicy | Select-Object -ExpandProperty DefaultUserRolePermissions | Format-List ``` Review the DefaultUserRolePermissions section of the output. Ensure that `AllowedToCreateTenants` is not `True`.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/active-directory/fundamentals/users-default-permissions:https://learn.microsoft.com/en-us/azure/active-directory/roles/permissions-reference#tenant-creator:https://blog.admindroid.com/disable-users-creating-new-azure-ad-tenants-in-microsoft-365/", + "DefaultValue": "" + } + ] + }, + { + "Id": "10.1.2", + "Description": "Ensure 'SMB protocol version' is set to 'SMB 3.1.1' or higher for SMB file shares", + "Checks": [ + "storage_smb_protocol_version_is_latest" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.1 Azure Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that SMB file shares are configured to use the latest supported SMB protocol version. Keeping the SMB protocol updated helps mitigate risks associated with older SMB versions, which may contain vulnerabilities and lack essential security controls.", + "RationaleStatement": "Using the latest supported SMB protocol version enhances the security of SMB file shares by preventing the exploitation of known vulnerabilities in outdated SMB versions.", + "ImpactStatement": "Using the latest SMB protocol version may impact client compatibility.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. If `Profile` is set to `Maximum compatibility`, click the drop-down menu and select `Maximum security` or `Custom`. 1. If selecting `Custom`, under `SMB protocol versions`, uncheck the boxes next to `SMB 2.1` and `SMB 3.0`. 1. Click `Save`. 1. Repeat steps 1-7 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to set the SMB protocol version: ``` az storage account file-service-properties update --resource-group --account-name --versions SMB3.1.1 ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to set the SMB protocol version: ``` Update-AzStorageFileServiceProperty -ResourceGroupName -StorageAccountName -SmbProtocolVersion SMB3.1.1 ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. Under `SMB protocol versions`, ensure that `SMB3.1.1` is the only checked protocol version. 1. Repeat steps 1-5 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account file-service-properties show --resource-group --account-name ``` Ensure that under `protocolSettings` > `smb`, `versions` is set to `SMB3.1.1;` only. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the file service properties for a storage account in a resource group with a given name: ``` $storageaccountfileservice = Get-AzStorageFileServiceProperty -ResourceGroupName -AccountName ``` Run the following command to get the SMB protocol version setting: ``` $storageaccountfileservice.ProtocolSettings.Smb.Versions ``` Ensure that the command returns `SMB3.1.1` only. Repeat for each storage account.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-files#recommendations-for-smb-file-shares:https://learn.microsoft.com/en-us/azure/storage/files/files-smb-protocol#smb-security-settings:https://learn.microsoft.com/en-us/cli/azure/storage/account/file-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstoragefileserviceproperty", + "DefaultValue": "By default, all SMB versions are allowed." + } + ] + }, + { + "Id": "10.1.3", + "Description": "Ensure 'SMB channel encryption' is set to 'AES-256-GCM' or higher for SMB file shares", + "Checks": [ + "storage_smb_channel_encryption_with_secure_algorithm" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.1 Azure Files", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Implement SMB channel encryption with AES-256-GCM for SMB file shares to ensure data confidentiality and integrity in transit. This method offers strong protection against eavesdropping and man-in-the-middle attacks, safeguarding sensitive information.", + "RationaleStatement": "AES-256-GCM encryption enhances the security of data transmitted over SMB channels by safeguarding it from unauthorized interception and tampering.", + "ImpactStatement": "Using the AES-256-GCM SMB channel encryption may impact client compatibility.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. If `Profile` is set to `Maximum compatibility`, click the drop-down menu and select `Maximum security` or `Custom`. 1. If selecting `Custom`, under `SMB channel encryption`, uncheck the boxes next to `AES-128-CCM` and `AES-128-GCM`. 1. Click `Save`. 1. Repeat steps 1-7 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to set the SMB channel encryption: ``` az storage account file-service-properties update --resource-group --account-name --channel-encryption AES-256-GCM ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to set the SMB channel encryption: ``` Update-AzStorageFileServiceProperty -ResourceGroupName -StorageAccountName -SmbChannelEncryption AES-256-GCM ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Data storage`, click `File shares`. 1. Under `File share settings`, click the link next to `Security`. 1. Under `SMB channel encryption`, ensure that `AES-256-GCM`, or higher, is the only checked SMB channel encryption setting. 1. Repeat steps 1-5 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account file-service-properties show --resource-group --account-name ``` Ensure that under `protocolSettings` > `smb`, `channelEncryption` is set to `AES-256-GCM;`, or higher, only. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the file service properties for a storage account in a resource group with a given name: ``` $storageaccountfileservice = Get-AzStorageFileServiceProperty -ResourceGroupName -AccountName ``` Run the following command to get the SMB channel encryption setting: ``` $storageaccountfileservice.ProtocolSettings.Smb.ChannelEncryption ``` Ensure that the command returns `AES-256-GCM`, or higher, only. Repeat for each storage account.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/well-architected/service-guides/azure-files#recommendations-for-smb-file-shares:https://learn.microsoft.com/en-us/azure/storage/files/files-smb-protocol?tabs=azure-portal#smb-security-settings:https://learn.microsoft.com/en-us/cli/azure/storage/account/file-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragefileserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstoragefileserviceproperty", + "DefaultValue": "By default, the following SMB channel encryption algorithms are allowed: - AES-128-CCM - AES-128-GCM - AES-256-GCM" + } + ] + }, + { + "Id": "10.2.1", + "Description": "Ensure that soft delete for blobs on Azure Blob Storage storage accounts is Enabled", + "Checks": [ + "storage_ensure_soft_delete_is_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.2 Azure Blob Storage", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Blobs in Azure storage accounts may contain sensitive or personal data, such as ePHI or financial information. Data that is erroneously modified or deleted by an application or a user can lead to data loss or unavailability. It is recommended that soft delete be enabled on Azure storage accounts with blob storage to allow for the preservation and recovery of data when blobs or blob snapshots are deleted.", + "RationaleStatement": "Blobs can be deleted incorrectly. An attacker or malicious user may do this deliberately in order to cause disruption. Deleting an Azure storage blob results in immediate data loss. Enabling this configuration for Azure storage accounts ensures that even if blobs are deleted from the storage account, the blobs are recoverable for a specific period of time, which is defined in the Retention policies, ranging from 7 to 365 days.", + "ImpactStatement": "All soft-deleted data is billed at the same rate as active data. Additional costs may be incurred for deleted blobs until the soft delete period ends and the data is permanently removed.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account with blob storage, under `Data management`, go to `Data protection`. 1. Check the box next to `Enable soft delete for blobs`. 1. Set the retention period to a sufficient length for your organization. 1. Click `Save`. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable soft delete for blobs: ``` az storage blob service-properties delete-policy update --days-retained --account-name --enable true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account with blob storage, under `Data management`, go to `Data protection`. 1. Ensure that `Enable soft delete for blobs` is checked. 1. Ensure that the retention period is a sufficient length for your organization. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` Run the following command to determine if a storage account has containers: ``` az storage container list --account-name ``` For each storage account with containers, ensure that the output of the below command contains `enabled: true` and `days` is not `null`: ``` az storage blob service-properties delete-policy show --account-name ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/soft-delete-blob-overview", + "DefaultValue": "Soft delete for blob storage is **enabled** by default on storage accounts created via the Azure Portal, and **disabled** by default on storage accounts created via Azure CLI or PowerShell." + } + ] + }, + { + "Id": "10.2.2", + "Description": "Ensure 'Versioning' is set to 'Enabled' on Azure Blob Storage storage accounts", + "Checks": [ + "storage_blob_versioning_is_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.2 Azure Blob Storage", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Enabling blob versioning allows for the automatic retention of previous versions of objects. With blob versioning enabled, earlier versions of a blob are accessible for data recovery in the event of modifications or deletions.", + "RationaleStatement": "Blob versioning safeguards data integrity and enables recovery by retaining previous versions of stored objects, facilitating quick restoration from accidental deletion, modification, or malicious activity.", + "ImpactStatement": "Enabling blob versioning for a storage account creates a new version with each write operation to a blob, which can increase storage costs. To control these costs, a lifecycle management policy can be applied to automatically delete older versions.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account with blob storage. 1. In the `Overview` page, on the `Properties` tab, under `Blob service`, click `Disabled` next to `Versioning`. 1. Under `Tracking`, check the box next to `Enable versioning for blobs`. 1. Select the radio button next to `Keep all versions` or `Delete versions after (in days)`. 1. If selecting to delete versions, enter a number of in the box after which to delete blob versions. 1. Click `Save`. 1. Repeat steps 1-7 for each storage account with blob storage. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable blob versioning: ``` az storage account blob-service-properties update --account-name --enable-versioning true ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to enable blob versioning: ``` Update-AzStorageBlobServiceProperty -ResourceGroupName -StorageAccountName -IsVersioningEnabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account with blob storage. 1. In the `Overview` page, on the `Properties` tab, under `Blob service`, ensure `Versioning` is set to `Enabled`. 1. Repeat steps 1-3 for each storage account with blob storage. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` Run the following command to determine if a storage account has containers: ``` az storage container list --account-name ``` For each storage account with containers, ensure that the output of the below command contains `isVersioningEnabled: true`: ``` az storage account blob-service-properties show --account-name ``` **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to create an Azure Storage context for a storage account: ``` $context = New-AzStorageContext -StorageAccountName ``` Run the following command to list containers for the storage account: ``` Get-AzStorageContainer -Context $context ``` If the storage account has containers, run the following command to get the blob service properties of the storage account: ``` $account = Get-AzStorageBlobServiceProperty -ResourceGroupName -AccountName ``` Run the following command to get the blob versioning setting for the storage account: ``` $account.IsVersioningEnabled ``` Ensure that the command returns `True`. Repeat for each storage account. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [c36a325b-ae04-4863-ad4f-19c6678f8e08](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc36a325b-ae04-4863-ad4f-19c6678f8e08) **- Name:** 'Configure your Storage account to enable blob versioning'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/cli/azure/storage/account:https://learn.microsoft.com/en-us/cli/azure/storage/account/blob-service-properties:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstorageaccount:https://learn.microsoft.com/en-us/powershell/module/az.storage/new-azstoragecontext:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstoragecontainer:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstorageblobserviceproperty:https://learn.microsoft.com/en-us/powershell/module/az.storage/update-azstorageblobserviceproperty:https://learn.microsoft.com/en-us/azure/storage/blobs/versioning-overview:https://learn.microsoft.com/en-us/azure/storage/blobs/lifecycle-management-overview", + "DefaultValue": "Blob versioning is disabled by default on storage accounts." + } + ] + }, + { + "Id": "10.3.8", + "Description": "Ensure 'Cross Tenant Replication' is not enabled", + "Checks": [ + "storage_cross_tenant_replication_disabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Cross Tenant Replication in Azure allows data to be replicated across multiple Azure tenants. While this feature can be beneficial for data sharing and availability, it also poses a significant security risk if not properly managed. Unauthorized data access, data leakage, and compliance violations are potential risks. Disabling Cross Tenant Replication ensures that data is not inadvertently replicated across different tenant boundaries without explicit authorization.", + "RationaleStatement": "Disabling Cross Tenant Replication minimizes the risk of unauthorized data access and ensures that data governance policies are strictly adhered to. This control is especially critical for organizations with stringent data security and privacy requirements, as it prevents the accidental sharing of sensitive information.", + "ImpactStatement": "Disabling Cross Tenant Replication may affect data availability and sharing across different Azure tenants. Ensure that this change aligns with your organizational data sharing and availability requirements.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Data management`, click `Object replication`. 1. Click `Advanced settings`. 1. Uncheck `Allow cross-tenant replication`. 1. Click `OK`. **Remediate from Azure CLI** Replace the information within <> with appropriate values: ``` az storage account update --name --resource-group --allow-cross-tenant-replication false ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Data management`, click `Object replication`. 1. Click `Advanced settings`. 1. Ensure `Allow cross-tenant replication` is not checked. **Audit from Azure CLI** ``` az storage account list --query [*].[name,allowCrossTenantReplication] ``` The value of `false` should be returned for each storage account listed. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [92a89a79-6c52-4a7e-a03f-61306fc49312](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F92a89a79-6c52-4a7e-a03f-61306fc49312) **- Name:** 'Storage accounts should prevent cross tenant object replication'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/object-replication-prevent-cross-tenant-policies?tabs=portal", + "DefaultValue": "For new storage accounts created after Dec 15, 2023 cross tenant replication is not enabled." + } + ] + }, + { + "Id": "10.3.9", + "Description": "Ensure that 'Allow Blob Anonymous Access' is set to 'Disabled'", + "Checks": [ + "storage_blob_public_access_level_is_disabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The Azure Storage setting ‘Allow Blob Anonymous Access’ (aka allowBlobPublicAccess) controls whether anonymous access is allowed for blob data in a storage account. When this property is set to True, it enables public read access to blob data, which can be convenient for sharing data but may carry security risks. When set to False, it disallows public access to blob data, providing a more secure storage environment.", + "RationaleStatement": "If Allow Blob Anonymous Access is enabled, blobs can be accessed by adding the blob name to the URL to see the contents. An attacker can enumerate a blob using methods, such as brute force, and access them. Exfiltration of data by brute force enumeration of items from a storage account may occur if this setting is set to 'Enabled'.", + "ImpactStatement": "Additional consideration may be required for exceptional circumstances where elements of a storage account require public accessibility. In these circumstances, it is highly recommended that all data stored in the public facing storage account be reviewed for sensitive or potentially compromising data, and that sensitive or compromising data is never stored in these storage accounts.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Set `Allow Blob Anonymous Access` to `Disabled`. 1. Click `Save`. **Remediate from Powershell** For every storage account in scope, run the following: ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName -Name $storageAccount.AllowBlobPublicAccess = $false Set-AzStorageAccount -InputObject $storageAccount ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Ensure `Allow Blob Anonymous Access` is set to `Disabled`. **Audit from Azure CLI** For every storage account in scope: ``` az storage account show --name --query allowBlobPublicAccess ``` Ensure that every storage account in scope returns `false` for the allowBlobPublicAccess setting. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [4fa4b6c0-31ca-4c0d-b10d-24b96f62a751](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4fa4b6c0-31ca-4c0d-b10d-24b96f62a751) **- Name:** 'Storage account public access should be disallowed'", + "AdditionalInformation": "Azure Storage accounts that use the classic deployment model will be retired on August 31, 2024.", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent?tabs=portal:https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent?source=recommendations&tabs=portal:Classic Storage Accounts: https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent-classic?tabs=portal", + "DefaultValue": "Disabled" + } + ] + }, + { + "Id": "10.3.12", + "Description": "Ensure Redundancy is set to 'geo-redundant storage (GRS)' on critical Azure Storage Accounts", + "Checks": [ + "storage_geo_redundant_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Geo-redundant storage (GRS) in Azure replicates data three times within the primary region using locally redundant storage (LRS) and asynchronously copies it to a secondary region hundreds of miles away. This setup ensures high availability and resilience by providing 16 nines (99.99999999999999%) durability over a year, safeguarding data against regional outages.", + "RationaleStatement": "Enabling GRS protects critical data from regional failures by maintaining a copy in a geographically separate location. This significantly reduces the risk of data loss, supports business continuity, and meets high availability requirements for disaster recovery.", + "ImpactStatement": "Enabling geo-redundant storage on Azure storage accounts increases costs due to cross-region data replication.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Data management`, click `Redundancy`. 1. From the `Redundancy` drop-down menu, select `Geo-redundant storage (GRS)`. 1. Click `Save`. 1. Repeat steps 1-5 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable geo-redundant storage: ``` az storage account update --resource-group --name --sku Standard_GRS ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to enable geo-redundant storage: ``` Set-AzStorageAccount -ResourceGroupName -Name -SkuName Standard_GRS ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Data management`, click `Redundancy`. 1. Ensure that `Redundancy` is set to `Geo-redundant storage (GRS)`. 1. Repeat steps 1-4 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account show --resource-group --name ``` Under `sku`, ensure that `name` is set to `Standard_GRS`. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the storage account in a resource group with a given name: ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName -Name ``` Run the following command to get the redundancy setting for the storage account: ``` $storageAccount.SKU.Name ``` Ensure that the command returns `Standard_GRS`. Repeat for each storage account. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [bf045164-79ba-4215-8f95-f8048dc1780b](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fbf045164-79ba-4215-8f95-f8048dc1780b) **- Name:** 'Geo-redundant storage should be enabled for Storage Accounts'", + "AdditionalInformation": "When choosing the best redundancy option, weigh the trade-offs between lower costs and higher availability. Key factors to consider include: - The method of data replication within the primary region. - The replication of data from a primary to a geographically distant secondary region for protection against regional disasters (geo-replication). - The necessity for read access to replicated data in the secondary region during an outage in the primary region (geo-replication with read access).", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy:https://learn.microsoft.com/en-us/azure/storage/common/redundancy-migration:https://learn.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest#az-storage-account-update:https://learn.microsoft.com/en-us/powershell/module/az.storage/set-azstorageaccount?view=azps-12.4.0:https://learn.microsoft.com/en-us/azure/storage/common/storage-disaster-recovery-guidance", + "DefaultValue": "When creating a storage account in the Azure Portal, the default redundancy setting is geo-redundant storage (GRS). Using the Azure CLI, the default is read-access geo-redundant storage (RA-GRS). In PowerShell, a redundancy level must be explicitly specified during account creation." + } + ] + }, + { + "Id": "6.12", + "Description": "Ensure that 'User consent for applications' is set to 'Do not allow user consent'", + "Checks": [ + "entra_policy_restricts_user_consent_for_apps" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Require administrators to provide consent for applications before use.", + "RationaleStatement": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.", + "ImpactStatement": "Enforcing this setting may create additional requests that administrators need to review.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security`, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Set `User consent for applications` to `Do not allow user consent` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security`, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Ensure `User consent for applications` is set to `Do not allow user consent` **Audit from PowerShell** ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Select-Object -ExpandProperty PermissionGrantPoliciesAssigned ``` If the command returns no values in response, the configuration complies with the recommendation.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=ms-powershell#configure-user-consent-to-applications:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy", + "DefaultValue": "By default, `Users consent for applications` is set to `Allow user consent for apps`." + } + ] + }, + { + "Id": "6.13", + "Description": "Ensure that 'User consent for applications' is set to 'Allow user consent for apps from verified publishers, for selected permissions'", + "Checks": [ + "entra_policy_user_consent_for_verified_apps" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Allow users to provide consent for selected permissions when a request is coming from a verified publisher.", + "RationaleStatement": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.", + "ImpactStatement": "Enforcing this setting may create additional requests that administrators need to review.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Under `User consent for applications`, select `Allow user consent for apps from verified publishers, for selected permissions` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Enterprise applications` 1. Under `Security, select `Consent and permissions` 1. Under `Manage`, select `User consent settings` 1. Under `User consent for applications`, ensure `Allow user consent for apps from verified publishers, for selected permissions` is selected **Audit from PowerShell** ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Select-Object -ExpandProperty PermissionGrantPoliciesAssigned ``` The command should return either `ManagePermissionGrantsForSelf.microsoft-user-default-low` or a custom app consent policy id if one is in use.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=ms-graph#configure-user-consent-to-applications:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.signins/get-mgpolicyauthorizationpolicy?view=graph-powershell-1.0", + "DefaultValue": "By default, `User consent for applications` is set to `Allow user consent for apps`." + } + ] + }, + { + "Id": "6.14", + "Description": "Ensure that 'Users can register applications' is set to 'No'", + "Checks": [ + "entra_policy_ensure_default_user_cannot_create_apps" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Require administrators or appropriately delegated users to register third-party applications.", + "RationaleStatement": "It is recommended to only allow an administrator to register custom-developed applications. This ensures that the application undergoes a formal security review and approval process prior to exposing Microsoft Entra ID data. Certain users like developers or other high-request users may also be delegated permissions to prevent them from waiting on an administrative user. Your organization should review your policies and decide your needs.", + "ImpactStatement": "Enforcing this setting will create additional requests for approval that will need to be addressed by an administrator. If permissions are delegated, a user may approve a malevolent third party application, potentially giving it access to your data.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Set `Users can register applications` to `No` 1. Click `Save` **Remediate from PowerShell** ``` $param = @{ AllowedToCreateApps = $false } Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions $param ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Ensure that `Users can register applications` is set to `No` **Audit from PowerShell** ``` (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Format-List AllowedToCreateApps ``` Command should return the value of `False`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications:https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-azure-ad-instance:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.signins/get-mgpolicyauthorizationpolicy?view=graph-powershell-1.0", + "DefaultValue": "By default, `Users can register applications` is set to Yes." + } + ] + }, + { + "Id": "6.15", + "Description": "Ensure that 'Guest users access restrictions' is set to 'Guest user access is restricted to properties and memberships of their own directory objects'", + "Checks": [ + "entra_policy_guest_users_access_restrictions" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Limit guest user permissions.", + "RationaleStatement": "Limiting guest access ensures that guest accounts do not have permission for certain directory tasks, such as enumerating users, groups or other directory resources, and cannot be assigned to administrative roles in your directory. Guest access has three levels of restriction. 1. Guest users have the same access as members (most inclusive), 2. Guest users have limited access to properties and memberships of directory objects (default value), 3. Guest user access is restricted to properties and memberships of their own directory objects (most restrictive). The recommended option is the 3rd, most restrictive: Guest user access is restricted to their own directory object.", + "ImpactStatement": "This may create additional requests for permissions to access resources that administrators will need to approve. According to https://learn.microsoft.com/en-us/azure/active-directory/enterprise-users/users-restrict-guest-permissions#services-currently-not-supported Service without current support might have compatibility issues with the new guest restriction setting. - Forms - Project - Yammer - Planner in SharePoint", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `External Identities` 1. Select `External collaboration settings` 1. Under `Guest user access`, set `Guest user access restrictions` to `Guest user access is restricted to properties and memberships of their own directory objects` 1. Click `Save` **Remediate from PowerShell** 1. Enter the following to update the policy ID: ``` Update-MgPolicyAuthorizationPolicy -GuestUserRoleId 2af84b1e-32c8-42b7-82bc-daa82404023b ``` 1. Check the GuestUserRoleId again: ``` (Get-MgPolicyAuthorizationPolicy).GuestUserRoleId ``` 1. Ensure that the GuestUserRoleId is equal to the earlier entered value of `2af84b1e-32c8-42b7-82bc-daa82404023b`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `External Identities` 1. Select `External collaboration settings` 1. Under `Guest user access`, ensure that `Guest user access restrictions ` is set to `Guest user access is restricted to properties and memberships of their own directory objects` **Audit from PowerShell** 1. Enter the following: ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).GuestUserRoleId ``` Which will give a result like: ``` Id : authorizationPolicy OdataType : Description : Used to manage authorization related settings across the company. DisplayName : Authorization Policy EnabledPreviewFeatures : {} GuestUserRoleId : 10dae51f-b6af-4016-8d66-8c2a99b929b3 PermissionGrantPolicyIdsAssignedToDefaultUserRole : {user-default-legacy} ``` If the GuestUserRoleID property does not equal `2af84b1e-32c8-42b7-82bc-daa82404023b` then it is not set to most restrictive.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions", + "DefaultValue": "By default, `Guest user access restrictions` is set to `Guest users have limited access to properties and memberships of directory objects`." + } + ] + }, + { + "Id": "6.16", + "Description": "Ensure that 'Guest invite restrictions' is set to 'Only users assigned to specific admin roles can invite guest users'", + "Checks": [ + "entra_policy_guest_invite_only_for_admin_roles" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Restrict invitations to users with specific administrative roles only.", + "RationaleStatement": "Restricting invitations to users with specific administrator roles ensures that only authorized accounts have access to cloud resources. This helps to maintain Need to Know permissions and prevents inadvertent access to data. By default the setting `Guest invite restrictions` is set to `Anyone in the organization can invite guest users including guests and non-admins`. This would allow anyone within the organization to invite guests and non-admins to the tenant, posing a security risk.", + "ImpactStatement": "With the option of `Only users assigned to specific admin roles can invite guest users` selected, users with specific admin roles will be in charge of sending invitations to the external users, requiring additional overhead by them to manage user accounts. This will mean coordinating with other departments as they are onboarding new users.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `External Identities` 1. Select `External collaboration settings` 1. Under `Guest invite settings`, set `Guest invite restrictions`, to `Only users assigned to specific admin roles can invite guest users` 1. Click `Save` **Remediate from Powershell** Enter the following: ``` Connect-MgGraph Update-MgPolicyAuthorizationPolicy -AllowInvitesFrom adminsAndGuestInviters ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `External Identities` 1. Select `External collaboration settings` 1. Under `Guest invite settings`, for `Guest invite restrictions`, ensure that `Only users assigned to specific admin roles can invite guest users` is selected Note: This setting has 4 levels of restriction, which include: - Anyone in the organization can invite guest users including guests and non-admins (most inclusive), - Member users and users assigned to specific admin roles can invite guest users including guests with member permissions, - Only users assigned to specific admin roles can invite guest users, - No one in the organization can invite guest users including admins (most restrictive). **Audit from Powershell** Enter the following: ``` Connect-MgGraph (Get-MgPolicyAuthorizationPolicy).AllowInvitesFrom ``` If the resulting value is `adminsAndGuestInviters` the configuration complies.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.signins/update-mgpolicyauthorizationpolicy?view=graph-powershell-1.0", + "DefaultValue": "By default, `Guest invite restrictions` is set to `Anyone in the organization can invite guest users including guests and non-admins`" + } + ] + }, + { + "Id": "10.3.1.3", + "Description": "Ensure 'Allow storage account key access' for Azure Storage Accounts is 'Disabled'", + "Checks": [ + "storage_account_key_access_disabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Every secure request to an Azure Storage account must be authorized. By default, requests can be authorized with either Microsoft Entra credentials or by using the account access key for Shared Key authorization.", + "RationaleStatement": "Microsoft Entra ID provides superior security and ease of use compared to Shared Key and is recommended by Microsoft. To require clients to use Microsoft Entra ID for authorizing requests, you can disallow requests to the storage account that are authorized with Shared Key.", + "ImpactStatement": "When you disallow Shared Key authorization for a storage account, any requests to the account that are authorized with Shared Key, including shared access signatures (SAS), will be denied. Client applications that currently access the storage account using the Shared Key will no longer function.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Settings`, click `Configuration`. 1. Under `Allow storage account key access`, click the radio button next to `Disabled`. 1. Click `Save`. 1. Repeat steps 1-5 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to disallow shared key authorization: ``` az storage account update --resource-group --name --allow-shared-key-access false ``` **Remediate from PowerShell** For each storage account requiring remediation, run the following command to disallow shared key authorization: ``` Set-AzStorageAccount -ResourceGroupName -Name -AllowSharedKeyAccess $false ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click on a storage account. 1. Under `Settings`, click `Configuration`. 1. Under `Allow storage account key access`, ensure that the radio button next to `Disabled` is selected. 1. Repeat steps 1-4 for each storage account. **Audit from Azure CLI** Run the following command to list storage accounts: ``` az storage account list ``` For each storage account, run the following command: ``` az storage account show --resource-group --name ``` Ensure that `allowSharedKeyAccess` is set to `false`. **Audit from PowerShell** Run the following command to list storage accounts: ``` Get-AzStorageAccount ``` Run the following command to get the storage account in a resource group with a given name: ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName -Name ``` Run the following command to get the shared key access setting for the storage account: ``` $storageAccount.allowSharedKeyAccess ``` Ensure that the command returns `False`. Repeat for each storage account. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [8c6a50c6-9ffd-4ae7-986f-5fa6111f9a54](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F8c6a50c6-9ffd-4ae7-986f-5fa6111f9a54) **- Name:** 'Storage accounts should prevent shared key access'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/shared-key-authorization-prevent:https://learn.microsoft.com/en-us/cli/azure/storage/account:https://learn.microsoft.com/en-us/powershell/module/az.storage/get-azstorageaccount:https://learn.microsoft.com/en-us/powershell/module/az.storage/set-azstorageaccount", + "DefaultValue": "The AllowSharedKeyAccess property of a storage account is not set by default and does not return a value until you explicitly set it. The storage account permits requests that are authorized with the Shared Key when the property value is **null** or when it is **true**." + } + ] + }, + { + "Id": "10.3.3.1", + "Description": "Ensure that 'Default to Microsoft Entra authorization in the Azure portal' is set to 'Enabled'", + "Checks": [ + "storage_default_to_entra_authorization_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "When this property is enabled, the Azure portal authorizes requests to blobs, files, queues, and tables with Microsoft Entra ID by default.", + "RationaleStatement": "Microsoft Entra ID provides superior security and ease of use over Shared Key.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Settings`, click `Configuration`. 1. Under `Default to Microsoft Entra authorization in the Azure portal`, click the radio button next to `Enabled`. 1. Click `Save`. 1. Repeat steps 1-5 for each storage account requiring remediation. **Remediate from Azure CLI** For each storage account requiring remediation, run the following command to enable `defaultToOAuthAuthentication`: ``` az storage account update --resource-group --name --set defaultToOAuthAuthentication=true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage accounts`. 1. Click the name of a storage account. 1. Under `Settings`, click `Configuration`. 1. Ensure that `Default to Microsoft Entra authorization in the Azure portal` is set to `Enabled`. 1. Repeat steps 1-4 for each storage account. **Audit from Azure CLI** Run the following command to get the `name` and `defaultToOAuthAuthentication` setting for each storage account: ``` az storage account list --query [*].[name,defaultToOAuthAuthentication] ``` Ensure that `true` is returned for each storage account.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-data-operations-portal#default-to-microsoft-entra-authorization-in-the-azure-portal:https://learn.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest", + "DefaultValue": "By default, `defaultToOAuthAuthentication` is disabled." + } + ] + }, + { + "Id": "6.19", + "Description": "Ensure that 'Users can create security groups in Azure portals, API or PowerShell' is set to 'No'", + "Checks": [ + "entra_policy_default_users_cannot_create_security_groups" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Restrict security group creation to administrators only.", + "RationaleStatement": "When creating security groups is enabled, all users in the directory are allowed to create new security groups and add members to those groups. Unless a business requires this day-to-day delegation, security group creation should be restricted to administrators only.", + "ImpactStatement": "Enabling this setting could create a number of requests that would need to be managed by an administrator.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Security Groups`, set `Users can create security groups in Azure portals, API or PowerShell` to `No` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Security Groups`, ensure that `Users can create security groups in Azure portals, API or PowerShell` is set to `No`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management#making-a-group-available-for-end-user-self-service:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements", + "DefaultValue": "By default, `Users can create security groups in Azure portals, API or PowerShell` is set to `Yes`" + } + ] + }, + { + "Id": "2.1.1.1.1", + "Description": "Ensure Critical Data is Encrypted with Microsoft Managed Keys (MMK)", + "Checks": [], + "Attributes": [ + { + "Section": "2 Common Reference Recommendations", + "SubSection": "2.1 Secrets and Keys", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Microsoft Managed Keys (MMK) (also known as Platform-managed keys (PMK)) provides a very low overhead method of encrypting data at rest and implementing encryption key management. Keys maintained in an MMK implementation are automatically managed by Azure and require no customer interaction.", + "RationaleStatement": "The encryption of data at rest is a foundational component of data security. Data at rest without encryption is easily compromised through loss or theft. Encrypting data at rest introduces confidentiality to the data by obfuscating the data contents with a cipher algorithm and provides an authentication requirement through the use of cryptographic keys. MMK makes the encryption of data at rest very easy to implement and maintain.", + "ImpactStatement": "", + "RemediationProcedure": "", + "AuditProcedure": "", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-5-use-customer-managed-key-option-in-data-at-rest-encryption-when-required:https://learn.microsoft.com/en-us/azure/security/fundamentals/key-management", + "DefaultValue": "By default, Encryption type is set to Microsoft Managed Keys." + } + ] + }, + { + "Id": "6.21", + "Description": "Ensure that 'Users can create Microsoft 365 groups in Azure portals, API or PowerShell' is set to 'No'", + "Checks": [ + "entra_users_cannot_create_microsoft_365_groups" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Restrict Microsoft 365 group creation to administrators only.", + "RationaleStatement": "Restricting Microsoft 365 group creation to administrators only ensures that creation of Microsoft 365 groups is controlled by the administrator. Appropriate groups should be created and managed by the administrator and group creation rights should not be delegated to any other user.", + "ImpactStatement": "Enabling this setting could create a number of requests that would need to be managed by an administrator.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Microsoft 365 Groups`, set `Users can create Microsoft 365 groups in Azure portals, API or PowerShell` to `No` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Microsoft 365 Groups`, ensure that `Users can create Microsoft 365 groups in Azure portals, API or PowerShell` is set to `No`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide&redirectSourcePath=%252fen-us%252farticle%252fControl-who-can-create-Office-365-Groups-4c46c8cb-17d0-44b5-9776-005fced8e618:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements", + "DefaultValue": "By default, `Users can create Microsoft 365 groups in Azure portals, API or PowerShell` is set to `Yes`." + } + ] + }, + { + "Id": "3.1.2", + "Description": "Ensure that network security groups are configured for Databricks subnets", + "Checks": [], + "Attributes": [ + { + "Section": "3 Analytics Services", + "SubSection": "3.1 Azure Databricks", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Network Security Groups (NSGs) should be implemented to control inbound and outbound traffic to Azure Databricks subnets, ensuring only authorized communication. NSGs should be configured with deny rules to block unwanted traffic and restrict communication to essential sources only.", + "RationaleStatement": "", + "ImpactStatement": "* NSGs require periodic maintenance to ensure rule accuracy. * Misconfigured NSGs could inadvertently block required traffic.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Assign NSG to Databricks subnets under Networking > NSG Settings.", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to Virtual Networks > Subnets, and review NSG assignments. **Audit from Azure CLI** ``` az network nsg list --query [].{Name:name, Rules:securityRules} ``` **Audit from PowerShell** ``` Get-AzNetworkSecurityGroup -ResourceGroupName ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/baselines/azure-databricks-security-baseline:https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/vnet-inject#network-security-group-rules", + "DefaultValue": "By default, Databricks subnets do not have NSGs assigned." + } + ] + }, + { + "Id": "6.23", + "Description": "Ensure that no custom subscription administrator roles exist", + "Checks": [ + "iam_subscription_roles_owner_custom_not_created" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The principle of least privilege should be followed and only necessary privileges should be assigned instead of allowing full administrative access.", + "RationaleStatement": "Custom roles in Azure with administrative access can obfuscate the permissions granted and introduce complexity and blind spots to the management of privileged identities. For less mature security programs without regular identity audits, the creation of Custom roles should be avoided entirely. For more mature security programs with regular identity audits, Custom Roles should be audited for use and assignment, used minimally, and the principle of least privilege should be observed when granting permissions", + "ImpactStatement": "Subscriptions will need to be handled by Administrators with permissions.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Select `Roles`. 1. Click `Type` and select `Custom role` from the drop-down menu. 1. Check the box next to each role which grants subscription administrator privileges. 1. Select `Delete`. 1. Select `Yes`. **Remediate from Azure CLI** List custom roles: ``` az role definition list --custom-role-only True ``` Check for entries with `assignableScope` of the `subscription`, and an action of `*`. To remove a violating role: ``` az role definition delete --name ``` Note that any role assignments must be removed before a custom role can be deleted. Ensure impact is assessed before deleting a custom role granting subscription administrator privileges.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Select `Roles`. 1. Click `Type` and select `Custom role` from the drop-down menu. 1. Select `View` next to a role. 1. Select `JSON`. 1. Check for `assignableScopes` set to the subscription, and `actions` set to `*`. 1. Repeat steps 7-9 for each custom role. **Audit from Azure CLI** List custom roles: ``` az role definition list --custom-role-only True ``` Check for entries with `assignableScope` of the `subscription`, and an action of `*` **Audit from PowerShell** ``` Connect-AzAccount Get-AzRoleDefinition |Where-Object {($_.IsCustom -eq $true) -and ($_.Actions.contains('*'))} ``` Check the output for `AssignableScopes` value set to the subscription. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [a451c1ef-c6ca-483d-87ed-f49761e3ffb5](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fa451c1ef-c6ca-483d-87ed-f49761e3ffb5) **- Name:** 'Audit usage of custom RBAC roles'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/billing/billing-add-change-azure-subscription-administrator:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle", + "DefaultValue": "By default, no custom owner roles are created." + } + ] + }, + { + "Id": "6.24", + "Description": "Ensure that a custom role is assigned permissions for administering resource locks", + "Checks": [ + "iam_custom_role_has_permissions_to_administer_resource_locks" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Resource locking is a powerful protection mechanism that can prevent inadvertent modification or deletion of resources within Azure subscriptions and resource groups, and it is a recommended NIST configuration.", + "RationaleStatement": "Given that the resource lock functionality is outside of standard Role-Based Access Control (RBAC), it would be prudent to create a resource lock administrator role to prevent inadvertent unlocking of resources.", + "ImpactStatement": "By adding this role, specific permissions may be granted for managing only resource locks rather than needing to provide the broad Owner or User Access Administrator role, reducing the risk of the user being able to cause unintentional damage.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. In the Azure portal, navigate to a subscription or resource group. 1. Click `Access control (IAM)`. 1. Click `+ Add`. 1. Click `Add custom role`. 1. In the `Custom role name` field enter `Resource Lock Administrator`. 1. In the `Description` field enter `Can Administer Resource Locks`. 1. For `Baseline permissions` select `Start from scratch`. 1. Click `Next`. 1. Click `Add permissions`. 1. In the `Search for a permission` box, type `Microsoft.Authorization/locks`. 1. Click the result. 1. Check the box next to `Permission`. 1. Click `Add`. 1. Click `Review + create`. 1. Click `Create`. 1. Click `OK`. 1. Click `+ Add`. 1. Click `Add role assignment`. 1. In the `Search by role name, description, permission, or ID` box, type `Resource Lock Administrator`. 1. Select the role. 1. Click `Next`. 1. Click `+ Select members`. 1. Select appropriate members. 1. Click `Select`. 1. Click `Review + assign`. 1. Click `Review + assign` again. 1. Repeat steps 1-26 for each subscription or resource group requiring remediation. **Remediate from PowerShell:** Below is a PowerShell definition for a resource lock administrator role created at an Azure Management group level ``` Import-Module Az.Accounts Connect-AzAccount $role = Get-AzRoleDefinition User Access Administrator $role.Id = $null $role.Name = Resource Lock Administrator $role.Description = Can Administer Resource Locks $role.Actions.Clear() $role.Actions.Add(Microsoft.Authorization/locks/*) $role.AssignableScopes.Clear() * Scope at the Management group level Management group $role.AssignableScopes.Add(/providers/Microsoft.Management/managementGroups/MG-Name) New-AzRoleDefinition -Role $role Get-AzureRmRoleDefinition Resource Lock Administrator ```", + "AuditProcedure": "**Audit from Azure Portal** 1. In the Azure portal, navigate to a subscription or resource group. 1. Click `Access control (IAM)`. 1. Click `Roles`. 1. Click `Type : All`. 1. Click to view the drop-down menu. 1. Select `Custom role`. 1. Click `View` in the `Details` column of a custom role. 1. Review the role permissions. 1. Click `Assignments` and review the assignments. 1. Click the `X` to exit the custom role details page. 1. Repeat steps 7-10. Ensure that at least one custom role exists that assigns the `Microsoft.Authorization/locks` permission to appropriate members. 1. Repeat steps 1-11 for each subscription or resource group.", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/role-based-access-control/custom-roles:https://docs.microsoft.com/en-us/azure/role-based-access-control/check-access:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy", + "DefaultValue": "A role for administering resource locks does not exist by default." + } + ] + }, + { + "Id": "3.1.3", + "Description": "Ensure that traffic is encrypted between cluster worker nodes", + "Checks": [], + "Attributes": [ + { + "Section": "3 Analytics Services", + "SubSection": "3.1 Azure Databricks", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "By default, data exchanged between worker nodes in an Azure Databricks cluster is not encrypted. To ensure that data is encrypted at all times, whether at rest or in transit, you can create an initialization script that configures your clusters to encrypt traffic between worker nodes using AES 256-bit encryption over a TLS 1.3 connection.", + "RationaleStatement": "* Protects sensitive data during transit between cluster nodes, mitigating risks of data interception or unauthorized access. * Aligns with organizational security policies and compliance requirements that mandate encryption of data in transit. * Enhances overall security posture by ensuring that all inter-node communications within the cluster are encrypted.", + "ImpactStatement": "* Enabling encryption may introduce a performance penalty due to the computational overhead associated with encrypting and decrypting traffic. This can result in longer query execution times, especially for data-intensive operations. * Implementing encryption requires creating and managing init scripts, which adds complexity to cluster configuration and maintenance. * The shared encryption secret is derived from the hash of the keystore stored in DBFS. If the keystore is updated or rotated, all running clusters must be restarted to prevent authentication failures between Spark workers and drivers.", + "RemediationProcedure": "Create a JKS keystore: 1. Generate a Java KeyStore (JKS) file that will be used for SSL/TLS encryption. 2. Upload the keystore file to a secure directory in DBFS (e.g. /dbfs//jetty_ssl_driver_keystore.jks). Develop an init script: 3. Create an init script that performs the following tasks: - Retrieves the JKS keystore file and password. - Derives a shared encryption secret from the keystore. - Configures Spark driver and executor settings to enable encryption. 4. Example init script: ``` #!/bin/bash set -euo pipefail keystore_dbfs_file=/dbfs//jetty_ssl_driver_keystore.jks max_attempts=30 while [ ! -f ${keystore_dbfs_file} ]; do if [ $max_attempts == 0 ]; then echo ERROR: Unable to find the file : $keystore_dbfs_file. Failing the script. exit 1 fi sleep 2s ((max_attempts--)) done sasl_secret=$(sha256sum $keystore_dbfs_file | cut -d' ' -f1) if [ -z ${sasl_secret} ]; then echo ERROR: Unable to derive the secret. Failing the script. exit 1 fi local_keystore_file=$DB_HOME/keys/jetty_ssl_driver_keystore.jks local_keystore_password=gb1gQqZ9ZIHS if [[ $DB_IS_DRIVER = TRUE ]]; then driver_conf=${DB_HOME}/driver/conf/spark-branch.conf echo Configuring driver conf at $driver_conf if [ ! -e $driver_conf ]; then echo spark.authenticate true >> $driver_conf echo spark.authenticate.secret $sasl_secret >> $driver_conf echo spark.authenticate.enableSaslEncryption true >> $driver_conf echo spark.network.crypto.enabled true >> $driver_conf echo spark.network.crypto.keyLength 256 >> $driver_conf echo spark.network.crypto.keyFactoryAlgorithm PBKDF2WithHmacSHA1 >> $driver_conf echo spark.io.encryption.enabled true >> $driver_conf echo spark.ssl.enabled true >> $driver_conf echo spark.ssl.keyPassword $local_keystore_password >> $driver_conf echo spark.ssl.keyStore $local_keystore_file >> $driver_conf echo spark.ssl.keyStorePassword $local_keystore_password >> $driver_conf echo spark.ssl.protocol TLSv1.3 >> $driver_conf fi fi executor_conf=${DB_HOME}/conf/spark.executor.extraJavaOptions echo Configuring executor conf at $executor_conf if [ ! -e $executor_conf ]; then echo -Dspark.authenticate=true >> $executor_conf echo -Dspark.authenticate.secret=$sasl_secret >> $executor_conf echo -Dspark.authenticate.enableSaslEncryption=true >> $executor_conf echo -Dspark.network.crypto.enabled=true >> $executor_conf echo -Dspark.network.crypto.keyLength=256 >> $executor_conf echo -Dspark.network.crypto.keyFactoryAlgorithm=PBKDF2WithHmacSHA1 >> $executor_conf echo -Dspark.io.encryption.enabled=true >> $executor_conf echo -Dspark.ssl.enabled=true >> $executor_conf echo -Dspark.ssl.keyPassword=$local_keystore_password >> $executor_conf echo -Dspark.ssl.keyStore=$local_keystore_file >> $executor_conf echo -Dspark.ssl.keyStorePassword=$local_keystore_password >> $executor_conf echo -Dspark.ssl.protocol=TLSv1.3 >> $executor_conf fi ``` 5. Save.", + "AuditProcedure": "**Audit from Azure Portal** Review cluster init scripts: 1. Navigate to your Azure Databricks workspace, go to the Clusters section, select a cluster, and check the Advanced Options for any init scripts that configure encryption settings. Verify spark configuration: 2. Ensure that the following Spark configurations are set: ``` spark.authenticate true spark.authenticate.enableSaslEncryption true spark.network.crypto.enabled true spark.network.crypto.keyLength 256 spark.network.crypto.keyFactoryAlgorithm PBKDF2WithHmacSHA1 spark.io.encryption.enabled true ``` These settings can be found in the cluster's Spark configuration properties. Check keystone management: 3. Verify that the Java KeyStore (JKS) file is securely stored in DBFS and that its integrity is maintained. 4. Ensure that the keystore password is securely managed and not hardcoded in scripts.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/databricks/security/keys/encrypt-otw", + "DefaultValue": "By default, traffic is not encrypted between cluster worker nodes." + } + ] + }, + { + "Id": "6.26", + "Description": "Ensure fewer than 5 users have global administrator assignment", + "Checks": [ + "entra_global_admin_in_less_than_five_users" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "This recommendation aims to maintain a balance between security and operational efficiency by ensuring that a minimum of 2 and a maximum of 4 users are assigned the Global Administrator role in Microsoft Entra ID. Having at least two Global Administrators ensures redundancy, while limiting the number to four reduces the risk of excessive privileged access.", + "RationaleStatement": "The Global Administrator role has extensive privileges across all services in Microsoft Entra ID. The Global Administrator role should never be used in regular daily activities; administrators should have a regular user account for daily activities, and a separate account for administrative responsibilities. Limiting the number of Global Administrators helps mitigate the risk of unauthorized access, reduces the potential impact of human error, and aligns with the principle of least privilege to reduce the attack surface of an Azure tenant. Conversely, having at least two Global Administrators ensures that administrative functions can be performed without interruption in case of unavailability of a single admin.", + "ImpactStatement": "Implementing this recommendation may require changes in administrative workflows or the redistribution of roles and responsibilities. Adequate training and awareness should be provided to all Global Administrators.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Roles and administrators` 1. Under `Administrative Roles`, select `Global Administrator` If more than 4 users are assigned: 1. Remove Global Administrator role for users which do not or no longer require the role. 1. Assign Global Administrator role via PIM which can be activated when required. 1. Assign more granular roles to users to conduct their duties. If only one user is assigned: 1. Provide the Global Administrator role to a trusted user or create a break glass admin account.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Roles and administrators` 1. Under `Administrative Roles`, select `Global Administrator` 1. Ensure less than 5 users are actively assigned the role. 1. Ensure that at least 2 users are actively assigned the role.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5:https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide#security-guidelines-for-assigning-roles:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users", + "DefaultValue": "" + } + ] + }, + { + "Id": "6.1.1", + "Description": "Ensure that 'security defaults' is enabled in Microsoft Entra ID", + "Checks": [ + "entra_security_defaults_enabled" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.1 Security Defaults (Per-User MFA)", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "[**IMPORTANT - Please read the section overview:** If your organization pays for Microsoft Entra ID licensing (included in Microsoft 365 E3, E5, F5, or Business Premium, and EM&S E3 or E5 licenses) and **CAN** use Conditional Access, ignore the recommendations in this section and proceed to the Conditional Access section.] Security defaults in Microsoft Entra ID make it easier to be secure and help protect your organization. Security defaults contain preconfigured security settings for common attacks. Security defaults is available to everyone. The goal is to ensure that all organizations have a basic level of security enabled at no extra cost. You may turn on security defaults in the Azure portal.", + "RationaleStatement": "Security defaults provide secure default settings that we manage on behalf of organizations to keep customers safe until they are ready to manage their own identity security settings. For example, doing the following: - Requiring all users and admins to register for MFA. - Challenging users with MFA - when necessary, based on factors such as location, device, role, and task. - Disabling authentication from legacy authentication clients, which can’t do MFA.", + "ImpactStatement": "This recommendation should be implemented initially and then may be overridden by other service/product specific CIS Benchmarks. Administrators should also be aware that certain configurations in Microsoft Entra ID may impact other Microsoft services such as Microsoft 365.", + "RemediationProcedure": "**Remediate from Azure Portal** To enable security defaults in your directory: 1. From Azure Home select the Portal Menu. 1. Browse to `Microsoft Entra ID` > `Properties`. 1. Select `Manage security defaults`. 1. Under `Security defaults`, select `Enabled (recommended)`. 1. Select `Save`.", + "AuditProcedure": "**Audit from Azure Portal** To ensure security defaults is enabled in your directory: 1. From Azure Home select the Portal Menu. 2. Browse to `Microsoft Entra ID` > `Properties`. 3. Select `Manage security defaults`. 4. Under `Security defaults`, verify that `Enabled (recommended)` is selected.", + "AdditionalInformation": "This recommendation differs from the [Microsoft 365 Benchmark](https://workbench.cisecurity.org/benchmarks/5741). This is because the potential impact associated with disabling Security Defaults is dependent upon the security settings implemented in the environment. It is recommended that organizations disabling Security Defaults implement appropriate security settings to replace the settings configured by Security Defaults.", + "References": "https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/concept-fundamentals-security-defaults:https://techcommunity.microsoft.com/t5/azure-active-directory-identity/introducing-security-defaults/ba-p/1061414:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-2-protect-identity-and-authentication-systems", + "DefaultValue": "If your tenant was created on or after October 22, 2019, security defaults may already be enabled in your tenant." + } + ] + }, + { + "Id": "6.1.2", + "Description": "Ensure that 'multifactor authentication' is 'enabled' for all users", + "Checks": [ + "entra_privileged_user_has_mfa", + "entra_non_privileged_user_has_mfa" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.1 Security Defaults (Per-User MFA)", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "[**IMPORTANT - Please read the section overview:** If your organization pays for Microsoft Entra ID licensing (included in Microsoft 365 E3, E5, F5, or Business Premium, and EM&S E3 or E5 licenses) and **CAN** use Conditional Access, ignore the recommendations in this section and proceed to the Conditional Access section.] Enable multifactor authentication for all users. **Note:** Since 2024, Azure has been rolling out mandatory multifactor authentication. For more information: - https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in - https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication", + "RationaleStatement": "Multifactor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multifactor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multifactor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.", + "ImpactStatement": "Users would require two forms of authentication before any access is granted. Additional administrative time will be required for managing dual forms of authentication when enabling multifactor authentication.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA` from the top menu. 1. Click the box next to a user with `Status` `disabled`. 1. Click `Enable MFA`. 1. Click `Enable`. 1. Repeat steps 1-6 for each user requiring remediation. **Other options within Azure Portal** - [https://docs.microsoft.com/en-us/azure/active-directory/authentication/tutorial-enable-azure-mfa](https://docs.microsoft.com/en-us/azure/active-directory/authentication/tutorial-enable-azure-mfa) - [https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-mfasettings](https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-mfasettings) - [https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-admin-mfa](https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-admin-mfa) - [https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-getstarted#enable-multi-factor-authentication-with-conditional-access](https://docs.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-getstarted#enable-multi-factor-authentication-with-conditional-access)", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Users`. 1. Click `Per-user MFA` from the top menu. 1. Ensure that `Status` is `enabled` for all users. **Audit from REST API** Run the following Graph PowerShell command: ``` get-mguser -All | where {$_.StrongAuthenticationMethods.Count -eq 0} | Select-Object -Property UserPrincipalName ``` If the output contains any `UserPrincipalName`, then this recommendation is non-compliant.", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/multi-factor-authentication/multi-factor-authentication:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication:https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in/:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-4-authenticate-server-and-services", + "DefaultValue": "Multifactor authentication is not enabled for all users by default. Starting in 2024, multifactor authentication is enabled for administrative accounts by default." + } + ] + }, + { + "Id": "3.1.4", + "Description": "Ensure that users and groups are synced from Microsoft Entra ID to Azure Databricks", + "Checks": [], + "Attributes": [ + { + "Section": "3 Analytics Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "To ensure centralized identity and access management, users and groups from Microsoft Entra ID should be synchronized with Azure Databricks. This is achieved through SCIM provisioning, which automates the creation, update, and deactivation of users and groups in Databricks based on Entra ID assignments. Enabling this integration ensures that access controls in Databricks remain consistent with corporate identity governance policies, reducing the risk of orphaned accounts, stale permissions, and unauthorized access.", + "RationaleStatement": "Syncing users and groups from Microsoft Entra ID centralizes access control, enforces the least privilege principle by automatically revoking unnecessary access, reduces administrative overhead by eliminating manual user management, and ensures auditability and compliance with industry regulations.", + "ImpactStatement": "SCIM provisioning requires role mapping to avoid misconfigured user privileges.", + "RemediationProcedure": "**Remediate from Azure Portal** Enable provisioning in Azure Portal: 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Enterprise applications`. 1. Click the name of the Azure Databricks SCIM application. 1. Under `Provisioning`, select `Automatic` and enter the SCIM endpoint and API token from Databricks. Enable provisioning in Databricks: 5. Navigate to `Admin Console` > `Identity and Access Management`. 6. Enable SCIM provisioning and generate an API token. Configure role assignments: 7. Ensure groups from Entra ID are mapped to appropriate Databricks roles. 8. Restrict administrative privileges to designated security groups. Regularly monitor sync logs: 9. Periodically review sync logs in Microsoft Entra ID and Databricks Admin Console. 10. Configure Azure Monitor alerts for provisioning failures. Disable manual user creation in Databricks: 11. Ensure that all user management is controlled via SCIM sync from Entra ID. 12. Disable personal access token usage for authentication. **Remediate from Azure CLI** Enable SCIM User and Group Provisioning in Azure Databricks: ``` az ad app update --id --set provisioning.provisioningMode=Automatic ```", + "AuditProcedure": "**Audit from Azure Portal** Verify SCIM provisioning is enabled: 1. Go to `Microsoft Entra ID`. 1. Under `Manage`, click `Enterprise applications`. 1. Click the name of the Azure Databricks SCIM application. 1. Under `Provisioning`, confirm that SCIM provisioning is enabled and running. Check user sync status in Azure Portal: 5. Under `Provisioning Logs`, verify the last successful sync and any failed entries. Check user sync status in Databricks: 6. Go to `Admin Console` > `Identity and Access Management`. 7. Confirm that Users and Groups match those assigned in Microsoft Entra ID. Ensure role-based access control (RBAC) mapping is correct: 8. Verify that users are assigned appropriate Databricks roles (e.g. Admin, User, Contributor). 9. Confirm that groups are mapped to workspace access roles.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/databricks/administration-guide/users-groups/scim/aad", + "DefaultValue": "By default, Azure Databricks does not sync users and groups from Microsoft Entra ID." + } + ] + }, + { + "Id": "6.2.1", + "Description": "Ensure that 'trusted locations' are defined", + "Checks": [ + "entra_trusted_named_locations_exists" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.2 Conditional Access", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Microsoft Entra ID Conditional Access allows an organization to configure `Named locations` and configure whether those locations are trusted or untrusted. These settings provide organizations the means to specify Geographical locations for use in conditional access policies, or define actual IP addresses and IP ranges and whether or not those IP addresses and/or ranges are trusted by the organization.", + "RationaleStatement": "Defining trusted source IP addresses or ranges helps organizations create and enforce Conditional Access policies around those trusted or untrusted IP addresses and ranges. Users authenticating from trusted IP addresses and/or ranges may have less access restrictions or access requirements when compared to users that try to authenticate to Microsoft Entra ID from untrusted locations or untrusted source IP addresses/ranges.", + "ImpactStatement": "When configuring `Named locations`, the organization can create locations using Geographical location data or by defining source IP addresses or ranges. Configuring `Named locations` using a Country location does not provide the organization the ability to mark those locations as trusted, and any Conditional Access policy relying on those `Countries location` setting will not be able to use the `All trusted locations` setting within the Conditional Access policy. They instead will have to rely on the `Select locations` setting. This may add additional resource requirements when configuring and will require thorough organizational testing. In general, Conditional Access policies may completely prevent users from authenticating to Microsoft Entra ID, and thorough testing is recommended. To avoid complete lockout, a 'Break Glass' account with full Global Administrator rights is recommended in the event all other administrators are locked out of authenticating to Microsoft Entra ID. This 'Break Glass' account should be excluded from Conditional Access Policies and should be configured with the longest pass phrase feasible in addition to a FIDO2 security key or certificate kept in a very secure physical location. This account should only be used in the event of an emergency and complete administrator lockout. **NOTE:** Starting July 2024, Microsoft will begin requiring MFA for All Users - including Break Glass Accounts. By the end of October 2024, this requirement will be enforced. Physical FIDO2 security keys, or a certificate kept on secure removable storage can fulfill this MFA requirement. If opting for a physical device, that device should be kept in a very secure, documented physical location.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. In the Azure Portal, navigate to `Microsoft Entra ID` 1. Under `Manage`, click `Security` 1. Under `Protect`, click `Conditional Access` 1. Under `Manage`, click `Named locations` 1. Within the `Named locations` blade, click on `IP ranges location` 1. Enter a name for this location setting in the `Name` text box 1. Click on the `+` sign 1. Add an IP Address Range in CIDR notation inside the text box that appears 1. Click on the `Add` button 1. Repeat steps 7 through 9 for each IP Range that needs to be added 1. If the information entered are trusted ranges, select the `Mark as trusted location` check box 1. Once finished, click on `Create` **Remediate from PowerShell** Create a new trusted IP-based Named location policy ``` [System.Collections.Generic.List`1[Microsoft.Open.MSGraph.Model.IpRange]]$ipRanges = @() $ipRanges.Add() $ipRanges.Add() $ipRanges.Add() New-MgIdentityConditionalAccessNamedLocation -dataType #microsoft.graph.ipNamedLocation -DisplayName -IsTrusted $true -IpRanges $ipRanges ``` Set an existing IP-based Named location policy to trusted ``` Update-MgIdentityConditionalAccessNamedLocation -PolicyId -dataType #microsoft.graph.ipNamedLocation -IsTrusted $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. In the Azure Portal, navigate to `Microsoft Entra ID` 1. Under `Manage`, click `Security` 1. Under `Protect`, click `Conditional Access` 1. Under `Manage`, click `Named locations` Ensure there are `IP ranges location` settings configured and marked as `Trusted` **Audit from PowerShell** ``` Get-MgIdentityConditionalAccessNamedLocation ``` In the output from the above command, for each Named location group, make sure at least one entry contains the `IsTrusted` parameter with a value of `True`. Otherwise, if there is no output as a result of the above command or all of the entries contain the `IsTrusted` parameter with an empty value, a `NULL` value, or a value of `False`, the results are out of compliance with this check.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-assignment-network:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access", + "DefaultValue": "By default, no locations are configured under the `Named locations` blade within the Microsoft Entra ID Conditional Access blade." + } + ] + }, + { + "Id": "3.1.5", + "Description": "Ensure that Unity Catalog is configured for Azure Databricks", + "Checks": [], + "Attributes": [ + { + "Section": "3 Analytics Services", + "SubSection": "3.1 Azure Databricks", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Unity Catalog is a centralized governance model for managing and securing data in Azure Databricks. It provides fine-grained access control to databases, tables, and views using Microsoft Entra ID identities. Unity Catalog also enhances data lineage, audit logging, and compliance monitoring, making it a critical component for security and governance.", + "RationaleStatement": "* Enforces centralized access control policies and reduces data security risks. * Enables identity-based authentication via Microsoft Entra ID. * Improves compliance with industry regulations (e.g. GDPR, HIPAA, SOC 2) by providing audit logs and access visibility. * Prevents unauthorized data access through table-, row-, and column-level security (RLS & CLS).", + "ImpactStatement": "* Improperly configured permissions may lead to data exfiltration or unauthorized access. * Unity Catalog requires structured governance policies to be effective and prevent overly permissive access.", + "RemediationProcedure": "Use the remediation procedure written in this article: https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/get-started.", + "AuditProcedure": "Method 1: Verify unity catalog deployment: 1. As an Azure Databricks account admin, log into the account console. 1. Click Workspaces. 1. Find your workspace and check the Metastore column. If a metastore name is present, your workspace is attached to a Unity Catalog metastore and therefore enabled for Unity Catalog. Method 2: Run a SQL query to confirm Unity Catalog enablement Run the following SQL query in the SQL query editor or a notebook that is attached to a Unity Catalog-enabled compute resource. No admin role is required. ``` SELECT CURRENT_METASTORE(); ``` If the query returns a metastore ID like the following, then your workspace is attached to a Unity Catalog metastore and therefore enabled for Unity Catalog.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/:https://learn.microsoft.com/en-us/azure/databricks/admin/users-groups/:https://learn.microsoft.com/en-us/azure/databricks/data-governance/unity-catalog/enable-workspaces", + "DefaultValue": "New workspaces have Unity Catalog enabled by default. Existing workspaces may require manual enablement." + } + ] + }, + { + "Id": "3.1.6", + "Description": "Ensure that usage is restricted and expiry is enforced for Databricks personal access tokens", + "Checks": [], + "Attributes": [ + { + "Section": "3 Analytics Services", + "SubSection": "3.1 Azure Databricks", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Databricks personal access tokens (PATs) provide API-based authentication for users and applications. By default, users can generate API tokens without expiration, leading to potential security risks if tokens are leaked, improperly stored, or not rotated regularly. To mitigate these risks, administrators should: * Restrict token creation to approved users and service principals. * Enforce expiration policies to prevent long-lived tokens. * Monitor token usage and revoke unused or compromised tokens.", + "RationaleStatement": "Restricting usage and enforcing expiry for personal access tokens reduces exposure to long-lived tokens, minimizes the risk of API abuse if compromised, and aligns with security best practices through controlled issuance and enforced expiry.", + "ImpactStatement": "If revoked improperly, applications relying on these tokens may fail, requiring a remediation plan for token rotation. Increased administrative effort is required to track and manage API tokens effectively.", + "RemediationProcedure": "**Remediate from Azure Portal** Disable personal access tokens: If your workspace does not require PATs, you can disable them entirely to prevent their use.", + "AuditProcedure": "Azure Databricks administrators can monitor and revoke personal access tokens within their workspace. Detailed instructions are available in the Monitor and Revoke Personal Access Tokens section of the Microsoft documentation: https://learn.microsoft.com/en-us/azure/databricks/admin/access-control/tokens. To evaluate the usage of personal access tokens in your Azure Databricks account, you can utilize the provided notebook that lists all PATs not rotated or updated in the last 90 days, allowing you to identify tokens that may require revocation. This process is detailed here: https://docs.azure.cn/en-us/databricks/security/auth/oauth-pat-usage. Implementing diagnostic logging provides a comprehensive reference of audit log services and events, enabling you to track activities related to personal access tokens. More information can be found in the diagnostic log reference section: https://docs.azure.cn/en-us/databricks/security/auth/oauth-pat-usage.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/databricks/administration-guide/access-control/tokens:https://learn.microsoft.com/en-us/azure/databricks/dev-tools/auth/", + "DefaultValue": "By default, personal access tokens are enabled and users can create the Personal access token and their expiry time." + } + ] + }, + { + "Id": "3.1.7", + "Description": "Ensure that diagnostic log delivery is configured for Azure Databricks", + "Checks": [], + "Attributes": [ + { + "Section": "3 Analytics Services", + "SubSection": "3.1 Azure Databricks", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Azure Databricks Diagnostic Logging provides insights into system operations, user activities, and security events within a Databricks workspace. Enabling diagnostic logs helps organizations: * Detect security threats by logging access, job executions, and cluster activities. * Ensure compliance with industry regulations such as SOC 2, HIPAA, and GDPR. * Monitor operational performance and troubleshoot issues proactively.", + "RationaleStatement": "Diagnostic logging provides visibility into security and operational activities within Databricks workspaces while maintaining an audit trail for forensic investigations, and it supports compliance with regulatory standards that require logging and monitoring.", + "ImpactStatement": "Logs consume storage and may require additional monitoring tools, leading to increased operational overhead and costs. Incomplete log configurations may result in missing critical events, reducing monitoring effectiveness.", + "RemediationProcedure": "**Remediate from Azure Portal** Enable diagnostic logging for Azure Databricks: 1. Navigate to your Azure Databricks workspace. 1. In the left-hand menu, select `Monitoring` > `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Under `Category details`, select the log categories you wish to capture, such as AuditLogs, Clusters, Notebooks, and Jobs. 1. Choose a destination for the logs: - `Log Analytics workspace`: For advanced querying and monitoring. - `Storage account`: For long-term retention. - `Event Hub`: For integration with third-party systems. 1. Provide a `Name` for the diagnostic setting. 1. Click `Save`. Implement log retention policies: 1. Navigate to your Log Analytics workspace. 1. Under `General`, select `Usage and estimated costs`. 1. Click `Data Retention`. 1. Adjust the retention period slider to the desired number of days (up to 730 days). 1. Click `OK`. Monitor logs for anomalies: 1. Navigate to `Azure Monitor`. 1. Select `Alerts` > `+ New alert rule`. 1. Under `Scope`, specify the Databricks resource. 1. Define `Condition` based on log queries that identify anomalies (e.g. unauthorized access attempts). 1. Configure `Actions` to notify stakeholders or trigger automated responses. 1. Provide an Alert rule `name` and `description`. 1. Click `Create alert rule`. **Remediate from Azure CLI** Enable diagnostic logging for Azure Databricks: ``` az monitor diagnostic-settings create --name DatabricksLogging --resource --logs '[{category: AuditLogs, enabled: true}, {category: Clusters, enabled: true}, {category: Notebooks, enabled: true}, {category: Jobs, enabled: true}]' --workspace ``` Implement log retention policies: ``` az monitor log-analytics workspace update --resource-group --name --retention-time 365 ``` Monitor logs for anomalies: ``` az monitor activity-log alert create --name DatabricksAnomalyAlert --resource-group --scopes --condition contains 'UnauthorizedAccess' ```", + "AuditProcedure": "**Audit from Azure Portal** Check if diagnostic logging is enabled for the Databricks workspace: 1. Go to `Azure Databricks`. 1. Select a workspace. 1. In the left-hand menu, select `Monitoring` > `Diagnostic settings`. 1. Verify if a diagnostic setting is configured. If not, diagnostic logging is not enabled. Ensure that logging is enabled for the following categories:", + "AdditionalInformation": "* Ensure that the Azure Databricks workspace is on the Premium plan to utilize diagnostic logging features. * Regularly review and update alert rules to adapt to evolving security threats and operational requirements.", + "References": "https://learn.microsoft.com/en-us/azure/databricks/admin/account-settings/audit-log-delivery:https://learn.microsoft.com/en-us/troubleshoot/azure/azure-monitor/log-analytics/billing/configure-data-retention", + "DefaultValue": "" + } + ] + }, + { + "Id": "6.5", + "Description": "Ensure that 'Number of methods required to reset' is set to '2'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensures that two alternate forms of identification are provided before allowing a password reset.", + "RationaleStatement": "A Self-service Password Reset (SSPR) through Azure Multi-factor Authentication (MFA) ensures the user's identity is confirmed using two separate methods of identification. With multiple methods set, an attacker would have to compromise both methods before they could maliciously reset a user's password.", + "ImpactStatement": "There may be administrative overhead, as users who lose access to their secondary authentication methods will need an administrator with permissions to remove it. There will also need to be organization-wide security policies and training to teach administrators to verify the identity of the requesting user so that social engineering cannot render this setting useless.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Select `Authentication methods` 1. Set the `Number of methods required to reset` to `2` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Select `Authentication methods` 1. Ensure that `Number of methods required to reset` is set to `2`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-registration-mfa-sspr-combined:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls:https://learn.microsoft.com/en-us/entra/identity/authentication/passwords-faq#password-reset-registration:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods", + "DefaultValue": "By default, the `Number of methods required to reset` is set to 2." + } + ] + }, + { + "Id": "6.2.6", + "Description": "Ensure that multifactor authentication is required for Windows Azure Service Management API", + "Checks": [ + "entra_conditional_access_policy_require_mfa_for_management_api" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.2 Conditional Access", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "This recommendation ensures that users accessing the Windows Azure Service Management API (i.e. Azure Powershell, Azure CLI, Azure Resource Manager API, etc.) are required to use multi-factor authentication (MFA) credentials when accessing resources through the Windows Azure Service Management API.", + "RationaleStatement": "Administrative access to the Windows Azure Service Management API should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multi-factor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings. **IMPORTANT**: While this recommendation allows exceptions to specific Users or Groups, they should be very carefully tracked and reviewed for necessity on a regular interval through an Access Review process. It is important that this rule be built to include All Users to ensure that all users not specifically excepted will be required to use MFA to access the Azure Service Management API.", + "ImpactStatement": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be regularly reviewed or investigated.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. Click `Security` in the Entra ID blade. 1. Click `Conditional Access` in the Security blade. 1. Click `Policies` in the Conditional Access blade. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the `Select` button. 1. Click the blue text under `Target resources`. 1. Under `Include`, click the `Select apps` radio button. 1. Click the blue text under `Select`. 1. Check the box next to `Windows Azure Service Management APIs` then click the `Select` button. 1. Click the blue text under `Grant`. 1. Under `Grant access` check the box for `Require multi-factor authentication` then click the `Select` button. 1. Before creating, set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. In the menu on the left of the Entra ID blade, click `Security`. 1. In the menu on the left of the Security blade, click `Conditional Access`. 1. In the menu on the left of the Conditional Access blade, click `Policies`. 1. Click on the name of the policy you wish to audit. 1. Click the blue text under `Users`. 1. Under the `Include` section of Users, ensure that `All Users` is selected. 1. Under the `Exclude` section of Users, review the `Users and Groups` that are excluded from the policy (NOTE: this should be limited to break-glass emergency access accounts, non-interactive service accounts, and other carefully considered exceptions). 1. On the left side, click the blue text under `Target resources`. 1. Under the `Include` section of Target Resources, ensure that the `Select apps` radio button is selected. 1. Under `Select`, ensure that `Windows Azure Service Management API` is listed.", + "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with administrators changing settings until they use an MFA device linked to their accounts. An emergency access account is recommended for this eventuality if all administrators are locked out. Please see the documentation in the references for further information. Similarly further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.", + "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-users-groups:https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management:https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps#windows-azure-service-management-api", + "DefaultValue": "MFA is not enabled by default for administrative actions." + } + ] + }, + { + "Id": "6.2.7", + "Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals", + "Checks": [ + "defender_ensure_defender_for_server_is_on" + ], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.2 Conditional Access", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "This recommendation ensures that users accessing Microsoft Admin Portals (i.e. Microsoft 365 Admin, Microsoft 365 Defender, Exchange Admin Center, Azure Portal, etc.) are required to use multi-factor authentication (MFA) credentials when logging into an Admin Portal.", + "RationaleStatement": "Administrative Portals for Microsoft Azure should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multi-factor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings. **IMPORTANT**: While this recommendation allows exceptions to specific Users or Groups, they should be very carefully tracked and reviewed for necessity on a regular interval through an Access Review process. It is important that this rule be built to include All Users to ensure that all users not specifically excepted will be required to use MFA to access Admin Portals.", + "ImpactStatement": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be reviewed or investigated.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. Click `Security` in the Entra ID blade. 1. Click `Conditional Access` in the Security blade. 1. Click `Policies` in the Conditional Access blade. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the `Select` button. 1. Click the blue text under `Target resources`. 1. Under `Include`, click the `Select apps` radio button. 1. Click the blue text under `Select`. 1. Check the box next to `Microsoft Admin Portals` then click the `Select` button. 1. Click the blue text under `Grant`. 1. Under `Grant access` check the box for `Require multifactor authentication` then click the `Select` button. 1. Before creating, set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Admin Portal dashboard, open `Microsoft Entra ID`. 1. In the menu on the left of the Entra ID blade, click `Security`. 1. In the menu on the left of the Security blade, click `Conditional Access`. 1. In the menu on the left of the Conditional Access blade, click `Policies`. 1. Click on the name of the policy you wish to audit. 1. Click the blue text under `Users`. 1. Under the `Include` section of Users, review `Users and Groups` to ensure that `All Users` is selected. 1. Under the `Exclude` section of Users, review the `Users and Groups` that are excluded from the policy (NOTE: this should be limited to break-glass emergency access accounts, non-interactive service accounts, and other carefully considered exceptions). 1. On the left side, click the blue text under `Target Resources`. 1. Under the `Include` section of Target resources, ensure the `Select apps` radio button is selected. 1. Under `Select`, ensure `Microsoft Admin Portals` is listed.", + "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with administrators changing settings until they use an MFA device linked to their accounts. An emergency access account is recommended for this eventuality if all administrators are locked out. Please see the documentation in the references for further information. Similarly further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.", + "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-users-groups:https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-admin-portals", + "DefaultValue": "MFA is not enabled by default for administrative actions." + } + ] + }, + { + "Id": "6.6", + "Description": "Ensure that account 'Lockout threshold' is less than or equal to '10'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The account lockout threshold determines how many failed login attempts are permitted prior to placing the account in a locked-out state and initiating a variable lockout duration.", + "RationaleStatement": "Account lockout is a method of protecting against brute-force and password spray attacks. Once the lockout threshold has been exceeded, the account enters a locked-out state which prevents all login attempts for a variable duration. The lockout in combination with a reasonable duration reduces the total number of failed login attempts that a malicious actor can execute in a given period of time.", + "ImpactStatement": "If account lockout threshold is set too low (less than 3), users may experience frequent lockout events and the resulting security alerts may contribute to alert fatigue. If account lockout threshold is set too high (more than 10), malicious actors can programmatically execute more password attempts in a given period of time.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Set the `Lockout threshold` to `10` or fewer. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Ensure that `Lockout threshold` is set to `10` or fewer.", + "AdditionalInformation": "**NOTE:** The variable number for failed login attempts allowed before lockout is prescribed by many security and compliance frameworks. The **appropriate** setting for this variable should be determined by the most restrictive security or compliance framework that your organization follows.", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-smart-lockout#manage-microsoft-entra-smart-lockout-values", + "DefaultValue": "By default, Lockout threshold is set to `10`." + } + ] + }, + { + "Id": "6.7", + "Description": "Ensure that account 'Lockout duration in seconds' is greater than or equal to '60'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The account lockout duration value determines how long an account retains the status of lockout, and therefore how long before a user can continue to attempt to login after passing the lockout threshold.", + "RationaleStatement": "Account lockout is a method of protecting against brute-force and password spray attacks. Once the lockout threshold has been exceeded, the account enters a locked-out state which prevents all login attempts for a variable duration. The lockout in combination with a reasonable duration reduces the total number of failed login attempts that a malicious actor can execute in a given period of time.", + "ImpactStatement": "If account lockout duration is set too low (less than 60 seconds), malicious actors can perform more password spray and brute-force attempts over a given period of time. If the account lockout duration is set too high (more than 300 seconds) users may experience inconvenient delays during lockout.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Set the `Lockout duration in seconds` to `60` or higher. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Ensure that `Lockout duration in seconds` is set to `60` or higher.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-smart-lockout#manage-microsoft-entra-smart-lockout-values", + "DefaultValue": "By default, Lockout duration in seconds is set to `60`." + } + ] + }, + { + "Id": "6.8", + "Description": "Ensure that a 'Custom banned password list' is set to 'Enforce'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Microsoft Azure applies a default global banned password list to all user and admin accounts that are created and managed directly in Microsoft Entra ID. The Microsoft Entra password policy does not apply to user accounts that are synchronized from an on-premises Active Directory environment, unless Microsoft Entra ID Connect is used and `EnforceCloudPasswordPolicyForPasswordSyncedUsers` is enabled. Review the `Default Value` section for more detail on the password policy. For increased password security, a custom banned password list is recommended", + "RationaleStatement": "Implementing a custom banned password list gives your organization further control over the password policy. Disallowing easy-to-guess passwords increases the security of your Azure resources.", + "ImpactStatement": "Increasing password complexity may increase user account administration overhead. Utilizing the default global banned password list and a custom list requires a Microsoft Entra ID P1 or P2 license. On-premises Active Directory Domain Services users who aren't synchronized to Microsoft Entra ID still benefit from Microsoft Entra ID Password Protection based on the existing licensing of synchronized users.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Set the `Enforce custom list` option to `Yes`. 1. Click in the `Custom banned password list` text box. 1. Add a list of words, one per line, to prevent users from using in passwords. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Security`. 1. Under `Manage`, select `Authentication methods`. 1. Under `Manage`, select `Password protection`. 1. Ensure `Enforce custom list` is set to `Yes`. 1. Review the list of words banned from use in passwords.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad-combined-policy:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad:https://docs.microsoft.com/en-us/powershell/module/Azuread/:https://www.microsoft.com/en-us/research/publication/password-guidance/:https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-configure-custom-password-protection:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls", + "DefaultValue": "By default the custom banned password list is not 'Enabled'. Organization-specific terms can be added to the custom banned password list, such as the following examples: - Brand names - Product names - Locations, such as company headquarters - Company-specific terms - Abbreviations that have specific company meaning - Months and weekdays with your company's local languages The default global banned password list is already applied to your resources which applies the following basic requirements: **Characters allowed:** - Uppercase characters (A - Z) - Lowercase characters (a - z) - Numbers (0 - 9) - Symbols: - @ # $ % ^ & * - _ ! + = [ ] { } | \\ : ' , . ? / ` ~ ( ) ; < > - blank space **Characters not allowed:** - Unicode characters **Password length:** Passwords require: - A minimum of eight characters - A maximum of 256 characters **Password complexity:** Passwords require three out of four of the following categories: - Uppercase characters - Lowercase characters - Numbers - Symbols Note: Password complexity check isn't required for Education tenants. **Password not recently used:** - When a user changes or resets their password, the new password can't be the same as the current or recently used passwords. - Password isn't banned by Entra ID Password Protection. - The password can't be on the global list of banned passwords for Azure AD Password Protection, or on the customizable list of banned passwords specific to your organization. **Evaluation** New passwords are evaluated for strength and complexity by validating against the combined list of terms from the global and custom banned password lists. Even if a user's password contains a banned password, the password may be accepted if the overall password is otherwise strong enough." + } + ] + }, + { + "Id": "6.9", + "Description": "Ensure that 'Number of days before users are asked to re-confirm their authentication information' is not set to '0'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that the number of days before users are asked to re-confirm their authentication information is not set to 0.", + "RationaleStatement": "This setting is necessary if 'Require users to register when signing in' is enabled. If authentication re-confirmation is disabled, registered users will never be prompted to re-confirm their existing authentication information. If the authentication information for a user changes, such as a phone number or email, then the password reset information for that user reverts to the previously registered authentication information.", + "ImpactStatement": "Users will be prompted to re-confirm their authentication information after the number of days specified.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Users`. 1. Select `Password reset`. 1. Under `Manage`, select `Registration`. 1. Set the `Number of days before users are asked to re-confirm their authentication information` to your organization-defined frequency. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Entra ID`. 1. Under `Manage`, select `Users`. 1. Select `Password reset`. 1. Under `Manage`, select `Registration`. 1. Ensure that `Number of days before users are asked to re-confirm their authentication information` is not set to `0`.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-howitworks#registration:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods", + "DefaultValue": "By default, the `Number of days before users are asked to re-confirm their authentication information` is set to 180 days." + } + ] + }, + { + "Id": "6.1", + "Description": "Ensure that 'Notify users on password resets?' is set to 'Yes'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that users are notified on their primary and alternate emails on password resets.", + "RationaleStatement": "User notification on password reset is a proactive way of confirming password reset activity. It helps the user to recognize unauthorized password reset activities.", + "ImpactStatement": "Users will receive emails alerting them to password changes to both their primary and alternate emails.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Set `Notify users on password resets?` to `Yes` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Ensure that `Notify users on password resets?` is set to `Yes`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr#set-up-notifications-and-customizations:https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-howitworks#notifications:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy", + "DefaultValue": "By default, `Notify users on password resets?` is set to Yes." + } + ] + }, + { + "Id": "6.11", + "Description": "Ensure that 'Notify all admins when other admins reset their password?' is set to 'Yes'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Ensure that all Global Administrators are notified if any other administrator resets their password.", + "RationaleStatement": "Administrator accounts are sensitive. Any password reset activity notification, when sent to all Administrators, ensures that all Global Administrators can passively confirm if such a reset is a common pattern within their group. For example, if all Administrators change their password every 30 days, any password reset activity before that may require administrator(s) to evaluate any unusual activity and confirm its origin.", + "ImpactStatement": "All Global Administrators will receive a notification from Azure every time a password is reset. This is useful for auditing procedures to confirm that there are no out of the ordinary password resets for Administrators. There is additional overhead, however, in the time required for Global Administrators to audit the notifications. This setting is only useful if all Global Administrators pay attention to the notifications and audit each one.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Set `Notify all admins when other admins reset their password?` to `Yes` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `Password reset` 1. Under `Manage`, select `Notifications` 1. Ensure that `Notify all admins when other admins reset their password?` is set to `Yes`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-howitworks#notifications:https://support.microsoft.com/en-us/account-billing/reset-your-work-or-school-password-using-security-info-23dde81f-08bb-4776-ba72-e6b72b9dda9e:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr#set-up-notifications-and-customizations", + "DefaultValue": "By default, `Notify all admins when other admins reset their password?` is set to No." + } + ] + }, + { + "Id": "6.17", + "Description": "Ensure that 'Restrict access to Microsoft Entra admin center' is set to 'Yes'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Restrict access to the Microsoft Entra ID administration center to administrators only. **NOTE**: This only affects access to the Entra ID administrator's web portal. This setting does not prohibit privileged users from using other methods such as Rest API or Powershell to obtain sensitive information from Microsoft Entra ID.", + "RationaleStatement": "The Microsoft Entra ID administrative center has sensitive data and permission settings. All non-administrators should be prohibited from accessing any Microsoft Entra ID data in the administration center to avoid exposure.", + "ImpactStatement": "All administrative tasks will need to be done by Administrators, causing additional overhead in management of users and resources.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Under `Administration centre`, set `Restrict access to Microsoft Entra admin center` to `Yes` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Under `Manage`, select `User settings` 1. Under `Administration centre`, ensure that `Restrict access to Microsoft Entra admin center` is set to `Yes`", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/active-directory/active-directory-assign-admin-roles-azure-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users", + "DefaultValue": "By default, `Restrict access to Microsoft Entra admin center` is set to `No`" + } + ] + }, + { + "Id": "7.1.1.1", + "Description": "Ensure that a 'Diagnostic Setting' exists for Subscription Activity Logs", + "Checks": [ + "monitor_diagnostic_settings_exists" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Enable Diagnostic settings for exporting activity logs. Diagnostic settings are available for each individual resource within a subscription. Settings should be configured for all appropriate resources for your environment.", + "RationaleStatement": "A diagnostic setting controls how a diagnostic log is exported. By default, logs are retained only for 90 days. Diagnostic settings should be defined so that logs can be exported and stored for a longer duration to analyze security activities within an Azure subscription.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** To enable Diagnostic Settings on a Subscription: 1. Go to `Monitor` 2. Click on `Activity log` 3. Click on `Export Activity Logs` 4. Click `+ Add diagnostic setting` 5. Enter a `Diagnostic setting name` 6. Select `Categories` for the diagnostic setting 7. Select the appropriate `Destination details` (this may be Log Analytics, Storage Account, Event Hub, or Partner solution) 8. Click `Save` To enable Diagnostic Settings on a specific resource: 1. Go to `Monitoring` 1. Click `Diagnostic settings` 1. Select `Add diagnostic setting` 1. Enter a `Diagnostic setting name` 1. Select the appropriate log, metric, and destination (this may be Log Analytics, Storage Account, Event Hub, or Partner solution) 1. Click `Save` Repeat these step for all resources as needed. **Remediate from Azure CLI** To configure Diagnostic Settings on a Subscription: ``` az monitor diagnostic-settings subscription create --subscription --name --location <[--event-hub --event-hub-auth-rule ] [--storage-account ] [--workspace ] --logs (e.g. [{category:Security,enabled:true},{category:Administrative,enabled:true},{category:Alert,enabled:true},{category:Policy,enabled:true}]) ``` To configure Diagnostic Settings on a specific resource: ``` az monitor diagnostic-settings create --subscription --resource --name <[--event-hub --event-hub-rule ] [--storage-account ] [--workspace ] --logs --metrics ``` **Remediate from PowerShell** To configure Diagnostic Settings on a subscription: ``` $logCategories = @(); $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Administrative -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Security -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category ServiceHealth -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Alert -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Recommendation -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Policy -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Autoscale -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category ResourceHealth -Enabled $true New-AzSubscriptionDiagnosticSetting -SubscriptionId -Name <[-EventHubAuthorizationRule -EventHubName ] [-StorageAccountId ] [-WorkSpaceId ] [-MarketplacePartner ID ]> -Log $logCategories ``` To configure Diagnostic Settings on a specific resource: ``` $logCategories = @() $logCategories += New-AzDiagnosticSettingLogSettingsObject -Category -Enabled $true Repeat command and variable assignment for each Log category specific to the resource where this Diagnostic Setting will get configured. $metricCategories = @() $metricCategories += New-AzDiagnosticSettingMetricSettingsObject -Enabled $true [-Category ] [-RetentionPolicyDay ] [-RetentionPolicyEnabled $true] Repeat command and variable assignment for each Metric category or use the 'AllMetrics' category. New-AzDiagnosticSetting -ResourceId -Name -Log $logCategories -Metric $metricCategories [-EventHubAuthorizationRuleId -EventHubName ] [-StorageAccountId ] [-WorkspaceId ] [-MarketplacePartnerId ]>", + "AuditProcedure": "**Audit from Azure Portal** To identify Diagnostic Settings on a subscription: 1. Go to `Monitor` 2. Click `Activity Log` 3. Click `Export Activity Logs` 4. Select a `Subscription` 5. Ensure a `Diagnostic setting` exists for the selected Subscription To identify Diagnostic Settings on specific resources: 1. Go to `Monitoring` 2. Click `Diagnostic settings` 3. Ensure a `Diagnostic setting` exists for all appropriate resources. **Audit from Azure CLI** To identify Diagnostic Settings on a subscription: ``` az monitor diagnostic-settings subscription list --subscription ``` To identify Diagnostic Settings on a resource ``` az monitor diagnostic-settings list --resource ``` **Audit from PowerShell** To identify Diagnostic Settings on a Subscription: ``` Get-AzDiagnosticSetting -SubscriptionId ``` To identify Diagnostic Settings on a specific resource: ``` Get-AzDiagnosticSetting -ResourceId ```", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/monitoring-and-diagnostics/monitoring-overview-activity-logs#export-the-activity-log-with-a-log-profile:https://learn.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, diagnostic setting is not set." + } + ] + }, + { + "Id": "7.1.1.2", + "Description": "Ensure Diagnostic Setting captures appropriate categories", + "Checks": [ + "monitor_diagnostic_setting_with_appropriate_categories" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "**Prerequisite**: A Diagnostic Setting must exist. If a Diagnostic Setting does not exist, the navigation and options within this recommendation will not be available. Please review the recommendation at the beginning of this subsection titled: Ensure that a 'Diagnostic Setting' exists. The diagnostic setting should be configured to log the appropriate activities from the control/management plane.", + "RationaleStatement": "A diagnostic setting controls how the diagnostic log is exported. Capturing the diagnostic setting categories for appropriate control/management plane activities allows proper alerting.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Monitor`. 1. Click `Activity log`. 1. Click on `Export Activity Logs`. 1. Select the `Subscription` from the drop down menu. 1. Click `Edit setting` next to a diagnostic setting. 1. Check the following categories: `Administrative, Alert, Policy, and Security`. 1. Choose the destination details according to your organization's needs. 1. Click `Save`. **Remediate from Azure CLI** ``` az monitor diagnostic-settings subscription create --subscription --name --location <[--event-hub --event-hub-auth-rule ] [--storage-account ] [--workspace ] --logs [{category:Security,enabled:true},{category:Administrative,enabled:true},{category:Alert,enabled:true},{category:Policy,enabled:true}] ``` **Remediate from PowerShell** ``` $logCategories = @(); $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Administrative -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Security -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Alert -Enabled $true $logCategories += New-AzDiagnosticSettingSubscriptionLogSettingsObject -Category Policy -Enabled $true New-AzSubscriptionDiagnosticSetting -SubscriptionId -Name <[-EventHubAuthorizationRule -EventHubName ] [-StorageAccountId ] [-WorkSpaceId ] [-MarketplacePartner ID ]> -Log $logCategories ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Monitor`. 1. Click `Activity log`. 1. Click on `Export Activity Logs`. 1. Select the appropriate `Subscription`. 1. Click `Edit setting` next to a diagnostic setting. 1. Ensure that the following categories are checked: `Administrative, Alert, Policy, and Security`. **Audit from Azure CLI** Ensure the categories `'Administrative', 'Alert', 'Policy', and 'Security'` set to: 'enabled: true' ``` az monitor diagnostic-settings subscription list --subscription ``` **Audit from PowerShell** Ensure the categories Administrative, Alert, Policy, and Security are set to Enabled:True ``` Get-AzSubscriptionDiagnosticSetting -Subscription ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [3b980d31-7904-4bb7-8575-5665739a8052](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F3b980d31-7904-4bb7-8575-5665739a8052) **- Name:** 'An activity log alert should exist for specific Security operations' - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations' - **Policy ID:** [c5447c04-a4d7-4ba8-a263-c9ee321a6858](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc5447c04-a4d7-4ba8-a263-c9ee321a6858) **- Name:** 'An activity log alert should exist for specific Policy operations'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-settings:https://docs.microsoft.com/en-us/azure/azure-monitor/samples/resource-manager-diagnostic-settings:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation:https://learn.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest:https://learn.microsoft.com/en-us/powershell/module/az.monitor/new-azsubscriptiondiagnosticsetting?view=azps-9.2.0", + "DefaultValue": "When the diagnostic setting is created using Azure Portal, by default no categories are selected." + } + ] + }, + { + "Id": "7.1.1.3", + "Description": "Ensure the storage account containing the container with activity logs is encrypted with Customer Managed Key (CMK)", + "Checks": [ + "monitor_storage_account_with_activity_logs_cmk_encrypted" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Storage accounts with the activity log exports can be configured to use Customer Managed Keys (CMK).", + "RationaleStatement": "Configuring the storage account with the activity log export container to use CMKs provides additional confidentiality controls on log data, as a given user must have read permission on the corresponding storage account and must be granted decrypt permission by the CMK.", + "ImpactStatement": "**NOTE:** You must have your key vault setup to utilize this. All Audit Logs will be encrypted with a key you provide. You will need to set up customer managed keys separately, and you will select which key to use via the instructions here. You will be responsible for the lifecycle of the keys, and will need to manually replace them at your own determined intervals to keep the data secure.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Monitor`. 1. Select `Activity log`. 1. Select `Export Activity Logs`. 1. Select a `Subscription`. 1. Note the name of the `Storage Account` for the diagnostic setting. 1. Navigate to `Storage accounts`. 1. Click on the storage account. 1. Under `Security + networking`, click `Encryption`. 1. Next to `Encryption type`, select `Customer-managed keys`. 1. Complete the steps to configure a customer-managed key for encryption of the storage account. **Remediate from Azure CLI** ``` az storage account update --name --resource-group --encryption-key-source=Microsoft.Keyvault --encryption-key-vault --encryption-key-name --encryption-key-version ``` **Remediate from PowerShell** ``` Set-AzStorageAccount -ResourceGroupName -Name -KeyvaultEncryption -KeyVaultUri -KeyName ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Monitor`. 1. Select `Activity log`. 1. Select `Export Activity Logs`. 1. Select a `Subscription`. 1. Note the name of the `Storage Account` for the diagnostic setting. 1. Navigate to `Storage accounts`. 1. Click on the storage account name noted in Step 5. 1. Under `Security + networking`, click `Encryption`. 1. Ensure `Customer-managed keys` is selected and a key is set. **Audit from Azure CLI** 1. Get storage account id configured with log profile: ``` az monitor diagnostic-settings subscription list --subscription --query 'value[*].storageAccountId' ``` 2. Ensure the storage account is encrypted with CMK: ``` az storage account list --query [?name==''] ``` In command output ensure `keySource` is set to `Microsoft.Keyvault` and `keyVaultProperties` is not set to `null` **Audit from PowerShell** ``` Get-AzStorageAccount -ResourceGroupName -Name |select-object -ExpandProperty encryption|format-list ``` Ensure the value of `KeyVaultProperties` is not `null` or empty, and ensure `KeySource` is not set to `Microsoft.Storage`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [fbb99e8e-e444-4da0-9ff1-75c92f5a85b2](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ffbb99e8e-e444-4da0-9ff1-75c92f5a85b2) **- Name:** 'Storage account containing the container with activity logs must be encrypted with BYOK'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-5-use-customer-managed-key-option-in-data-at-rest-encryption-when-required:https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log?tabs=cli#managing-legacy-log-profiles", + "DefaultValue": "By default, for a storage account `keySource` is set to `Microsoft.Storage` allowing encryption with vendor Managed key and not a Customer Managed Key." + } + ] + }, + { + "Id": "7.1.1.4", + "Description": "Ensure that logging for Azure Key Vault is 'Enabled'", + "Checks": [ + "keyvault_logging_enabled" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable AuditEvent logging for key vault instances to ensure interactions with key vaults are logged and available.", + "RationaleStatement": "Monitoring how and when key vaults are accessed, and by whom, enables an audit trail of interactions with confidential information, keys, and certificates managed by Azure Key Vault. Enabling logging for Key Vault saves information in a user provided destination of either an Azure storage account or Log Analytics workspace. The same destination can be used for collecting logs for multiple Key Vaults.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. Select a Key vault. 3. Under `Monitoring`, select `Diagnostic settings`. 4. Click `Edit setting` to update an existing diagnostic setting, or `Add diagnostic setting` to create a new one. 5. If creating a new diagnostic setting, provide a name. 6. Configure an appropriate destination. 7. Under `Category groups`, check `audit` and `allLogs`. 8. Click `Save`. **Remediate from Azure CLI** To update an existing `Diagnostic Settings` ``` az monitor diagnostic-settings update --name --resource ``` To create a new `Diagnostic Settings` ``` az monitor diagnostic-settings create --name --resource --logs [{category:audit,enabled:true},{category:allLogs,enabled:true}] --metrics [{category:AllMetrics,enabled:true}] <[--event-hub --event-hub-rule | --storage-account |--workspace | --marketplace-partner-id ]> ``` **Remediate from PowerShell** Create the `Log` settings object ``` $logSettings = @() $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -Category audit $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -Category allLogs ``` Create the `Metric` settings object ``` $metricSettings = @() $metricSettings += New-AzDiagnosticSettingMetricSettingsObject -Enabled $true -Category AllMetrics ``` Create the `Diagnostic Settings` for each `Key Vault` ``` New-AzDiagnosticSetting -Name -ResourceId -Log $logSettings -Metric $metricSettings [-StorageAccountId | -EventHubName -EventHubAuthorizationRuleId | -WorkSpaceId | -MarketPlacePartnerId ] ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 1. For each Key vault, under `Monitoring`, go to `Diagnostic settings`. 1. Click `Edit setting` next to a diagnostic setting. 1. Ensure that a destination is configured. 1. Under `Category groups`, ensure that `audit` and `allLogs` are checked. **Audit from Azure CLI** List all key vaults ``` az keyvault list ``` For each keyvault `id` ``` az monitor diagnostic-settings list --resource ``` Ensure that `storageAccountId` reflects your desired destination and that `categoryGroup` and `enabled` are set as follows in the sample outputs below. ``` logs: [ { categoryGroup: audit, enabled: true, }, { categoryGroup: allLogs, enabled: true, } ``` **Audit from PowerShell** List the key vault(s) in the subscription ``` Get-AzKeyVault ``` For each key vault, run the following: ``` Get-AzDiagnosticSetting -ResourceId ``` Ensure that `StorageAccountId`, `ServiceBusRuleId`, `MarketplacePartnerId`, or `WorkspaceId` is set as appropriate. Also, ensure that `enabled` is set to `true`, and that `categoryGroup` reflects both `audit` and `allLogs` category groups. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [cf820ca0-f99e-4f3e-84fb-66e913812d21](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fcf820ca0-f99e-4f3e-84fb-66e913812d21) **- Name:** 'Resource logs in Key Vault should be enabled'", + "AdditionalInformation": "**DEPRECATION WARNING** Retention rules for Key Vault logging is being migrated to Azure Storage Lifecycle Management. Retention rules should be set based on the needs of your organization and security or compliance frameworks. Please visit [https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/migrate-to-azure-storage-lifecycle-policy?tabs=portal](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/migrate-to-azure-storage-lifecycle-policy?tabs=portal) for detail on migrating your retention rules. Microsoft has provided the following deprecation timeline: March 31, 2023 – The Diagnostic Settings Storage Retention feature will no longer be available to configure new retention rules for log data. This includes using the portal, CLI PowerShell, and ARM and Bicep templates. If you have configured retention settings, you'll still be able to see and change them in the portal. March 31, 2024 – You will no longer be able to use the API (CLI, Powershell, or templates), or Azure portal to configure retention setting unless you're changing them to 0. Existing retention rules will still be respected. September 30, 2025 – All retention functionality for the Diagnostic Settings Storage Retention feature will be disabled across all environments.", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/general/howto-logging:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, Diagnostic AuditEvent logging is not enabled for Key Vault instances." + } + ] + }, + { + "Id": "7.1.1.5", + "Description": "Ensure that Network Security Group Flow logs are captured and sent to Log Analytics", + "Checks": [ + "network_flow_log_captured_sent" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that network flow logs are captured and fed into a central log analytics workspace. **Retirement Notice** On September 30, 2027, network security group (NSG) flow logs will be retired. Starting June 30, 2025, it will no longer be possible to create new NSG flow logs. Azure recommends migrating to virtual network flow logs. Review https://azure.microsoft.com/en-gb/updates?id=Azure-NSG-flow-logs-Retirement for more information. For virtual network flow logs, consider applying the recommendation `Ensure that virtual network flow logs are captured and sent to Log Analytics` in this section.", + "RationaleStatement": "Network Flow Logs provide valuable insight into the flow of traffic around your network and feed into both Azure Monitor and Azure Sentinel (if in use), permitting the generation of visual flow diagrams to aid with analyzing for lateral movement, etc.", + "ImpactStatement": "The impact of configuring NSG Flow logs is primarily one of cost and configuration. If deployed, it will create storage accounts that hold minimal amounts of data on a 5-day lifecycle before feeding to Log Analytics Workspace. This will increase the amount of data stored and used by Azure Monitor.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Select `+ Create`. 1. Select the desired Subscription. 1. For `Flow log type`, select `Network security group`. 1. Select `+ Select target resource`. 1. Select `Network security group`. 1. Select a network security group. 1. Click `Confirm selection`. 1. Select or create a new Storage Account. 1. If using a v2 storage account, input the retention in days to retain the log. 1. Click `Next`. 1. Under `Analytics`, for `Flow log version`, select `Version 2`. 1. Check the box next to `Enable traffic analytics`. 1. Select a processing interval. 1. Select a `Log Analytics Workspace`. 1. Select `Next`. 1. Optionally add Tags. 1. Select `Review + create`. 1. Select `Create`. ***Warning*** The remediation policy creates remediation deployment and names them by concatenating the subscription name and the resource group name. The MAXIMUM permitted length of a deployment name is 64 characters. Exceeding this will cause the remediation task to fail.", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down, select `Flow log type`. 1. From the `Value` drop-down, check `Network security group` only. 1. Click `Apply`. 1. Ensure that at least one network security group flow log is listed and is configured to send logs to a `Log Analytics Workspace`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [27960feb-a23c-4577-8d36-ef8b5f35e0be](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F27960feb-a23c-4577-8d36-ef8b5f35e0be) **- Name:** 'All flow log resources should be in enabled state' - **Policy ID:** [c251913d-7d24-4958-af87-478ed3b9ba41](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc251913d-7d24-4958-af87-478ed3b9ba41) **- Name:** 'Flow logs should be configured for every network security group' - **Policy ID:** [4c3c6c5f-0d47-4402-99b8-aa543dd8bcee](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4c3c6c5f-0d47-4402-99b8-aa543dd8bcee) **- Name:** 'Flow logs should be configured for every virtual network'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-nsg-flow-logging-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-4-enable-network-logging-for-security-investigation", + "DefaultValue": "By default Network Security Group logs are not sent to Log Analytics." + } + ] + }, + { + "Id": "7.1.1.6", + "Description": "Ensure that logging for Azure AppService 'HTTP logs' is enabled", + "Checks": [ + "app_http_logs_enabled" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Enable AppServiceHTTPLogs diagnostic log category for Azure App Service instances to ensure all http requests are captured and centrally logged.", + "RationaleStatement": "Capturing web requests can be important supporting information for security analysts performing monitoring and incident response activities. Once logging, these logs can be ingested into SIEM or other central aggregation point for the organization.", + "ImpactStatement": "Log consumption and processing will incur additional cost.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `App Services`. For each `App Service`: 2. Under `Monitoring`, go to `Diagnostic settings`. 3. To update an existing diagnostic setting, click `Edit setting` against the setting. To create a new diagnostic setting, click `Add diagnostic setting` and provide a name for the new setting. 4. Check the checkbox next to `HTTP logs`. 5. Configure a destination based on your specific logging consumption capability (for example Stream to an event hub and then consuming with SIEM integration for Event Hub logging). 6. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `App Services`. For each `App Service`: 2. Under `Monitoring`, go to `Diagnostic settings`. 3. Ensure a diagnostic setting exists that logs `HTTP logs` to a destination aligned to your environment's approach to log consumption (event hub, storage account, etc. dependent on what is consuming the logs such as SIEM or other log aggregation utility). **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [91a78b24-f231-4a8a-8da9-02c35b2b6510](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F91a78b24-f231-4a8a-8da9-02c35b2b6510) **- Name:** 'App Service apps should have resource logs enabled' - **Policy ID:** [d639b3af-a535-4bef-8dcf-15078cddf5e2](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fd639b3af-a535-4bef-8dcf-15078cddf5e2) **- Name:** 'App Service app slots should have resource logs enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/app-service/troubleshoot-diagnostic-logs:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "Not configured." + } + ] + }, + { + "Id": "6.18", + "Description": "Ensure that 'Restrict user ability to access groups features in My Groups' is set to 'Yes'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Restrict access to group web interface in the Access Panel portal.", + "RationaleStatement": "Self-service group management enables users to create and manage security groups or Office 365 groups in Microsoft Entra ID. Unless a business requires this day-to-day delegation for some users, self-service group management should be disabled. Any user can access the Access Panel, where they can reset their passwords, view their information, etc. By default, users are also allowed to access the Group feature, which shows groups, members, related resources (SharePoint URL, Group email address, Yammer URL, and Teams URL). By setting this feature to 'Yes', users will no longer have access to the web interface, but still have access to the data using the API. This is useful to prevent non-technical users from enumerating groups-related information, but technical users will still be able to access this information using APIs.", + "ImpactStatement": "Setting to `Yes` could create administrative overhead by customers seeking certain group memberships that will have to be manually managed by administrators with appropriate permissions.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, set `Restrict user ability to access groups features in My Groups` to `Yes` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, ensure that `Restrict user ability to access groups features in My Groups` is set to `Yes`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy", + "DefaultValue": "By default, `Restrict user ability to access groups features in the Access Pane` is set to `No`" + } + ] + }, + { + "Id": "6.2", + "Description": "Ensure that 'Owners can manage group membership requests in My Groups' is set to 'No'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Restrict security group management to administrators only.", + "RationaleStatement": "Restricting security group management to administrators only prohibits users from making changes to security groups. This ensures that security groups are appropriately managed and their management is not delegated to non-administrators.", + "ImpactStatement": "Group Membership for user accounts will need to be handled by Admins and cause administrative overhead.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, set `Owners can manage group membership requests in My Groups` to `No` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Groups` 1. Under `Settings`, select `General` 1. Under `Self Service Group Management`, ensure that `Owners can manage group membership requests in My Groups` is set to `No`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management#making-a-group-available-for-end-user-self-service:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-3-manage-lifecycle-of-identities-and-entitlements:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-8-determine-access-process-for-cloud-provider-support:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy", + "DefaultValue": "By default, `Owners can manage group membership requests in My Groups` is set to `No`." + } + ] + }, + { + "Id": "6.22", + "Description": "Ensure that 'Require Multifactor Authentication to register or join devices with Microsoft Entra' is set to 'Yes'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "**NOTE:** This recommendation is only relevant if your subscription is using Per-User MFA. If your organization is licensed to use Conditional Access, the preferred method of requiring MFA to join devices to Entra ID is to use a Conditional Access policy (see additional information below for link). Joining or registering devices to Microsoft Entra ID should require multi-factor authentication.", + "RationaleStatement": "Multi-factor authentication is recommended when adding devices to Microsoft Entra ID. When set to `Yes`, users who are adding devices from the internet must first use the second method of authentication before their device is successfully added to the directory. This ensures that rogue devices are not added to the domain using a compromised user account.", + "ImpactStatement": "A slight impact of additional overhead, as Administrators will now have to approve every access to the domain.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Devices` 1. Under `Manage`, select `Device settings` 1. Under `Microsoft Entra join and registration settings`, set `Require Multifactor Authentication to register or join devices with Microsoft Entra` to `Yes` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Devices` 1. Under `Manage`, select `Device settings` 1. Under `Microsoft Entra join and registration settings`, ensure that `Require Multifactor Authentication to register or join devices with Microsoft Entra` is set to `Yes`", + "AdditionalInformation": "If Conditional Access is available, this recommendation should be bypassed in favor of the Conditional Access implementation of requiring Multifactor Authentication to register or join devices with Microsoft Entra. https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-device-register-join", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-device-register-join:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls", + "DefaultValue": "By default, `Require Multifactor Authentication to register or join devices with Microsoft Entra` is set to `No`." + } + ] + }, + { + "Id": "6.25", + "Description": "Ensure that 'Subscription leaving Microsoft Entra tenant' and 'Subscription entering Microsoft Entra tenant' is set to 'Permit no one'", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Users who are set as subscription owners are able to make administrative changes to the subscriptions and move them into and out of Microsoft Entra ID.", + "RationaleStatement": "Permissions to move subscriptions in and out of a Microsoft Entra tenant must only be given to appropriate administrative personnel. A subscription that is moved into a Microsoft Entra tenant may be within a folder to which other users have elevated permissions. This prevents loss of data or unapproved changes of the objects within by potential bad actors.", + "ImpactStatement": "Subscriptions will need to have these settings turned off to be moved.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From the Azure Portal Home select the portal menu 1. Select `Subscriptions` 1. In the `Advanced options` drop-down menu, select `Manage Policies` 1. Set `Subscription leaving Microsoft Entra tenant` and `Subscription entering Microsoft Entra tenant` to `Permit no one` 1. Click `Save changes`", + "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Portal Home select the portal menu 1. Select `Subscriptions` 1. In the `Advanced options` drop-down menu, select `Manage Policies` 1. Ensure `Subscription leaving Microsoft Entra tenant` and `Subscription entering Microsoft Entra tenant` are set to `Permit no one`", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/cost-management-billing/manage/manage-azure-subscription-policy:https://learn.microsoft.com/en-us/entra/fundamentals/how-subscriptions-associated-directory:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-2-protect-identity-and-authentication-systems", + "DefaultValue": "By default `Subscription leaving Microsoft Entra tenant` and `Subscription entering Microsoft Entra tenant` are set to `Allow everyone (default)`" + } + ] + }, + { + "Id": "7.1.2.1", + "Description": "Ensure that Activity Log Alert exists for Create Policy Assignment", + "Checks": [ + "monitor_alert_create_policy_assignment" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create Policy Assignment event.", + "RationaleStatement": "Monitoring for create policy assignment events gives insight into changes done in Azure policy - assignments and can reduce the time it takes to detect unsolicited changes.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create policy assignment (Policy assignment)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Authorization/policyAssignments/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Authorization/policyAssignments/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Get the `Action Group` information and store it in a variable, then create a new `Action` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` variable. ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Authorization/policyAssignments/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Authorization/policyAssignments/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create policy assignment'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Authorization/policyAssignments/write` in the output. If it's missing, generate a finding. **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Authorization/policyAssignments/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` If the output is empty, an `alert rule` for `Create Policy Assignments` is not configured. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [c5447c04-a4d7-4ba8-a263-c9ee321a6858](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc5447c04-a4d7-4ba8-a263-c9ee321a6858) **- Name:** 'An activity log alert should exist for specific Policy operations'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation:https://docs.microsoft.com/en-in/rest/api/policy/policy-assignments:https://docs.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-log", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.2", + "Description": "Ensure that Activity Log Alert exists for Delete Policy Assignment", + "Checks": [ + "monitor_alert_delete_policy_assignment" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Policy Assignment event.", + "RationaleStatement": "Monitoring for delete policy assignment events gives insight into changes done in azure policy - assignments and can reduce the time it takes to detect unsolicited changes.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete policy assignment (Policy assignment)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Authorization/policyAssignments/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the conditions object ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Authorization/policyAssignments/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Action` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` variable. ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Authorization/policyAssignments/delete`. ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Authorization/policyAssignments/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete policy assignment'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Authorization/policyAssignments/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Authorization/policyAssignments/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [c5447c04-a4d7-4ba8-a263-c9ee321a6858](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc5447c04-a4d7-4ba8-a263-c9ee321a6858) **- Name:** 'An activity log alert should exist for specific Policy operations'", + "AdditionalInformation": "This log alert also applies for Azure Blueprints.", + "References": "https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation:https://azure.microsoft.com/en-us/services/blueprints/", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.3", + "Description": "Ensure that Activity Log Alert exists for Create or Update Network Security Group", + "Checks": [ + "monitor_alert_create_update_nsg" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an Activity Log Alert for the Create or Update Network Security Group event.", + "RationaleStatement": "Monitoring for Create or Update Network Security Group events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create or Update Network Security Group (Network Security Group)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/write and level=verbose --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/networkSecurityGroups/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/networkSecurityGroups/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/networkSecurityGroups/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create or Update Network Security Group'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/networkSecurityGroups/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/networkSecurityGroups/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.4", + "Description": "Ensure that Activity Log Alert exists for Delete Network Security Group", + "Checks": [ + "monitor_alert_delete_nsg" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Network Security Group event.", + "RationaleStatement": "Monitoring for Delete Network Security Group events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete Network Security Group (Network Security Group)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/networkSecurityGroups/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/networkSecurityGroups/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/networkSecurityGroups/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete Network Security Group'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/networkSecurityGroups/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/networkSecurityGroups/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.5", + "Description": "Ensure that Activity Log Alert exists for Create or Update Security Solution", + "Checks": [ + "monitor_alert_create_update_security_solution" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create or Update Security Solution event.", + "RationaleStatement": "Monitoring for Create or Update Security Solution events gives insight into changes to the active security solutions and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create or Update Security Solutions (Security Solutions)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Security/securitySolutions/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Security/securitySolutions/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Security/securitySolutions/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Security/securitySolutions/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create or Update Security Solutions'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Security/securitySolutions/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Security/securitySolutions/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.6", + "Description": "Ensure that Activity Log Alert exists for Delete Security Solution", + "Checks": [ + "monitor_alert_delete_security_solution" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Security Solution event.", + "RationaleStatement": "Monitoring for Delete Security Solution events gives insight into changes to the active security solutions and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete Security Solutions (Security Solutions)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Security/securitySolutions/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Security/securitySolutions/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Security/securitySolutions/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Security/securitySolutions/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete Security Solutions'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Security/securitySolutions/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Security/securitySolutions/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.7", + "Description": "Ensure that Activity Log Alert exists for Create or Update SQL Server Firewall Rule", + "Checks": [ + "monitor_alert_create_update_sqlserver_fr" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create or Update SQL Server Firewall Rule event.", + "RationaleStatement": "Monitoring for Create or Update SQL Server Firewall Rule events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create/Update server firewall rule (Server Firewall Rule)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Sql/servers/firewallRules/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Sql/servers/firewallRules/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Sql/servers/firewallRules/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create/Update server firewall rule'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Sql/servers/firewallRules/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Sql/servers/firewallRules/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.8", + "Description": "Ensure that Activity Log Alert exists for Delete SQL Server Firewall Rule", + "Checks": [ + "monitor_alert_delete_sqlserver_fr" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete SQL Server Firewall Rule.", + "RationaleStatement": "Monitoring for Delete SQL Server Firewall Rule events gives insight into SQL network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete server firewall rule (Server Firewall Rule)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Sql/servers/firewallRules/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Sql/servers/firewallRules/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Sql/servers/firewallRules/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Sql/servers/firewallRules/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete server firewall rule'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Sql/servers/firewallRules/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Sql/servers/firewallRules/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b954148f-4c11-4c38-8221-be76711e194a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb954148f-4c11-4c38-8221-be76711e194a) **- Name:** 'An activity log alert should exist for specific Administrative operations'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.9", + "Description": "Ensure that Activity Log Alert exists for Create or Update Public IP Address rule", + "Checks": [ + "monitor_alert_create_update_public_ip_address_rule" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Create or Update Public IP Addresses rule.", + "RationaleStatement": "Monitoring for Create or Update Public IP Address events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Create or Update Public Ip Address (Public Ip Address)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/publicIPAddresses/write and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/publicIPAddresses/write -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/publicIPAddresses/write` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/publicIPAddresses/write`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Create or Update Public Ip Address'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/publicIPAddresses/write` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/publicIPAddresses/write}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [1513498c-3091-461a-b321-e9b433218d28](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F1513498c-3091-461a-b321-e9b433218d28) **- Name:** 'Enable logging by category group for Public IP addresses (microsoft.network/publicipaddresses) to Log Analytics'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "7.1.2.10", + "Description": "Ensure that Activity Log Alert exists for Delete Public IP Address rule", + "Checks": [ + "monitor_alert_delete_public_ip_address_rule" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Create an activity log alert for the Delete Public IP Address rule.", + "RationaleStatement": "Monitoring for Delete Public IP Address events gives insight into network access changes and may reduce the time it takes to detect suspicious activity.", + "ImpactStatement": "There will be a substantial increase in log size if there are a large number of administrative actions on a server.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Select `Alerts`. 1. Select `Create`. 1. Select `Alert rule`. 1. Choose a subscription. 1. Select `Apply`. 1. Select the `Condition` tab. 1. Click `See all signals`. 1. Select `Delete Public Ip Address (Public Ip Address)`. 1. Click `Apply`. 1. Select the `Actions` tab. 1. Click `Select action groups` to select an existing action group, or `Create action group` to create a new action group. 1. Follow the prompts to choose or create an action group. 1. Select the `Details` tab. 1. Select a `Resource group`, provide an `Alert rule name` and an optional `Alert rule description`. 1. Click `Review + create`. 1. Click `Create`. **Remediate from Azure CLI** ``` az monitor activity-log alert create --resource-group --condition category=Administrative and operationName=Microsoft.Network/publicIPAddresses/delete and level= --scope /subscriptions/ --name --subscription --action-group ``` **Remediate from PowerShell** Create the `Conditions` object. ``` $conditions = @() $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Administrative -Field category $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Microsoft.Network/publicIPAddresses/delete -Field operationName $conditions += New-AzActivityLogAlertAlertRuleAnyOfOrLeafConditionObject -Equal Verbose -Field level ``` Retrieve the `Action Group` information and store in a variable, then create the `Actions` object. ``` $actionGroup = Get-AzActionGroup -ResourceGroupName -Name $actionObject = New-AzActivityLogAlertActionGroupObject -Id $actionGroup.Id ``` Create the `Scope` object ``` $scope = /subscriptions/ ``` Create the `Activity Log Alert Rule` for `Microsoft.Network/publicIPAddresses/delete` ``` New-AzActivityLogAlert -Name -ResourceGroupName -Condition $conditions -Scope $scope -Location global -Action $actionObject -Subscription -Enabled $true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the `Monitor` blade. 1. Click on `Alerts`. 1. In the Alerts window, click on `Alert rules`. 1. Ensure an alert rule exists where the Condition column contains `Operation name=Microsoft.Network/publicIPAddresses/delete`. 1. Click on the Alert `Name` associated with the previous step. 1. Ensure the `Condition` panel displays the text `Whenever the Activity Log has an event with Category='Administrative', Operation name='Delete Public Ip Address'` and does not filter on `Level`, `Status` or `Caller`. 1. Ensure the `Actions` panel displays an Action group is assigned to notify the appropriate personnel in your organization. **Audit from Azure CLI** ``` az monitor activity-log alert list --subscription --query [].{Name:name,Enabled:enabled,Condition:condition.allOf,Actions:actions} ``` Look for `Microsoft.Network/publicIPAddresses/delete` in the output **Audit from PowerShell** ``` Get-AzActivityLogAlert -SubscriptionId |where-object {$_.ConditionAllOf.Equal -match Microsoft.Network/publicIPAddresses/delete}|select-object Location,Name,Enabled,ResourceGroupName,ConditionAllOf ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [1513498c-3091-461a-b321-e9b433218d28](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F1513498c-3091-461a-b321-e9b433218d28) **- Name:** 'Enable logging by category group for Public IP addresses (microsoft.network/publicipaddresses) to Log Analytics'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/updates/classic-alerting-monitoring-retirement:https://docs.microsoft.com/en-in/azure/azure-monitor/platform/alerts-activity-log:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/createorupdate:https://docs.microsoft.com/en-in/rest/api/monitor/activitylogalerts/listbysubscriptionid:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "DefaultValue": "By default, no monitoring alerts are created." + } + ] + }, + { + "Id": "6.1.3", + "Description": "Ensure that 'Allow users to remember multifactor authentication on devices they trust' is disabled", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.1 Security Defaults (Per-User MFA)", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "[**IMPORTANT - Please read the section overview:** If your organization pays for Microsoft Entra ID licensing (included in Microsoft 365 E3, E5, F5, or Business Premium, and EM&S E3 or E5 licenses) and **CAN** use Conditional Access, ignore the recommendations in this section and proceed to the Conditional Access section.] Do not allow users to remember multi-factor authentication on devices.", + "RationaleStatement": "Remembering Multi-Factor Authentication (MFA) for devices and browsers allows users to have the option to bypass MFA for a set number of days after performing a successful sign-in using MFA. This can enhance usability by minimizing the number of times a user may need to perform two-step verification on the same device. However, if an account or device is compromised, remembering MFA for trusted devices may affect security. Hence, it is recommended that users not be allowed to bypass MFA.", + "ImpactStatement": "For every login attempt, the user will be required to perform multi-factor authentication.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, click `Users` 1. Click the `Per-user MFA` button on the top bar 1. Click on `Service settings` 1. Uncheck the box next to `Allow users to remember multi-factor authentication on devices they trust` 1. Click `Save`", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, click `Users` 1. Click the `Per-user MFA` button on the top bar 1. Click on `Service settings` 1. Ensure that `Allow users to remember multi-factor authentication on devices they trust` is not enabled", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-mfasettings#remember-multi-factor-authentication-for-devices-that-users-trust:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-identity-management#im-4-use-strong-authentication-controls-for-all-azure-active-directory-based-access:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-6-use-strong-authentication-controls", + "DefaultValue": "By default, `Allow users to remember multi-factor authentication on devices they trust` is disabled." + } + ] + }, + { + "Id": "7.1.3.1", + "Description": "Ensure Application Insights are Configured", + "Checks": [ + "appinsights_ensure_is_configured" + ], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Application Insights within Azure act as an Application Performance Monitoring solution providing valuable data into how well an application performs and additional information when performing incident response. The types of log data collected include application metrics, telemetry data, and application trace logging data providing organizations with detailed information about application activity and application transactions. Both data sets help organizations adopt a proactive and retroactive means to handle security and performance related metrics within their modern applications.", + "RationaleStatement": "Configuring Application Insights provides additional data not found elsewhere within Azure as part of a much larger logging and monitoring program within an organization's Information Security practice. The types and contents of these logs will act as both a potential cost saving measure (application performance) and a means to potentially confirm the source of a potential incident (trace logging). Metrics and Telemetry data provide organizations with a proactive approach to cost savings by monitoring an application's performance, while the trace logging data provides necessary details in a reactive incident response scenario by helping organizations identify the potential source of an incident within their application.", + "ImpactStatement": "Because Application Insights relies on a Log Analytics Workspace, an organization will incur additional expenses when using this service.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to `Application Insights`. 2. Under the `Basics` tab within the `PROJECT DETAILS` section, select the `Subscription`. 3. Select the `Resource group`. 4. Within the `INSTANCE DETAILS`, enter a `Name`. 5. Select a `Region`. 6. Next to `Resource Mode`, select `Workspace-based`. 7. Within the `WORKSPACE DETAILS`, select the `Subscription` for the log analytics workspace. 8. Select the appropriate `Log Analytics Workspace`. 9. Click `Next:Tags >`. 10. Enter the appropriate `Tags` as `Name`, `Value` pairs. 11. Click `Next:Review+Create`. 12. Click `Create`. **Remediate from Azure CLI** ``` az monitor app-insights component create --app --resource-group --location --kind web --retention-time --workspace --subscription ``` **Remediate from PowerShell** ``` New-AzApplicationInsights -Kind web -ResourceGroupName -Name -location -RetentionInDays -SubscriptionID -WorkspaceResourceId ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to `Application Insights`. 2. Ensure an `Application Insights` service is configured and exists. **Audit from Azure CLI** ``` az monitor app-insights component show --query [].{ID:appId, Name:name, Tenant:tenantId, Location:location, Provisioning_State:provisioningState} ``` Ensure the above command produces output, otherwise `Application Insights` has not been configured. **Audit from PowerShell** ``` Get-AzApplicationInsights|select location,name,appid,provisioningState,tenantid ```", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview", + "DefaultValue": "Application Insights are not enabled by default." + } + ] + }, + { + "Id": "8.1", + "Description": "Ensure that RDP access from the Internet is evaluated and restricted", + "Checks": [ + "network_rdp_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where RDP is not explicitly required and narrowly configured for resources attached to a network security group, Internet-level access to Azure resources should be restricted or eliminated.", + "RationaleStatement": "The potential security problem with using RDP over the Internet is that attackers can use various brute force techniques to gain access to Azure Virtual Machines. Once the attackers gain access, they can use a virtual machine as a launch point for compromising other machines on an Azure Virtual Network or even attack networked devices outside of Azure.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network security groups`. 1. Under `Settings`, click `Inbound security rules`. 1. Check the box next to any inbound security rule matching: - Port: `3389` or range including 3389 - Protocol: `TCP` or `Any` - Source: `0.0.0.0/0`, `Internet`, or `Any` - Action: `Allow` 1. Click `Delete`. 1. Click `Yes`. **Remediate from Azure CLI** For each network security group rule requiring remediation, run the following command to delete a rule: ``` az network nsg rule delete --resource-group --nsg-name --name ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network security groups`. 1. Under `Settings`, click `Inbound security rules`. 1. Ensure that no inbound security rule exists that matches the following: - Port: `3389` or range including 3389 - Protocol: `TCP` or `Any` - Source: `0.0.0.0/0`, `Internet`, or `Any` - Action: `Allow` 1. Repeat steps 1-3 for each network security group. To audit from Azure Resource Graph: 1. Go to `Resource Graph Explorer`. 1. Click `New query`. 1. Paste the following into the query window: ``` resources | where type =~ microsoft.network/networksecuritygroups | project id, name, securityRule = properties.securityRules | mv-expand securityRule | extend access = securityRule.properties.access, direction = securityRule.properties.direction, protocol = securityRule.properties.protocol, destinationPort = case(isempty(securityRule.properties.destinationPortRange), securityRule.properties.destinationPortRanges, securityRule.properties.destinationPortRange), sourceAddress = case(isempty(securityRule.properties.sourceAddressPrefix), securityRule.properties.sourceAddressPrefixes, securityRule.properties.sourceAddressPrefix) | where access =~ Allow and direction =~ Inbound and protocol in~ (tcp, ) | mv-expand destinationPort | mv-expand sourceAddress | extend destinationPortMin = toint(split(destinationPort, -)[0]), destinationPortMax = toint(split(destinationPort, -)[-1]) | where (destinationPortMin <= 3389 and destinationPortMax >= 3389) or destinationPort == | where sourceAddress in~ (*, 0.0.0.0, internet, any) or sourceAddress endswith /0 ``` 1. Click `Run query`. 1. Ensure that no results are returned. **Audit from Azure CLI** List network security groups with non-default security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that no network security group has an inbound security rule that matches the following: ``` access : Allow destinationPortRange : 3389, *, or direction : Inbound protocol : TCP or * sourceAddressPrefix : 0.0.0.0/0, Internet, or * ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [22730e10-96f6-4aac-ad84-9383d35b5917](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F22730e10-96f6-4aac-ad84-9383d35b5917) **- Name:** 'Management ports should be closed on your virtual machines'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/azure-security-network-security-best-practices#disable-rdpssh-access-to-azure-virtual-machines:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries:Express Route: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal", + "DefaultValue": "By default, RDP access from internet is not `enabled`." + } + ] + }, + { + "Id": "8.2", + "Description": "Ensure that SSH access from the Internet is evaluated and restricted", + "Checks": [ + "network_ssh_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where certain ports and protocols may be exposed to the Internet, they should be evaluated for necessity and restricted wherever they are not explicitly required.", + "RationaleStatement": "The potential security problem with using SSH over the Internet is that attackers can use various brute force techniques to gain access to Azure Virtual Machines. Once the attackers gain access, they can use a virtual machine as a launch point for compromising other machines on the Azure Virtual Network or even attack networked devices outside of Azure.", + "ImpactStatement": "", + "RemediationProcedure": "Where SSH is not explicitly required and narrowly configured for resources attached to the Network Security Group, Internet-level access to your Azure resources should be restricted or eliminated. For internal access to relevant resources, configure an encrypted network tunnel such as: [ExpressRoute](https://docs.microsoft.com/en-us/azure/expressroute/) [Site-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal) [Point-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal)", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `Networking` blade for the specific Virtual machine in Azure portal 2. Verify that the `INBOUND PORT RULES` **does not** have a rule for SSH such as - port = `22`, - protocol = `TCP` OR `ANY`, - Source = `Any` OR `Internet` **Audit from Azure CLI** List Network security groups with corresponding non-default Security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that none of the NSGs have security rule as below ``` access : Allow destinationPortRange : 22 or * or [port range containing 22] direction : Inbound protocol : TCP or * sourceAddressPrefix : * or 0.0.0.0 or /0 or /0 or internet or any ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [22730e10-96f6-4aac-ad84-9383d35b5917](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F22730e10-96f6-4aac-ad84-9383d35b5917) **- Name:** 'Management ports should be closed on your virtual machines'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/azure-security-network-security-best-practices#disable-rdpssh-access-to-azure-virtual-machines:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries:Express Route: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal", + "DefaultValue": "By default, SSH access from internet is not `enabled`." + } + ] + }, + { + "Id": "8.3", + "Description": "Ensure that UDP access from the Internet is evaluated and restricted", + "Checks": [ + "network_udp_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where certain ports and protocols may be exposed to the Internet, they should be evaluated for necessity and restricted wherever they are not explicitly required.", + "RationaleStatement": "The potential security problem with broadly exposing UDP services over the Internet is that attackers can use DDoS amplification techniques to reflect spoofed UDP traffic from Azure Virtual Machines. The most common types of these attacks use exposed DNS, NTP, SSDP, SNMP, CLDAP and other UDP-based services as amplification sources for disrupting services of other machines on the Azure Virtual Network or even attack networked devices outside of Azure.", + "ImpactStatement": "", + "RemediationProcedure": "Where UDP is not explicitly required and narrowly configured for resources attached to the Network Security Group, Internet-level access to your Azure resources should be restricted or eliminated. For internal access to relevant resources, configure an encrypted network tunnel such as: [ExpressRoute](https://docs.microsoft.com/en-us/azure/expressroute/) [Site-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal) [Point-to-site VPN](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal)", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `Networking` blade for the specific Virtual machine in Azure portal 2. Verify that the `INBOUND PORT RULES` **does not** have a rule for UDP such as - protocol = `UDP`, - Source = `Any` OR `Internet` **Audit from Azure CLI** List Network security groups with corresponding non-default Security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that none of the NSGs have security rule as below ``` access : Allow destinationPortRange : * or [port range containing 53, 123, 161, 389, 1900, or other vulnerable UDP-based services] direction : Inbound protocol : UDP sourceAddressPrefix : * or 0.0.0.0 or /0 or /0 or internet or any ```", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security/fundamentals/network-best-practices#secure-your-critical-azure-service-resources-to-only-your-virtual-networks:https://docs.microsoft.com/en-us/azure/security/fundamentals/ddos-best-practices:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries:ExpressRoute: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal", + "DefaultValue": "By default, UDP access from internet is not `enabled`." + } + ] + }, + { + "Id": "8.4", + "Description": "Ensure that HTTP(S) access from the Internet is evaluated and restricted", + "Checks": [ + "network_http_internet_access_restricted" + ], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Network security groups should be periodically evaluated for port misconfigurations. Where certain ports and protocols may be exposed to the Internet, they should be evaluated for necessity and restricted wherever they are not explicitly required and narrowly configured.", + "RationaleStatement": "The potential security problem with using HTTP(S) over the Internet is that attackers can use various brute force techniques to gain access to Azure resources. Once the attackers gain access, they can use the resource as a launch point for compromising other resources within the Azure tenant.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Virtual machines`. 2. For each VM, open the `Networking` blade. 3. Click on `Inbound port rules`. 4. Delete the rule with: * Port = 80/443 OR \\[port range containing 80/443\\] * Protocol = TCP OR Any * Source = Any (\\*) OR IP Addresses(0.0.0.0/0) OR Service Tag(Internet) * Action = Allow **Remediate from Azure CLI** 1. Run below command to list network security groups: ``` az network nsg list --subscription --output table ``` 2. For each network security group, run below command to list the rules associated with the specified port: ``` az network nsg rule list --resource-group --nsg-name --query [?destinationPortRange=='80 or 443'] ``` 3. Run the below command to delete the rule with: * Port = 80/443 OR \\[port range containing 80/443\\] * Protocol = TCP OR * * Source = Any (\\*) OR IP Addresses(0.0.0.0/0) OR Service Tag(Internet) * Action = Allow ``` az network nsg rule delete --resource-group --nsg-name --name ```", + "AuditProcedure": "**Audit from Azure Portal** 1. For each VM, open the Networking blade 2. Verify that the INBOUND PORT RULES does not have a rule for HTTP(S) such as - port = `80`/ `443`, - protocol = `TCP`, - Source = `Any` OR `Internet` **Audit from Azure CLI** List Network security groups with corresponding non-default Security rules: ``` az network nsg list --query [*].[name,securityRules] ``` Ensure that none of the NSGs have security rule as below ``` access : Allow destinationPortRange : 80/443 or * or [port range containing 80/443] direction : Inbound protocol : TCP sourceAddressPrefix : * or 0.0.0.0 or /0 or /0 or internet or any ```", + "AdditionalInformation": "", + "References": "Express Route: https://docs.microsoft.com/en-us/azure/expressroute/:Site-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal:Point-to-Site VPN: https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-point-to-site-resource-manager-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-1-establish-network-segmentation-boundaries", + "DefaultValue": "" + } + ] + }, + { + "Id": "8.5", + "Description": "Ensure that Network Security Group Flow Log retention period is 'greater than 90 days'", + "Checks": [ + "network_flow_log_more_than_90_days" + ], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Network Security Group Flow Logs should be enabled and the retention period set to greater than or equal to 90 days. **Retirement Notice** On September 30, 2027, network security group (NSG) flow logs will be retired. Starting June 30, 2025, it will no longer be possible to create new NSG flow logs. Azure recommends migrating to virtual network flow logs. Review https://azure.microsoft.com/en-gb/updates?id=Azure-NSG-flow-logs-Retirement for more information. For virtual network flow logs, consider applying the recommendation `Ensure that virtual network flow log retention days is set to greater than or equal to 90` in this section.", + "RationaleStatement": "Flow logs enable capturing information about IP traffic flowing in and out of network security groups. Logs can be used to check for anomalies and give insight into suspected breaches.", + "ImpactStatement": "This will keep IP traffic logs for longer than 90 days. As a level 2, first determine your need to retain data, then apply your selection here. As this is data stored for longer, your monthly storage costs will increase depending on your data use.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network Watcher` 2. Select `NSG flow logs` blade in the Logs section 3. Select each Network Security Group from the list 4. Ensure `Status` is set to `On` 5. Ensure `Retention (days)` setting `greater than 90 days` 6. Select your storage account in the `Storage account` field 7. Select `Save` **Remediate from Azure CLI** Enable the `NSG flow logs` and set the Retention (days) to greater than or equal to 90 days. ``` az network watcher flow-log configure --nsg --enabled true --resource-group --retention 91 --storage-account ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network Watcher` 2. Select `NSG flow logs` blade in the Logs section 3. Select each Network Security Group from the list 4. Ensure `Status` is set to `On` 5. Ensure `Retention (days)` setting `greater than 90 days` **Audit from Azure CLI** ``` az network watcher flow-log show --resource-group --nsg --query 'retentionPolicy' ``` Ensure that `enabled` is set to `true` and `days` is set to `greater then or equal to 90`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [5e1cd26a-5090-4fdb-9d6a-84a90335e22d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F5e1cd26a-5090-4fdb-9d6a-84a90335e22d) **- Name:** 'Configure network security groups to use specific workspace, storage account and flowlog retention policy for traffic analytics'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-nsg-flow-logging-overview:https://docs.microsoft.com/en-us/cli/azure/network/watcher/flow-log?view=azure-cli-latest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-6-configure-log-storage-retention", + "DefaultValue": "By default, Network Security Group Flow Logs are `disabled`." + } + ] + }, + { + "Id": "8.6", + "Description": "Ensure that Network Watcher is 'Enabled' for Azure Regions that are in use", + "Checks": [ + "network_watcher_enabled" + ], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Enable Network Watcher for physical regions in Azure subscriptions.", + "RationaleStatement": "Network diagnostic and visualization tools available with Network Watcher help users understand, diagnose, and gain insights to the network in Azure.", + "ImpactStatement": "There are additional costs per transaction to run and store network data. For high-volume networks these charges will add up quickly.", + "RemediationProcedure": "Opting out of Network Watcher automatic enablement is a permanent change. Once you opt-out you cannot opt-in without contacting support. To manually enable Network Watcher in each region where you want to use Network Watcher capabilities, follow the steps below. **Remediate from Azure Portal** 1. Use the Search bar to search for and click on the `Network Watcher` service. 1. Click `Create`. 1. Select a `Region` from the drop-down menu. 1. Click `Add`. **Remediate from Azure CLI** ``` az network watcher configure --locations --enabled true --resource-group ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Use the Search bar to search for and click on the `Network Watcher` service. 1. From the Overview menu item, review each Network Watcher listed, and ensure that a network watcher is listed for each region in use by the subscription. **Audit from Azure CLI** ``` az network watcher list --query [].{Location:location,State:provisioningState} -o table ``` This will list all network watchers and their provisioning state. Ensure `provisioningState` is `Succeeded` for each network watcher. ``` az account list-locations --query [?metadata.regionType=='Physical'].{Name:name,DisplayName:regionalDisplayName} -o table ``` This will list all physical regions that exist in the subscription. Compare this list to the previous one to ensure that for each region in use, a network watcher exists with `provisioningState` set to `Succeeded`. **Audit from PowerShell** Get a list of Network Watchers ``` Get-AzNetworkWatcher ``` Make sure each watcher is set with the `ProvisioningState` setting set to `Succeeded` and all `Locations` that are in use by the subscription are using a watcher. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b6e2945c-0b7b-40f5-9233-7a5323b5cdc6](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb6e2945c-0b7b-40f5-9233-7a5323b5cdc6) **- Name:** 'Network Watcher should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-monitoring-overview:https://learn.microsoft.com/en-us/cli/azure/network/watcher?view=azure-cli-latest:https://learn.microsoft.com/en-us/cli/azure/network/watcher?view=azure-cli-latest#az-network-watcher-configure:https://learn.microsoft.com/en-us/azure/network-watcher/network-watcher-create:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-4-enable-network-logging-for-security-investigation:https://azure.microsoft.com/en-ca/pricing/details/network-watcher/", + "DefaultValue": "Network Watcher is automatically enabled. When you create or update a virtual network in your subscription, Network Watcher will be enabled automatically in your Virtual Network's region. There is no impact to your resources or associated charge for automatically enabling Network Watcher." + } + ] + }, + { + "Id": "6.2.2", + "Description": "Ensure that an exclusionary geographic Conditional Access policy is considered", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.2 Conditional Access", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "**CAUTION**: If these policies are created without first auditing and testing the result, misconfiguration can potentially lock out administrators or create undesired access issues. Conditional Access Policies can be used to block access from geographic locations that are deemed out-of-scope for your organization or application. The scope and variables for this policy should be carefully examined and defined.", + "RationaleStatement": "Conditional Access, when used as a deny list for the tenant or subscription, is able to prevent ingress or egress of traffic to countries that are outside of the scope of interest (e.g.: customers, suppliers) or jurisdiction of an organization. This is an effective way to prevent unnecessary and long-lasting exposure to international threats such as APTs.", + "ImpactStatement": "Microsoft Entra ID P1 or P2 is required. Limiting access geographically will deny access to users that are traveling or working remotely in a different part of the world. A point-to-site or site to site tunnel such as a VPN is recommended to address exceptions to geographic access policies.", + "RemediationProcedure": "**Remediate from Azure Portal** Part 1 of 2 - Create the policy and enable it in `Report-only` mode. 1. From Azure Home open the portal menu in the top left, and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left, and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Click the `+ New policy` button, then: 1. Provide a name for the policy. 1. Under `Assignments`, select `Users` then: - Under `Include`, select `All users` - Under `Exclude`, check Users and groups and only select emergency access accounts and service accounts (**NOTE**: Service accounts are excluded here because service accounts are non-interactive and cannot complete MFA) 1. Under `Assignments`, select `Target resources` then: - Under `Include`, select `All cloud apps` - Leave `Exclude` blank unless you have a well defined exception 1. Under `Conditions`, select `Locations` then: - Select `Include`, then add entries for locations for those that should be **blocked** - Select `Exclude`, then add entries for those that should be allowed (**IMPORTANT**: Ensure that all Trusted Locations are in the `Exclude` list.) 1. Under `Access Controls`, select `Grant` select `Block Access`. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. Allow some time to pass to ensure the sign-in logs capture relevant conditional access events. These events will need to be reviewed to determine if additional considerations are necessary for your organization (e.g. legitimate locations are being blocked and investigation is needed for exception). **NOTE:** The policy is not yet 'live,' since `Report-only` is being used to audit the effect of the policy. Part 2 of 2 - Confirm that the policy is not blocking access that should be granted, then toggle to `On`. 1. With your policy now in report-only mode, return to the Microsoft Entra blade and click on `Sign-in logs`. 1. Review the recent sign-in events - click an event then review the event details (specifically the `Report-only` tab) to ensure: - The sign-in event you're reviewing occurred **after** turning on the policy in report-only mode - The policy name from step 6 above is listed in the `Policy Name` column - The `Result` column for the new policy shows that the policy was `Not applied` (indicating the location origin was not blocked) 1. If the above conditions are present, navigate back to the policy name in Conditional Access and open it. 1. Toggle the policy from `Report-only` to `On`. 1. Click `Save`. **Remediate from PowerShell** First, set up the conditions objects values before updating an existing conditional access policy or before creating a new one. You may need to use additional PowerShell cmdlets to retrieve specific IDs such as the `Get-MgIdentityConditionalAccessNamedLocation` which outputs the `Location IDs` for use with conditional access policies. ``` $conditions = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessConditionSet $conditions.Applications = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessApplicationCondition $conditions.Applications.IncludeApplications = $conditions.Applications.ExcludeApplications = $conditions.Users = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessUserCondition $conditions.Users.IncludeUsers = $conditions.Users.ExcludeUsers = $conditions.Users.IncludeGroups = $conditions.Users.ExcludeGroups = $conditions.Users.IncludeRoles = $conditions.Users.ExcludeRoles = $conditions.Locations = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessLocationCondition $conditions.Locations.IncludeLocations = $conditions.Locations.ExcludeLocations = $controls = New-Object -TypeName Microsoft.Open.MSGraph.Model.ConditionalAccessGrantControls $controls._Operator = OR $controls.BuiltInControls = block ``` Next, update the existing conditional access policy with the condition set options configured with the previous commands. ``` Update-MgIdentityConditionalAccessPolicy -PolicyId -Conditions $conditions -GrantControls $controls ``` To create a new conditional access policy that complies with this best practice, run the following commands after creating the condition set above ``` New-MgIdentityConditionalAccessPolicy -Name Policy Name -State -Conditions $conditions -GrantControls $controls ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal menu in the top left, and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left, and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit, then: - Under `Assignments` > `Users`, review the users and groups for the personnel the policy will apply to - Under `Assignments` > `Target resources`, review the cloud apps or actions for the systems the policy will apply to - Under `Conditions` > `Locations`, Review the `Include` locations for those that should be **blocked** - Under `Conditions` > `Locations`, Review the `Exclude` locations for those that should be allowed (Note: locations set up in the previous recommendation for Trusted Location should be in the `Exclude` list.) - Under `Access Controls` > `Grant` - Confirm that `Block access` is selected. **Audit from Azure CLI** ``` As of this writing there are no subcommands for Conditional Access Policies within the Azure CLI ``` **Audit from PowerShell** ``` $conditionalAccessPolicies = Get-MgIdentityConditionalAccessPolicy foreach($policy in $conditionalAccessPolicies) {$policy | Select-Object @{N='Policy ID'; E={$policy.id}}, @{N=Included Locations; E={$policy.Conditions.Locations.IncludeLocations}}, @{N=Excluded Locations; E={$policy.Conditions.Locations.ExcludeLocations}}, @{N=BuiltIn GrantControls; E={$policy.GrantControls.BuiltInControls}}} ``` Make sure there is at least 1 row in the output of the above PowerShell command that contains `Block` under the `BuiltIn GrantControls` column and location IDs under the `Included Locations` and `Excluded Locations` columns. If not, a policy containing these options has not been created and is considered a finding.", + "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.", + "References": "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-location:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-report-only:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions", + "DefaultValue": "This policy does not exist by default." + } + ] + }, + { + "Id": "6.2.3", + "Description": "Ensure that an exclusionary device code flow policy is considered", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.2 Conditional Access", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Conditional Access Policies can be used to prevent the Device code authentication flow. Device code flow should be permitted only for users that regularly perform duties that explicitly require the use of Device Code to authenticate, such as utilizing Azure with PowerShell.", + "RationaleStatement": "Attackers use Device code flow in phishing attacks and, if successful, results in the attacker gaining access tokens and refresh tokens which are scoped to user_impersonation, which can perform any action the user has permission to perform.", + "ImpactStatement": "Microsoft Entra ID P1 or P2 is required. This policy should be tested using the `Report-only mode` before implementation. Without a full and careful understanding of the accounts and personnel who require Device code authentication flow, implementing this policy can block authentication for users and devices who rely on Device code flow. For users and devices that rely on device code flow authentication, more secure alternatives should be implemented wherever possible.", + "RemediationProcedure": "**Remediate from Azure Portal** Part 1 of 2 - Create the policy and enable it in `Report-only` mode. 1. From Azure Home open the portal menu in the top left and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Click the `+ New policy` button, then: 1. Provide a name for the policy. 1. Under `Assignments`, select `Users` then: - Under `Include`, select `All users` - Under `Exclude`, check Users and groups and only select emergency access accounts 1. Under `Assignments`, select `Target resources` then: - Under `Include`, select `All cloud apps` - Leave `Exclude` blank unless you have a well defined exception 1. Under `Conditions` > `Authentication Flows`, set Configure to `Yes` then: - Select `Device code flow` - Select `Done` 1. Under `Access Controls` > `Grant`, select `Block Access`. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. Allow some time to pass to ensure the sign-in logs capture relevant conditional access events. These events will need to be reviewed to determine if additional considerations are necessary for your organization (e.g. many legitimate use cases of device code authentication are observed). **NOTE:** The policy is not yet 'live,' since `Report-only` is being used to audit the effect of the policy. Part 2 of 2 - Confirm that the policy is not blocking access that should be granted, then toggle to `On`. 1. With your policy now in report-only mode, return to the Microsoft Entra blade and click on `Sign-in logs`. 1. Review the recent sign-in events - click an event then review the event details (specifically the `Report-only` tab) to ensure: - The sign-in event you're reviewing occurred **after** turning on the policy in report-only mode - The policy name from step 6 above is listed in the `Policy Name` column - The `Result` column for the new policy shows that the policy was `Not applied` (indicating the device code authentication flow was not blocked) 1. If the above conditions are present, navigate back to the policy name in Conditional Access and open it. 1. Toggle the policy from `Report-only` to `On`. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal menu in the top left and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit, then: - Under `Assignments` > `Users`, review the users and groups for the personnel the policy will apply to - Under `Assignments` > `Target resources`, review the cloud apps or actions for the systems the policy will apply to - Under `Conditions` > `Authentication Flows`, review the configuration to ensure `Device code flow` is selected - Under `Access Controls` > `Grant` - Confirm that `Block access` is selected.", + "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource in References which monitors Azure sign ins.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-authentication-flows#device-code-flow:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/concept-conditional-access-report-only:https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-authentication-flows", + "DefaultValue": "This policy does not exist by default." + } + ] + }, + { + "Id": "6.2.4", + "Description": "Ensure that a multifactor authentication policy exists for all users", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.2 Conditional Access", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "A Conditional Access policy can be enabled to ensure that users are required to use Multifactor Authentication (MFA) to login. **Note:** Since 2024, Azure has been rolling out mandatory multifactor authentication. For more information: - https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in - https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication", + "RationaleStatement": "Multifactor authentication is strongly recommended to increase the confidence that a claimed identity can be proven to be the subject of the identity. This results in a stronger authentication chain and reduced likelihood of exploitation.", + "ImpactStatement": "There is an increased cost associated with Conditional Access policies because of the requirement of Microsoft Entra ID P1 or P2 licenses. Additional support overhead may also need to be considered.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home open Portal menu in the top left, and select `Microsoft Entra ID`. 1. Select `Security`. 1. Select `Conditional Access`. 1. Select `Policies`. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users this policy should not apply to and click `Select`. 1. Click the blue text under `Target resources`. 1. Select `All cloud apps`. 1. Click the blue text under `Grant`. 1. Under `Grant access`, check `Require multifactor authentication` and click `Select`. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal Menu in the top left, and select `Microsoft Entra ID`. 1. Scroll down in the menu on the left, and select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit. 1. Click the blue text under `Users`. 1. Under `Include` ensure that `All Users` is specified. 1. Under `Exclude` ensure that no users or groups are specified. If there are users or groups specified for exclusion, a very strong justification should exist for each exception, and all excepted account-level objects should be recorded in documentation along with the justification for comparison in future audits.", + "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource in the References which monitors Azure sign ins.", + "References": "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-all-users-mfa:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/troubleshoot-conditional-access-what-if:https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-insights-reporting:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions", + "DefaultValue": "Starting October 2024, MFA will be required for all accounts by default." + } + ] + }, + { + "Id": "9.1.11", + "Description": "Ensure that Microsoft Cloud Security Benchmark policies are not set to 'Disabled'", + "Checks": [ + "policy_ensure_asc_enforcement_enabled" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "The Microsoft Cloud Security Benchmark (or MCSB) is an Azure Policy Initiative containing many security policies to evaluate resource configuration against best practice recommendations. If a policy in the MCSB is set with effect type `Disabled`, it is not evaluated and may prevent administrators from being informed of valuable security recommendations.", + "RationaleStatement": "A security policy defines the desired configuration of resources in your environment and helps ensure compliance with company or regulatory security requirements. The MCSB Policy Initiative a set of security recommendations based on best practices and is associated with every subscription by default. When a policy Effect is set to `Audit`, policies in the MCSB ensure that Defender for Cloud evaluates relevant resources for supported recommendations. To ensure that policies within the MCSB are not being missed when the Policy Initiative is evaluated, none of the policies should have an Effect of `Disabled`.", + "ImpactStatement": "Policies within the MCSB default to an effect of `Audit` and will evaluate—but not enforce—policy recommendations. Ensuring these policies are set to `Audit` simply ensures that the evaluation occurs to allow administrators to understand where an improvement may be possible. Administrators will need to determine if the recommendations are relevant and desirable for their environment, then manually take action to resolve the status if desired.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Management Group or Subscription. 1. Click on `Security policies` in the left column. 1. Click on `Microsoft cloud security benchmark` 1. Click `Add Filter` and select `Effect` 1. Check the `Disabled` box to search for all disabled policies 1. Click `Apply` 1. Click the blue ellipsis `...` to the right of a policy name. 1. Click `Manage effect and parameters`. 1. Under `Policy effect`, select the radio button next to `Audit`. 1. Click `Save`. 1. Click `Refresh`. 1. Repeat steps 10-14 until all disabled policies are updated. 1. Repeat steps 1-15 for each Management Group or Subscription requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Management Group or Subscription. 1. Click on `Security policies` in the left column. 1. Click on `Microsoft cloud security benchmark`. 1. Click `Add filter` and select `Effect`. 1. Check the `Disabled` box to search for all disabled policies. 1. Click `Apply`. 1. Ensure that no policies are displayed, signifying that there are no disabled policies. 1. Repeat steps 1-10 for each Management Group or Subscription.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-in/azure/defender-for-cloud/security-policy-concept:https://docs.microsoft.com/en-us/azure/security-center/security-center-policies:https://learn.microsoft.com/en-us/azure/defender-for-cloud/implement-security-recommendations:https://learn.microsoft.com/en-us/rest/api/policy/policy-assignments/get:https://learn.microsoft.com/en-us/rest/api/policy/policy-assignments/create:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-7-define-and-implement-logging-threat-detection-and-incident-response-strategy", + "DefaultValue": "By default, the MCSB policy initiative is assigned on all subscriptions, and **most** policies will have an effect of `Audit`. Some policies will have a default effect of `Disabled`." + } + ] + }, + { + "Id": "9.1.12", + "Description": "Ensure That 'All users with the following roles' is set to 'Owner'", + "Checks": [ + "defender_ensure_notify_emails_to_owners" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable security alert emails to subscription owners.", + "RationaleStatement": "Enabling security alert emails to subscription owners ensures that they receive security alert emails from Microsoft. This ensures that they are aware of any potential security issues and can mitigate the risk in a timely fashion.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Click on the appropriate Management Group, Subscription, or Workspace 1. Click on `Email notifications` 1. In the drop down of the `All users with the following roles` field select `Owner` 1. Click `Save` **Remediate from Azure CLI** Use the below command to set `Send email also to subscription owners` to `On`. ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts/default1?api-version=2017-08-01-preview -d@input.json' ``` Where `input.json` contains the data below, replacing `validEmailAddress` with a single email address or multiple comma-separated email addresses: ``` { id: /subscriptions//providers/Microsoft.Security/securityContacts/default1, name: default1, type: Microsoft.Security/securityContacts, properties: { email: , alertNotifications: On, alertsToAdmins: On, notificationsByRole: Owner } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Click on the appropriate Management Group, Subscription, or Workspace 1. Click on `Email notifications` 1. Ensure that `All users with the following roles` is set to `Owner` **Audit from Azure CLI** Ensure the command below returns state of `On` and that `Owner` appears in roles. ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview'| jq '.[] | select(.name==default).properties.notificationsByRole' ```", + "AdditionalInformation": "Excluding any entries in the input.json properties block disables the specific setting by default.", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details:https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/security-contacts:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification", + "DefaultValue": "By default, `Owner` is selected" + } + ] + }, + { + "Id": "9.1.13", + "Description": "Ensure 'Additional email addresses' is Configured with a Security Contact Email", + "Checks": [ + "defender_additional_email_configured_with_a_security_contact" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Cloud emails the subscription owners whenever a high-severity alert is triggered for their subscription. You should provide a security contact email address as an additional email address.", + "RationaleStatement": "Microsoft Defender for Cloud emails the Subscription Owner to notify them about security alerts. Adding your Security Contact's email address to the 'Additional email addresses' field ensures that your organization's Security Team is included in these alerts. This ensures that the proper people are aware of any potential compromise in order to mitigate the risk in a timely fashion.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the appropriate Management Group, Subscription, or Workspace. 1. Click on `Email notifications`. 1. Enter a valid security contact email address (or multiple addresses separated by commas) in the `Additional email addresses` field. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to set `Security contact emails` to `On`. ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts/default?api-version=2020-01-01-preview -d@input.json' ``` Where `input.json` contains the data below, replacing `validEmailAddress` with a single email address or multiple comma-separated email addresses: ``` { id: /subscriptions//providers/Microsoft.Security/securityContacts/default, name: default, type: Microsoft.Security/securityContacts, properties: { email: , alertNotifications: On, alertsToAdmins: On } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the appropriate Management Group, Subscription, or Workspace. 1. Click on `Email notifications`. 1. Ensure that a valid security contact email address is listed in the `Additional email addresses` field. **Audit from Azure CLI** Ensure the output of the below command is not empty and is set with appropriate email ids: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview' | jq '.|.[] | select(.name==default)'|jq '.properties.emails' ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [4f4f78b8-e367-4b10-a341-d9a4ad5cf1c7](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4f4f78b8-e367-4b10-a341-d9a4ad5cf1c7) **- Name:** 'Subscriptions should have a contact email address for security issues'", + "AdditionalInformation": "Excluding any entries in the input.json properties block disables the specific setting by default.", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details:https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/security-contacts:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification", + "DefaultValue": "By default, there are no additional email addresses entered." + } + ] + }, + { + "Id": "9.1.14", + "Description": "Ensure that 'Notify about alerts with the following severity (or higher)' is enabled", + "Checks": [ + "defender_ensure_notify_alerts_severity_is_high" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enables emailing security alerts to the subscription owner or other designated security contact.", + "RationaleStatement": "Enabling security alert emails ensures that security alert emails are sent by Microsoft. This ensures that the right people are aware of any potential security issues and can mitigate the risk.", + "ImpactStatement": "Enabling security alert emails can cause alert fatigue, increasing the risk of missing important alerts. Select an appropriate severity level to manage notifications. Azure aims to reduce alert fatigue by limiting the daily email volume per severity level. Learn more: https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications#email-frequency.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under `Notification types`, check box next to `Notify about alerts with the following severity (or higher)` and select an appropriate severity level from the drop-down menu. 1. Click `Save`. 1. Repeat steps 1-7 for each Subscription requiring remediation. **Remediate from Azure CLI** Use the below command to enable `Send email notification for high severity alerts`: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/<$0>/providers/Microsoft.Security/securityContacts/default1?api-version=2017-08-01-preview -d@input.json' ``` Where `input.json` contains the data below, replacing `validEmailAddress` with a single email address or multiple comma-separated email addresses: ``` { id: /subscriptions//providers/Microsoft.Security/securityContacts/default, name: default, type: Microsoft.Security/securityContacts, properties: { email: , alertNotifications: On, alertsToAdmins: On } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on the appropriate Subscription. 1. Click on `Email notifications`. 1. Under `Notification types`, ensure that the box next to `Notify about alerts with the following severity (or higher)` is checked, and an appropriate severity level is selected. 1. Repeat steps 1-6 for each Subscription. **Audit from Azure CLI** Including a Subscription ID at the `$0` in `/subscriptions/$0/providers`, ensure the below command returns `state: On`, and that `minimalSeverity` is set to an appropriate severity level: ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions/$0/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview' | jq '.|.[] | select(.name==default)'|jq '.properties.alertNotifications' ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [6e2593d9-add6-4083-9c9b-4b7d2188c899](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F6e2593d9-add6-4083-9c9b-4b7d2188c899) **- Name:** 'Email notification for high severity alerts should be enabled'", + "AdditionalInformation": "Excluding any entries in the `input.json` properties block disables the specific setting by default. This recommendation has been updated to reflect recent changes to Microsoft REST APIs for getting and updating security contact information.", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details:https://docs.microsoft.com/en-us/rest/api/securitycenter/security-contacts:https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification", + "DefaultValue": "By default, subscription owners receive email notifications for high-severity alerts." + } + ] + }, + { + "Id": "6.2.5", + "Description": "Ensure that multifactor authentication is required for risky sign-ins", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.2 Conditional Access", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Entra ID tracks the behavior of sign-in events. If the Entra ID domain is licensed with P2, the sign-in behavior can be used as a detection mechanism for additional scrutiny during the sign-in event. If this policy is set up, then Risky Sign-in events will prompt users to use multi-factor authentication (MFA) tokens on login for additional verification.", + "RationaleStatement": "Enabling multi-factor authentication is a recommended setting to limit the potential of accounts being compromised and limiting access to authenticated personnel. Enabling this policy allows Entra ID's risk-detection mechanisms to force additional scrutiny on the login event, providing a deterrent response to potentially malicious sign-in events, and adding an additional authentication layer as a reaction to potentially malicious behavior.", + "ImpactStatement": "Risk Policies for Conditional Access require Microsoft Entra ID P2. Additional overhead to support or maintain these policies may also be required if users lose access to their MFA tokens.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu in the top left and select `Microsoft Entra ID`. 1. Select `Security` 1. Select `Conditional Access`. 1. Select `Policies`. 1. Click `+ New policy`. 1. Enter a name for the policy. 1. Click the blue text under `Users`. 1. Under `Include`, select `All users`. 1. Under `Exclude`, check `Users and groups`. 1. Select users this policy should not apply to and click `Select`. 1. Click the blue text under `Target resources`. 1. Select `All cloud apps`. 1. Click the blue text under `Conditions`. 1. Select `Sign-in risk`. 1. Update the `Configure` toggle to `Yes`. 1. Check the sign-in risk level this policy should apply to, e.g. `High` and `Medium`. 1. Select `Done`. 1. Click the blue text under `Grant` and check `Require multifactor authentication` then click the `Select` button. 1. Click the blue text under `Session` then check `Sign-in frequency` and select `Every time` and click the `Select` button. 1. Set `Enable policy` to `Report-only`. 1. Click `Create`. After testing the policy in report-only mode, update the `Enable policy` setting from `Report-only` to `On`.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu in the top left and select `Microsoft Entra ID`. 1. Select `Security`. 1. Select on the left side `Conditional Access`. 1. Select `Policies`. 1. Select the policy you wish to audit. 1. Click the blue text under `Users`. 1. View under `Include` the corresponding users and groups to whom the policy is applied. 1. View under `Exclude` to determine which users and groups to whom the policy is not applied.", + "AdditionalInformation": "These policies should be tested by using the What If tool in the References. Setting these can and will create issues with logging in for users until they use an MFA device linked to their accounts. Further testing can also be done via the insights and reporting resource the in References which monitors Azure sign ins.", + "References": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-risk:https://learn.microsoft.com/en-us/entra/identity/conditional-access/troubleshoot-conditional-access-what-if:https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-insights-reporting:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions:https://learn.microsoft.com/en-us/entra/id-protection/overview-identity-protection#license-requirements", + "DefaultValue": "MFA is not enabled by default." + } + ] + }, + { + "Id": "6.3.1", + "Description": "Ensure that Azure admin accounts are not used for daily operations", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.3 Periodic Identity Reviews", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Microsoft Azure admin accounts should not be used for routine, non-administrative tasks.", + "RationaleStatement": "Using admin accounts for daily operations increases the risk of accidental misconfigurations and security breaches.", + "ImpactStatement": "Minor administrative overhead includes managing separate accounts, enforcing stricter access controls, and potential licensing costs for advanced security features.", + "RemediationProcedure": "If admin accounts are being used for daily operations, consider the following: - Monitor and alert on unusual activity. - Enforce the principle of least privilege. - Revoke any unnecessary administrative access. - Use Conditional Access to limit access to resources. - Ensure that administrators have separate admin and user accounts. - Use Microsoft Entra ID Protection helps organizations detect, investigate, and remediate identity-based risks. - Use Privileged Identity Management (PIM) in Microsoft Entra ID to limit standing administrator access to privileged roles, discover who has access, and review privileged access.", + "AuditProcedure": "**Audit from Azure Portal** Monitor: 1. Go to `Monitor`. 1. Click `Activity log`. 1. Review the activity log and ensure that admin accounts are not being used for daily operations. Microsoft Entra ID: 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Sign-in logs`. 1. Review the sign-in logs and ensure that admin accounts are not being accessed more frequently than necessary.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/security/privileged-access-workstations/critical-impact-accounts", + "DefaultValue": "" + } + ] + }, + { + "Id": "9.1.17", + "Description": "[LEGACY] Ensure That Microsoft Defender for DNS Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_dns_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "[**NOTE:** As of August 1, 2023 customers with an existing subscription to Defender for DNS can continue to use the service, but new subscribers will receive alerts about suspicious DNS activity as part of Defender for Servers P2.] Microsoft Defender for DNS scans all network traffic exiting from within a subscription.", + "RationaleStatement": "DNS lookups within a subscription are scanned and compared to a dynamic list of websites that might be potential security threats. These threats could be a result of a security breach within your services, thus scanning for them could prevent a potential security threat from being introduced.", + "ImpactStatement": "Enabling Microsoft Defender for DNS requires enabling Microsoft Defender for your subscription. Both will incur additional charges, with Defender for DNS being a small amount per million queries.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Select `On` under `Status` for `DNS`. 6. Select `Save`. **Remediate from Azure CLI** Enable Standard pricing tier for DNS: ``` az security pricing create -n 'DNS' --tier 'Standard' ``` **Remediate from PowerShell** Enable Standard pricing tier for DNS: ``` Set-AzSecurityPricing -Name 'DNS' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud` 2. Under `Management`, select `Environment Settings` 3. Click on the subscription name 4. Select the `Defender plans` blade 5. Ensure `Status` is set to `On` for `DNS`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n 'DNS' --query 'PricingTier' ``` **Audit from PowerShell** ``` Get-AzSecurityPricing --Name 'DNS' | Select-Object Name,PricingTier ``` Ensure output of `PricingTier` is `Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [bdc59948-5574-49b3-bb91-76b7c986428d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fbdc59948-5574-49b3-bb91-76b7c986428d) **- Name:** 'Azure Defender for DNS should be enabled'", + "AdditionalInformation": "[**NOTE:** As of August 1, 2023 customers with an existing subscription to Defender for DNS can continue to use the service, but new subscribers will receive alerts about suspicious DNS activity as part of Defender for Servers P2.]", + "References": "https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/:https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/dns-security-baseline:https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-dns-alerts:https://docs.microsoft.com/en-us/azure/defender-for-cloud/enable-enhanced-security:https://docs.microsoft.com/en-us/azure/defender-for-cloud/alerts-overview:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-network-security#ns-10-ensure-domain-name-system-dns-security:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender for DNS is not enabled." + } + ] + }, + { + "Id": "9.1.3.1", + "Description": "Ensure that Defender for Servers is set to 'On'", + "Checks": [ + "defender_ensure_defender_for_server_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "The Defender for Servers plan in Microsoft Defender for Cloud reduces security risk by providing actionable recommendations to improve and remediate machine security posture. Defender for Servers also helps to protect machines against real-time security threats and attacks. Defender for Servers offers two paid plans: **Plan 1** The following components are enabled by default: - Log Analytics agent (deprecated) - Endpoint protection Plan 1 also offers the following components, disabled by default: - Vulnerability assessment for machines - Guest Configuration agent (preview) **Plan 2** The following components are enabled by default: - Log Analytics agent (deprecated) - Vulnerability assessment for machines - Endpoint protection - Agentless scanning for machines Plan 2 also offers the following components, disabled by default: - Guest Configuration agent (preview) - File Integrity Monitoring", + "RationaleStatement": "Enabling Defender for Servers allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Enabling Defender for Servers in Microsoft Defender for Cloud incurs an additional cost per resource. Refer to https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/ and https://azure.microsoft.com/en-us/pricing/calculator/ to estimate potential costs. - Plan 1: Subscription only - Plan 2: Subscription and workspace", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on a subscription name. 1. Click `Defender plans` in the left pane. 1. Under `Cloud Workload Protection (CWP)`, locate `Servers` in the Plan column, set Status to `On`. 1. Select `Save`. 1. Repeat steps 1-6 for each subscription requiring remediation. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n VirtualMachines --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name 'VirtualMachines' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment settings`. 1. Click on a subscription name. 1. Select `Defender plans` in the left pane. 1. Under `Cloud Workload Protection (CWP)`, locate `Servers` in the Plan column, ensure Status is set to `On`. 1. Repeat steps 1-5 for each subscription. **Audit from Azure CLI** Run the following command: ``` az security pricing show -n VirtualMachines --query pricingTier ``` If the tenant is licensed and enabled, the output will indicate `Standard`. **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'VirtualMachines' |Select-Object Name,PricingTier ``` If the tenant is licensed and enabled, the `-PricingTier` parameter will indicate `Standard`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [4da35fc9-c9e7-4960-aec9-797fe7d9051d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4da35fc9-c9e7-4960-aec9-797fe7d9051d) **- Name:** 'Azure Defender for servers should be enabled'", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/defender-for-servers-overview:https://learn.microsoft.com/en-us/azure/defender-for-cloud/plan-defender-for-servers:https://learn.microsoft.com/en-us/rest/api/defenderforcloud/pricings/list:https://learn.microsoft.com/en-us/rest/api/defenderforcloud/pricings/update:https://learn.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/powershell/module/az.security/set-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-endpoint-security#es-1-use-endpoint-detection-and-response-edr", + "DefaultValue": "By default, the Defender for Servers plan is disabled." + } + ] + }, + { + "Id": "9.1.3.2", + "Description": "Ensure that 'Vulnerability assessment for machines' component status is set to 'On'", + "Checks": [ + "defender_auto_provisioning_vulnerabilty_assessments_machines_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Enable vulnerability assessment for machines on both Azure and hybrid (Arc enabled) machines.", + "RationaleStatement": "Vulnerability assessment for machines scans for various security-related configurations and events such as system updates, OS vulnerabilities, and endpoint protection, then produces alerts on threat and vulnerability findings.", + "ImpactStatement": "Microsoft Defender for Servers plan 2 licensing is required, and configuration of Azure Arc introduces complexity beyond this recommendation.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Select a subscription 1. Click on `Settings & Monitoring` 1. Set the `Status` of `Vulnerability assessment for machines` to `On` 1. Click `Continue` Repeat the above for any additional subscriptions.", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Defender for Cloud` 1. Under `Management`, select `Environment Settings` 1. Select a subscription 1. Click on `Settings & monitoring` 1. Ensure that `Vulnerability assessment for machines` is set to `On` Repeat the above for any additional subscriptions.", + "AdditionalInformation": "While this feature is generally available as of publication, it is not yet available for Azure Government tenants.", + "References": "https://docs.microsoft.com/en-us/azure/defender-for-cloud/enable-data-collection?tabs=autoprovision-va:https://msdn.microsoft.com/en-us/library/mt704062.aspx:https://msdn.microsoft.com/en-us/library/mt704063.aspx:https://docs.microsoft.com/en-us/rest/api/securitycenter/autoprovisioningsettings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/autoprovisioningsettings/create:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-posture-vulnerability-management#pv-5-perform-vulnerability-assessments", + "DefaultValue": "By default, `Automatic provisioning of monitoring agent` is set to `Off`." + } + ] + }, + { + "Id": "6.3.2", + "Description": "Ensure that guest users are reviewed on a regular basis", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.3 Periodic Identity Reviews", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Microsoft Entra ID has native and extended identity functionality allowing you to invite people from outside your organization to be guest users in your cloud account and sign in with their own work, school, or social identities.", + "RationaleStatement": "Guest users are typically added outside your employee on-boarding/off-boarding process and could potentially be overlooked indefinitely. To prevent this, guest users should be reviewed on a regular basis. During this audit, guest users should also be determined to not have administrative privileges.", + "ImpactStatement": "Before removing guest users, determine their use and scope. Like removing any user, there may be unforeseen consequences to systems if an account is removed without careful consideration.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Click on `Add filter` 1. Select `User type` 1. Select `Guest` from the Value dropdown 1. Click `Apply` 1. Check the box next to all `Guest` users that are no longer required or are inactive 1. Click `Delete` 1. Click `OK` **Remediate from Azure CLI** Before deleting the user, set it to inactive using the ID from the Audit Procedure to determine if there are any dependent systems. ``` az ad user update --id --account-enabled {false} ``` After determining that there are no dependent systems, delete the user. ``` Remove-AzureADUser -ObjectId ``` **Remediate from Azure PowerShell** Before deleting the user, set it to inactive using the ID from the Audit Procedure to determine if there are any dependent systems. ``` Set-AzureADUser -ObjectId -AccountEnabled false ``` After determining that there are no dependent systems, delete the user. ``` PS C:\\>Remove-AzureADUser -ObjectId ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu 1. Select `Microsoft Entra ID` 1. Under `Manage`, select `Users` 1. Click on `Add filter` 1. Select `User type` 1. Select `Guest` from the Value dropdown 1. Click `Apply` 1. Audit the listed guest users **Audit from Azure CLI** ``` az ad user list --query [?userType=='Guest'] ``` Ensure all users listed are still required and not inactive. **Audit from Azure PowerShell** ``` Get-AzureADUser |Where-Object {$_.UserType -like Guest} |Select-Object DisplayName, UserPrincipalName, UserType -Unique ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [e9ac8f8e-ce22-4355-8f04-99b911d6be52](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fe9ac8f8e-ce22-4355-8f04-99b911d6be52) **- Name:** 'Guest accounts with read permissions on Azure resources should be removed' - **Policy ID:** [94e1c2ac-cbbe-4cac-a2b5-389c812dee87](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F94e1c2ac-cbbe-4cac-a2b5-389c812dee87) **- Name:** 'Guest accounts with write permissions on Azure resources should be removed' - **Policy ID:** [339353f6-2387-4a45-abe4-7f529d121046](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F339353f6-2387-4a45-abe4-7f529d121046) **- Name:** 'Guest accounts with owner permissions on Azure resources should be removed'", + "AdditionalInformation": "It is good practice to use a dynamic security group to manage guest users. To create the dynamic security group: 1. Navigate to the 'Microsoft Entra ID' blade in the Azure Portal 2. Select the 'Groups' item 3. Create new 4. Type of 'dynamic' 5. Use the following dynamic selection rule. (user.userType -eq Guest) 6. Once the group has been created, select access reviews option and create a new access review with a period of monthly and send to relevant administrators for review.", + "References": "https://learn.microsoft.com/en-us/entra/external-id/user-properties:https://learn.microsoft.com/en-us/entra/fundamentals/how-to-create-delete-users#delete-a-user:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-4-review-and-reconcile-user-access-regularly:https://www.microsoft.com/en-us/security/business/identity-access-management/azure-ad-pricing:https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-manage-inactive-user-accounts:https://learn.microsoft.com/en-us/entra/fundamentals/users-restore", + "DefaultValue": "By default no guest users are created." + } + ] + }, + { + "Id": "6.3.4", + "Description": "Ensure that all 'privileged' role assignments are periodically reviewed", + "Checks": [], + "Attributes": [ + { + "Section": "6 Identity Services", + "SubSection": "6.3 Periodic Identity Reviews", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Periodic review of privileged role assignments is performed to ensure that the privileged roles assigned to users are accurate and appropriate.", + "RationaleStatement": "Privileged roles are crown jewel assets that can be used by malicious insiders, threat actors, and even through mistake to significantly damage an organization in numerous ways. These roles should be periodically reviewed to: - identify lingering permissions assignment (e.g. an administrator has been terminated, the administrator account is being retained, but the permissions are no longer necessary and has not been properly addressed by process) - detect lateral movement through privilege escalation (e.g. an account with administrative permission has been compromised and is elevating other accounts in an attempt to circumvent detection mechanisms)", + "ImpactStatement": "Increased administrative effort to manage and remove role assignments appropriately.", + "RemediationProcedure": "", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Subscriptions`. 1. Select a subscription. 1. Select `Access control (IAM)`. 1. Look for the number under the word `Privileged` accompanied by a link titled `View Assignments.` Click the `View assignments` link. 1. For each privileged role listed, evaluate whether the assignment is appropriate and current for each User, Group, or App assigned to each privileged role. **NOTE:** The judgement of what constitutes 'appropriate and current' assignments requires a clear understanding of your organization's personnel, systems, policy, and security requirements. This cannot be effectively prescribed in procedure.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles", + "DefaultValue": "" + } + ] + }, + { + "Id": "7.2", + "Description": "Ensure that Resource Locks are set for Mission-Critical Azure Resources", + "Checks": [], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Resource Manager Locks provide a way for administrators to lock down Azure resources to prevent deletion of, or modifications to, a resource. These locks sit outside of the Role Based Access Controls (RBAC) hierarchy and, when applied, will place restrictions on the resource for all users. These locks are very useful when there is an important resource in a subscription that users should not be able to delete or change. Locks can help prevent accidental and malicious changes or deletion.", + "RationaleStatement": "As an administrator, it may be necessary to lock a subscription, resource group, or resource to prevent other users in the organization from accidentally deleting or modifying critical resources. The lock level can be set to to `CanNotDelete` or `ReadOnly` to achieve this purpose. - `CanNotDelete` means authorized users can still read and modify a resource, but they cannot delete the resource. - `ReadOnly` means authorized users can read a resource, but they cannot delete or update the resource. Applying this lock is similar to restricting all authorized users to the permissions granted by the Reader role.", + "ImpactStatement": "There can be unintended outcomes of locking a resource. Applying a lock to a parent service will cause it to be inherited by all resources within. Conversely, applying a lock to a resource may not apply to connected storage, leaving it unlocked. Please see the documentation for further information.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the specific Azure Resource or Resource Group. 2. For each mission critical resource, click on `Locks`. 3. Click `Add`. 4. Give the lock a name and a description, then select the type, `Read-only` or `Delete` as appropriate. 5. Click OK. **Remediate from Azure CLI** To lock a resource, provide the name of the resource, its resource type, and its resource group name. ``` az lock create --name --lock-type --resource-group --resource-name --resource-type ``` **Remediate from PowerShell** ``` Get-AzResourceLock -ResourceName -ResourceType -ResourceGroupName -Locktype ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the specific Azure Resource or Resource Group. 2. Click on `Locks`. 3. Ensure the lock is defined with name and description, with type `Read-only` or `Delete` as appropriate. **Audit from Azure CLI** Review the list of all locks set currently: ``` az lock list --resource-group --resource-name --namespace --resource-type --parent ``` **Audit from PowerShell** Run the following command to list all resources. ``` Get-AzResource ``` For each resource, run the following command to check for Resource Locks. ``` Get-AzResourceLock -ResourceName -ResourceType -ResourceGroupName ``` Review the output of the `Properties` setting. Compliant settings will have the `CanNotDelete` or `ReadOnly` value.", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-lock-resources:https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-subscription-governance#azure-resource-locks:https://docs.microsoft.com/en-us/azure/governance/blueprints/concepts/resource-locking:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-asset-management#am-4-limit-access-to-asset-management", + "DefaultValue": "By default, no locks are set." + } + ] + }, + { + "Id": "9.1.4.1", + "Description": "Ensure That Microsoft Defender for Containers Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_containers_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Containers helps improve, monitor, and maintain the security of containerized assets—including Kubernetes clusters, nodes, workloads, container registries, and images—across multi-cloud and on-premises environments. By default, when enabling the plan through the Azure Portal, Microsoft Defender for Containers automatically configures the following components: - **Agentless scanning for machines** - **Defender sensor** for runtime protection - **Azure Policy** for enforcing security best practices - **K8S API access** for monitoring and threat detection - **Registry access** for vulnerability assessment **Note:** Microsoft Defender for Container Registries ('ContainerRegistry') is deprecated and has been replaced by Microsoft Defender for Containers ('Containers').", + "RationaleStatement": "Enabling Microsoft Defender for Containers enhances defense-in-depth by providing advanced threat detection, vulnerability assessment, and security monitoring for containerized environments, leveraging insights from the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Microsoft Defender for Containers incurs a charge per vCore. Refer to https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/ and https://azure.microsoft.com/en-us/pricing/calculator/ to estimate potential costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, click `Environment settings`. 1. Click the name of a subscription. 1. Under `Settings`, click `Defender plans`. 1. Under `Cloud Workload Protection (CWP)`, in the row for `Containers`, click `On` in the `Status` column. 1. If `Monitoring coverage` displays `Partial`, click `Settings` under `Partial`. 1. Set the status of each of the components to `On`. 1. Click `Continue`. 1. Click `Save`. 1. Repeat steps 1-9 for each subscription. **Remediate from Azure CLI** **Note:** Microsoft Defender for Container Registries ('ContainerRegistry') is deprecated and has been replaced by Microsoft Defender for Containers ('Containers'). Run the below command to enable the Microsoft Defender for Containers plan and its components: ``` az security pricing create -n 'Containers' --tier 'standard' --extensions name=ContainerRegistriesVulnerabilityAssessments isEnabled=True --extensions name=AgentlessDiscoveryForKubernetes isEnabled=True --extensions name=AgentlessVmScanning isEnabled=True --extensions name=ContainerSensor isEnabled=True ``` **Remediate from PowerShell** **Note:** Microsoft Defender for Container Registries ('ContainerRegistry') is deprecated and has been replaced by Microsoft Defender for Containers ('Containers'). Run the below command to enable the Microsoft Defender for Containers plan and its components: ``` Set-AzSecurityPricing -Name 'Containers' -PricingTier 'Standard' -Extension '[{name:ContainerRegistriesVulnerabilityAssessments,isEnabled:True},{name:AgentlessDiscoveryForKubernetes,isEnabled:True},{name:AgentlessVmScanning,isEnabled:True},{name:ContainerSensor,isEnabled:True}]' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, click `Environment settings`. 1. Click the name of a subscription. 1. Under `Settings`, click `Defender plans`. 1. Under `Cloud Workload Protection (CWP)`, in the row for `Containers`, ensure that the `Status` is set to `On` and `Monitoring coverage` displays `Full`. 1. Repeat steps 1-5 for each subscription. **Audit from Azure CLI** For Microsoft Defender for Container Registries (deprecated), run the following command: ``` az security pricing show --name ContainerRegistry --query pricingTier ``` Ensure that the command returns `Standard`. For Microsoft Defender for Containers, run the following command: ``` az security pricing show --name Containers --query [pricingTier,extensions[*].[name,isEnabled]] ``` Ensure that the command returns `Standard`, and that each of the extensions (ContainerRegistriesVulnerabilityAssessments, AgentlessDiscoveryForKubernetes, AgentlessVmScanning, ContainerSensor) returns `True`. Repeat for each subscription. **Audit from PowerShell** For Microsoft Defender for Container Registries (deprecated), run the following command: ``` Get-AzSecurityPricing -Name 'ContainerRegistry' | Select-Object Name,PricingTier ``` Ensure the command returns `PricingTier` `Standard`. For Microsoft Defender for Containers, run the following command: ``` Get-AzSecurityPricing -Name 'Containers' ``` Ensure that `PricingTier` is set to `Standard`, and that each of the extensions (ContainerRegistriesVulnerabilityAssessments, AgentlessDiscoveryForKubernetes, AgentlessVmScanning, ContainerSensor) has `isEnabled` set to `True`. Repeat for each subscription. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [1c988dd6-ade4-430f-a608-2a3e5b0a6d38](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F1c988dd6-ade4-430f-a608-2a3e5b0a6d38) **- Name:** 'Microsoft Defender for Containers should be enabled'", + "AdditionalInformation": "The Azure Policy 'Microsoft Defender for Containers should be enabled' checks only that the `pricingTier` for `Containers` is set to `Standard`. It does not check the status of the plan's components.", + "References": "https://learn.microsoft.com/en-us/cli/azure/security/pricing:https://learn.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/powershell/module/az.security/set-azsecuritypricing:https://learn.microsoft.com/en-us/azure/defender-for-cloud/defender-for-containers-introduction:https://learn.microsoft.com/en-us/azure/defender-for-cloud/tutorial-enable-containers-azure:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "The Microsoft Defender for Containers plan is disabled by default." + } + ] + }, + { + "Id": "9.1.5.1", + "Description": "Ensure That Microsoft Defender for Storage Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_storage_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Storage enables threat detection for Storage, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Storage allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for Storage incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Set `Status` to `On` for `Storage`. 6. Select `Save`. **Remediate from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing create -n StorageAccounts --tier 'standard' ``` **Remediate from PowerShell** ``` Set-AzSecurityPricing -Name 'StorageAccounts' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Ensure `Status` is set to `On` for `Storage`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n StorageAccounts ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'StorageAccounts' | Select-Object Name,PricingTier ``` Ensure output for `Name PricingTier` is `StorageAccounts Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [640d2586-54d2-465f-877f-9ffc1d2109f4](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F640d2586-54d2-465f-877f-9ffc1d2109f4) **- Name:** 'Microsoft Defender for Storage should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "9.1.6.1", + "Description": "Ensure That Microsoft Defender for App Services Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_app_services_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for App Service enables threat detection for App Service, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for App Service allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for App Service incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud` 2. Under `Management`, select `Environment Settings` 3. Click on the subscription name 4. Select `Defender plans` 5. Set `App Service` Status to `On` 6. Select `Save` **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n Appservices --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name AppServices -PricingTier Standard ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud` 2. Under `Management`, select `Environment Settings` 3. Click on the subscription name 4. Select `Defender plans` 5. Ensure Status is `On` for `App Service` **Audit from Azure CLI** Run the following command: ``` az security pricing show -n AppServices ``` Ensure `-PricingTier` is set to `Standard` **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'AppServices' |Select-Object Name,PricingTier ``` Ensure the `-PricingTier` is set to `Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [2913021d-f2fd-4f3d-b958-22354e2bdbcb](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F2913021d-f2fd-4f3d-b958-22354e2bdbcb) **- Name:** 'Azure Defender for App Service should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "9.1.7.1", + "Description": "Ensure That Microsoft Defender for Azure Cosmos DB Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_cosmosdb_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Azure Cosmos DB scans all incoming network requests for threats to your Azure Cosmos DB resources.", + "RationaleStatement": "In scanning Azure Cosmos DB requests within a subscription, requests are compared to a heuristic list of potential security threats. These threats could be a result of a security breach within your services, thus scanning for them could prevent a potential security threat from being introduced.", + "ImpactStatement": "Enabling Microsoft Defender for Azure Cosmos DB requires enabling Microsoft Defender for your subscription. Both will incur additional charges.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. On the `Database` row click on `Select types >`. 6. Set the toggle switch next to `Azure Cosmos DB` to `On`. 7. Click `Continue`. 8. Click `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n 'CosmosDbs' --tier 'standard' ``` **Remediate from PowerShell** Use the below command to enable Standard pricing tier for Azure Cosmos DB ``` Set-AzSecurityPricing -Name 'CosmosDbs' -PricingTier 'Standard ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. On the `Database` row click on `Select types >`. 6. Ensure the toggle switch next to `Azure Cosmos DB` is set to `On`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n CosmosDbs --query pricingTier ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'CosmosDbs' | Select-Object Name,PricingTier ``` Ensure output of `-PricingTier` is `Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [adbe85b5-83e6-4350-ab58-bf3a4f736e5e](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fadbe85b5-83e6-4350-ab58-bf3a4f736e5e) **- Name:** 'Microsoft Defender for Azure Cosmos DB should be enabled'", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/:https://docs.microsoft.com/en-us/azure/defender-for-cloud/enable-enhanced-security:https://docs.microsoft.com/en-us/azure/defender-for-cloud/alerts-overview:https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/cosmos-db-security-baseline:https://docs.microsoft.com/en-us/azure/defender-for-cloud/quickstart-enable-database-protections:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender for Azure Cosmos DB is not enabled." + } + ] + }, + { + "Id": "9.1.7.2", + "Description": "Ensure That Microsoft Defender for Open-Source Relational Databases Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_os_relational_databases_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Open-source relational databases enables threat detection for Open-source relational databases, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Open-source relational databases allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for Open-source relational databases incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Set the toggle switch next to `Open-source relational databases` to `On`. 7. Select `Continue`. 8. Select `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n 'OpenSourceRelationalDatabases' --tier 'standard' ``` **Remediate from PowerShell** Use the below command to enable Standard pricing tier for Open-source relational databases ``` set-azsecuritypricing -name OpenSourceRelationalDatabases -pricingtier Standard ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the subscription name. 1. Select the `Defender plans` blade. 1. Click `Select types >` in the row for `Databases`. 1. Ensure the toggle switch next to `Open-source relational databases` is set to `On`. **Audit from Azure CLI** Run the following command: ``` az security pricing show -n OpenSourceRelationalDatabases --query pricingTier ``` **Audit from PowerShell** ``` Get-AzSecurityPricing | Where-Object {$_.Name -eq 'OpenSourceRelationalDatabases'} | Select-Object Name, PricingTier ``` Ensure output for `Name PricingTier` is `OpenSourceRelationalDatabases Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [0a9fbe0d-c5c4-4da8-87d8-f4fd77338835](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F0a9fbe0d-c5c4-4da8-87d8-f4fd77338835) **- Name:** 'Azure Defender for open-source relational databases should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-2-monitor-anomalies-and-threats-targeting-sensitive-data:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "9.1.7.3", + "Description": "Ensure That Microsoft Defender for (Managed Instance) Azure SQL Databases Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_azure_sql_databases_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Azure SQL Databases enables threat detection for Managed Instance Azure SQL databases, providing threat intelligence, anomaly detection, and behavior analytics in Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Azure SQL Databases allows for greater defense-in-depth, includes functionality for discovering and classifying sensitive data, surfacing and mitigating potential database vulnerabilities, and detecting anomalous activities that could indicate a threat to your database.", + "ImpactStatement": "Turning on Microsoft Defender for Azure SQL Databases incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Set the toggle switch next to `Azure SQL Databases` to `On`. 7. Select `Continue`. 8. Select `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n SqlServers --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name 'SqlServers' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Ensure the toggle switch next to `Azure SQL Databases` is set to `On`. **Audit from Azure CLI** Run the following command: ``` az security pricing show -n SqlServers ``` Ensure `-PricingTier` is set to `Standard` **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'SqlServers' | Select-Object Name,PricingTier ``` Ensure the `-PricingTier` is set to `Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [7fe3b40f-802b-4cdd-8bd4-fd799c948cc2](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F7fe3b40f-802b-4cdd-8bd4-fd799c948cc2) **- Name:** 'Azure Defender for Azure SQL Database servers should be enabled' - **Policy ID:** [abfb7388-5bf4-4ad7-ba99-2cd2f41cebb9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fabfb7388-5bf4-4ad7-ba99-2cd2f41cebb9) **- Name:** 'Azure Defender for SQL should be enabled for unprotected SQL Managed Instances'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-2-monitor-anomalies-and-threats-targeting-sensitive-data:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "9.1.7.4", + "Description": "Ensure That Microsoft Defender for SQL Servers on Machines Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_sql_servers_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for SQL servers on machines enables threat detection for SQL servers on machines, providing threat intelligence, anomaly detection, and behavior analytics in Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for SQL servers on machines allows for greater defense-in-depth, functionality for discovering and classifying sensitive data, surfacing and mitigating potential database vulnerabilities, and detecting anomalous activities that could indicate a threat to your database.", + "ImpactStatement": "Turning on Microsoft Defender for SQL servers on machines incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Set the toggle switch next to `SQL servers on machines` to `On`. 7. Select `Continue`. 8. Select `Save`. **Remediate from Azure CLI** Run the following command: ``` az security pricing create -n SqlServerVirtualMachines --tier 'standard' ``` **Remediate from PowerShell** Run the following command: ``` Set-AzSecurityPricing -Name 'SqlServerVirtualMachines' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Click `Select types >` in the row for `Databases`. 6. Ensure the toggle switch next to `SQL servers on machines` is set to `On`. **Audit from Azure CLI** Ensure Defender for SQL is licensed with the following command: ``` az security pricing show -n SqlServerVirtualMachines ``` Ensure the 'PricingTier' is set to 'Standard' **Audit from PowerShell** Run the following command: ``` Get-AzSecurityPricing -Name 'SqlServerVirtualMachines' | Select-Object Name,PricingTier ``` Ensure the 'PricingTier' is set to 'Standard' **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [6581d072-105e-4418-827f-bd446d56421b](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F6581d072-105e-4418-827f-bd446d56421b) **- Name:** 'Azure Defender for SQL servers on machines should be enabled' - **Policy ID:** [abfb4388-5bf4-4ad7-ba82-2cd2f41ceae9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fabfb4388-5bf4-4ad7-ba82-2cd2f41ceae9) **- Name:** 'Azure Defender for SQL should be enabled for unprotected Azure SQL servers'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/defender-for-sql-usage:https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-2-monitor-anomalies-and-threats-targeting-sensitive-data:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "9.1.8.1", + "Description": "Ensure That Microsoft Defender for Key Vault Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_keyvault_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Turning on Microsoft Defender for Key Vault enables threat detection for Key Vault, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.", + "RationaleStatement": "Enabling Microsoft Defender for Key Vault allows for greater defense-in-depth, with threat detection provided by the Microsoft Security Response Center (MSRC).", + "ImpactStatement": "Turning on Microsoft Defender for Key Vault incurs an additional cost per resource.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Select `On` under `Status` for `Key Vault`. 6. Select `Save`. **Remediate from Azure CLI** Enable Standard pricing tier for Key Vault: ``` az security pricing create -n 'KeyVaults' --tier 'Standard' ``` **Remediate from PowerShell** Enable Standard pricing tier for Key Vault: ``` Set-AzSecurityPricing -Name 'KeyVaults' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Ensure `Status` is set to `On` for `Key Vault`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n 'KeyVaults' --query 'pricingTier' ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'KeyVaults' | Select-Object Name,PricingTier ``` Ensure output for `PricingTier` is `Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [0e6763cc-5078-4e64-889d-ff4d9a839047](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F0e6763cc-5078-4e64-889d-ff4d9a839047) **- Name:** 'Azure Defender for Key Vault should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/security-center/security-center-detection-capabilities:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/pricings/update:https://docs.microsoft.com/en-us/powershell/module/az.security/get-azsecuritypricing:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender plan is off." + } + ] + }, + { + "Id": "9.1.9.1", + "Description": "Ensure That Microsoft Defender for Resource Manager Is Set To 'On'", + "Checks": [ + "defender_ensure_defender_for_arm_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft Defender for Resource Manager scans incoming administrative requests to change your infrastructure from both CLI and the Azure portal.", + "RationaleStatement": "Scanning resource requests lets you be alerted every time there is suspicious activity in order to prevent a security threat from being introduced.", + "ImpactStatement": "Enabling Microsoft Defender for Resource Manager requires enabling Microsoft Defender for your subscription. Both will incur additional charges.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Select `On` under `Status` for `Resource Manager`. 6. Select `Save. **Remediate from Azure CLI** Use the below command to enable Standard pricing tier for Defender for Resource Manager ``` az security pricing create -n 'Arm' --tier 'Standard' ``` **Remediate from PowerShell** Use the below command to enable Standard pricing tier for Defender for Resource Manager ``` Set-AzSecurityPricing -Name 'Arm' -PricingTier 'Standard' ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender for Cloud`. 2. Under `Management`, select `Environment Settings`. 3. Click on the subscription name. 4. Select the `Defender plans` blade. 5. Ensure `Status` is set to `On` for `Resource Manager`. **Audit from Azure CLI** Ensure the output of the below command is Standard ``` az security pricing show -n 'Arm' --query 'pricingTier' ``` **Audit from PowerShell** ``` Get-AzSecurityPricing -Name 'Arm' | Select-Object Name,PricingTier ``` Ensure the output of `PricingTier` is `Standard` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [c3d20c29-b36d-48fe-808b-99a87530ad99](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc3d20c29-b36d-48fe-808b-99a87530ad99) **- Name:** 'Azure Defender for Resource Manager should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/defender-for-cloud/enable-enhanced-security:https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-resource-manager-introduction:https://azure.microsoft.com/en-us/pricing/details/defender-for-cloud/:https://docs.microsoft.com/en-us/azure/defender-for-cloud/alerts-overview:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities", + "DefaultValue": "By default, Microsoft Defender for Resource Manager is not enabled." + } + ] + }, + { + "Id": "9.2.1", + "Description": "Ensure That Microsoft Defender for IoT Hub Is Set To 'On'", + "Checks": [ + "defender_ensure_iot_hub_defender_is_on" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.2 Microsoft Defender for IoT", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Microsoft Defender for IoT acts as a central security hub for IoT devices within your organization.", + "RationaleStatement": "IoT devices are very rarely patched and can be potential attack vectors for enterprise networks. Updating their network configuration to use a central security hub allows for detection of these breaches.", + "ImpactStatement": "Enabling Microsoft Defender for IoT will incur additional charges dependent on the level of usage.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `IoT Hub`. 2. Select an `IoT Hub` to validate. 3. Select `Overview` in `Defender for IoT`. 4. Click on `Secure your IoT solution`, and complete the onboarding.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `IoT Hub`. 2. Select an `IoT Hub` to validate. 3. Select `Overview` in `Defender for IoT`. 4. The Threat prevention and Threat detection screen will appear, if `Defender for IoT` is Enabled.", + "AdditionalInformation": "There are additional configurations for Microsoft Defender for IoT that allow for types of deployments called hybrid or local. Both run on your physical infrastructure. These are complicated setups and are primarily outside of the scope of a purely Azure benchmark. Please see the references to consider these options for your organization.", + "References": "https://azure.microsoft.com/en-us/services/iot-defender/#overview:https://docs.microsoft.com/en-us/azure/defender-for-iot/:https://azure.microsoft.com/en-us/pricing/details/iot-defender/:https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/defender-for-iot-security-baseline:https://docs.microsoft.com/en-us/cli/azure/iot?view=azure-cli-latest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-1-enable-threat-detection-capabilities:https://learn.microsoft.com/en-us/azure/defender-for-iot/device-builders/quickstart-onboard-iot-hub", + "DefaultValue": "By default, Microsoft Defender for IoT is not enabled." + } + ] + }, + { + "Id": "9.3.1", + "Description": "Ensure that the Expiration Date is set for all Keys in RBAC Key Vaults", + "Checks": [ + "keyvault_rbac_key_expiration_set" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Keys in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The `exp` (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for encryption of new data, wrapping of new keys, and signing. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys to help enforce the key rotation. This ensures that the keys cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that an appropriate `Expiration date` is set for any keys that are `Enabled`. **Remediate from Azure CLI** Update the `Expiration date` for the key using the below command: ``` az keyvault key set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` **Note:** To view the expiration date on all keys in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the keys: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Crypto Officer to the appropriate user. **Remediate from PowerShell** ``` Set-AzKeyVaultKeyAttribute -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that an appropriate `Expiration date` is set for any keys that are `Enabled`. **Audit from Azure CLI** Get a list of all the key vaults in your Azure environment by running the following command: ``` az keyvault list ``` Then for each key vault listed ensure that the output of the below command contains Key ID (kid), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault key list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Azure Key vaults: ``` Get-AzKeyVault ``` For each Key vault run the following command to determine which vaults are configured to use RBAC. ``` Get-AzKeyVault -VaultName ``` For each Key vault with the `EnableRbacAuthorizatoin` setting set to `True`, run the following command. ``` Get-AzKeyVaultKey -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0) **- Name:** 'Key Vault keys should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultkeyattribute?view=azps-0.10.0", + "DefaultValue": "By default, keys do not expire." + } + ] + }, + { + "Id": "9.3.2", + "Description": "Ensure that the Expiration Date is set for all Keys in Non-RBAC Key Vaults.", + "Checks": [ + "keyvault_key_expiration_set_in_non_rbac" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Keys in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The `exp` (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for a cryptographic operation. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys. This ensures that the keys cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that the status of the key is `Enabled`. 4. For each enabled key, ensure that an appropriate `Expiration date` is set. **Remediate from Azure CLI** Update the `Expiration date` for the key using the below command: ``` az keyvault key set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` **Note:** To view the expiration date on all keys in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the keys: 1. Go to Key vault, click on `Access policies`. 2. Click on `Create` and add an access policy with the `Update` permission (in the Key Permissions - Key Management Operations section). **Remediate from PowerShell** ``` Set-AzKeyVaultKeyAttribute -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Keys`. 3. In the main pane, ensure that the status of the key is `Enabled`. 4. For each enabled key, ensure that an appropriate `Expiration date` is set. **Audit from Azure CLI** Get a list of all the key vaults in your Azure environment by running the following command: ``` az keyvault list ``` For each key vault, ensure that the output of the below command contains Key ID (kid), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault key list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Azure Key vaults: ``` Get-AzKeyVault ``` For each Key vault, run the following command to determine which vaults are configured to not use RBAC: ``` Get-AzKeyVault -VaultName ``` For each Key vault with the `EnableRbacAuthorizatoin` setting set to `False` or empty, run the following command. ``` Get-AzKeyVaultKey -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F152b15f7-8e1f-4c1f-ab71-8c010ba5dbc0) **- Name:** 'Key Vault keys should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultkeyattribute?view=azps-0.10.0", + "DefaultValue": "By default, keys do not expire." + } + ] + }, + { + "Id": "9.3.3", + "Description": "Ensure that the Expiration Date is set for all Secrets in RBAC Key Vaults", + "Checks": [ + "keyvault_rbac_secret_expiration_set" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Secrets in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The `exp` (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. For each enabled secret, ensure that an appropriate `Expiration date` is set. **Remediate from Azure CLI** Update the Expiration date for the secret using the below command: ``` az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the `List Secret` permission is required. To update the expiration date for the secrets: 1. Go to the Key vault, click on `Access Control (IAM)`. 2. Click on `Add role assignment` and assign the role of `Key Vault Secrets Officer` to the appropriate user. **Remediate from PowerShell** ``` Set-AzKeyVaultSecretAttribute -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. For each enabled secret, ensure that an appropriate `Expiration date` is set. **Audit from Azure CLI** Ensure that the output of the below command contains ID (id), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault secret list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Key vaults: ``` Get-AzKeyVault ``` For each Key vault, run the following command to determine which vaults are configured to use RBAC: ``` Get-AzKeyVault -VaultName ``` For each Key vault with the `EnableRbacAuthorization` setting set to `True`, run the following command: ``` Get-AzKeyVaultSecret -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [98728c90-32c7-4049-8429-847dc0f4fe37](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F98728c90-32c7-4049-8429-847dc0f4fe37) **- Name:** 'Key Vault secrets should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultsecretattribute?view=azps-0.10.0", + "DefaultValue": "By default, secrets do not expire." + } + ] + }, + { + "Id": "9.3.4", + "Description": "Ensure that the Expiration Date is set for all Secrets in Non-RBAC Key Vaults", + "Checks": [ + "keyvault_non_rbac_secret_expiration_set" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Ensure that all Secrets in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.", + "RationaleStatement": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The `exp` (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.", + "ImpactStatement": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. Set an appropriate `Expiration date` on all secrets. **Remediate from Azure CLI** Update the `Expiration date` for the secret using the below command: ``` az keyvault secret set-attributes --name --vault-name --expires Y-m-d'T'H:M:S'Z' ``` Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the `List` Secret permission is required. To update the expiration date for the secrets: 1. Go to Key vault, click on `Access policies`. 2. Click on `Create` and add an access policy with the `Update` permission (in the Secret Permissions - Secret Management Operations section). **Remediate from PowerShell** For each Key vault with the `EnableRbacAuthorization` setting set to `False` or empty, run the following command. ``` Set-AzKeyVaultSecret -VaultName -Name -Expires ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key vaults`. 2. For each Key vault, click on `Secrets`. 3. In the main pane, ensure that the status of the secret is `Enabled`. 4. Set an appropriate `Expiration date` on all secrets. **Audit from Azure CLI** Get a list of all the key vaults in your Azure environment by running the following command: ``` az keyvault list ``` For each key vault, ensure that the output of the below command contains ID (id), enabled status as `true` and Expiration date (expires) is not empty or null: ``` az keyvault secret list --vault-name --query '[*].{kid:kid,enabled:attributes.enabled,expires:attributes.expires}' ``` **Audit from PowerShell** Retrieve a list of Key vaults: ``` Get-AzKeyVault ``` For each Key vault run the following command to determine which vaults are configured to use RBAC: ``` Get-AzKeyVault -VaultName ``` For each Key Vault with the `EnableRbacAuthorization` setting set to `False` or empty, run the following command. ``` Get-AzKeyVaultSecret -VaultName ``` Make sure the `Expires` setting is configured with a value as appropriate wherever the `Enabled` setting is set to `True`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [98728c90-32c7-4049-8429-847dc0f4fe37](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F98728c90-32c7-4049-8429-847dc0f4fe37) **- Name:** 'Key Vault secrets should have an expiration date'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis:https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultsecret?view=azps-7.4.0", + "DefaultValue": "By default, secrets do not expire." + } + ] + }, + { + "Id": "9.3.5", + "Description": "Ensure the Key Vault is Recoverable", + "Checks": [ + "keyvault_recoverable" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Key Vaults contain object keys, secrets, and certificates. Deletion of a Key Vault can cause immediate data loss or loss of security functions (authentication, validation, verification, non-repudiation, etc.) supported by the Key Vault objects. It is recommended the Key Vault be made recoverable by enabling the Do Not Purge and Soft Delete functions. This is in order to prevent loss of encrypted data, including storage accounts, SQL databases, and/or dependent services provided by Key Vault objects (Keys, Secrets, Certificates) etc. This may happen in the case of accidental deletion by a user or from disruptive activity by a malicious user. **NOTE:** In February 2025, Microsoft will enable soft-delete protection on all key vaults, and users will no longer be able to opt out of or turn off soft-delete. **WARNING:** A current limitation is that role assignments disappearing when Key Vault is deleted. All role assignments will need to be recreated after recovery.", + "RationaleStatement": "Users may accidentally run delete/purge commands on a Key Vault, or an attacker or malicious user may do so deliberately in order to cause disruption. Deleting or purging a Key Vault leads to immediate data loss, as keys encrypting data and secrets/certificates allowing access/services will become non-accessible. Setting `enablePurgeProtection` to true for a Key Vault ensures that even if Key Vault is deleted, Key Vault itself or its objects remain recoverable for the next 90 days. Key Vault/objects can either be recovered or purged (permanent deletion) during those 90 days. If no action is taken, the key vault and its objects will subsequently be purged. Enabling the enablePurgeProtection parameter on Key Vaults ensures that Key Vaults and their objects cannot be deleted/purged permanently.", + "ImpactStatement": "Once purge-protection and soft-delete are enabled for a Key Vault, the action is irreversible.", + "RemediationProcedure": "To enable Do Not Purge and Soft Delete for a Key Vault: **Remediate from Azure Portal** 1. Go to `Key Vaults`. 1. Click the name of a Key Vault. 1. Under `Settings`, click `Properties`. 1. Select the radio button next to `Enable purge protection (enforce a mandatory retention period for deleted vaults and vault objects) `. **Note:** Once enabled, this option cannot be disabled. 1. Click `Save`. 1. Repeat steps 1-5 for each Key Vault requiring remediation. **Remediate from Azure CLI** ``` az resource update --id /subscriptions/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups//providers/Microsoft.KeyVault /vaults/ --set properties.enablePurgeProtection=true ``` **Remediate from PowerShell** ``` Update-AzKeyVault -VaultName /providers/Microsoft.KeyVault /vaults/ ``` **Audit from PowerShell** List all Key Vaults: ``` Get-AzKeyVault ``` For each Key Vault run the following command: ``` Get-AzKeyVault -VaultName ``` Ensure `EnablePurgeProtection` is set to `True`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [0b60c0b2-2dc2-4e1c-b5c9-abbed971de53](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F0b60c0b2-2dc2-4e1c-b5c9-abbed971de53) **- Name:** 'Key vaults should have deletion protection enabled'", + "AdditionalInformation": "When a key is used for SQL server TDE or Encrypting Storage Account, both the features Do Not Purge and Soft Delete are enabled for the corresponding Key Vault by default by Azure Backend. WARNING: A current limitation of the soft-delete feature across all Azure services is role assignments disappearing when Key Vault is deleted. All role assignments will need to be recreated after recovery.", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-soft-delete-cli:https://learn.microsoft.com/en-us/azure/key-vault/general/soft-delete-overview:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-8-define-and-implement-backup-and-recovery-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository", + "DefaultValue": "When a new Key Vault is created, - `enableSoftDelete` is enabled by default, and - `enablePurgeProtection` is disabled by default. **NOTE:** In February 2025, Microsoft will enable soft-delete protection on all key vaults, and users will no longer be able to opt out of or turn off soft-delete." + } + ] + }, + { + "Id": "9.3.6", + "Description": "Ensure that Role Based Access Control for Azure Key Vault is enabled", + "Checks": [ + "keyvault_rbac_enabled" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "The recommended way to access Key Vaults is to use the Azure Role-Based Access Control (RBAC) permissions model. Azure RBAC is an authorization system built on Azure Resource Manager that provides fine-grained access management of Azure resources. It allows users to manage Key, Secret, and Certificate permissions. It provides one place to manage all permissions across all key vaults.", + "RationaleStatement": "The new RBAC permissions model for Key Vaults enables a much finer grained access control for key vault secrets, keys, certificates, etc., than the vault access policy. This in turn will permit the use of privileged identity management over these roles, thus securing the key vaults with JIT Access management.", + "ImpactStatement": "Implementation needs to be properly designed from the ground up, as this is a fundamental change to the way key vaults are accessed/managed. Changing permissions to key vaults will result in loss of service as permissions are re-applied. For the least amount of downtime, map your current groups and users to their corresponding permission needs.", + "RemediationProcedure": "**Remediate from Azure Portal** Key Vaults can be configured to use `Azure role-based access control` on creation. For existing Key Vaults: 1. From Azure Home open the Portal Menu in the top left corner 2. Select `Key Vaults` 3. Select a Key Vault to audit 4. Select `Access configuration` 5. Set the Permission model radio button to `Azure role-based access control`, taking note of the warning message 6. Click `Save` 7. Select `Access Control (IAM)` 8. Select the `Role Assignments` tab 9. Reapply permissions as needed to groups or users **Remediate from Azure CLI** To enable RBAC Authorization for each Key Vault, run the following Azure CLI command: ``` az keyvault update --resource-group --name --enable-rbac-authorization true ``` **Remediate from PowerShell** To enable RBAC authorization on each Key Vault, run the following PowerShell command: ``` Update-AzKeyVault -ResourceGroupName -VaultName -EnableRbacAuthorization $True ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal Menu in the top left corner 2. Select Key Vaults 3. Select a Key Vault to audit 4. Select Access configuration 5. Ensure the Permission Model radio button is set to `Azure role-based access control` **Audit from Azure CLI** Run the following command for each Key Vault in each Resource Group: ``` az keyvault show --resource-group --name ``` Ensure the `enableRbacAuthorization` setting is set to `true` within the output of the above command. **Audit from PowerShell** Run the following PowerShell command: ``` Get-AzKeyVault -Vaultname -ResourceGroupName ``` Ensure the `Enabled For RBAC Authorization` setting is set to `True` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [12d4fa5e-1f9f-4c21-97a9-b99b3c6611b5](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F12d4fa5e-1f9f-4c21-97a9-b99b3c6611b5) **- Name:** 'Azure Key Vault should use RBAC permission model'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-gb/azure/key-vault/general/rbac-migration#vault-access-policy-to-azure-rbac-migration-steps:https://docs.microsoft.com/en-gb/azure/role-based-access-control/role-assignments-portal?tabs=current:https://docs.microsoft.com/en-gb/azure/role-based-access-control/overview:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository", + "DefaultValue": "The default value for Access control in Key Vaults is Vault Policy." + } + ] + }, + { + "Id": "7.1.4", + "Description": "Ensure that Azure Monitor Resource Logging is Enabled for All Services that Support it", + "Checks": [], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Resource Logs capture activity to the data access plane while the Activity log is a subscription-level log for the control plane. Resource-level diagnostic logs provide insight into operations that were performed within that resource itself; for example, reading or updating a secret from a Key Vault. Currently, 95 Azure resources support Azure Monitoring (See the more information section for a complete list), including Network Security Groups, Load Balancers, Key Vault, AD, Logic Apps, and CosmosDB. The content of these logs varies by resource type. A number of back-end services were not configured to log and store Resource Logs for certain activities or for a sufficient length. It is crucial that monitoring is correctly configured to log all relevant activities and retain those logs for a sufficient length of time. Given that the mean time to detection in an enterprise is 240 days, a minimum retention period of two years is recommended.", + "RationaleStatement": "A lack of monitoring reduces the visibility into the data plane, and therefore an organization's ability to detect reconnaissance, authorization attempts or other malicious activity. Unlike Activity Logs, Resource Logs are not enabled by default. Specifically, without monitoring it would be impossible to tell which entities had accessed a data store that was breached. In addition, alerts for failed attempts to access APIs for Web Services or Databases are only possible when logging is enabled.", + "ImpactStatement": "Costs for monitoring varies with Log Volume. Not every resource needs to have logging enabled. It is important to determine the security classification of the data being processed by the given resource and adjust the logging based on which events need to be tracked. This is typically determined by governance and compliance requirements.", + "RemediationProcedure": "Azure Subscriptions should log every access and operation for all resources. Logs should be sent to Storage and a Log Analytics Workspace or equivalent third-party system. Logs should be kept in readily-accessible storage for a minimum of one year, and then moved to inexpensive cold storage for a duration of time as necessary. If retention policies are set but storing logs in a Storage Account is disabled (for example, if only Event Hubs or Log Analytics options are selected), the retention policies have no effect. Enable all monitoring at first, and then be more aggressive moving data to cold storage if the volume of data becomes a cost concern. **Remediate from Azure Portal** The specific steps for configuring resources within the Azure console vary depending on resource, but typically the steps are: 1. Go to the resource 2. Click on Diagnostic settings 3. In the blade that appears, click Add diagnostic setting 4. Configure the diagnostic settings 5. Click on Save **Remediate from Azure CLI** For each `resource`, run the following making sure to use a `resource` appropriate JSON encoded `category` for the `--logs` option. ``` az monitor diagnostic-settings create --name --resource --logs [{category:,enabled:true,rentention-policy:{enabled:true,days:180}}] --metrics [{category:AllMetrics,enabled:true,retention-policy:{enabled:true,days:180}}] <[--event-hub --event-hub-rule | --storage-account |--workspace | --marketplace-partner-id ]> ``` **Remediate from PowerShell** Create the `log` settings object ``` $logSettings = @() $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -RetentionPolicyDay 180 -RetentionPolicyEnabled $true -Category $logSettings += New-AzDiagnosticSettingLogSettingsObject -Enabled $true -RetentionPolicyDay 180 -RetentionPolicyEnabled $true -Category ``` Create the `metric` settings object ``` $metricSettings = @() $metricSettings += New-AzDiagnosticSettingMetricSettingsObject -Enabled $true -RetentionPolicyDay 180 -RetentionPolicyEnabled $true -Category AllMetrics ``` Create the diagnostic setting for a specific resource ``` New-AzDiagnosticSetting -Name -ResourceId -Log $logSettings -Metric $metricSettings ```", + "AuditProcedure": "**Audit from Azure Portal** The specific steps for configuring resources within the Azure console vary depending on resource, but typically the steps are: 1. Go to the resource 2. Click on Diagnostic settings 3. In the blade that appears, click Add diagnostic setting 4. Configure the diagnostic settings 5. Click on Save **Audit from Azure CLI** List all `resources` for a `subscription` ``` az resource list --subscription ``` For each `resource` run the following ``` az monitor diagnostic-settings list --resource ``` An empty result means a `diagnostic settings` is not configured for that resource. An error message means a `diagnostic settings` is not supported for that resource. **Audit from PowerShell** Get a list of `resources` in a `subscription` context and store in a variable ``` $resources = Get-AzResource ``` Loop through each `resource` to determine if a diagnostic setting is configured or not. ``` foreach ($resource in $resources) {$diagnosticSetting = Get-AzDiagnosticSetting -ResourceId $resource.id -ErrorAction SilentlyContinue; if ([string]::IsNullOrEmpty($diagnosticSetting)) {$message = Diagnostic Settings not configured for resource: + $resource.Name;Write-Output $message}else{$diagnosticSetting}} ``` A result of `Diagnostic Settings not configured for resource: ` means a `diagnostic settings` is not configured for that resource. Otherwise, the output of the above command will show configured `Diagnostic Settings` for a resource. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [cf820ca0-f99e-4f3e-84fb-66e913812d21](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fcf820ca0-f99e-4f3e-84fb-66e913812d21) **- Name:** 'Resource logs in Key Vault should be enabled' - **Policy ID:** [91a78b24-f231-4a8a-8da9-02c35b2b6510](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F91a78b24-f231-4a8a-8da9-02c35b2b6510) **- Name:** 'App Service apps should have resource logs enabled' - **Policy ID:** [428256e6-1fac-4f48-a757-df34c2b3336d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F428256e6-1fac-4f48-a757-df34c2b3336d) **- Name:** 'Resource logs in Batch accounts should be enabled' - **Policy ID:** [057ef27e-665e-4328-8ea3-04b3122bd9fb](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F057ef27e-665e-4328-8ea3-04b3122bd9fb) **- Name:** 'Resource logs in Azure Data Lake Store should be enabled' - **Policy ID:** [c95c74d9-38fe-4f0d-af86-0c7d626a315c](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc95c74d9-38fe-4f0d-af86-0c7d626a315c) **- Name:** 'Resource logs in Data Lake Analytics should be enabled' - **Policy ID:** [83a214f7-d01a-484b-91a9-ed54470c9a6a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F83a214f7-d01a-484b-91a9-ed54470c9a6a) **- Name:** 'Resource logs in Event Hub should be enabled' - **Policy ID:** [383856f8-de7f-44a2-81fc-e5135b5c2aa4](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F383856f8-de7f-44a2-81fc-e5135b5c2aa4) **- Name:** 'Resource logs in IoT Hub should be enabled' - **Policy ID:** [34f95f76-5386-4de7-b824-0d8478470c9d](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F34f95f76-5386-4de7-b824-0d8478470c9d) **- Name:** 'Resource logs in Logic Apps should be enabled' - **Policy ID:** [b4330a05-a843-4bc8-bf9a-cacce50c67f4](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb4330a05-a843-4bc8-bf9a-cacce50c67f4) **- Name:** 'Resource logs in Search services should be enabled' - **Policy ID:** [f8d36e2f-389b-4ee4-898d-21aeb69a0f45](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ff8d36e2f-389b-4ee4-898d-21aeb69a0f45) **- Name:** 'Resource logs in Service Bus should be enabled' - **Policy ID:** [f9be5368-9bf5-4b84-9e0a-7850da98bb46](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ff9be5368-9bf5-4b84-9e0a-7850da98bb46) **- Name:** 'Resource logs in Azure Stream Analytics should be enabled'", + "AdditionalInformation": "For an up-to-date list of Azure resources which support Azure Monitor, refer to the Supported Log Categories reference.", + "References": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-5-centralize-security-log-management-and-analysis:https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/monitor-azure-resource:Supported Log Categories: https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-categories:Logs and Audit - Fundamentals: https://docs.microsoft.com/en-us/azure/security/fundamentals/log-audit:Collecting Logs: https://docs.microsoft.com/en-us/azure/azure-monitor/platform/collect-activity-logs:Key Vault Logging: https://docs.microsoft.com/en-us/azure/key-vault/key-vault-logging:Monitor Diagnostic Settings: https://docs.microsoft.com/en-us/cli/azure/monitor/diagnostic-settings?view=azure-cli-latest:Overview of Diagnostic Logs: https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-logs-overview:Supported Services for Diagnostic Logs: https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-logs-schema:Diagnostic Logs for CDNs: https://docs.microsoft.com/en-us/azure/cdn/cdn-azure-diagnostic-logs", + "DefaultValue": "By default, Azure Monitor Resource Logs are 'Disabled' for all resources." + } + ] + }, + { + "Id": "9.3.8", + "Description": "Ensure that Private Endpoints are Used for Azure Key Vault", + "Checks": [ + "keyvault_private_endpoints" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Private endpoints will secure network traffic from Azure Key Vault to the resources requesting secrets and keys.", + "RationaleStatement": "Private endpoints will keep network requests to Azure Key Vault limited to the endpoints attached to the resources that are whitelisted to communicate with each other. Assigning the Key Vault to a network without an endpoint will allow other resources on that network to view all traffic from the Key Vault to its destination. In spite of the complexity in configuration, this is recommended for high security secrets.", + "ImpactStatement": "Incorrect or poorly-timed changing of network configuration could result in service interruption. There are also additional costs tiers for running a private endpoint per petabyte or more of networking traffic.", + "RemediationProcedure": "**Please see the additional information about the requirements needed before starting this remediation procedure.** **Remediate from Azure Portal** 1. From Azure Home open the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Select `Networking` in the left column. 5. Select `Private endpoint connections` from the top row. 6. Select `+ Create`. 7. Select the subscription the Key Vault is within, and other desired configuration. 8. Select `Next`. 9. For resource type select `Microsoft.KeyVault/vaults`. 10. Select the Key Vault to associate the Private Endpoint with. 11. Select `Next`. 12. In the `Virtual Networking` field, select the network to assign the Endpoint. 13. Select other configuration options as desired, including an existing or new application security group. 14. Select `Next`. 15. Select the private DNS the Private Endpoints will use. 16. Select `Next`. 17. Optionally add `Tags`. 18. Select `Next : Review + Create`. 19. Review the information and select `Create`. Follow the Audit Procedure to determine if it has successfully applied. 20. Repeat steps 3-19 for each Key Vault. **Remediate from Azure CLI** 1. To create an endpoint, run the following command: ``` az network private-endpoint create --resource-group --subnet --name --private-connection-resource-id /subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/ --group-ids vault --connection-name --location --manual-request ``` 2. To manually approve the endpoint request, run the following command: ``` az keyvault private-endpoint-connection approve --resource-group --vault-name –name ``` 3. Determine the Private Endpoint's IP address to connect the Key Vault to the Private DNS you have previously created: 4. Look for the property networkInterfaces then id; the value must be placed in the variable within step 7. ``` az network private-endpoint show -g -n ``` 5. Look for the property networkInterfaces then id; the value must be placed on in step 7. ``` az network nic show --ids ``` 6. Create a Private DNS record within the DNS Zone you created for the Private Endpoint: ``` az network private-dns record-set a add-record -g -z privatelink.vaultcore.azure.net -n -a ``` 7. nslookup the private endpoint to determine if the DNS record is correct: ``` nslookup .vault.azure.net nslookup .privatelink.vaultcore.azure.n ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home open the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Select `Networking` in the left column. 5. Select `Private endpoint connections` from the top row. 6. View if there is an endpoint attached. **Audit from Azure CLI** Run the following command within a subscription for each Key Vault you wish to audit. ``` az keyvault show --name ``` Ensure that `privateEndpointConnections` is not `null`. **Audit from PowerShell** Run the following command within a subscription for each Key Vault you wish to audit. ``` Get-AzPrivateEndpointConnection -PrivateLinkResourceId '/subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults//' ``` Ensure that the response contains details of a private endpoint. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [a6abeaec-4d90-4a02-805f-6b26c4d3fbe9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fa6abeaec-4d90-4a02-805f-6b26c4d3fbe9) **- Name:** 'Azure Key Vaults should use private link'", + "AdditionalInformation": "This recommendation assumes that you have created a Resource Group containing a Virtual Network that the services are already associated with and configured private DNS. A Bastion on the virtual network is also required, and the service to which you are connecting must already have a Private Endpoint. For information concerning the installation of these services, please see the attached documentation. Microsoft's own documentation lists the requirements as: A Key Vault. An Azure virtual network. A subnet in the virtual network. Owner or contributor permissions for both the Key Vault and the virtual network.", + "References": "https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview:https://docs.microsoft.com/en-us/azure/storage/common/storage-private-endpoints:https://azure.microsoft.com/en-us/pricing/details/private-link/:https://docs.microsoft.com/en-us/azure/key-vault/general/private-link-service?tabs=portal:https://docs.microsoft.com/en-us/azure/virtual-network/quick-create-portal:https://docs.microsoft.com/en-us/azure/private-link/tutorial-private-endpoint-storage-portal:https://docs.microsoft.com/en-us/azure/bastion/bastion-overview:https://docs.microsoft.com/azure/dns/private-dns-getstarted-cli#create-an-additional-dns-record:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-8-ensure-security-of-key-and-certificate-repository", + "DefaultValue": "By default, Private Endpoints are not enabled for any services within Azure." + } + ] + }, + { + "Id": "9.3.9", + "Description": "Ensure automatic key rotation is enabled within Azure Key Vault", + "Checks": [ + "keyvault_key_rotation_enabled" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Automated cryptographic key rotation in Key Vault allows users to configure Key Vault to automatically generate a new key version at a specified frequency. A key rotation policy can be defined for each individual key.", + "RationaleStatement": "Automatic key rotation reduces risk by ensuring that keys are rotated without manual intervention. Azure and NIST recommend that keys be rotated every two years or less. Refer to 'Table 1: Suggested cryptoperiods for key types' on page 46 of the following document for more information: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf.", + "ImpactStatement": "There is an additional cost for each scheduled key rotation.", + "RemediationProcedure": "**Note:** Azure CLI and PowerShell use the ISO8601 duration format for time spans. The format is `P(Y,M,D)`. The leading P is required and is referred to as `period`. The `(Y,M,D)` are for the duration of Year, Month, and Day, respectively. A time frame of 2 years, 2 months, 2 days would be `P2Y2M2D`. For Azure CLI and PowerShell, it is easiest to supply the policy flags in a `.json file`, for example: ``` { lifetimeActions: [ { trigger: { timeAfterCreate: P(Y,M,D), timeBeforeExpiry : null }, action: { type: Rotate } }, { trigger: { timeBeforeExpiry : P(Y,M,D) }, action: { type: Notify } } ], attributes: { expiryTime: P(Y,M,D) } } ``` **Remediate from Azure Portal** 1. Go to `Key Vaults`. 1. Select a Key Vault. 1. Under `Objects`, select `Keys`. 1. Select a key. 1. From the top row, select `Rotation policy`. 1. Select an appropriate `Expiry time`. 1. Set `Enable auto rotation` to `Enabled`. 1. Set an appropriate `Rotation option` and `Rotation time`. 1. Optionally, set a `Notification time`. 1. Click `Save`. 1. Repeat steps 1-10 for each Key Vault and Key. **Remediate from Azure CLI** Run the following command for each key to enable automatic rotation: ``` az keyvault key rotation-policy update --vault-name --name --value ``` **Remediate from PowerShell** Run the following command for each key to enable automatic rotation: ``` Set-AzKeyVaultKeyRotationPolicy -VaultName -Name -PolicyPath ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Key Vaults`. 1. Select a Key Vault. 1. Under `Objects`, select `Keys`. 1. Select a key. 1. From the top row, select `Rotation policy`. 1. Ensure `Enable auto rotation` is set to `Enabled`. 1. Ensure the `Rotation time` is set to an appropriate value. 1. Repeat steps 1-7 for each Key Vault and Key. **Audit from Azure CLI** Run the following command: ``` az keyvault key rotation-policy show --vault-name --name ``` Ensure that the response contains a `lifetimeAction` of `Rotate` and that `timeAfterCreate` is set to an appropriate value. **Audit from PowerShell** Run the following command: ``` Get-AzKeyVaultKeyRotationPolicy -VaultName -Name ``` Ensure that the response contains a `LifetimeAction` of `Rotate` and that `TimeAfterCreate` is set to an appropriate value. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [d8cf8476-a2ec-4916-896e-992351803c44](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fd8cf8476-a2ec-4916-896e-992351803c44) **- Name:** 'Keys should have a rotation policy ensuring that their rotation is scheduled within the specified number of days after creation.'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation:https://docs.microsoft.com/en-us/azure/storage/common/customer-managed-keys-overview#update-the-key-version:https://docs.microsoft.com/en-us/azure/virtual-machines/windows/disks-enable-customer-managed-keys-powershell#set-up-an-azure-key-vault-and-diskencryptionset-optionally-with-automatic-key-rotation:https://azure.microsoft.com/en-us/updates/public-preview-automatic-key-rotation-of-customermanaged-keys-for-encrypting-azure-managed-disks/:https://docs.microsoft.com/en-us/cli/azure/keyvault/key/rotation-policy?view=azure-cli-latest#az-keyvault-key-rotation-policy-update:https://docs.microsoft.com/en-us/powershell/module/az.keyvault/set-azkeyvaultkeyrotationpolicy?view=azps-8.1.0:https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-6-use-a-secure-key-management-process:https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf", + "DefaultValue": "By default, automatic key rotation is not enabled." + } + ] + }, + { + "Id": "7.1.5", + "Description": "Ensure that SKU Basic/Consumption is not used on artifacts that need to be monitored (Particularly for Production Workloads)", + "Checks": [], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "The use of Basic or Free SKUs in Azure whilst cost effective have significant limitations in terms of what can be monitored and what support can be realized from Microsoft. Typically, these SKU’s do not have a service SLA and Microsoft may refuse to provide support for them. Consequently Basic/Free SKUs should never be used for production workloads.", + "RationaleStatement": "Typically, production workloads need to be monitored and should have an SLA with Microsoft, using Basic SKUs for any deployed product will mean that that these capabilities do not exist. The following resource types should use standard SKUs as a minimum. - Public IP Addresses - Network Load Balancers - REDIS Cache - SQL PaaS Databases - VPN Gateways", + "ImpactStatement": "The impact of enforcing Standard SKU's is twofold 1) There will be a cost increase 2) The monitoring and service level agreements will be available and will support the production service. All resources should be either tagged or in separate Management Groups/Subscriptions", + "RemediationProcedure": "Each resource has its own process for upgrading from basic to standard SKUs that should be followed if required. - Public IP Address: https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/public-ip-upgrade. - Basic Load Balancer: https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-basic-upgrade-guidance. - Azure Cache for Redis: https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-how-to-scale. - Azure SQL Database: https://learn.microsoft.com/en-us/azure/azure-sql/database/scale-resources. - VPN Gateway: https://learn.microsoft.com/en-us/azure/vpn-gateway/gateway-sku-resize.", + "AuditProcedure": "This needs to be audited by Azure Policy (one for each resource type) and denied for each artifact that is production. **Audit from Azure Portal** 1. Open `Azure Resource Graph Explorer` 1. Click `New query` 1. Paste the following into the query window: ``` Resources | where sku contains 'Basic' or sku contains 'consumption' | order by type ``` 4. Click `Run query` then evaluate the results in the results window. 5. Ensure that no production artifacts are returned. **Audit from Azure CLI** ``` az graph query -q Resources | where sku contains 'Basic' or sku contains 'consumption' | order by type ``` Alternatively, to filter on a specific resource group: ``` az graph query -q Resources | where resourceGroup == '' | where sku contains 'Basic' or sku contains 'consumption' | order by type ``` Ensure that no production artifacts are returned. **Audit from PowerShell** ``` Get-AzResource | ?{ $_.Sku -EQ Basic} ``` Ensure that no production artifacts are returned.", + "AdditionalInformation": "", + "References": "https://azure.microsoft.com/en-us/support/plans:https://azure.microsoft.com/en-us/support/plans/response/:https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/public-ip-upgrade:https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-basic-upgrade-guidance:https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-how-to-scale:https://learn.microsoft.com/en-us/azure/azure-sql/database/scale-resources:https://learn.microsoft.com/en-us/azure/vpn-gateway/gateway-sku-resize", + "DefaultValue": "Policy should enforce standard SKUs for the following artifacts: - Public IP Addresses - Network Load Balancers - REDIS Cache - SQL PaaS Databases - VPN Gateways" + } + ] + }, + { + "Id": "9.4.1", + "Description": "Ensure an Azure Bastion Host Exists", + "Checks": [ + "network_bastion_host_exists" + ], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.4 Azure Bastion", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "The Azure Bastion service allows secure remote access to Azure Virtual Machines over the Internet without exposing remote access protocol ports and services directly to the Internet. The Azure Bastion service provides this access using TLS over 443/TCP, and subscribes to hardened configurations within an organization's Azure Active Directory service.", + "RationaleStatement": "The Azure Bastion service allows organizations a more secure means of accessing Azure Virtual Machines over the Internet without assigning public IP addresses to those Virtual Machines. The Azure Bastion service provides Remote Desktop Protocol (RDP) and Secure Shell (SSH) access to Virtual Machines using TLS within a web browser, thus preventing organizations from opening up 3389/TCP and 22/TCP to the Internet on Azure Virtual Machines. Additional benefits of the Bastion service includes Multi-Factor Authentication, Conditional Access Policies, and any other hardening measures configured within Azure Active Directory using a central point of access.", + "ImpactStatement": "The Azure Bastion service incurs additional costs and requires a specific virtual network configuration. The `Standard` tier offers additional configuration options compared to the `Basic` tier and may incur additional costs for those added features.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Click on `Bastions` 2. Select the `Subscription` 3. Select the `Resource group` 4. Type a `Name` for the new Bastion host 5. Select a `Region` 6. Choose `Standard` next to `Tier` 7. Use the slider to set the `Instance count` 8. Select the `Virtual network` or `Create new` 9. Select the `Subnet` named `AzureBastionSubnet`. Create a `Subnet` named `AzureBastionSubnet` using a `/26` CIDR range if it doesn't already exist. 10. Select the appropriate `Public IP address` option. 11. If `Create new` is selected for the `Public IP address` option, provide a `Public IP address name`. 12. If `Use existing` is selected for `Public IP address` option, select an IP address from `Choose public IP address` 13. Click `Next: Tags >` 14. Configure the appropriate `Tags` 15. Click `Next: Advanced >` 16. Select the appropriate `Advanced` options 17. Click `Next: Review + create >` 18. Click `Create` **Remediate from Azure CLI** ``` az network bastion create --location --name --public-ip-address --resource-group --vnet-name --scale-units --sku Standard [--disable-copy-paste true|false] [--enable-ip-connect true|false] [--enable-tunneling true|false] ``` **Remediate from PowerShell** Create the appropriate `Virtual network` settings and `Public IP Address` settings. ``` $subnetName = AzureBastionSubnet $subnet = New-AzVirtualNetworkSubnetConfig -Name $subnetName -AddressPrefix $virtualNet = New-AzVirtualNetwork -Name -ResourceGroupName -Location -AddressPrefix -Subnet $subnet $publicip = New-AzPublicIpAddress -ResourceGroupName -Name -Location -AllocationMethod Dynamic -Sku Standard ``` Create the `Azure Bastion` service using the information within the created variables from above. ``` New-AzBastion -ResourceGroupName -Name -PublicIpAddress $publicip -VirtualNetwork $virtualNet -Sku Standard -ScaleUnit ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Click on `Bastions` 2. Ensure there is at least one `Bastion` host listed under the `Name` column **Audit from Azure CLI** **Note:** The Azure CLI `network bastion` module is in `Preview` as of this writing ``` az network bastion list --subscription ``` Ensure the output of the above command is not empty. **Audit from PowerShell** Retrieve the `Bastion` host(s) information for a specific `Resource Group` ``` Get-AzBastion -ResourceGroupName ``` Ensure the output of the above command is not empty.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/bastion/bastion-overview#sku:https://learn.microsoft.com/en-us/powershell/module/az.network/get-azbastion?view=azps-9.2.0:https://learn.microsoft.com/en-us/cli/azure/network/bastion?view=azure-cli-latest", + "DefaultValue": "By default, the Azure Bastion service is not configured." + } + ] + }, + { + "Id": "7.1.1.7", + "Description": "Ensure that virtual network flow logs are captured and sent to Log Analytics", + "Checks": [], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that virtual network flow logs are captured and fed into a central log analytics workspace.", + "RationaleStatement": "Virtual network flow logs provide critical visibility into traffic patterns. Sending logs to a Log Analytics workspace enables centralized analysis, correlation, and alerting for faster threat detection and response.", + "ImpactStatement": "* Virtual network flow logs are charged per gigabyte of network flow logs collected and come with a free tier of 5 GB/month per subscription. * If traffic analytics is enabled with virtual network flow logs, traffic analytics pricing applies at per gigabyte processing rates. * The storage of logs is charged separately.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, click `Flow logs`. 1. Click `+ Create`. 1. Select a subscription. 1. Next to `Flow log type`, select `Virtual network`. 1. Click `+ Select target resource`. 1. Select `Virtual network`. 1. Select a virtual network. 1. Click `Confirm selection`. 1. Select a storage account, or create a new storage account. 1. Set the retention in days for the storage account. 1. Click `Next`. 1. Under `Analytics`, for `Flow logs version`, select `Version 2`. 1. Check the box next to `Enable traffic analytics`. 1. Select a processing interval. 1. Select a `Log Analytics Workspace`. 1. Click `Next`. 1. Optionally, add `Tags`. 1. Click `Review + create`. 1. Click `Create`. 1. Repeat steps 1-20 for each subscription or virtual network requiring remediation.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Network Watcher`. 1. Under `Logs`, select `Flow logs`. 1. Click `Add filter`. 1. From the `Filter` drop-down menu, select `Flow log type`. 1. From the `Value` drop-down menu, check `Virtual network` only. 1. Click `Apply`. 1. Ensure that at least one virtual network flow log is listed and is configured to send logs to a `Log Analytics Workspace`. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [2f080164-9f4d-497e-9db6-416dc9f7b48a](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F2f080164-9f4d-497e-9db6-416dc9f7b48a) **- Name:** 'Network Watcher flow logs should have traffic analytics enabled' - **Policy ID:** [4c3c6c5f-0d47-4402-99b8-aa543dd8bcee](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F4c3c6c5f-0d47-4402-99b8-aa543dd8bcee) **- Name:** 'Audit flow logs configuration for every virtual network'", + "AdditionalInformation": "On September 30, 2027, NSG flow logs will be retired, and creating new NSG flow logs will no longer be possible after June 30, 2025. Azure recommends migrating to virtual network flow logs, which address NSG flow log limitations. After retirement, traffic analytics using NSG flow logs will no longer be supported, and existing NSG flow log resources will be deleted. Previously collected NSG flow log records will remain available per their retention policies. For details, see the official announcement: https://azure.microsoft.com/en-gb/updates?id=Azure-NSG-flow-logs-Retirement.", + "References": "https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-overview:https://learn.microsoft.com/en-us/azure/network-watcher/vnet-flow-logs-cli", + "DefaultValue": "" + } + ] + }, + { + "Id": "7.1.1.8", + "Description": "Ensure that a Microsoft Entra diagnostic setting exists to send Microsoft Graph activity logs to an appropriate destination", + "Checks": [], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that a Microsoft Entra diagnostic setting is configured to send Microsoft Graph activity logs to a suitable destination, such as a Log Analytics workspace, storage account, or event hub. This enables centralized monitoring and analysis of all HTTP requests that the Microsoft Graph service receives and processes for a tenant.", + "RationaleStatement": "Microsoft Graph activity logs provide visibility into HTTP requests made to the Microsoft Graph service, helping detect unauthorized access, suspicious activity, and security threats. Configuring diagnostic settings in Microsoft Entra ensures these logs are collected and sent to an appropriate destination for monitoring, analysis, and retention.", + "ImpactStatement": "A Microsoft Entra ID P1 or P2 tenant license is required to access the Microsoft Graph activity logs. The amount of data logged and, thus, the cost incurred can vary significantly depending on the tenant size and the applications in your tenant that interact with the Microsoft Graph APIs. See the following pricing calculations for respective services: - Log Analytics: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#pricing-model. - Azure Storage: https://azure.microsoft.com/en-gb/pricing/details/storage/blobs/. - Event Hubs: https://azure.microsoft.com/en-gb/pricing/details/event-hubs/", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Provide a `Diagnostic setting name`. 1. Under `Logs > Categories`, check the box next to `MicrosoftGraphActivityLogs`. 1. Configure an appropriate destination for the logs. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Next to each diagnostic setting, click `Edit setting`, and review the selected log categories and destination details. 1. Ensure that at least one diagnostic setting is configured to send `MicrosoftGraphActivityLogs` to an appropriate destination.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings:https://learn.microsoft.com/en-us/graph/microsoft-graph-activity-logs-overview:https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#pricing-model:https://azure.microsoft.com/en-gb/pricing/details/storage/blobs/:https://azure.microsoft.com/en-gb/pricing/details/event-hubs/", + "DefaultValue": "By default, Microsoft Entra diagnostic settings do not exist." + } + ] + }, + { + "Id": "7.1.1.9", + "Description": "Ensure that a Microsoft Entra diagnostic setting exists to send Microsoft Entra activity logs to an appropriate destination", + "Checks": [], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that a Microsoft Entra diagnostic setting is configured to send Microsoft Entra activity logs to a suitable destination, such as a Log Analytics workspace, storage account, or event hub. This enables centralized monitoring and analysis of Microsoft Entra activity logs.", + "RationaleStatement": "Microsoft Entra activity logs enables you to assess many aspects of your Microsoft Entra tenant. Configuring diagnostic settings in Microsoft Entra ensures these logs are collected and sent to an appropriate destination for monitoring, analysis, and retention.", + "ImpactStatement": "To export sign-in data, your organization needs an Azure AD P1 or P2 license. The amount of data logged and, thus, the cost incurred can vary significantly depending on the tenant size. See the following pricing calculations for respective services: - Log Analytics: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#pricing-model. - Azure Storage: https://azure.microsoft.com/en-gb/pricing/details/storage/blobs/. - Event Hubs: https://azure.microsoft.com/en-gb/pricing/details/event-hubs/", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Provide a `Diagnostic setting name`. 1. Under `Logs > Categories`, check the box next to each of the following logs: - `AuditLogs` - `SignInLogs` - `NonInteractiveUserSignInLogs` - `ServicePrincipalSignInLogs` - `ManagedIdentitySignInLogs` - `ProvisioningLogs` - `ADFSSignInLogs` - `RiskyUsers` - `UserRiskEvents` - `NetworkAccessTrafficLogs` - `RiskyServicePrincipals` - `ServicePrincipalRiskEvents` - `EnrichedOffice365AuditLogs` - `MicrosoftGraphActivityLogs` - `RemoteNetworkHealthLogs` - `NetworkAccessAlerts` 1. Configure an appropriate destination for the logs. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Entra ID`. 1. Under `Monitoring`, click `Diagnostic settings`. 1. Next to each diagnostic setting, click `Edit setting`, and review the selected log categories and destination details. 1. Ensure that at least one diagnostic setting is configured to send the following logs to an appropriate destination: - `AuditLogs` - `SignInLogs` - `NonInteractiveUserSignInLogs` - `ServicePrincipalSignInLogs` - `ManagedIdentitySignInLogs` - `ProvisioningLogs` - `ADFSSignInLogs` - `RiskyUsers` - `UserRiskEvents` - `NetworkAccessTrafficLogs` - `RiskyServicePrincipals` - `ServicePrincipalRiskEvents` - `EnrichedOffice365AuditLogs` - `MicrosoftGraphActivityLogs` - `RemoteNetworkHealthLogs` - `NetworkAccessAlerts`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings:https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-access-activity-logs?tabs=microsoft-entra-activity-logs%2Carchive-activity-logs-to-a-storage-account", + "DefaultValue": "By default, Microsoft Entra diagnostic settings do not exist." + } + ] + }, + { + "Id": "7.1.1.10", + "Description": "Ensure that Intune logs are captured and sent to Log Analytics", + "Checks": [], + "Attributes": [ + { + "Section": "7 Management and Governance Services", + "SubSection": "7.1 Logging and Monitoring", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Ensure that Intune logs are captured and fed into a central log analytics workspace.", + "RationaleStatement": "Intune includes built-in logs that provide information about your environments. Sending logs to a Log Analytics workspace enables centralized analysis, correlation, and alerting for faster threat detection and response.", + "ImpactStatement": "A Microsoft Intune plan is required to access Intune: https://www.microsoft.com/en-gb/security/business/microsoft-intune-pricing. The amount of data logged and, thus, the cost incurred can vary significantly depending on the tenant size. For information on Log Analytics workspace costs, visit: https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Intune`. 1. Click `Reports`. 1. Under `Azure monitor`, click `Diagnostic settings`. 1. Click `+ Add diagnostic setting`. 1. Provide a `Diagnostic setting name`. 1. Under `Logs > Categories`, check the box next to each of the following logs: - `AuditLogs` - `OperationalLogs` - `DeviceComplianceOrg` - `Devices` - `Windows365AuditLogs` 1. Under `Destination details`, check the box next to `Send to Log Analytics workspace`. 1. Select a `Subscription`. 1. Select a `Log Analytics workspace`. 1. Click `Save`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Intune`. 1. Click `Reports`. 1. Under `Azure monitor`, click `Diagnostic settings`. 1. Next to each diagnostic setting, click `Edit setting`, and review the selected log categories and destination details. 1. Ensure that at least one diagnostic setting is configured to send the following logs to a Log Analytics workspace: - `AuditLogs` - `OperationalLogs` - `DeviceComplianceOrg` - `Devices` - `Windows365AuditLogs`", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/mem/intune/fundamentals/review-logs-using-azure-monitor:https://www.microsoft.com/en-gb/security/business/microsoft-intune-pricing:https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs", + "DefaultValue": "By default, Intune diagnostic settings do not exist." + } + ] + }, + { + "Id": "8.7", + "Description": "Ensure that Public IP addresses are Evaluated on a Periodic Basis", + "Checks": [], + "Attributes": [ + { + "Section": "8 Networking Services", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Public IP Addresses provide tenant accounts with Internet connectivity for resources contained within the tenant. During the creation of certain resources in Azure, a Public IP Address may be created. All Public IP Addresses within the tenant should be periodically reviewed for accuracy and necessity.", + "RationaleStatement": "Public IP Addresses allocated to the tenant should be periodically reviewed for necessity. Public IP Addresses that are not intentionally assigned and controlled present a publicly facing vector for threat actors and significant risk to the tenant.", + "ImpactStatement": "", + "RemediationProcedure": "Remediation will vary significantly depending on your organization's security requirements for the resources attached to each individual Public IP address.", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `All Resources` blade 2. Click on `Add Filter` 3. In the Add Filter window, select the following: Filter: `Type` Operator: `Equals` Value: `Public IP address` 4. Click the `Apply` button 5. For each Public IP address in the list, use Overview (or Properties) to review the `Associated to:` field and determine if the associated resource is still relevant to your tenant environment. If the associated resource is relevant, ensure that additional controls exist to mitigate risk (e.g. Firewalls, VPNs, Traffic Filtering, Virtual Gateway Appliances, Web Application Firewalls, etc.) on all subsequently attached resources. **Audit from Azure CLI** List all Public IP addresses: ``` az network public-ip list ``` For each Public IP address in the output, review the `name` property and determine if the associated resource is still relevant to your tenant environment. If the associated resource is relevant, ensure that additional controls exist to mitigate risk (e.g. Firewalls, VPNs, Traffic Filtering, Virtual Gateway Appliances, Web Application Firewalls, etc.) on all subsequently attached resources.", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/cli/azure/network/public-ip?view=azure-cli-latest:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security", + "DefaultValue": "During Virtual Machine and Application creation, a setting may create and attach a public IP." + } + ] + }, + { + "Id": "10.3.4", + "Description": "Ensure that 'Secure transfer required' is set to 'Enabled'", + "Checks": [ + "storage_secure_transfer_required_is_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Enable data encryption in transit.", + "RationaleStatement": "The secure transfer option enhances the security of a storage account by only allowing requests to the storage account by a secure connection. For example, when calling REST APIs to access storage accounts, the connection must use HTTPS. Any requests using HTTP will be rejected when 'secure transfer required' is enabled. When using the Azure files service, connection without encryption will fail, including scenarios using SMB 2.1, SMB 3.0 without encryption, and some flavors of the Linux SMB client. Because Azure storage doesn’t support HTTPS for custom domain names, this option is not applied when using a custom domain name.", + "ImpactStatement": "", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Set `Secure transfer required` to `Enabled`. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to enable `Secure transfer required` for a `Storage Account` ``` az storage account update --name --resource-group --https-only true ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Ensure that `Secure transfer required` is set to `Enabled`. **Audit from Azure CLI** Use the below command to ensure the `Secure transfer required` is enabled for all the `Storage Accounts` by ensuring the output contains `true` for each of the `Storage Accounts`. ``` az storage account list --query [*].[name,enableHttpsTrafficOnly] ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [404c3081-a854-4457-ae30-26a93ef643f9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F404c3081-a854-4457-ae30-26a93ef643f9) **- Name:** 'Secure transfer to storage accounts should be enabled'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/blobs/security-recommendations#encryption-in-transit:https://docs.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest#az_storage_account_list:https://docs.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest#az_storage_account_update:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-3-encrypt-sensitive-data-in-transit", + "DefaultValue": "By default, `Secure transfer required` is set to `Disabled`." + } + ] + }, + { + "Id": "10.3.5", + "Description": "Ensure 'Allow Azure services on the trusted services list to access this storage account' is Enabled for Storage Account Access", + "Checks": [ + "storage_ensure_azure_services_are_trusted_to_access_is_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "_NOTE:_ This recommendation assumes that the `Public network access` parameter is set to `Enabled from selected virtual networks and IP addresses`. Please ensure the prerequisite recommendation has been implemented before proceeding: - Ensure Default Network Access Rule for Storage Accounts is Set to Deny Some Azure services that interact with storage accounts operate from networks that can't be granted access through network rules. To help this type of service work as intended, allow the set of trusted Azure services to bypass the network rules. These services will then use strong authentication to access the storage account. If the `Allow Azure services on the trusted services list to access this storage account` exception is enabled, the following services are granted access to the storage account: Azure Backup, Azure Data Box, Azure DevTest Labs, Azure Event Grid, Azure Event Hubs, Azure File Sync, Azure HDInsight, Azure Import/Export, Azure Monitor, Azure Networking Services, and Azure Site Recovery (when registered in the subscription).", + "RationaleStatement": "Turning on firewall rules for a storage account will block access to incoming requests for data, including from other Azure services. We can re-enable this functionality by allowing access to `trusted Azure services` through networking exceptions.", + "ImpactStatement": "This creates authentication credentials for services that need access to storage resources so that services will no longer need to communicate via network request. There may be a temporary loss of communication as you set each Storage Account. It is recommended to not do this on mission-critical resources during business hours.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Security + networking`, click `Networking`. 1. Click on the `Firewalls and virtual networks` heading. 1. Under `Exceptions`, check the box next to `Allow Azure services on the trusted services list to access this storage account`. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to update `bypass` to `Azure services`. ``` az storage account update --name --resource-group --bypass AzureServices ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Security + networking`, click `Networking`. 1. Click on the `Firewalls and virtual networks` heading. 1. Under `Exceptions`, ensure that `Allow Azure services on the trusted services list to access this storage account` is checked. **Audit from Azure CLI** Ensure `bypass` contains `AzureServices` ``` az storage account list --query '[*].networkRuleSet' ``` **Audit from PowerShell** ``` Connect-AzAccount Set-AzContext -Subscription Get-AzStorageAccountNetworkRuleset -ResourceGroupName -Name |Select-Object Bypass ``` If the response from the above command is `None`, the storage account configuration is out of compliance with this check. If the response is `AzureServices`, the storage account configuration is in compliance with this check. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [c9d007d0-c057-4772-b18c-01e546713bcd](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fc9d007d0-c057-4772-b18c-01e546713bcd) **- Name:** 'Storage accounts should allow access from trusted Microsoft services'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-network-security:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, Storage Accounts will accept connections from clients on any network." + } + ] + }, + { + "Id": "10.3.6", + "Description": "Ensure Soft Delete is Enabled for Azure Containers and Blob Storage", + "Checks": [ + "storage_ensure_soft_delete_is_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "The Azure Storage blobs contain data like ePHI or Financial, which can be secret or personal. Data that is erroneously modified or deleted by an application or other storage account user will cause data loss or unavailability. It is recommended that both Azure Containers with attached Blob Storage and standalone containers with Blob Storage be made recoverable by enabling the **soft delete** configuration. This is to save and recover data when blobs or blob snapshots are deleted.", + "RationaleStatement": "Containers and Blob Storage data can be incorrectly deleted. An attacker/malicious user may do this deliberately in order to cause disruption. Deleting an Azure Storage blob causes immediate data loss. Enabling this configuration for Azure storage ensures that even if blobs/data were deleted from the storage account, Blobs/data objects are recoverable for a particular time which is set in the Retention policies, ranging from 7 days to 365 days.", + "ImpactStatement": "Additional storage costs may be incurred as snapshots are retained.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account, under `Data management`, go to `Data protection`. 1. Check the box next to `Enable soft delete for blobs`. 1. Check the box next to `Enable soft delete for containers`. 1. Set the retention period for both to a sufficient length for your organization. 1. Click `Save`. **Remediate from Azure CLI** Update blob storage retention days in below command ``` az storage blob service-properties delete-policy update --days-retained --account-name --account-key --enable true ``` Update container retention with the below command ``` az storage account blob-service-properties update --enable-container-delete-retention true --container-delete-retention-days --account-name --resource-group ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each Storage Account, under `Data management`, go to `Data protection`. 1. Ensure that `Enable soft delete for blobs` is checked. 1. Ensure that `Enable soft delete for containers` is checked. 1. Ensure that the retention period for both is a sufficient length for your organization. **Audit from Azure CLI** Blob Storage: Ensure that the output of the below command contains enabled status as true and days is not empty or null ``` az storage blob service-properties delete-policy show --account-name --account-key ``` Azure Containers: Ensure that within `containerDeleteRetentionPolicy`, the `enabled` property is set to `true`. ``` az storage account blob-service-properties show --account-name --resource-group ```", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-soft-delete:https://docs.microsoft.com/en-us/azure/storage/blobs/soft-delete-container-overview:https://docs.microsoft.com/en-us/azure/storage/blobs/soft-delete-container-enable?tabs=azure-portal", + "DefaultValue": "Soft delete for containers and blob storage is **enabled** by default on storage accounts created via the Azure Portal, and **disabled** by default on storage accounts created via Azure CLI or PowerShell." + } + ] + }, + { + "Id": "10.3.7", + "Description": "Ensure the 'Minimum TLS version' for storage accounts is set to 'Version 1.2'", + "Checks": [ + "storage_ensure_minimum_tls_version_12" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "In some cases, Azure Storage sets the minimum TLS version to be version 1.0 by default. TLS 1.0 is a legacy version and has known vulnerabilities. This minimum TLS version can be configured to be later protocols such as TLS 1.2.", + "RationaleStatement": "TLS 1.0 has known vulnerabilities and has been replaced by later versions of the TLS protocol. Continued use of this legacy protocol affects the security of data in transit.", + "ImpactStatement": "When set to TLS 1.2 all requests must leverage this version of the protocol. Applications leveraging legacy versions of the protocol will fail.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Set the `Minimum TLS version` to `Version 1.2`. 1. Click `Save`. **Remediate from Azure CLI** ``` az storage account update \\ --name \\ --resource-group \\ --min-tls-version TLS1_2 ``` **Remediate from PowerShell** To set the minimum TLS version, run the following command: ``` Set-AzStorageAccount -AccountName ` -ResourceGroupName ` -MinimumTlsVersion TLS1_2 ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Settings`, click `Configuration`. 1. Ensure that the `Minimum TLS version` is set to `Version 1.2`. **Audit from Azure CLI** Get a list of all storage accounts and their resource groups ``` az storage account list | jq '.[] | {name, resourceGroup}' ``` Then query the minimumTLSVersion field ``` az storage account show \\ --name \\ --resource-group \\ --query minimumTlsVersion \\ --output tsv ``` **Audit from PowerShell** To get the minimum TLS version, run the following command: ``` (Get-AzStorageAccount -Name -ResourceGroupName ).MinimumTlsVersion ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [fe83a0eb-a853-422d-aac2-1bffd182c5d0](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Ffe83a0eb-a853-422d-aac2-1bffd182c5d0) **- Name:** 'Storage accounts should have the specified minimum TLS version'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/transport-layer-security-configure-minimum-version?tabs=portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-data-protection#dp-3-encrypt-sensitive-data-in-transit", + "DefaultValue": "If a storage account is created through the portal, the MinimumTlsVersion property for that storage account will be set to TLS 1.2. If a storage account is created through PowerShell or CLI, the MinimumTlsVersion property for that storage account will not be set, and defaults to TLS 1.0." + } + ] + }, + { + "Id": "9.1.16", + "Description": "Ensure that Microsoft Defender External Attack Surface Monitoring (EASM) is enabled", + "Checks": [], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "An organization's attack surface is the collection of assets with a public network identifier or URI that an external threat actor can see or access from outside your cloud. It is the set of points on the boundary of a system, a system element, system component, or an environment where an attacker can try to enter, cause an effect on, or extract data from, that system, system element, system component, or environment. The larger the attack surface, the harder it is to protect. This tool can be configured to scan your organization's online infrastructure such as specified domains, hosts, CIDR blocks, and SSL certificates, and store them in an Inventory. Inventory items can be added, reviewed, approved, and removed, and may contain enrichments (insights) and additional information collected from the tool's different scan engines and open-source intelligence sources. A Defender EASM workspace will generate an Inventory of publicly exposed assets by crawling and scanning the internet using _Seeds_ you provide when setting up the tool. Seeds can be FQDNs, IP CIDR blocks, and WHOIS records. Defender EASM will generate Insights within 24-48 hours after Seeds are provided, and these insights include vulnerability data (CVEs), ports and protocols, and weak or expired SSL certificates that could be used by an attacker for reconnaissance or exploitation. Results are classified High/Medium/Low and some of them include proposed mitigations.", + "RationaleStatement": "This tool can monitor the externally exposed resources of an organization, provide valuable insights, and export these findings in a variety of formats (including CSV) for use in vulnerability management operations and red/purple team exercises.", + "ImpactStatement": "Microsoft Defender EASM workspaces are currently available as Azure Resources with a 30-day free trial period but can quickly accrue significant charges. The costs are calculated daily as (Number of billable inventory items) x (item cost per day; approximately: $0.017). Estimated cost is not provided within the tool, and users are strongly advised to contact their Microsoft sales representative for pricing and set a calendar reminder for the end of the trial period. For an EASM workspace having an Inventory of 5k-10k billable items (IP addresses, hostnames, SSL certificates, etc) a typical cost might be approximately $85-170 per day or $2500-5000 USD/month at the time of publication. If the workspace is deleted by the last day of a free trial period, no charges are billed.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Microsoft Defender EASM`. 1. Click `+ Create`. 1. Under `Project details`, select a subscription. 1. Select or create a resource group. 1. Under `Instance details`, enter a name for the workspace. 1. Select a region. 1. Click `Review + create`. 1. Click `Create`. 1. Once the deployment has completed, go to `Microsoft Defender EASM`. 1. Click the workspace name. 1. Configure the workspace appropriately for your environment and organization.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Microsoft Defender EASM`. 1. Ensure that at least one Microsoft Defender EASM workspace is listed. 1. Click the name of a workspace. 1. Ensure the workspace is configured appropriately for your environment and organization. 1. Repeat steps 3-4 for each workspace.", + "AdditionalInformation": "Microsoft added its Defender for External Attack Surface management (EASM) offering to Azure following its 2022 acquisition of EASM SaaS tool company RiskIQ.", + "References": "https://learn.microsoft.com/en-us/azure/external-attack-surface-management/:https://learn.microsoft.com/en-us/azure/external-attack-surface-management/deploying-the-defender-easm-azure-resource:https://www.microsoft.com/en-us/security/blog/2022/08/02/microsoft-announces-new-solutions-for-threat-intelligence-and-attack-surface-management/", + "DefaultValue": "Microsoft Defender EASM is an optional, paid Azure Resource that must be created and configured inside a Subscription and Resource Group." + } + ] + }, + { + "Id": "9.1.3.3", + "Description": "Ensure that 'Endpoint protection' component status is set to 'On'", + "Checks": [], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "The Endpoint protection component enables Microsoft Defender for Endpoint (formerly 'Advanced Threat Protection' or 'ATP' or 'WDATP' - see additional info) to communicate with Microsoft Defender for Cloud. **IMPORTANT:** When enabling integration between DfE & DfC it needs to be taken into account that this will have some side effects that may be undesirable. 1. For server 2019 & above if defender is installed (default for these server SKUs) this will trigger a deployment of the new unified agent and link to any of the extended configuration in the Defender portal. 1. If the new unified agent is required for server SKUs of Win 2016 or Linux and lower there is additional integration that needs to be switched on and agents need to be aligned.", + "RationaleStatement": "Microsoft Defender for Endpoint integration brings comprehensive Endpoint Detection and Response (EDR) capabilities within Microsoft Defender for Cloud. This integration helps to spot abnormalities, as well as detect and respond to advanced attacks on endpoints monitored by Microsoft Defender for Cloud. MDE works only with Standard Tier subscriptions.", + "ImpactStatement": "Endpoint protection requires licensing and is included in these plans: - Defender for Servers plan 1 - Defender for Servers plan 2", + "RemediationProcedure": "**Remediate from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Go to `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the subscription name. 1. Click `Settings & monitoring`. 1. Set the `Status` for `Endpoint protection` to `On`. 1. Click `Continue`. **Remediate from Azure CLI** Use the below command to enable Standard pricing tier for Storage Accounts ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X PUT -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions//providers/Microsoft.Security/settings/WDATP?api-version=2021-06-01 -d@input.json' ``` Where input.json contains the Request body json data as mentioned below. ``` { id: /subscriptions//providers/Microsoft.Security/settings/WDATP, kind: DataExportSettings, type: Microsoft.Security/settings, properties: { enabled: true } } ```", + "AuditProcedure": "**Audit from Azure Portal** 1. From Azure Home select the Portal Menu. 1. Select `Microsoft Defender for Cloud`. 1. Under `Management`, select `Environment Settings`. 1. Click on the subscription name. 1. Click `Settings & monitoring`. 1. Ensure the `Status` for `Endpoint protection` is set to `On`. **Audit from Azure CLI** Ensure the output of the below command is `True` ``` az account get-access-token --query {subscription:subscription,accessToken:accessToken} --out tsv | xargs -L1 bash -c 'curl -X GET -H Authorization: Bearer $1 -H Content-Type: application/json https://management.azure.com/subscriptions//providers/Microsoft.Security/settings?api-version=2021-06-01' | jq '.|.value[] | select(.name==WDATP)'|jq '.properties.enabled' ``` **Audit from PowerShell** Run the following commands to login and audit this check ``` Connect-AzAccount Set-AzContext -Subscription Get-AzSecuritySetting | Select-Object name,enabled |where-object {$_.name -eq WDATP} ``` **PowerShell Output - Non-Compliant** ``` Name Enabled ---- ------- WDATP False ``` **PowerShell Output - Compliant** ``` Name Enabled ---- ------- WDATP True ```", + "AdditionalInformation": "**IMPORTANT:** When enabling integration between DfE & DfC it needs to be taken into account that this will have some side effects that may be undesirable. 1. For server 2019 & above if defender is installed (default for these server SKUs) this will trigger a deployment of the new unified agent and link to any of the extended configuration in the Defender portal. 1. If the new unified agent is required for server SKUs of Win 2016 or Linux and lower there is additional integration that needs to be switched on and agents need to be aligned. NOTE: Microsoft Defender for Endpoint (MDE) was formerly known as Windows Defender Advanced Threat Protection (WDATP). There are a number of places (e.g. Azure CLI) where the WDATP acronym is still used within Azure.", + "References": "https://docs.microsoft.com/en-in/azure/defender-for-cloud/integration-defender-for-endpoint?tabs=windows:https://docs.microsoft.com/en-us/rest/api/securitycenter/settings/list:https://docs.microsoft.com/en-us/rest/api/securitycenter/settings/update:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-endpoint-security#es-1-use-endpoint-detection-and-response-edr:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-endpoint-security#es-2-use-modern-anti-malware-software", + "DefaultValue": "By default, Endpoint protection is `off`." + } + ] + }, + { + "Id": "9.1.3.4", + "Description": "Ensure that 'Agentless scanning for machines' component status is set to 'On'", + "Checks": [], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Using disk snapshots, the agentless scanner scans for installed software, vulnerabilities, and plain text secrets.", + "RationaleStatement": "The Microsoft Defender for Cloud agentless machine scanner provides threat detection, vulnerability detection, and discovery of sensitive information.", + "ImpactStatement": "Agentless scanning for machines requires licensing and is included in these plans: - Defender CSPM - Defender for Servers plan 2", + "RemediationProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `Agentless scanning for machines` 1. Select `On` 1. Click `Continue` in the top left Repeat the above for any additional subscriptions.", + "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `Agentless scanning for machines` 1. Ensure that `On` is selected Repeat the above for any additional subscriptions.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/concept-agentless-data-collection:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification:https://learn.microsoft.com/en-us/azure/defender-for-cloud/enable-agentless-scanning-vms", + "DefaultValue": "By default, Agentless scanning for machines is `off`." + } + ] + }, + { + "Id": "9.1.3.5", + "Description": "Ensure that 'File Integrity Monitoring' component status is set to 'On'", + "Checks": [], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.1 Microsoft Defender for Cloud", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "File Integrity Monitoring (FIM) is a feature that monitors critical system files in Windows or Linux for potential signs of attack or compromise.", + "RationaleStatement": "FIM provides a detection mechanism for compromised files. When FIM is enabled, critical system files are monitored for changes that might indicate a threat actor is attempting to modify system files for lateral compromise within a host operating system.", + "ImpactStatement": "File Integrity Monitoring requires licensing and is included in these plans: - Defender for Servers plan 2", + "RemediationProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `File Integrity Monitoring` 1. Select `On` 1. Click `Continue` in the top left Repeat the above for any additional subscriptions.", + "AuditProcedure": "**Audit from Azure Portal** 1. From the Azure Portal `Home` page, select `Microsoft Defender for Cloud` 1. Under `Management` select `Environment Settings` 1. Select a subscription 1. Under `Settings` > `Defender Plans`, click `Settings & monitoring` 1. Under the Component column, locate the row for `File Integrity Monitoring` 1. Ensure that `On` is selected Repeat the above for any additional subscriptions.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/file-integrity-monitoring-overview:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-incident-response#ir-2-preparation---setup-incident-notification:https://learn.microsoft.com/en-us/azure/defender-for-cloud/file-integrity-monitoring-enable-defender-endpoint", + "DefaultValue": "By default, File Integrity Monitoring is `Off`." + } + ] + }, + { + "Id": "9.3.10", + "Description": "Ensure that Azure Key Vault Managed HSM is used when required", + "Checks": [], + "Attributes": [ + { + "Section": "9 Security Services", + "SubSection": "9.3 Key Vault", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Azure Key Vault Managed HSM is a fully managed, highly available, single-tenant cloud service that safeguards cryptographic keys using FIPS 140-2 Level 3 validated HSMs. **Note:** This recommendation to use Managed HSM applies only to scenarios where specific regulatory and compliance requirements mandate the use of a dedicated hardware security module.", + "RationaleStatement": "Managed HSM is a fully managed, highly available, single-tenant service that ensures FIPS 140-2 Level 3 compliance. It provides centralized key management, isolated access control, and private endpoints for secure access. Integrated with Azure services, it supports migration from Key Vault, ensures data residency, and offers monitoring and auditing for enhanced security.", + "ImpactStatement": "Managed HSM incurs a cost of $0.40 to $5 per month for each actively used HSM-protected key, depending on the key type and quantity. Each key version is billed separately. Additionally, there is an hourly usage fee of $3.20 per Managed HSM pool.", + "RemediationProcedure": "**Remediate from Azure CLI** Run the following command to set `oid` to be the `OID` of the signed-in user: ``` $oid = az ad signed-in-user show --query id -o tsv ``` Alternatively, prepare a space-separated list of OIDs to be provided as the `administrators` of the HSM. Run the following command to create a Managed HSM: ``` az keyvault create --resource-group --hsm-name --retention-days --administrators $oid ``` The command can take several minutes to complete. After the HSM has been created, it must be activated before it can be used. Activation requires providing a minimum of three and a maximum of ten RSA key pairs, as well as the minimum number of keys required to decrypt the security domain (called a quorum). OpenSSL can be used to generate the self-signed certificates, for example: ``` openssl req -newkey rsa:2048 -nodes -keyout cert_1.key -x509 -days 365 -out cert_1.cer ``` Run the following command to download the security domain and activate the Managed HSM: ``` az keyvault security-domain download --hsm-name --sd-wrapping-keys --sd-quorum --security-domain-file .json ``` Store the security domain file and the RSA key pairs securely. They will be required for disaster recovery or for creating another Managed HSM that shares the same security domain so that the two can share keys. The Managed HSM will now be in an active state and ready for use.", + "AuditProcedure": "**Audit from Azure CLI** Run the following command to list key vaults: ``` az keyvault list --query [*].[name,type] ``` Ensure that at least one key vault with type `Microsoft.KeyVault/managedHSMs` exists.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/security/fundamentals/key-management-choose:https://learn.microsoft.com/en-us/azure/key-vault/managed-hsm/overview:https://azure.microsoft.com/en-gb/pricing/details/key-vault/:https://learn.microsoft.com/en-us/azure/key-vault/managed-hsm/quick-create-cli:https://learn.microsoft.com/en-us/cli/azure/keyvault", + "DefaultValue": "" + } + ] + }, + { + "Id": "10.3.1.1", + "Description": "Ensure that 'Enable key rotation reminders' is enabled for each Storage Account", + "Checks": [ + "storage_infrastructure_encryption_is_enabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Access Keys authenticate application access requests to data contained in Storage Accounts. A periodic rotation of these keys is recommended to ensure that potentially compromised keys cannot result in a long-term exploitable credential. The Rotation Reminder is an automatic reminder feature for a manual procedure.", + "RationaleStatement": "Reminders such as those generated by this recommendation will help maintain a regular and healthy cadence for activities which improve the overall efficacy of a security program. Cryptographic key rotation periods will vary depending on your organization's security requirements and the type of data which is being stored in the Storage Account. For example, PCI DSS mandates that cryptographic keys be replaced or rotated 'regularly,' and advises that keys for static data stores be rotated every 'few months.' For the purposes of this recommendation, 90 days will be prescribed for the reminder. Review and adjustment of the 90 day period is recommended, and may even be necessary. Your organization's security requirements should dictate the appropriate setting.", + "ImpactStatement": "This recommendation only creates a periodic reminder to regenerate access keys. Regenerating access keys can affect services in Azure as well as the organization's applications that are dependent on the storage account. All clients that use the access key to access the storage account must be updated to use the new key.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts` 1. For each Storage Account that is not compliant, under `Security + networking`, go to `Access keys` 1. Click `Set rotation reminder` 1. Check `Enable key rotation reminders` 1. In the `Send reminders` field select `Custom`, then set the `Remind me every` field to `90` and the period drop down to `Days` 1. Click `Save` **Remediate from Powershell** ``` $rgName = $accountName = $account = Get-AzStorageAccount -ResourceGroupName $rgName -Name $accountName if ($account.KeyCreationTime.Key1 -eq $null -or $account.KeyCreationTime.Key2 -eq $null){ Write-output (You must regenerate both keys at least once before setting expiration policy) } else { $account = Set-AzStorageAccount -ResourceGroupName $rgName -Name $accountName -KeyExpirationPeriodInDay 90 } $account.KeyPolicy.KeyExpirationPeriodInDays ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts` 2. For each Storage Account, under `Security + networking`, go to `Access keys` 3. If the button `Edit rotation reminder` is displayed, the Storage Account is compliant. Click `Edit rotation reminder` and review the `Remind me every` field for a desirable periodic setting that fits your security program's needs. If the button `Set rotation reminder` is displayed, the Storage Account is not compliant. **Audit from Powershell** ``` $rgName = $accountName = $account = Get-AzStorageAccount -ResourceGroupName $rgName -Name $accountName Write-Output $accountName -> Write-Output Expiration Reminder set to: $($account.KeyPolicy.KeyExpirationPeriodInDays) Days Write-Output Key1 Last Rotated: $($account.KeyCreationTime.Key1.ToShortDateString()) Write-Output Key2 Last Rotated: $($account.KeyCreationTime.Key2.ToShortDateString()) ``` Key rotation is recommended if the creation date for any key is empty. If the reminder is set, the period in days will be returned. The recommended period is 90 days. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [044985bb-afe1-42cd-8a36-9d5d42424537](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F044985bb-afe1-42cd-8a36-9d5d42424537) **- Name:** 'Storage account keys should not be expired'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-create-storage-account#regenerate-storage-access-keys:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-3-manage-application-identities-securely-and-automatically:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-8-restrict-the-exposure-of-credentials-and-secrets:https://www.pcidssguide.com/pci-dss-key-rotation-requirements/:https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf", + "DefaultValue": "By default, Key rotation reminders are not configured." + } + ] + }, + { + "Id": "10.3.1.2", + "Description": "Ensure that Storage Account access keys are periodically regenerated", + "Checks": [ + "storage_key_rotation_90_days" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "For increased security, regenerate storage account access keys periodically.", + "RationaleStatement": "When a storage account is created, Azure generates two 512-bit storage access keys which are used for authentication when the storage account is accessed. Rotating these keys periodically ensures that any inadvertent access or exposure does not result from the compromise of these keys. Cryptographic key rotation periods will vary depending on your organization's security requirements and the type of data which is being stored in the Storage Account. For example, PCI DSS mandates that cryptographic keys be replaced or rotated 'regularly,' and advises that keys for static data stores be rotated every 'few months.' For the purposes of this recommendation, 90 days will be prescribed for the reminder. Review and adjustment of the 90 day period is recommended, and may even be necessary. Your organization's security requirements should dictate the appropriate setting.", + "ImpactStatement": "Regenerating access keys can affect services in Azure as well as the organization's applications that are dependent on the storage account. All clients who use the access key to access the storage account must be updated to use the new key.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 2. For each Storage Account with outdated keys, under `Security + networking`, go to `Access keys`. 3. Click `Rotate key` next to the outdated key, then click `Yes` to the prompt confirming that you want to regenerate the access key. After Azure regenerates the Access Key, you can confirm that `Access keys` reflects a `Last rotated` date of `(0 days ago)`.", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 2. For each Storage Account, under `Security + networking`, go to `Access keys`. 3. Review the date and days in the `Last rotated` field for **each** key. If the `Last rotated` field indicates a number or days greater than 90 [or greater than your organization's period of validity], the key should be rotated. **Audit from Azure CLI** 1. Get a list of storage accounts ``` az storage account list --subscription ``` Make a note of `id`, `name` and `resourceGroup`. 2. For every storage account make sure that key is regenerated in the past 90 days. ``` az monitor activity-log list --namespace Microsoft.Storage --offset 90d --query [?contains(authorization.action, 'regenerateKey')] --resource-id ``` The output should contain ``` authorization/scope: AND authorization/action: Microsoft.Storage/storageAccounts/regeneratekey/action AND status/localizedValue: Succeeded status/Value: Succeeded ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [044985bb-afe1-42cd-8a36-9d5d42424537](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F044985bb-afe1-42cd-8a36-9d5d42424537) **- Name:** 'Storage account keys should not be expired'", + "AdditionalInformation": "", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-create-storage-account#regenerate-storage-access-keys:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-2-protect-identity-and-authentication-systems:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-6-define-and-implement-identity-and-privileged-access-strategy:https://www.pcidssguide.com/pci-dss-key-rotation-requirements/:https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf", + "DefaultValue": "By default, access keys are not regenerated periodically." + } + ] + }, + { + "Id": "10.3.10", + "Description": "Ensure Azure Resource Manager Delete locks are applied to Azure Storage Accounts", + "Checks": [], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Azure Resource Manager _CannotDelete (Delete)_ locks can prevent users from accidentally or maliciously deleting a storage account. This feature ensures that while the Storage account can still be modified or used, deletion of the Storage account resource requires removal of the lock by a user with appropriate permissions. This feature is a protective control for the availability of data. By ensuring that a storage account or its parent resource group cannot be deleted without first removing the lock, the risk of data loss is reduced.", + "RationaleStatement": "Applying a _Delete_ lock on storage accounts protects the availability of data by preventing the accidental or unauthorized deletion of the entire storage account. It is a fundamental protective control that can prevent data loss", + "ImpactStatement": "- Prevents the deletion of the Storage account Resource entirely. - Prevents the deletion of the parent Resource Group containing the locked Storage account resource. - Does not prevent other control plane operations, including modification of configurations, network settings, containers, and access. - Does not prevent deletion of containers or other objects within the storage account.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. Under the `Settings` section, select `Locks`. 1. Select `Add`. 1. Provide a Name, and choose `Delete` for the type of lock. 1. Add a note about the lock if desired. **Remediate from Azure CLI** Replace the information within <> with appropriate values: ``` az lock create --name \\ --resource-group \\ --resource \\ --lock-type CanNotDelete \\ --resource-type Microsoft.Storage/storageAccounts ``` **Remediate from PowerShell** Replace the information within <> with appropriate values: ``` New-AzResourceLock -LockLevel CanNotDelete ` -LockName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ` -ResourceGroupName ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. For each storage account, under `Settings`, click `Locks`. 1. Ensure that a `Delete` lock exists on the storage account. **Audit from Azure CLI** ``` az lock list --resource-group \\ --resource-name \\ --resource-type Microsoft.Storage/storageAccounts ``` **Audit from PowerShell** ``` Get-AzResourceLock -ResourceGroupName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ``` **Audit from Azure Policy** There is currently no built-in Microsoft policy to audit resource locks on storage accounts. Custom and community policy definitions can check for the existence of a “Microsoft.Authorization/locks” resource with an AuditIfNotExists effect.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/lock-account-resource:https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources", + "DefaultValue": "By default, no locks are applied to Azure resources, including storage accounts. Locks must be manually configured after resource creation." + } + ] + }, + { + "Id": "10.3.2.1", + "Description": "Ensure Private Endpoints are used to access Storage Accounts", + "Checks": [ + "storage_ensure_private_endpoints_in_storage_accounts" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Use private endpoints for your Azure Storage accounts to allow clients and services to securely access data located over a network via an encrypted Private Link. To do this, the private endpoint uses an IP address from the VNet for each service. Network traffic between disparate services securely traverses encrypted over the VNet. This VNet can also link addressing space, extending your network and accessing resources on it. Similarly, it can be a tunnel through public networks to connect remote infrastructures together. This creates further security through segmenting network traffic and preventing outside sources from accessing it.", + "RationaleStatement": "Securing traffic between services through encryption protects the data from easy interception and reading.", + "ImpactStatement": "If an Azure Virtual Network is not implemented correctly, this may result in the loss of critical network traffic. Private endpoints are charged per hour of use. Refer to https://azure.microsoft.com/en-us/pricing/details/private-link/ and https://azure.microsoft.com/en-us/pricing/calculator/ to estimate potential costs.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Open the `Storage Accounts` blade 1. For each listed Storage Account, perform the following: 1. Under the `Security + networking` heading, click on `Networking` 1. Click on the `Private endpoint connections` tab at the top of the networking window 1. Click the `+ Private endpoint` button 1. In the `1 - Basics` tab/step: - `Enter a name` that will be easily recognizable as associated with the Storage Account (*Note*: The Network Interface Name will be automatically completed, but you can customize it if needed.) - Ensure that the `Region` matches the region of the Storage Account - Click `Next` 1. In the `2 - Resource` tab/step: - Select the `target sub-resource` based on what type of storage resource is being made available - Click `Next` 1. In the `3 - Virtual Network` tab/step: - Select the `Virtual network` that your Storage Account will be connecting to - Select the `Subnet` that your Storage Account will be connecting to - (Optional) Select other network settings as appropriate for your environment - Click `Next` 1. In the `4 - DNS` tab/step: - (Optional) Select other DNS settings as appropriate for your environment - Click `Next` 1. In the `5 - Tags` tab/step: - (Optional) Set any tags that are relevant to your organization - Click `Next` 1. In the `6 - Review + create` tab/step: - A validation attempt will be made and after a few moments it should indicate `Validation Passed` - if it does not pass, double-check your settings before beginning more in depth troubleshooting. - If validation has passed, click `Create` then wait for a few minutes for the scripted deployment to complete. Repeat the above procedure for each Private Endpoint required within every Storage Account. **Remediate from PowerShell** ``` $storageAccount = Get-AzStorageAccount -ResourceGroupName '' -Name '' $privateEndpointConnection = @{ Name = 'connectionName' PrivateLinkServiceId = $storageAccount.Id GroupID = blob|blob_secondary|file|file_secondary|table|table_secondary|queue|queue_secondary|web|web_secondary|dfs|dfs_secondary } $privateLinkServiceConnection = New-AzPrivateLinkServiceConnection @privateEndpointConnection $virtualNetDetails = Get-AzVirtualNetwork -ResourceGroupName '' -Name '' $privateEndpoint = @{ ResourceGroupName = '' Name = '' Location = '' Subnet = $virtualNetDetails.Subnets[0] PrivateLinkServiceConnection = $privateLinkServiceConnection } New-AzPrivateEndpoint @privateEndpoint ``` **Remediate from Azure CLI** ``` az network private-endpoint create --resource-group --name --vnet-name --subnet --private-connection-resource-id --connection-name --group-id ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Open the `Storage Accounts` blade. 1. For each listed Storage Account, perform the following check: 1. Under the `Security + networking` heading, click on `Networking`. 1. Click on the `Private endpoint connections` tab at the top of the networking window. 1. Ensure that for each VNet that the Storage Account must be accessed from, a unique Private Endpoint is deployed and the `Connection state` for each Private Endpoint is `Approved`. Repeat the procedure for each Storage Account. **Audit from PowerShell** ``` $storageAccount = Get-AzStorageAccount -ResourceGroup '' -Name '' Get-AzPrivateEndpoint -ResourceGroup ''|Where-Object {$_.PrivateLinkServiceConnectionsText -match $storageAccount.id} ``` If the results of the second command returns information, the Storage Account is using a Private Endpoint and complies with this Benchmark, otherwise if the results of the second command are empty, the Storage Account generates a finding. **Audit from Azure CLI** ``` az storage account show --name '' --query privateEndpointConnections[0].id ``` If the above command returns data, the Storage Account complies with this Benchmark, otherwise if the results are empty, the Storage Account generates a finding. **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [6edd7eda-6dd8-40f7-810d-67160c639cd9](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F6edd7eda-6dd8-40f7-810d-67160c639cd9) **- Name:** 'Storage accounts should use private link'", + "AdditionalInformation": "A NAT gateway is the recommended solution for outbound internet access. This recommendation is based on the Common Reference Recommendation `Ensure Private Endpoints are used to access {service}`, from the `Common Reference Recommendations > Networking > Private Endpoints` section.", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-private-endpoints:https://docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-overview:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-portal:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-cli?tabs=dynamic-ip:https://docs.microsoft.com/en-us/azure/private-link/create-private-endpoint-powershell?tabs=dynamic-ip:https://docs.microsoft.com/en-us/azure/private-link/tutorial-private-endpoint-storage-portal:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, Private Endpoints are not created for Storage Accounts." + } + ] + }, + { + "Id": "10.3.2.2", + "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", + "Checks": [ + "storage_blob_public_access_level_is_disabled" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Disallowing public network access for a storage account overrides the public access settings for individual containers in that storage account for Azure Resource Manager Deployment Model storage accounts. Azure Storage accounts that use the classic deployment model will be retired on August 31, 2024.", + "RationaleStatement": "The default network configuration for a storage account permits a user with appropriate permissions to configure public network access to containers and blobs in a storage account. Keep in mind that public access to a container is always turned off by default and must be explicitly configured to permit anonymous requests. It grants read-only access to these resources without sharing the account key, and without requiring a shared access signature. It is recommended not to provide public network access to storage accounts until, and unless, it is strongly desired. A shared access signature token or Azure AD RBAC should be used for providing controlled and timed access to blob containers.", + "ImpactStatement": "Access will have to be managed using shared access signatures or via Azure AD RBAC. For classic storage accounts (to be retired on August 31, 2024), each container in the account must be configured to block anonymous access. Either configure all containers or to configure at the storage account level, migrate to the Azure Resource Manager deployment model.", + "RemediationProcedure": "**Remediate from Azure Portal** First, follow Microsoft documentation and create shared access signature tokens for your blob containers. Then, 1. Go to `Storage Accounts`. 1. For each storage account, under the `Security + networking` section, click `Networking`. 1. Set `Public network access` to `Disabled`. 1. Click `Save`. **Remediate from Azure CLI** Set 'Public Network Access' to `Disabled` on the storage account ``` az storage account update --name --resource-group --public-network-access Disabled ``` **Remediate from PowerShell** For each Storage Account, run the following to set the `PublicNetworkAccess` setting to `Disabled` ``` Set-AzStorageAccount -ResourceGroupName -Name -PublicNetworkAccess Disabled ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to `Storage Accounts`. 2. For each storage account, under the `Security + networking` section, click `Networking`. 3. Ensure the `Public network access` setting is set to `Disabled`. **Audit from Azure CLI** Ensure `publicNetworkAccess` is `Disabled` ``` az storage account show --name --resource-group --query {publicNetworkAccess:publicNetworkAccess} ``` **Audit from PowerShell** For each Storage Account, ensure `PublicNetworkAccess` is `Disabled` ``` Get-AzStorageAccount -Name -ResourceGroupName |select PublicNetworkAccess ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [b2982f36-99f2-4db5-8eff-283140c09693](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2Fb2982f36-99f2-4db5-8eff-283140c09693) **- Name:** 'Storage accounts should disable public network access'", + "AdditionalInformation": "This recommendation is based on the Common Reference Recommendation `Ensure public network access is Disabled`, from the `Common Reference Recommendations > Networking > Virtual Networks (VNets)` section.", + "References": "https://docs.microsoft.com/en-us/azure/storage/blobs/storage-manage-access-to-resources:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls:https://docs.microsoft.com/en-us/azure/storage/blobs/assign-azure-role-data-access:https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security?tabs=azure-portal", + "DefaultValue": "By default, `Public Network Access` is set to `Enabled from all networks` for the Storage Account." + } + ] + }, + { + "Id": "10.3.2.3", + "Description": "Ensure default network access rule for storage accounts is set to deny", + "Checks": [ + "storage_default_network_access_rule_is_denied" + ], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "Restricting default network access helps to provide a new layer of security, since storage accounts accept connections from clients on any network. To limit access to selected networks, the default action must be changed.", + "RationaleStatement": "Storage accounts should be configured to deny access to traffic from all networks (including internet traffic). Access can be granted to traffic from specific Azure Virtual networks, allowing a secure network boundary for specific applications to be built. Access can also be granted to public internet IP address ranges to enable connections from specific internet or on-premises clients. When network rules are configured, only applications from allowed networks can access a storage account. When calling from an allowed network, applications continue to require proper authorization (a valid access key or SAS token) to access the storage account.", + "ImpactStatement": "All allowed networks will need to be whitelisted on each specific network, creating administrative overhead. This may result in loss of network connectivity, so do not turn on for critical resources during business hours.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Go to `Storage Accounts`. 1. For each storage account, under `Security + networking`, click `Networking`. 1. Click the `Firewalls and virtual networks` heading. 1. Set `Public network access` to `Enabled from selected virtual networks and IP addresses`. 1. Add rules to allow traffic from specific networks and IP addresses. 1. Click `Save`. **Remediate from Azure CLI** Use the below command to update `default-action` to `Deny`. ``` az storage account update --name --resource-group --default-action Deny ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Go to Storage Accounts. 2. For each storage account, under `Security + networking`, click `Networking`. 4. Click the `Firewalls and virtual networks` heading. 3. Ensure that `Public network access` is not set to `Enabled from all networks`. **Audit from Azure CLI** Ensure `defaultAction` is not set to ` Allow`. ``` az storage account list --query '[*].networkRuleSet' ``` **Audit from PowerShell** ``` Connect-AzAccount Set-AzContext -Subscription Get-AzStorageAccountNetworkRuleset -ResourceGroupName -Name |Select-Object DefaultAction ``` PowerShell Result - Non-Compliant ``` DefaultAction : Allow ``` PowerShell Result - Compliant ``` DefaultAction : Deny ``` **Audit from Azure Policy** If referencing a digital copy of this Benchmark, clicking a Policy ID will open a link to the associated Policy definition in Azure. If referencing a printed copy, you can search Policy IDs from this URL: https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyMenuBlade/~/Definitions - **Policy ID:** [34c877ad-507e-4c82-993e-3452a6e0ad3c](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F34c877ad-507e-4c82-993e-3452a6e0ad3c) **- Name:** 'Storage accounts should restrict network access' - **Policy ID:** [2a1a9cdf-e04d-429a-8416-3bfb72a1b26f](https://portal.azure.com/#view/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F2a1a9cdf-e04d-429a-8416-3bfb72a1b26f) **- Name:** 'Storage accounts should restrict network access using virtual network rules'", + "AdditionalInformation": "This recommendation is based on the Common Reference Recommendation `Ensure Network Access Rules are set to Deny-by-default`, from the `Common Reference Recommendations > Networking > Virtual Networks (VNets)` section.", + "References": "https://docs.microsoft.com/en-us/azure/storage/common/storage-network-security:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-governance-strategy#gs-2-define-and-implement-enterprise-segmentationseparation-of-duties-strategy:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-network-security#ns-2-secure-cloud-native-services-with-network-controls", + "DefaultValue": "By default, Storage Accounts will accept connections from clients on any network." + } + ] + }, + { + "Id": "10.3.11", + "Description": "Ensure Azure Resource Manager ReadOnly locks are considered for Azure Storage Accounts", + "Checks": [], + "Attributes": [ + { + "Section": "10 Storage Services", + "SubSection": "10.3 Storage Accounts", + "Profile": "Level 2", + "AssessmentStatus": "Manual", + "Description": "Adding an Azure Resource Manager `ReadOnly` lock can prevent users from accidentally or maliciously deleting a storage account, modifying its properties and containers, or creating access assignments. The lock must be removed before the storage account can be deleted or updated. It provides more protection than a `CannotDelete`-type of resource manager lock. This feature prevents `POST` operations on a storage account and containers to the Azure Resource Manager control plane, _management.azure.com_. Blocked operations include `listKeys` which prevents clients from obtaining the account shared access keys. Microsoft does not recommend `ReadOnly` locks for storage accounts with Azure Files and Table service containers. This Azure Resource Manager REST API documentation (spec) provides information about the control plane `POST` operations for _Microsoft.Storage_ resources.", + "RationaleStatement": "Applying a `ReadOnly` lock on storage accounts protects the confidentiality and availability of data by preventing the accidental or unauthorized deletion of the entire storage account and modification of the account, container properties, or access permissions. It can offer enhanced protection for blob and queue workloads with tradeoffs in usability and compatibility for clients using account shared access keys.", + "ImpactStatement": "- Prevents the deletion of the Storage account Resource entirely. - Prevents the deletion of the parent Resource Group containing the locked Storage account resource. - Prevents clients from obtaining the storage account shared access keys using a `listKeys` operation. - Requires Entra credentials to access blob and queue data in the Portal. - Data in Azure Files or the Table service may be inaccessible to clients using the account shared access keys. - Prevents modification of account properties, network settings, containers, and RBAC assignments. - Does not prevent access using existing account shared access keys issued to clients. - Does not prevent deletion of containers or other objects within the storage account.", + "RemediationProcedure": "**Remediate from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. Under the `Settings` section, select `Locks`. 1. Select `Add`. 1. Provide a Name, and choose `ReadOnly` for the type of lock. 1. Add a note about the lock if desired. **Remediate from Azure CLI** Replace the information within <> with appropriate values: ``` az lock create --name \\ --resource-group \\ --resource \\ --lock-type ReadOnly \\ --resource-type Microsoft.Storage/storageAccounts ``` **Remediate from PowerShell** Replace the information within <> with appropriate values: ``` New-AzResourceLock -LockLevel ReadOnly ` -LockName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ` -ResourceGroupName ```", + "AuditProcedure": "**Audit from Azure Portal** 1. Navigate to the storage account in the Azure portal. 1. For each storage account, under `Settings`, click `Locks`. 1. Ensure that a `ReadOnly` lock exists on the storage account. **Audit from Azure CLI** ``` az lock list --resource-group \\ --resource-name \\ --resource-type Microsoft.Storage/storageAccounts ``` **Audit from PowerShell** ``` Get-AzResourceLock -ResourceGroupName ` -ResourceName ` -ResourceType Microsoft.Storage/storageAccounts ``` **Audit from Azure Policy** There is currently no built-in Microsoft policy to audit resource locks on storage accounts. Custom and community policy definitions can check for the existence of a “Microsoft.Authorization/locks” resource with an AuditIfNotExists effect.", + "AdditionalInformation": "", + "References": "https://learn.microsoft.com/en-us/azure/storage/common/lock-account-resource:https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources:https://github.com/Azure/azure-rest-api-specs/tree/main/specification/storage", + "DefaultValue": "By default, no locks are applied to Azure resources, including storage accounts. Locks must be manually configured after resource creation." + } + ] + } + ] +} From b051aeeb6409e1f3247e8871c532b43affdd63b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:54:01 +0200 Subject: [PATCH 23/28] chore(gha): automate e2e tests with new workflow (#8361) --- .github/workflows/ui-e2e-tests.yml | 107 ++++++++++++++++++++++++++ .github/workflows/ui-pull-request.yml | 98 ----------------------- 2 files changed, 107 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/ui-e2e-tests.yml diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml new file mode 100644 index 0000000000..08c1c89482 --- /dev/null +++ b/.github/workflows/ui-e2e-tests.yml @@ -0,0 +1,107 @@ +name: UI - E2E Tests + +on: + pull_request: + branches: + - master + - "v5.*" + paths: + - '.github/workflows/ui-e2e-tests.yml' + - 'ui/**' + +jobs: + e2e-tests: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + env: + AUTH_SECRET: 'fallback-ci-secret-for-testing' + AUTH_TRUST_HOST: true + NEXTAUTH_URL: 'http://localhost:3000' + NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1' + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Start API services + run: | + docker compose up -d api worker worker-beat + - name: Wait for API to be ready + run: | + echo "Waiting for prowler-api..." + timeout=150 # 5 minutes max + elapsed=0 + while [ $elapsed -lt $timeout ]; do + if curl -s ${NEXT_PUBLIC_API_BASE_URL}/docs >/dev/null 2>&1; then + echo "Prowler API is ready!" + exit 0 + fi + echo "Waiting for prowler-api... (${elapsed}s elapsed)" + sleep 5 + elapsed=$((elapsed + 5)) + done + echo "Timeout waiting for prowler-api to start" + exit 1 + - name: Run database migrations + run: | + echo "Running Django migrations..." + docker compose exec -T api sh -c ' + poetry run python manage.py migrate --database admin + ' + echo "Database migrations completed!" + # TODO: Delete this step once API image is built with new fixtures (Prowler 5.10.0) + - name: Copy local fixtures into API container + run: | + # Get the actual container name dynamically + CONTAINER_NAME=$(docker compose ps -q api) + docker cp ./api/src/backend/api/fixtures/dev/. ${CONTAINER_NAME}:/home/prowler/backend/api/fixtures/dev + - name: Load database fixtures for E2E tests + run: | + docker compose exec -T api sh -c ' + echo "Loading all fixtures from api/fixtures/dev/..." + for fixture in api/fixtures/dev/*.json; do + if [ -f "$fixture" ]; then + echo "Loading $fixture" + poetry run python manage.py loaddata "$fixture" --database admin + fi + done + echo "All database fixtures loaded successfully!" + ' + - name: Setup Node.js environment + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: './ui/package-lock.json' + - name: Install UI dependencies + working-directory: ./ui + run: npm ci + - name: Build UI application + working-directory: ./ui + run: npm run build + - name: Cache Playwright browsers + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('ui/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-playwright- + - name: Install Playwright browsers + working-directory: ./ui + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npm run test:e2e:install + - name: Run E2E tests + working-directory: ./ui + run: npm run test:e2e + - name: Upload test reports + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + if: failure() + with: + name: playwright-report + path: ui/playwright-report/ + retention-days: 30 + - name: Cleanup services + if: always() + run: | + echo "Shutting down services..." + docker compose down -v || true + echo "Cleanup completed" diff --git a/.github/workflows/ui-pull-request.yml b/.github/workflows/ui-pull-request.yml index ad22630e32..1925f6c230 100644 --- a/.github/workflows/ui-pull-request.yml +++ b/.github/workflows/ui-pull-request.yml @@ -46,104 +46,6 @@ jobs: working-directory: ./ui run: npm run build - e2e-tests: - runs-on: ubuntu-latest - env: - AUTH_SECRET: 'fallback-ci-secret-for-testing' - AUTH_TRUST_HOST: true - NEXTAUTH_URL: http://localhost:3000 - PROWLER_API_PORT: 8080 - NEXT_PUBLIC_API_BASE_URL: ${{ secrets.API_BASE_URL || 'http://localhost:8080/api/v1' }} - E2E_USER: ${{ secrets.E2E_USER }} - E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }} - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - name: Start needed services with docker compose - if: github.repository == 'prowler-cloud/prowler' - run: | - docker compose up -d api worker worker-beat - - name: Wait for prowler-api to respond - if: github.repository == 'prowler-cloud/prowler' - run: | - echo "Waiting for prowler-api..." - for i in {1..30}; do - if curl -s http://localhost:${PROWLER_API_PORT}/api/v1/docs >/dev/null 2>&1; then - echo "Prowler API is ready!" - break - fi - echo "Waiting for prowler-api... (attempt $i/30)" - sleep 3 - done - - name: Run database migrations - if: github.repository == 'prowler-cloud/prowler' - run: | - echo "Running Django migrations..." - docker compose exec -T api sh -c ' - poetry run python manage.py migrate --database admin - ' - echo "Database migrations completed!" - - name: Copy local fixtures into API container - if: github.repository == 'prowler-cloud/prowler' - run: | - docker cp ./api/src/backend/api/fixtures/dev/. prowler-api-1:/home/prowler/backend/api/fixtures/dev - - name: Load database fixtures for e2e tests - if: github.repository == 'prowler-cloud/prowler' - run: | - docker compose exec -T api sh -c ' - echo "Loading all fixtures from api/fixtures/dev/..." - for fixture in api/fixtures/dev/*.json; do - if [ -f "$fixture" ]; then - echo "Loading $fixture" - poetry run python manage.py loaddata "$fixture" --database admin - fi - done - echo "All database fixtures loaded successfully!" - ' - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: '20.x' - cache: 'npm' - cache-dependency-path: './ui/package-lock.json' - - name: Install dependencies - working-directory: ./ui - run: npm ci - - name: Build the application - working-directory: ./ui - run: npm run build - - name: Cache Playwright browsers - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - id: playwright-cache - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ hashFiles('ui/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-playwright- - - name: Install Playwright browsers - working-directory: ./ui - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npm run test:e2e:install - - name: Run Playwright tests - working-directory: ./ui - run: npm run test:e2e - - name: Upload Playwright report - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 - if: failure() - with: - name: playwright-report - path: ui/playwright-report/ - retention-days: 30 - - - name: Cleanup services - if: github.repository == 'prowler-cloud/prowler' - run: | - echo "Shutting down services..." - docker-compose down -v || true - echo "Cleanup completed" - test-container-build: runs-on: ubuntu-latest steps: From 285aea34589a236c52b265077269f35ada9788d2 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:45:30 +0200 Subject: [PATCH 24/28] fix(docs): change `Exchange Administrator` role to `Global Reader` for M365 (#8360) --- .../microsoft365/getting-started-m365.md | 10 ++++++---- .../img/assign-exchange-administrator-role.png | Bin 144851 -> 0 bytes .../img/assign-global-reader-role.png | Bin 0 -> 100276 bytes .../img/exchange-administrator-role.png | Bin 113935 -> 0 bytes .../microsoft365/img/global-reader-role.png | Bin 0 -> 207347 bytes 5 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 docs/tutorials/microsoft365/img/assign-exchange-administrator-role.png create mode 100644 docs/tutorials/microsoft365/img/assign-global-reader-role.png delete mode 100644 docs/tutorials/microsoft365/img/exchange-administrator-role.png create mode 100644 docs/tutorials/microsoft365/img/global-reader-role.png diff --git a/docs/tutorials/microsoft365/getting-started-m365.md b/docs/tutorials/microsoft365/getting-started-m365.md index 935e45ad5c..6bc918629c 100644 --- a/docs/tutorials/microsoft365/getting-started-m365.md +++ b/docs/tutorials/microsoft365/getting-started-m365.md @@ -156,19 +156,21 @@ To grant the permissions for the PowerShell modules via application authenticati ![Exchange.ManageAsApp Permission](./img/exchange-permission.png) - You also need to assign the `Exchange Administrator` role to the app. For that go to `Roles and administrators` and in the `Administrative roles` section click `here` to go to the directory level assignment: + You also need to assign the `Global Reader` role to the app. For that go to `Roles and administrators` and in the `Administrative roles` section click `here` to go to the directory level assignment: ![Roles and administrators](./img/here.png) - Once in the directory level assignment, search for `Exchange Administrator` and click on it to open the assginments page of that role. + Once in the directory level assignment, search for `Global Reader` and click on it to open the assginments page of that role. - ![Exchange Administrator Role](./img/exchange-administrator-role.png) + ![Global Reader Role](./img/global-reader-role.png) Click on `Add assignments`, search for your app and click on `Assign`. You have to select it as `Active` and click on `Assign` to assign the role to the app. - ![Assign Exchange Administrator Role](./img/assign-exchange-administrator-role.png) + ![Assign Global Reader Role](./img/assign-global-reader-role.png) + + For more information about the need of adding this role, see [Microsoft documentation](https://learn.microsoft.com/en-us/powershell/exchange/app-only-auth-powershell-v2?view=exchange-ps#step-5-assign-microsoft-entra-roles-to-the-application). You can select any other role of the specified. 2. Add Teams API: diff --git a/docs/tutorials/microsoft365/img/assign-exchange-administrator-role.png b/docs/tutorials/microsoft365/img/assign-exchange-administrator-role.png deleted file mode 100644 index 10e5752e8a4a287468fb89d7484cfbe56f5d2450..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144851 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