Checks/Section06-ManagementGovernance.ps1
|
# ============================================================================= # Section 6: Management and Governance Services - Custom Check Functions # CIS Microsoft Azure Foundations Benchmark v5.0.0 # ============================================================================= # Custom functions for diagnostic settings, monitoring alerts, and Application # Insights controls. Dispatched via 'Custom' CheckPattern. # Each function receives -ControlDef (hashtable) and -ResourceCache (hashtable). # ============================================================================= function Test-CIS6111-SubscriptionDiagnostics { <# .SYNOPSIS CIS 6.1.1.1 - Ensure a 'Diagnostic Setting' exists for Subscription Activity Logs. .DESCRIPTION Checks that at least one subscription-level diagnostic setting is configured using Get-AzSubscriptionDiagnosticSetting. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$ControlDef, [Parameter(Mandatory)] [hashtable]$ResourceCache ) try { $subId = (Get-AzContext -ErrorAction Stop).Subscription.Id $diagSettings = @(Get-AzSubscriptionDiagnosticSetting -SubscriptionId $subId -ErrorAction Stop) if ($diagSettings.Count -gt 0) { $settingNames = ($diagSettings | ForEach-Object { $_.Name }) -join ', ' return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "Found $($diagSettings.Count) subscription diagnostic setting(s): $settingNames" ` -TotalResources 1 -PassedResources 1 -FailedResources 0 } return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details "No diagnostic settings configured for the subscription activity log." ` -AffectedResources @("Subscription: $subId") ` -TotalResources 1 -PassedResources 0 -FailedResources 1 } catch { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'ERROR' ` -Details "Error checking subscription diagnostic settings: $(Format-CISErrorMessage -Message $_.Exception.Message)" } } function Test-CIS6112-DiagnosticCategories { <# .SYNOPSIS CIS 6.1.1.2 - Ensure Diagnostic Setting captures appropriate categories. .DESCRIPTION Checks that subscription diagnostic settings have Administrative, Alert, Policy, and Security categories enabled. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$ControlDef, [Parameter(Mandatory)] [hashtable]$ResourceCache ) try { $requiredCategories = @('Administrative', 'Alert', 'Policy', 'Security') $subId = (Get-AzContext -ErrorAction Stop).Subscription.Id $diagSettings = @(Get-AzSubscriptionDiagnosticSetting -SubscriptionId $subId -ErrorAction Stop) if ($diagSettings.Count -eq 0) { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details "No subscription diagnostic settings found. Required categories: $($requiredCategories -join ', ')" ` -AffectedResources @("Subscription: $subId") ` -TotalResources 1 -PassedResources 0 -FailedResources 1 } # Check if at least one diagnostic setting covers all required categories $allCategoriesCovered = $false $bestCoverage = @() foreach ($setting in $diagSettings) { $enabledCategories = @() # Support both $setting.Logs (newer Az.Monitor) and $setting.Log (older Az.Monitor) $logEntries = if ($setting.Logs) { $setting.Logs } elseif ($setting.Log) { $setting.Log } else { $null } if ($logEntries) { $enabledCategories = @($logEntries | Where-Object { $_.Enabled -eq $true } | ForEach-Object { $_.Category }) } $missingForSetting = @($requiredCategories | Where-Object { $_ -notin $enabledCategories }) if ($missingForSetting.Count -eq 0) { $allCategoriesCovered = $true break } if ($enabledCategories.Count -gt $bestCoverage.Count) { $bestCoverage = $enabledCategories } } if ($allCategoriesCovered) { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "Subscription diagnostic settings capture all required categories: $($requiredCategories -join ', ')." ` -TotalResources 1 -PassedResources 1 -FailedResources 0 } $missingCategories = @($requiredCategories | Where-Object { $_ -notin $bestCoverage }) return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details "Subscription diagnostic settings do not capture all required categories. Missing: $($missingCategories -join ', '). Required: $($requiredCategories -join ', ')." ` -AffectedResources @("Missing categories: $($missingCategories -join ', ')") ` -TotalResources 1 -PassedResources 0 -FailedResources 1 } catch { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'ERROR' ` -Details "Error checking diagnostic categories: $(Format-CISErrorMessage -Message $_.Exception.Message)" } } function Test-CIS6114-KeyVaultLogging { <# .SYNOPSIS CIS 6.1.1.4 - Ensure that logging for Azure Key Vault is 'Enabled'. .DESCRIPTION For each Key Vault, checks that a diagnostic setting exists with audit logs enabled. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$ControlDef, [Parameter(Mandatory)] [hashtable]$ResourceCache ) try { $keyVaults = @($ResourceCache.KeyVaults) if ($keyVaults.Count -eq 0) { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "N/A - No Key Vaults found in the subscription. Control not evaluated." ` -TotalResources 0 -PassedResources 0 -FailedResources 0 } $totalCount = $keyVaults.Count $failedList = [System.Collections.Generic.List[string]]::new() $passedCount = 0 foreach ($kv in $keyVaults) { try { $resourceId = $kv.ResourceId if (-not $resourceId) { # Build resource ID from available properties $resourceId = "/subscriptions/$((Get-AzContext).Subscription.Id)/resourceGroups/$($kv.ResourceGroupName)/providers/Microsoft.KeyVault/vaults/$($kv.VaultName)" } $diagSettings = @(Get-AzDiagnosticSetting -ResourceId $resourceId -ErrorAction Stop) if ($diagSettings.Count -eq 0) { $failedList.Add("$($kv.VaultName) (no diagnostic settings)") continue } # CIS requires both 'audit' and 'allLogs' category groups to be enabled. # Also accept the legacy 'AuditEvent' category for older Az.Monitor modules. $hasAudit = $false $hasAllLogs = $false foreach ($setting in $diagSettings) { # Support both $setting.Logs (newer Az.Monitor) and $setting.Log (older Az.Monitor) $logEntries = if ($setting.Logs) { $setting.Logs } elseif ($setting.Log) { $setting.Log } else { $null } if ($logEntries) { $auditLog = $logEntries | Where-Object { ($_.Category -eq 'AuditEvent' -or $_.Category -eq 'audit') -and $_.Enabled -eq $true } if ($auditLog) { $hasAudit = $true } } # Check category groups (newer approach) if ($setting.LogCategoryGroup) { foreach ($group in $setting.LogCategoryGroup) { if ($group.Enabled -eq $true) { if ($group.CategoryGroup -eq 'audit') { $hasAudit = $true } if ($group.CategoryGroup -eq 'allLogs') { $hasAllLogs = $true } } } } } if ($hasAudit -and $hasAllLogs) { $passedCount++ } elseif ($hasAudit) { # audit is enabled but allLogs is not — partial compliance $failedList.Add("$($kv.VaultName) (missing 'allLogs' category group)") } else { $failedList.Add("$($kv.VaultName) (audit logs not enabled)") } } catch { $failedList.Add("$($kv.VaultName) [Error: $(Format-CISErrorMessage -Message $_.Exception.Message)]") } } $failedCount = $failedList.Count if ($failedCount -gt 0) { $details = "Found $failedCount of $totalCount Key Vault(s) without proper audit logging: $($failedList -join '; ')" return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details $details ` -AffectedResources $failedList.ToArray() ` -TotalResources $totalCount ` -PassedResources $passedCount ` -FailedResources $failedCount } return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "All $totalCount Key Vault(s) have audit logging enabled." ` -TotalResources $totalCount ` -PassedResources $passedCount ` -FailedResources 0 } catch { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'ERROR' ` -Details "Error checking Key Vault logging: $(Format-CISErrorMessage -Message $_.Exception.Message)" } } function Test-CIS6116-AppServiceHTTPLogs { <# .SYNOPSIS CIS 6.1.1.6 - Ensure that logging for Azure AppService 'HTTP logs' is enabled. .DESCRIPTION Gets App Services and checks that each has a diagnostic setting with HTTP logs enabled. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$ControlDef, [Parameter(Mandatory)] [hashtable]$ResourceCache ) try { # Retrieve web apps - use cache if available, otherwise fetch $webApps = @($ResourceCache.WebApps) if ($webApps.Count -eq 0) { try { $webApps = @(Get-AzWebApp -ErrorAction Stop) } catch { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'ERROR' ` -Details "Failed to retrieve App Services: $(Format-CISErrorMessage -Message $_.Exception.Message)" } } if ($webApps.Count -eq 0) { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "N/A - No App Services found in the subscription. Control not evaluated." ` -TotalResources 0 -PassedResources 0 -FailedResources 0 } $totalCount = $webApps.Count $failedList = [System.Collections.Generic.List[string]]::new() $passedCount = 0 foreach ($app in $webApps) { try { $diagSettings = @(Get-AzDiagnosticSetting -ResourceId $app.Id -ErrorAction Stop) if ($diagSettings.Count -eq 0) { $failedList.Add("$($app.Name) (no diagnostic settings)") continue } $hasHttpLogs = $false foreach ($setting in $diagSettings) { # Support both $setting.Logs (newer Az.Monitor) and $setting.Log (older Az.Monitor) $logEntries = if ($setting.Logs) { $setting.Logs } elseif ($setting.Log) { $setting.Log } else { $null } if ($logEntries) { $httpLog = $logEntries | Where-Object { ($_.Category -eq 'AppServiceHTTPLogs' -or $_.Category -eq 'HttpLogs') -and $_.Enabled -eq $true } if ($httpLog) { $hasHttpLogs = $true break } } if ($setting.LogCategoryGroup) { $logGroup = $setting.LogCategoryGroup | Where-Object { $_.CategoryGroup -eq 'allLogs' -and $_.Enabled -eq $true } if ($logGroup) { $hasHttpLogs = $true break } } } if ($hasHttpLogs) { $passedCount++ } else { $failedList.Add("$($app.Name) (HTTP logs not enabled)") } } catch { $failedList.Add("$($app.Name) [Error: $(Format-CISErrorMessage -Message $_.Exception.Message)]") } } $failedCount = $failedList.Count if ($failedCount -gt 0) { $details = "Found $failedCount of $totalCount App Service(s) without HTTP logging: $($failedList -join '; ')" return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details $details ` -AffectedResources $failedList.ToArray() ` -TotalResources $totalCount ` -PassedResources $passedCount ` -FailedResources $failedCount } return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "All $totalCount App Service(s) have HTTP logging enabled." ` -TotalResources $totalCount ` -PassedResources $passedCount ` -FailedResources 0 } catch { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'ERROR' ` -Details "Error checking App Service HTTP logs: $(Format-CISErrorMessage -Message $_.Exception.Message)" } } function Test-CIS61211-ServiceHealthAlert { <# .SYNOPSIS CIS 6.1.2.11 - Ensure an Activity Log Alert exists for Service Health. .DESCRIPTION Checks for activity log alerts where the category is 'ServiceHealth', which monitors Azure service issues, planned maintenance, and security advisories. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$ControlDef, [Parameter(Mandatory)] [hashtable]$ResourceCache ) try { $alerts = @($ResourceCache.ActivityLogAlerts) if ($alerts.Count -eq 0) { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details "No activity log alerts found in the subscription. A Service Health alert is required." ` -TotalResources 0 -PassedResources 0 -FailedResources 1 } $serviceHealthAlerts = [System.Collections.Generic.List[string]]::new() foreach ($alert in $alerts) { if (-not $alert.Enabled) { continue } $isServiceHealth = $false # Check conditions/allOf for category = ServiceHealth $conditions = $null if ($alert.Condition -and $alert.Condition.AllOf) { $conditions = $alert.Condition.AllOf } elseif ($alert.ConditionAllOf) { $conditions = $alert.ConditionAllOf } if ($conditions) { foreach ($condition in $conditions) { $field = if ($condition.Field) { $condition.Field } else { $condition.field } $equals = if ($condition.Equals) { $condition.Equals } else { $condition.equals } if ($field -eq 'category' -and $equals -eq 'ServiceHealth') { $isServiceHealth = $true break } } } if ($isServiceHealth) { $serviceHealthAlerts.Add($alert.Name) } } if ($serviceHealthAlerts.Count -gt 0) { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "Found $($serviceHealthAlerts.Count) Service Health alert(s): $($serviceHealthAlerts -join ', ')" ` -TotalResources $serviceHealthAlerts.Count ` -PassedResources $serviceHealthAlerts.Count ` -FailedResources 0 } return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details "No activity log alert found with category 'ServiceHealth'. $($alerts.Count) alert(s) were checked." ` -AffectedResources @("No Service Health alert configured") ` -TotalResources $alerts.Count -PassedResources 0 -FailedResources 1 } catch { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'ERROR' ` -Details "Error checking Service Health alerts: $(Format-CISErrorMessage -Message $_.Exception.Message)" } } function Test-CIS6131-ApplicationInsights { <# .SYNOPSIS CIS 6.1.3.1 - Ensure Application Insights are Configured. .DESCRIPTION Checks if Application Insights resources exist in the subscription via Get-AzResource for microsoft.insights/components. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$ControlDef, [Parameter(Mandatory)] [hashtable]$ResourceCache ) try { $appInsights = @(Get-AzResource -ResourceType 'microsoft.insights/components' -ErrorAction Stop) if ($appInsights.Count -gt 0) { $names = ($appInsights | ForEach-Object { $_.Name }) -join ', ' return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'PASS' ` -Details "Found $($appInsights.Count) Application Insights resource(s): $names" ` -TotalResources $appInsights.Count ` -PassedResources $appInsights.Count ` -FailedResources 0 } return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'FAIL' ` -Details "No Application Insights resources found in the subscription. Application monitoring should be configured." ` -AffectedResources @("No Application Insights configured") ` -TotalResources 0 -PassedResources 0 -FailedResources 1 } catch { return New-CISCheckResult ` -ControlId $ControlDef.ControlId ` -Title $ControlDef.Title ` -Status 'ERROR' ` -Details "Error checking Application Insights: $(Format-CISErrorMessage -Message $_.Exception.Message)" } } |