diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index eeb7ca1..d0d636d 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -148,6 +148,16 @@ "control_name": "Ensure that Azure Key Vault Purge Protection is Enabled", "description": "Azure Key Vaults without purge protection enabled allow permanent deletion of vaults and their secrets, keys, and certificates during the soft-delete retention period. Even with soft delete enabled, a malicious insider or privileged account can purge vault objects before the retention period expires. Enabling purge protection prevents this by blocking purge operations for the full retention period." }, + "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 \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." + }, "AZ-KV-005": { "control_id": "8.5", "control_name": "Ensure that the expiration date is set on all certificates", diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index d17bc6a..c5deac2 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -157,6 +157,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." } } } diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index 4178ff8..365faae 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -157,6 +157,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." } } } diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index 342ed5d..ebf4866 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -151,7 +151,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." } } } 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..72cfeaf --- /dev/null +++ b/scanner/rules/az_idn_004.py @@ -0,0 +1,115 @@ +"""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 + + # 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, + timeout=30, + ) + response.raise_for_status() + role_definitions = response.json().get("value", []) + + # Step 2 — Get all PIM eligible role assignments + 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 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", "") + + 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