From ffb95ff88c20a74a53e1b287db975eb98bf6212e Mon Sep 17 00:00:00 2001 From: Mahfuzur Rahman Emon Date: Wed, 27 May 2026 11:54:35 +0100 Subject: [PATCH 1/3] feat: add AZ-IDN-004 PIM not configured for admin roles rule and playbook --- .../frameworks/cis_azure_benchmark.json | 7 +- compliance/frameworks/iso27001.json | 5 + compliance/frameworks/nist_csf.json | 5 + compliance/frameworks/soc2.json | 7 +- playbooks/cli/fix_az_idn_004.sh | 64 +++++++++ scanner/rules/az_idn_004.py | 122 ++++++++++++++++++ 6 files changed, 208 insertions(+), 2 deletions(-) create mode 100755 playbooks/cli/fix_az_idn_004.sh create mode 100644 scanner/rules/az_idn_004.py diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index 8377f8f..a6e4b58 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -146,7 +146,12 @@ "AZ-DB-004": { "control_id": "4.1.2", "control_name": "Ensure that 'Allow access to Azure services' for SQL Servers is disabled", - "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource — including services from other tenants — to connect to the server. This significantly increases the attack surface. Access should be restricted to specific trusted IP ranges or private endpoints." + "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource \u2014 including services from other tenants \u2014 to connect to the server. This significantly increases the attack surface. Access should be restricted to specific trusted IP ranges or private endpoints." + }, + "AZ-IDN-004": { + "control_id": "1.14", + "control_name": "Ensure that 'Privileged Identity Management' is used to manage privileged access", + "description": "Privileged Identity Management provides time-based and approval-based role activation to mitigate the risk of excessive, unnecessary, or misused access permissions on resources. Without PIM, admin roles are permanently assigned with no just-in-time controls or approval workflows." } } } \ No newline at end of file diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index ea21b47..adf3bd7 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -147,6 +147,11 @@ "control_id": "A.13.1.1", "control_name": "Network controls", "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall bypasses network controls by permitting any Azure-hosted resource to connect to the database server. Networks should be managed and controlled with explicit rules that restrict access to known and trusted sources only." + }, + "AZ-IDN-004": { + "control_id": "A.9.2.3", + "control_name": "Management of privileged access rights", + "description": "The allocation and use of privileged access rights should be restricted and controlled. PIM enforces just-in-time access with time limits and approval workflows, ensuring privileged access rights are tightly managed and not permanently assigned." } } } \ No newline at end of file diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index 28c5e8e..6a55bd8 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -147,6 +147,11 @@ "control_id": "PR.AC-3", "control_name": "Remote access is managed", "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall permits any Azure-hosted resource to connect to the database remotely without restriction. PR.AC-3 requires that remote access is managed and controlled. Access should be restricted to specific trusted IP ranges or private endpoints to ensure only authorised systems can reach the database." + }, + "AZ-IDN-004": { + "control_id": "PR.AC-4", + "control_name": "Access permissions and authorizations are managed", + "description": "PIM ensures privileged access permissions are managed with time-bound activation and approval workflows. Without PIM, permanently assigned admin roles violate the principle of least privilege and increase the blast radius of compromised accounts." } } } \ No newline at end of file diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index d320a76..75355ed 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -141,7 +141,12 @@ "AZ-DB-004": { "control_id": "CC6.6", "control_name": "Restricts Access from Outside the Network Boundary", - "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource — including services from other tenants — to connect to the database. CC6.6 requires that access from outside the network boundary is restricted to authorised sources. Disabling this setting and replacing it with explicit firewall rules or private endpoints enforces the network boundary and ensures only known and trusted systems can reach the SQL Server." + "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource \u2014 including services from other tenants \u2014 to connect to the database. CC6.6 requires that access from outside the network boundary is restricted to authorised sources. Disabling this setting and replacing it with explicit firewall rules or private endpoints enforces the network boundary and ensures only known and trusted systems can reach the SQL Server." + }, + "AZ-IDN-004": { + "control_id": "CC6.3", + "control_name": "Role-based access control", + "description": "PIM provides role-based access control with time-bound activation for privileged roles. Without PIM, admin roles are permanently assigned with no controls, violating the requirement for managed and restricted privileged access." } } } \ No newline at end of file diff --git a/playbooks/cli/fix_az_idn_004.sh b/playbooks/cli/fix_az_idn_004.sh new file mode 100755 index 0000000..8c043fc --- /dev/null +++ b/playbooks/cli/fix_az_idn_004.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Playbook: fix_az_idn_004.sh +# Rule: AZ-IDN-004 — No Privileged Identity Management for admin roles + +set -euo pipefail + +echo "========================================" +echo " AZ-IDN-004 Remediation Playbook" +echo " Enable PIM for Admin Roles" +echo "========================================" +echo "" +echo "NOTE: PIM must be configured manually in the Azure Portal." +echo "Automated PIM assignment requires Azure AD Premium P2 license." +echo "" +echo "Step 1 — Verify PIM is available" +echo " Navigate to: portal.azure.com" +echo " Go to: Entra ID > Identity Governance > Privileged Identity Management" +echo " Confirm your tenant has Azure AD Premium P2 licensing" +echo "" +echo "Step 2 — Configure PIM for each admin role" +echo " Go to: PIM > Azure AD roles > Roles" +echo " For each role listed below, click the role and select Settings:" +echo " - Global Administrator" +echo " - Privileged Role Administrator" +echo " - Security Administrator" +echo " - Exchange Administrator" +echo " - SharePoint Administrator" +echo " - Conditional Access Administrator" +echo " - Helpdesk Administrator" +echo " - User Administrator" +echo " - Application Administrator" +echo " - Cloud Application Administrator" +echo "" +echo "Step 3 — Configure each role with:" +echo " - Activation maximum duration: 8 hours or less" +echo " - Require MFA on activation: Enabled" +echo " - Require justification on activation: Enabled" +echo " - Require approval for activation: Enabled (for Global Admin)" +echo "" +echo "Step 4 — Convert permanent assignments to eligible" +echo " Go to: PIM > Azure AD roles > Assignments" +echo " For each permanent admin assignment:" +echo " Click the assignment > Update > Change to Eligible" +echo "" + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " + echo "Running in guidance-only mode — no tenant ID provided" + exit 0 +fi + +TENANT_ID="$1" + +echo "Step 5 — Verify PIM eligible assignments via CLI" +echo "Checking existing role eligibility schedules for tenant $TENANT_ID..." +az rest \ + --method GET \ + --url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules" \ + --query "value[].{role:roleDefinitionId, principal:principalId, status:status}" \ + --output table 2>/dev/null || echo "Run az login first and ensure RoleManagement.Read.Directory permission." + +echo "" +echo "Remediation guidance complete." +echo "Re-run the scanner after configuring PIM to verify compliance." diff --git a/scanner/rules/az_idn_004.py b/scanner/rules/az_idn_004.py new file mode 100644 index 0000000..dc6dec5 --- /dev/null +++ b/scanner/rules/az_idn_004.py @@ -0,0 +1,122 @@ +"""AZ-IDN-004: No Privileged Identity Management for admin roles.""" +import logging +from typing import Any, Dict, List + +RULE_ID = "AZ-IDN-004" +RULE_NAME = "No Privileged Identity Management for Admin Roles" +SEVERITY = "HIGH" +CATEGORY = "Identity" +FRAMEWORKS = {"CIS": "1.14", "NIST": "PR.AC-4", "ISO27001": "A.9.2.3", "SOC2": "CC6.3"} +DESCRIPTION = ( + "Privileged Identity Management (PIM) is not configured for one or more admin roles " + "in Entra ID. Without PIM, admin roles are permanently assigned with no just-in-time " + "access controls, approval workflows, or time-bound activation. Any compromised admin " + "account has constant unrestricted access with no time limit." +) +REMEDIATION = ( + "Enable Privileged Identity Management for all admin roles in Entra ID. " + "Navigate to: Entra ID > Identity Governance > Privileged Identity Management > " + "Azure AD roles > Settings. Configure eligible assignments with time-bound " + "activation, MFA on activation, and approval workflows for all privileged roles." +) +PLAYBOOK = "playbooks/cli/fix_az_idn_004.sh" + +logger = logging.getLogger(__name__) + +PRIVILEGED_ROLE_NAMES = { + "Global Administrator", + "Privileged Role Administrator", + "Security Administrator", + "Exchange Administrator", + "SharePoint Administrator", + "Conditional Access Administrator", + "Helpdesk Administrator", + "User Administrator", + "Application Administrator", + "Cloud Application Administrator", +} + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Detect admin roles without PIM eligible assignments configured.""" + findings: List[Dict[str, Any]] = [] + + try: + import requests + token = azure_client.credential.get_token( + "https://graph.microsoft.com/.default" + ) + headers = {"Authorization": f"Bearer {token.token}"} + + response = requests.get( + "https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions", + headers=headers, + timeout=30, + ) + response.raise_for_status() + role_definitions = response.json().get("value", []) + + except Exception as exc: + logger.error( + "AZ-IDN-004: Failed to fetch role definitions from Graph API: %s", exc + ) + logger.warning( + "AZ-IDN-004: Ensure the service principal has " + "RoleManagement.Read.Directory permission on Microsoft Graph." + ) + return findings + + try: + import requests + token = azure_client.credential.get_token( + "https://graph.microsoft.com/.default" + ) + headers = {"Authorization": f"Bearer {token.token}"} + + response = requests.get( + "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules", + headers=headers, + timeout=30, + ) + response.raise_for_status() + eligible_schedules = response.json().get("value", []) + + except Exception as exc: + logger.error( + "AZ-IDN-004: Failed to fetch PIM eligible schedules from Graph API: %s", exc + ) + return findings + + pim_protected_role_ids = { + schedule.get("roleDefinitionId", "") + for schedule in eligible_schedules + } + + for role in role_definitions: + role_name = role.get("displayName", "") + role_id = role.get("id", "") + + if role_name not in PRIVILEGED_ROLE_NAMES: + continue + + if role_id not in pim_protected_role_ids: + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": f"/roleManagement/directory/roleDefinitions/{role_id}", + "resource_name": role_name, + "resource_type": "Microsoft.Graph/roleDefinitions", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "role_id": role_id, + "role_name": role_name, + "pim_configured": False, + }, + }) + + return findings From 5d46db348dd61bed266256f3a08b0644ec585ef1 Mon Sep 17 00:00:00 2001 From: Mahfuzur Rahman Emon Date: Fri, 29 May 2026 17:18:29 +0100 Subject: [PATCH 2/3] fix: fetch Graph API token once and reuse headers for both API calls --- scanner/rules/az_idn_004.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/scanner/rules/az_idn_004.py b/scanner/rules/az_idn_004.py index dc6dec5..72cfeaf 100644 --- a/scanner/rules/az_idn_004.py +++ b/scanner/rules/az_idn_004.py @@ -43,11 +43,14 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: try: import requests + + # Fetch token once and reuse headers for both API calls token = azure_client.credential.get_token( "https://graph.microsoft.com/.default" ) headers = {"Authorization": f"Bearer {token.token}"} + # Step 1 — Get all role definitions response = requests.get( "https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions", headers=headers, @@ -56,23 +59,7 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: response.raise_for_status() role_definitions = response.json().get("value", []) - except Exception as exc: - logger.error( - "AZ-IDN-004: Failed to fetch role definitions from Graph API: %s", exc - ) - logger.warning( - "AZ-IDN-004: Ensure the service principal has " - "RoleManagement.Read.Directory permission on Microsoft Graph." - ) - return findings - - try: - import requests - token = azure_client.credential.get_token( - "https://graph.microsoft.com/.default" - ) - headers = {"Authorization": f"Bearer {token.token}"} - + # Step 2 — Get all PIM eligible role assignments response = requests.get( "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules", headers=headers, @@ -83,15 +70,21 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: except Exception as exc: logger.error( - "AZ-IDN-004: Failed to fetch PIM eligible schedules from Graph API: %s", exc + "AZ-IDN-004: Failed to fetch data from Graph API: %s", exc + ) + logger.warning( + "AZ-IDN-004: Ensure the service principal has " + "RoleManagement.Read.Directory permission on Microsoft Graph." ) return findings + # Build set of role definition IDs that have PIM eligible assignments pim_protected_role_ids = { schedule.get("roleDefinitionId", "") for schedule in eligible_schedules } + # Check each privileged role for role in role_definitions: role_name = role.get("displayName", "") role_id = role.get("id", "") From 90f51a5d2a85bbc06bccd884736f7fc8108436b5 Mon Sep 17 00:00:00 2001 From: Mahfuzur Rahman Emon Date: Fri, 29 May 2026 17:35:14 +0100 Subject: [PATCH 3/3] fix: correct malformed JSON in cis_azure_benchmark.json for AZ-IDN-004 entry --- compliance/frameworks/cis_azure_benchmark.json | 1 + 1 file changed, 1 insertion(+) diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index 2febb21..d0d636d 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -157,6 +157,7 @@ "control_id": "1.14", "control_name": "Ensure that 'Privileged Identity Management' is used to manage privileged access", "description": "Privileged Identity Management provides time-based and approval-based role activation to mitigate the risk of excessive, unnecessary, or misused access permissions on resources. Without PIM, admin roles are permanently assigned with no just-in-time controls or approval workflows." + }, "AZ-KV-005": { "control_id": "8.5", "control_name": "Ensure that the expiration date is set on all certificates",