Checks/Section8.ps1
|
# Section 8 — Security Services # CIS Microsoft Azure Foundations Benchmark v6.0.0 function Invoke-Section8Checks { [CmdletBinding()] param( [string]$SubscriptionId, [string]$SubscriptionName, [hashtable]$PrefetchData ) $results = [System.Collections.Generic.List[object]]::new() $sid = $SubscriptionId $sname = $SubscriptionName $keyvaults = @(Get-PrefetchData -PrefetchData $PrefetchData -Key "keyvaults" -SubscriptionId $sid) $vnets = @(Get-PrefetchData -PrefetchData $PrefetchData -Key "vnets" -SubscriptionId $sid) $vms = @(Get-PrefetchData -PrefetchData $PrefetchData -Key "vms" -SubscriptionId $sid) $bastion = @(Get-PrefetchData -PrefetchData $PrefetchData -Key "bastion" -SubscriptionId $sid) # Prefetch failure sentinels — dependent checks emit ERROR, never a false INFO/PASS $kvErr = Get-PrefetchError -PrefetchData $PrefetchData -Key "keyvaults" $vnetErr = Get-PrefetchError -PrefetchData $PrefetchData -Key "vnets" $vmErr = Get-PrefetchError -PrefetchData $PrefetchData -Key "vms" $bastionErr = Get-PrefetchError -PrefetchData $PrefetchData -Key "bastion" # ── 8.1.x — Microsoft Defender plans ───────────────────────────────────── $defenderPlans = @( @{ Id="8.1.1.1"; Plan="CloudPosture" } @{ Id="8.1.2.1"; Plan="Api" } @{ Id="8.1.3.1"; Plan="VirtualMachines" } @{ Id="8.1.4.1"; Plan="Containers" } @{ Id="8.1.5.1"; Plan="StorageAccounts" } @{ Id="8.1.6.1"; Plan="AppServices" } @{ Id="8.1.7.1"; Plan="CosmosDbs" } @{ Id="8.1.7.2"; Plan="OpenSourceRelationalDatabases" } @{ Id="8.1.7.3"; Plan="SqlServers" } @{ Id="8.1.7.4"; Plan="SqlServerVirtualMachines" } @{ Id="8.1.8.1"; Plan="KeyVaults" } @{ Id="8.1.9.1"; Plan="Arm" } ) foreach ($plan in $defenderPlans) { try { $pricing = Get-AzSecurityPricing -Name $plan.Plan -ErrorAction Stop $tier = [string]$pricing.PricingTier $enabled = $tier -eq "Standard" $results.Add((New-CISResult ` -ControlId $plan.Id ` -Status $(if ($enabled) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($enabled) { "Microsoft Defender pricing tier: Standard (enabled)." } else { "Microsoft Defender pricing tier: $tier — must be upgraded to Standard." }) ` -Remediation $(if (-not $enabled) { "Defender for Cloud > Environment Settings > $($plan.Plan) > Enable" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) } catch { $results.Add((New-ErrorResult $plan.Id $_.Exception.Message $sid $sname)) } } # ── 8.1.3.3 — WDATP / MDE integration ──────────────────────────────────── try { $url = "https://management.azure.com/subscriptions/$sid/providers/Microsoft.Security/settings/WDATP?api-version=2021-06-01" $r = Invoke-ArmRest -Uri $url if (-not $r.Success) { $results.Add((New-ErrorResult "8.1.3.3" $r.Error $sid $sname)) } else { $enabled = [string]$r.Data.properties.enabled -eq "True" -or [string]$r.Data.properties.enabled -eq "true" $results.Add((New-CISResult ` -ControlId "8.1.3.3" ` -Status $(if ($enabled) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($enabled) { "Microsoft Defender for Endpoint integration: Enabled." } else { "Microsoft Defender for Endpoint integration: Disabled." }) ` -Remediation $(if (-not $enabled) { "Defender for Cloud > Environment Settings > Integrations > Enable MDE integration" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) } } catch { $results.Add((New-ErrorResult "8.1.3.3" $_.Exception.Message $sid $sname)) } # ── 8.1.10 — MDE TVM VM OS update check ────────────────────────────────── try { $url = "https://management.azure.com/subscriptions/$sid/providers/Microsoft.Security/serverVulnerabilityAssessmentsSettings?api-version=2023-05-01" $r = Invoke-ArmRest -Uri $url if (-not $r.Success) { $results.Add((New-ErrorResult "8.1.10" $r.Error $sid $sname)) } else { $settingsArr = $r.Data.PSObject.Properties['value']?.Value $settings = if ($settingsArr) { @($settingsArr) } else { @() } $enabled = @($settings | Where-Object { $props = $_.PSObject.Properties['properties']?.Value $props -and [string]($props.PSObject.Properties['selectedProvider']?.Value) -eq "MdeTvm" }).Count -gt 0 $results.Add((New-CISResult ` -ControlId "8.1.10" ` -Status $(if ($enabled) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($enabled) { "MDE TVM vulnerability assessment: enabled." } else { "MDE TVM vulnerability assessment: NOT enabled." }) ` -Remediation $(if (-not $enabled) { "Defender for Cloud > Environment settings > VM vulnerability assessment: Microsoft Defender Vulnerability Management." } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) } } catch { $results.Add((New-ErrorResult "8.1.10" $_.Exception.Message $sid $sname)) } # ── 8.1.12–8.1.15 — Security contact notifications (preview REST API) ────── # Uses 2023-12-01-preview directly — it returns all data needed for all 4 checks. try { $previewUrl = "https://management.azure.com/subscriptions/$sid/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview" $r2 = Invoke-ArmRest -Uri $previewUrl if (-not $r2.Success) { throw ([string]$r2.Error) } $contactItems = @() if ($r2.Data) { $valProp = $r2.Data.PSObject.Properties['value'] if ($valProp) { $contactItems = @($valProp.Value) } } # 8.1.12 — notificationsByRole: state must be "On" and roles must contain "Owner" $ownersNotified = @($contactItems | Where-Object { $props = $_.PSObject.Properties['properties']?.Value if (-not $props) { return $false } $nbr = $props.PSObject.Properties['notificationsByRole']?.Value if (-not $nbr) { return $false } $state = [string]($nbr.PSObject.Properties['state']?.Value) if ($state -ne "On") { return $false } $roleList = $nbr.PSObject.Properties['roles']?.Value if (-not $roleList) { return $false } ($roleList | ForEach-Object { [string]$_ }) -contains "Owner" }).Count -gt 0 $results.Add((New-CISResult ` -ControlId "8.1.12" ` -Status $(if ($ownersNotified) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($ownersNotified) { "Role notifications On with Owner role configured." } else { "Role notifications are Off or Owner role NOT configured." }) ` -Remediation $(if (-not $ownersNotified) { "Defender for Cloud > Environment Settings > Email notifications > All users with Owner role." } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) # 8.1.13 — properties.emails must be non-empty $hasEmail = @($contactItems | Where-Object { $props = $_.PSObject.Properties['properties']?.Value if (-not $props) { return $false } $e = $props.PSObject.Properties['emails']?.Value [string]$e -ne "" }).Count -gt 0 $results.Add((New-CISResult ` -ControlId "8.1.13" ` -Status $(if ($hasEmail) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($hasEmail) { "Additional email address(es) configured." } else { "No additional email addresses configured." }) ` -Remediation $(if (-not $hasEmail) { "Defender for Cloud > Environment Settings > Email notifications > Additional email addresses." } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) # 8.1.14 — notificationsSources must contain a sourceType "Alert" entry with a # minimalSeverity set (v6 audit: state On + minimalSeverity at an appropriate level). # notificationsByRole.state governs role notifications (8.1.12), not alert severity. $alertSeverity = $null foreach ($ci in $contactItems) { $props = $ci.PSObject.Properties['properties']?.Value if (-not $props) { continue } $srcProp = $props.PSObject.Properties['notificationsSources'] if (-not $srcProp) { continue } foreach ($src in @($srcProp.Value)) { if ([string]($src.PSObject.Properties['sourceType']?.Value) -eq "Alert") { $sev = [string]($src.PSObject.Properties['minimalSeverity']?.Value) if ($sev) { $alertSeverity = $sev; break } } } if ($alertSeverity) { break } } $alertOn = [bool]$alertSeverity $results.Add((New-CISResult ` -ControlId "8.1.14" ` -Status $(if ($alertOn) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($alertOn) { "Alert notifications enabled (minimal severity: $alertSeverity)." } else { "Alert severity notifications are not enabled." }) ` -Remediation $(if (-not $alertOn) { "Defender for Cloud > Environment Settings > Email notifications > Notify about alerts with severity: High." } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) # 8.1.15 — notificationsSources must contain sourceType "AttackPath" with a # minimalRiskLevel set (v6 audit: sourceType AttackPath + minimalRiskLevel). $attackRisk = $null foreach ($ci in $contactItems) { $props = $ci.PSObject.Properties['properties']?.Value if (-not $props) { continue } $srcProp = $props.PSObject.Properties['notificationsSources'] if (-not $srcProp) { continue } foreach ($src in @($srcProp.Value)) { if ([string]($src.PSObject.Properties['sourceType']?.Value) -eq "AttackPath") { $risk = [string]($src.PSObject.Properties['minimalRiskLevel']?.Value) if ($risk) { $attackRisk = $risk; break } } } if ($attackRisk) { break } } $attackOn = [bool]$attackRisk $results.Add((New-CISResult ` -ControlId "8.1.15" ` -Status $(if ($attackOn) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($attackOn) { "Attack path notifications enabled (minimal risk level: $attackRisk)." } else { "Attack path notifications NOT configured." }) ` -Remediation $(if (-not $attackOn) { "Defender for Cloud > Environment Settings > Email notifications > Notify about attack paths with risk level: Critical." } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) } catch { foreach ($cid in @("8.1.12","8.1.13","8.1.14","8.1.15")) { $results.Add((New-ErrorResult $cid $_.Exception.Message $sid $sname)) } } # ── 8.3.x — Key Vault checks ────────────────────────────────────────────── # Static properties (purge protection, RBAC mode, network access) come from # Resource Graph prefetch data. Data-plane checks (key/secret/certificate # enumeration) require the Az.KeyVault cmdlets which call the vault's data-plane # endpoint directly — this is separate from the ARM control-plane used above. # # CIS controls are numbered differently depending on whether the vault uses RBAC # authorization (8.3.1/8.3.3) vs. legacy vault access policies (8.3.2/8.3.4). # The $rbac flag from prefetch data selects the correct control ID at runtime. # Per-vault 8.3.x block (8.3.10 is manual, emitted in tenant checks) $kvControlIds = @("8.3.1","8.3.2","8.3.3","8.3.4","8.3.5","8.3.6","8.3.7","8.3.8","8.3.9","8.3.11") if ($kvErr) { foreach ($cid in $kvControlIds) { $results.Add((New-ErrorResult $cid "Key Vault prefetch failed: $kvErr" $sid $sname)) } } elseif ($keyvaults.Count -eq 0) { foreach ($cid in $kvControlIds) { $results.Add((New-InfoResult $cid "No Key Vaults found." $sid $sname)) } } else { foreach ($kv in $keyvaults) { $kvName = [string]$kv.name # Static checks from Resource Graph data $purge = [string]$kv.purgeProtection -eq "True" -or [string]$kv.purgeProtection -eq "true" $rbac = [string]$kv.rbac -eq "True" -or [string]$kv.rbac -eq "true" $pub = [string]$kv.publicAccess $eps = [int]($kv.privateEps) $ctrlKeyExp = if ($rbac) { "8.3.1" } else { "8.3.2" } $cachedKeys = $null try { # Filter out certificate-backed keys (Managed = $true) — those are managed by the # certificate lifecycle and will be excluded by default in Az.KeyVault 7.0 / Az 16. $allKeys = @(Invoke-WithRetry -OperationName "list keys ($kvName)" -ScriptBlock { Get-AzKeyVaultKey -VaultName $kvName -WarningAction SilentlyContinue -ErrorAction Stop } | Where-Object { -not ($_.PSObject.Properties['Managed'] -and $_.Managed) }) $cachedKeys = $allKeys if ($allKeys.Count -eq 0) { $results.Add((New-InfoResult $ctrlKeyExp "No keys found in vault." $sid $sname $kvName)) } else { $noExpiry = @($allKeys | Where-Object { -not ($_.PSObject.Properties['Attributes'] -and $null -ne $_.Attributes -and $_.Attributes.Expires) }) $pass = $noExpiry.Count -eq 0 $results.Add((New-CISResult ` -ControlId $ctrlKeyExp ` -Status $(if ($pass) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($pass) { "All $($allKeys.Count) key(s) have expiration set." } else { "$($noExpiry.Count) key(s) without expiration: $(($noExpiry | ForEach-Object { $_.Name }) -join ', ')" }) ` -Remediation $(if (-not $pass) { "Key Vault > $kvName > Keys > Set expiration date on each key" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) } } catch { Add-ClassifiedErrorSet -Results $results -ControlIds @($ctrlKeyExp) -Message $_.Exception.Message ` -AuthzMessage "Insufficient permissions to list keys. Grant the 'Key Vault Reader' data-plane role or a Key Vault access policy with Key List permission." ` -FirewallMessage "Key Vault firewall is blocking access. Add the audit machine's IP to the vault's firewall allowlist." ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName } # 8.3.3/8.3.4 — Secret expiration set (RBAC vs access-policy vault) $ctrlSecExp = if ($rbac) { "8.3.3" } else { "8.3.4" } try { # Filter out certificate-backed secrets (Managed = $true) — those are managed by # the certificate lifecycle and will be excluded by default in Az.KeyVault 7.0 / Az 16. $allSecrets = @(Invoke-WithRetry -OperationName "list secrets ($kvName)" -ScriptBlock { Get-AzKeyVaultSecret -VaultName $kvName -WarningAction SilentlyContinue -ErrorAction Stop } | Where-Object { -not ($_.PSObject.Properties['Managed'] -and $_.Managed) }) if ($allSecrets.Count -eq 0) { $results.Add((New-InfoResult $ctrlSecExp "No secrets found in vault." $sid $sname $kvName)) } else { $noExpiry = @($allSecrets | Where-Object { -not ($_.PSObject.Properties['Attributes'] -and $null -ne $_.Attributes -and $_.Attributes.Expires) }) $pass = $noExpiry.Count -eq 0 $results.Add((New-CISResult ` -ControlId $ctrlSecExp ` -Status $(if ($pass) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($pass) { "All $($allSecrets.Count) secret(s) have expiration set." } else { "$($noExpiry.Count) secret(s) without expiration: $(($noExpiry | ForEach-Object { $_.Name }) -join ', ')" }) ` -Remediation $(if (-not $pass) { "Key Vault > $kvName > Secrets > Set expiration date" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) } } catch { Add-ClassifiedErrorSet -Results $results -ControlIds @($ctrlSecExp) -Message $_.Exception.Message ` -AuthzMessage "Insufficient permissions to list secrets. Grant the 'Key Vault Reader' data-plane role or a Key Vault access policy with Secret List permission." ` -FirewallMessage "Key Vault firewall is blocking access. Add the audit machine's IP to the vault's firewall allowlist." ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName } # 8.3.11 — Certificate validity <= 12 months try { $allCerts = @(Invoke-WithRetry -OperationName "list certificates ($kvName)" -ScriptBlock { Get-AzKeyVaultCertificate -VaultName $kvName -ErrorAction Stop }) if ($allCerts.Count -eq 0) { $results.Add((New-InfoResult "8.3.11" "No certificates found." $sid $sname $kvName)) } else { $longCerts = @($allCerts | Where-Object { $attr = if ($_.PSObject.Properties['Attributes'] -and $null -ne $_.Attributes) { $_.Attributes } else { $null } $exp = if ($null -ne $attr -and $attr.PSObject.Properties['Expires']) { $attr.Expires } else { $null } $crt = if ($null -ne $attr -and $attr.PSObject.Properties['Created']) { $attr.Created } else { $null } if (-not $exp -or -not $crt) { return $true } ($exp - $crt).TotalDays -gt 366 }) $pass = $longCerts.Count -eq 0 $results.Add((New-CISResult ` -ControlId "8.3.11" ` -Status $(if ($pass) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($pass) { "All certificates have valid (<= 12 month) lifetimes." } else { "$($longCerts.Count) certificate(s) with lifetime > 12 months: $(($longCerts | ForEach-Object { $_.Name }) -join ', ')" }) ` -Remediation $(if (-not $pass) { "Key Vault > $kvName > Certificates > Issuance policy > Validity <= 12 months" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) } } catch { Add-ClassifiedErrorSet -Results $results -ControlIds @("8.3.11") -Message $_.Exception.Message ` -AuthzMessage "Insufficient permissions to list certificates. Grant the 'Key Vault Reader' data-plane role or a Key Vault access policy with Certificate List permission." ` -FirewallMessage "Key Vault firewall is blocking access. Add the audit machine's IP to the vault's firewall allowlist." ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName } # 8.3.6 — RBAC authorization enabled $results.Add((New-CISResult ` -ControlId "8.3.6" ` -Status $(if ($rbac) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($rbac) { "RBAC authorization model enabled." } else { "Using legacy Vault Access Policy, not RBAC." }) ` -Remediation $(if (-not $rbac) { "Key Vault > $kvName > Access configuration > Permission model > Azure RBAC" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) # 8.3.5 — Purge protection enabled $results.Add((New-CISResult ` -ControlId "8.3.5" ` -Status $(if ($purge) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($purge) { "Purge protection enabled." } else { "Purge protection not enabled." }) ` -Remediation $(if (-not $purge) { "Key Vault > $kvName > Properties > Enable purge protection" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) # 8.3.7 — Public network access disabled $pubOk = $pub -eq "Disabled" $results.Add((New-CISResult ` -ControlId "8.3.7" ` -Status $(if ($pubOk) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($pubOk) { "Public network access: Disabled." } else { "Public network access: $pub." }) ` -Remediation $(if (-not $pubOk) { "Key Vault > $kvName > Networking > Allow access from > Disable public access" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) # 8.3.8 — Private endpoints configured $results.Add((New-CISResult ` -ControlId "8.3.8" ` -Status $(if ($eps -gt 0) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($eps -gt 0) { "Private endpoint(s) configured: $eps." } else { "No private endpoints configured." }) ` -Remediation $(if ($eps -eq 0) { "Key Vault > $kvName > Networking > Private endpoint connections > Add" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) # 8.3.9 — Automatic key rotation policy set (reuse cached key list from 8.3.1/8.3.2) try { $keys = if ($cachedKeys) { $cachedKeys } else { @(Get-AzKeyVaultKey -VaultName $kvName -WarningAction SilentlyContinue -ErrorAction Stop | Where-Object { -not ($_.PSObject.Properties['Managed'] -and $_.Managed) }) } if ($keys.Count -gt 0) { $noRotation = [System.Collections.Generic.List[string]]::new() foreach ($key in $keys) { $keyName = [string]$key.Name $policy = Get-AzKeyVaultKeyRotationPolicy -VaultName $kvName -Name $keyName -ErrorAction SilentlyContinue if ($policy) { $hasRotate = $false foreach ($la in @($policy.LifetimeActions)) { if ($la.Action -eq 'Rotate') { $hasRotate = $true; break } } if (-not $hasRotate) { $noRotation.Add($keyName) } } else { $noRotation.Add($keyName) } } $pass = $noRotation.Count -eq 0 $results.Add((New-CISResult ` -ControlId "8.3.9" ` -Status $(if ($pass) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($pass) { "All keys have automatic rotation configured." } else { "Keys without automatic rotation configured: $($noRotation -join ', ')" }) ` -Remediation $(if (-not $pass) { "Key Vault > $kvName > Keys > Rotation policy > Set rotation action" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName)) } else { $results.Add((New-InfoResult "8.3.9" "No keys found in vault." $sid $sname $kvName)) } } catch { Add-ClassifiedErrorSet -Results $results -ControlIds @("8.3.9") -Message $_.Exception.Message ` -AuthzMessage "Insufficient permissions to list keys. Grant the 'Key Vault Reader' data-plane role or a Key Vault access policy with Key List permission." ` -FirewallMessage "Key Vault firewall is blocking access. Add the audit machine's IP to the vault's firewall allowlist." ` -SubscriptionId $sid -SubscriptionName $sname -Resource $kvName } } } # ── 8.4.1 — Azure Bastion deployed (if VMs exist) ───────────────────────── if ($vmErr -or $bastionErr) { $bastionMsg = if ($vmErr) { "VM prefetch failed: $vmErr" } else { "Bastion prefetch failed: $bastionErr" } $results.Add((New-ErrorResult "8.4.1" $bastionMsg $sid $sname)) } elseif ($vms.Count -eq 0) { $results.Add((New-InfoResult "8.4.1" "No VMs found in subscription." $sid $sname)) } else { $hasBastion = $bastion.Count -gt 0 $results.Add((New-CISResult ` -ControlId "8.4.1" ` -Status $(if ($hasBastion) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($hasBastion) { "Azure Bastion found: $(($bastion | ForEach-Object { [string]$_.name }) -join ', ')" } else { "No Azure Bastion found. $($vms.Count) VM(s) present." }) ` -Remediation $(if (-not $hasBastion) { "Create an Azure Bastion host in a dedicated AzureBastionSubnet for secure VM access." } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname)) } # ── 8.5 — DDoS Network Protection on VNets ─────────────────────────────── if ($vnetErr) { $results.Add((New-ErrorResult "8.5" "VNet prefetch failed: $vnetErr" $sid $sname)) } elseif ($vnets.Count -eq 0) { $results.Add((New-InfoResult "8.5" "No VNets found." $sid $sname)) } else { foreach ($vnet in $vnets) { $hasDdos = [string]$vnet.hasDdos -in @("True", "true", "1") $results.Add((New-CISResult ` -ControlId "8.5" ` -Status $(if ($hasDdos) { $script:PASS } else { $script:FAIL }) ` -Details $(if ($hasDdos) { "DDoS Network Protection enabled." } else { "DDoS Network Protection not enabled." }) ` -Remediation $(if (-not $hasDdos) { "VNet > $([string]$vnet.name) > DDoS protection > Enable DDoS Network Protection plan" } else { "" }) ` -SubscriptionId $sid -SubscriptionName $sname -Resource ([string]$vnet.name))) } } return $results.ToArray() } function Invoke-Section8TenantChecks { <# .SYNOPSIS Section 8 controls v6 defines as Manual (no reliable automated read) — surfaced once so the report mirrors the benchmark's control set. #> $manual = @( @{ Id="8.1.3.2"; Msg="Manual verification required — Defender for Cloud > Environment settings > Defender for Servers > ensure the 'Vulnerability assessment for machines' component status is 'On'." } @{ Id="8.1.3.4"; Msg="Manual verification required — Defender for Cloud > Environment settings > Defender for Servers > ensure the 'Agentless scanning for machines' component status is 'On'." } @{ Id="8.1.3.5"; Msg="Manual verification required — Defender for Cloud > Environment settings > Defender for Servers > ensure the 'File Integrity Monitoring' component status is 'On'." } @{ Id="8.1.5.2"; Msg="Manual verification required — confirm that Microsoft Defender for Storage threat-protection alerts are reviewed and actioned by a responsible team." } @{ Id="8.1.11"; Msg="Manual verification required — Defender for Cloud > Environment settings > Security policy > ensure non-deprecated Microsoft Cloud Security Benchmark policy effects are not set to 'Disabled'." } @{ Id="8.1.16"; Msg="Manual verification required — confirm a Defender EASM workspace is deployed and monitoring the organisation's external attack surface." } @{ Id="8.2.1"; Msg="Manual verification required — for each IoT Hub, ensure Microsoft Defender for IoT is enabled." } @{ Id="8.3.10"; Msg="Manual verification required — where FIPS 140-2 Level 3 protection is required, confirm Azure Key Vault Managed HSM is used instead of the standard vault." } ) $results = [System.Collections.Generic.List[object]]::new() foreach ($m in $manual) { $results.Add((New-ManualResult $m.Id $m.Msg)) } return $results.ToArray() } |