Analyze/tests/12-Integration.ps1

#Integration services: Service Bus and Event Hubs, API Management, Automation

Add-AzTest @{
    Id            = 'AZ-MSG-001'
    Title         = 'Service Bus and Event Hubs namespaces have no custom namespace level SAS rules'
    Category      = 'Privileged access'
    Service       = 'Messaging'
    Severity      = 'Medium'
    Description   = 'Finds namespace level shared access authorization rules other than RootManageSharedAccessKey on namespaces that allow local (SAS) authentication.'
    Rationale     = 'Namespace level rules grant access to every queue, topic or event hub in the namespace. Clients should get entity level rules or, better, Entra RBAC.'
    Remediation   = 'Replace namespace level rules with entity level rules or Entra ID RBAC data roles and delete them; consider disabling local authentication entirely.'
    References    = @('https://learn.microsoft.com/azure/service-bus-messaging/service-bus-sas')
    Frameworks    = @{ MCSB = @('PA-7', 'IM-8'); ALZ = @('Enforce-GR-ServiceBus0', 'Enforce-GR-EventHub0') }
    Defender      = @{ '077cc54b-eea9-e565-c72d-1ca6b5373728' = 'All authorization rules except RootManageSharedAccessKey should be removed from Service Bus namespace' }
    Policy        = @{ 'a1817ec0-a368-432a-8057-8371e17ac6ee' = 'All authorization rules except RootManageSharedAccessKey should be removed from Service Bus namespace'; 'b278e460-7cfc-4451-8294-cccc40a940d7' = 'All authorization rules except RootManageSharedAccessKey should be removed from Event Hub namespace' }
    ResourceTypes = @('Microsoft.ServiceBus/namespaces', 'Microsoft.EventHub/namespaces')
    Evaluate      = {
        param($Record)
        if ($Record.resource.properties.disableLocalAuth -eq $true) { return New-Pass 'Local (SAS) authentication is disabled' }
        if (-not (Test-ChildCollected $Record 'authorizationRules')) { return New-Unknown 'Authorization rules could not be read' }
        $custom = @(Get-Child $Record 'authorizationRules' | Where-Object { $_ -and $_.name -ne 'RootManageSharedAccessKey' } | ForEach-Object { "$($_.name) ($(@($_.properties.rights) -join ','))" } | Sort-Object)
        $evidence = [ordered]@{ customRules = $custom }
        if ($custom) { return New-Fail "Namespace level rule(s): $($custom -join ', ')" $evidence }
        New-Pass 'Only RootManageSharedAccessKey' $evidence
    }
}

$apimType = @('Microsoft.ApiManagement/service')

Add-AzTest @{
    Id            = 'AZ-APIM-001'
    Title         = 'API Management direct management endpoint is disabled'
    Category      = 'Posture and vulnerability management'
    Service       = 'API Management'
    Severity      = 'Medium'
    Description   = 'Checks the tenant access setting that enables the legacy direct management REST API.'
    Rationale     = 'The direct management API uses shared access signatures instead of Entra ID and RBAC, bypassing Azure Resource Manager controls and logging.'
    Remediation   = 'Disable the direct management API (API Management > Management API > Enable API Management REST API: No).'
    References    = @('https://learn.microsoft.com/azure/api-management/api-management-howto-disable-management-api')
    Frameworks    = @{ MCSB = @('PV-2', 'IM-1'); ALZ = 'Enforce-GR-APIM0' }
    Defender      = @{ 'e2aeced9-6ef0-410e-b948-4aaa65ded9a7' = 'API Management direct management endpoint should not be enabled' }
    Policy        = @{ 'b741306c-968e-4b67-b916-5675e5c709f4' = 'API Management direct management endpoint should not be enabled' }
    ResourceTypes = $apimType
    Evaluate      = {
        param($Record)
        if (-not (Test-ChildCollected $Record 'tenant/access')) { return New-Unknown 'Tenant access settings could not be read' }
        $enabled = [bool](Get-Child $Record 'tenant/access').properties.enabled
        if ($enabled) { return New-Fail 'Direct management API enabled' ([ordered]@{ enabled = $true }) }
        New-Pass 'Direct management API disabled' ([ordered]@{ enabled = $false })
    }
}

Add-AzTest @{
    Id            = 'AZ-APIM-002'
    Title         = 'API Management APIs only use encrypted protocols'
    Category      = 'Data protection'
    Service       = 'API Management'
    Severity      = 'High'
    Description   = 'Finds APIs that accept HTTP or WS instead of only HTTPS or WSS.'
    Rationale     = 'Unencrypted protocols expose subscription keys, tokens and payloads in transit.'
    Remediation   = 'Set the URL scheme of each API to HTTPS (and WSS for WebSocket APIs).'
    References    = @('https://learn.microsoft.com/azure/api-management/api-management-howto-manage-protocols-ciphers')
    Frameworks    = @{ MCSB = 'DP-3'; WAF = 'SE:07'; ALZ = 'Enforce-GR-APIM0' }
    Defender      = @{ '741b141d-8111-4d86-a4e0-f74b06270a74' = 'API Management APIs should use only encrypted protocols' }
    Policy        = @{ 'ee7495e7-3ba7-40b6-bfee-c29e22cc75d4' = 'API Management APIs should use only encrypted protocols' }
    ResourceTypes = $apimType
    Evaluate      = {
        param($Record)
        if (-not (Test-ChildCollected $Record 'apis')) { return New-Unknown 'APIs could not be listed' }
        $insecure = @(Get-Child $Record 'apis' | Where-Object { $_ -and (@($_.properties.protocols) | Where-Object { $_ -in 'http', 'ws' }) } | ForEach-Object name | Sort-Object)
        $evidence = [ordered]@{ insecureApis = $insecure }
        if ($insecure) { return New-Fail "API(s) accepting HTTP/WS: $($insecure -join ', ')" $evidence }
        New-Pass 'All APIs use HTTPS/WSS only' $evidence
    }
}

Add-AzTest @{
    Id            = 'AZ-APIM-003'
    Title         = 'API Management secret named values are stored in Key Vault'
    Category      = 'Identity management'
    Service       = 'API Management'
    Severity      = 'Medium'
    Description   = 'Finds named values marked as secret that are stored in API Management instead of referenced from Key Vault.'
    Rationale     = 'Secrets stored in API Management are not rotated centrally, not audited by Key Vault and are readable by anyone with API Management contributor rights.'
    Remediation   = 'Store the secrets in Key Vault and convert the named values to Key Vault references using the API Management managed identity.'
    References    = @('https://learn.microsoft.com/azure/api-management/api-management-howto-properties')
    Frameworks    = @{ MCSB = @('IM-8', 'DP-6'); WAF = 'SE:09'; ALZ = 'Enforce-GR-APIM0' }
    Policy        = @{ 'f1cc7827-022c-473e-836e-5a51cae0b249' = 'API Management secret named values should be stored in Azure Key Vault' }
    ResourceTypes = $apimType
    Evaluate      = {
        param($Record)
        if (-not (Test-ChildCollected $Record 'namedValues')) { return New-Unknown 'Named values could not be listed' }
        $local = @(Get-Child $Record 'namedValues' | Where-Object { $_ -and $_.properties.secret -and -not $_.properties.keyVault } | ForEach-Object { $_.properties.displayName } | Sort-Object)
        $evidence = [ordered]@{ secretsNotInKeyVault = $local }
        if ($local) { return New-Fail "Secret named value(s) stored in API Management: $($local -join ', ')" $evidence }
        New-Pass 'All secret named values reference Key Vault' $evidence
    }
}

Add-AzTest @{
    Id            = 'AZ-APIM-004'
    Title         = 'API Management subscriptions are not scoped to all APIs'
    Category      = 'Privileged access'
    Service       = 'API Management'
    Severity      = 'Medium'
    Description   = 'Finds subscriptions (other than the built-in master subscription) whose scope is all APIs.'
    Rationale     = 'An all-APIs subscription key grants access to every current and future API, including ones never intended for that consumer.'
    Remediation   = 'Scope subscriptions to products or individual APIs and regenerate or cancel all-APIs subscriptions.'
    References    = @('https://learn.microsoft.com/azure/api-management/api-management-subscriptions')
    Frameworks    = @{ MCSB = 'PA-7'; ALZ = 'Enforce-GR-APIM0' }
    Defender      = @{ '44aae697-8cc1-4ed1-a136-44a644bfd51f' = 'API Management subscriptions should not be scoped to all APIs' }
    Policy        = @{ '3aa03346-d8c5-4994-a5bc-7652c2a2aef1' = 'API Management subscriptions should not be scoped to all APIs' }
    ResourceTypes = $apimType
    Evaluate      = {
        param($Record)
        if (-not (Test-ChildCollected $Record 'subscriptions')) { return New-Unknown 'Subscriptions could not be listed' }
        $wide = @(Get-Child $Record 'subscriptions' | Where-Object { $_ -and $_.name -ne 'master' -and $_.properties.state -eq 'active' -and [string]$_.properties.scope -match '/apis/?$' } | ForEach-Object { $_.properties.displayName } | Sort-Object)
        $evidence = [ordered]@{ allApisSubscriptions = $wide }
        if ($wide) { return New-Fail "Subscription(s) scoped to all APIs: $($wide -join ', ')" $evidence }
        New-Pass 'No active subscriptions scoped to all APIs' $evidence
    }
}

Add-AzTest @{
    Id            = 'AZ-APIM-005'
    Title         = 'API Management validates backend certificates'
    Category      = 'Data protection'
    Service       = 'API Management'
    Severity      = 'Medium'
    Description   = 'Finds backends that disable certificate chain or certificate name validation.'
    Rationale     = 'Without certificate validation the gateway accepts any certificate, enabling man-in-the-middle attacks between API Management and the backend.'
    Remediation   = 'Enable validateCertificateChain and validateCertificateName on all backends and use certificates from a trusted CA.'
    References    = @('https://learn.microsoft.com/azure/api-management/backends')
    Frameworks    = @{ MCSB = @('IM-4', 'DP-3') }
    Defender      = @{ 'e0905114-2b51-4728-ab31-550f2058ec6c' = 'API Management calls to API backends should not bypass certificate thumbprint or name validation' }
    Policy        = @{ '92bb331d-ac71-416a-8c91-02f2cb734ce4' = 'API Management calls to API backends should not bypass certificate thumbprint or name validation' }
    ResourceTypes = $apimType
    Evaluate      = {
        param($Record)
        if (-not (Test-ChildCollected $Record 'backends')) { return New-Unknown 'Backends could not be listed' }
        $bypass = @(Get-Child $Record 'backends' | Where-Object { $_ -and ($_.properties.tls.validateCertificateChain -eq $false -or $_.properties.tls.validateCertificateName -eq $false) } | ForEach-Object name | Sort-Object)
        $evidence = [ordered]@{ backendsWithoutValidation = $bypass }
        if ($bypass) { return New-Fail "Backend(s) without certificate validation: $($bypass -join ', ')" $evidence }
        New-Pass 'Backend certificates are validated' $evidence
    }
}

Add-AzTest @{
    Id            = 'AZ-APIM-006'
    Title         = 'API Management disables legacy TLS and SSL protocols'
    Category      = 'Data protection'
    Service       = 'API Management'
    Severity      = 'Medium'
    Description   = 'Checks the gateway and backend protocol settings for SSL 3.0, TLS 1.0 and TLS 1.1.'
    Rationale     = 'Legacy protocols have known weaknesses and allow downgrade of client and backend connections.'
    Remediation   = 'Disable SSL 3.0, TLS 1.0 and TLS 1.1 for client and backend connections (Protocols + ciphers blade).'
    References    = @('https://learn.microsoft.com/azure/api-management/api-management-howto-manage-protocols-ciphers')
    Frameworks    = @{ MCSB = @('DP-3', 'NS-8'); ALZ = 'Enforce-TLS-SSL-Q225' }
    ResourceTypes = $apimType
    Evaluate      = {
        param($Record)
        $custom = $Record.resource.properties.customProperties
        $enabled = @($custom.PSObject.Properties | Where-Object { $_.Name -match 'Security\.(Backend\.)?Protocols\.(Tls10|Tls11|Ssl30)$' -and [string]$_.Value -eq 'True' } | ForEach-Object { $_.Name -replace '^.*Security\.', '' } | Sort-Object)
        $evidence = [ordered]@{ legacyProtocolsEnabled = $enabled }
        if ($enabled) { return New-Fail "Enabled: $($enabled -join ', ')" $evidence }
        New-Pass 'Legacy protocols disabled' $evidence
    }
}

Add-AzTest @{
    Id            = 'AZ-APIM-007'
    Title         = 'API Management runs on the stv2 platform'
    Category      = 'Asset management'
    Service       = 'API Management'
    Severity      = 'High'
    Description   = 'Checks the compute platform version of API Management instances.'
    Rationale     = 'The stv1 platform was retired on 31 August 2024 and no longer receives support or security updates.'
    Remediation   = 'Migrate the instance to the stv2 platform.'
    References    = @('https://learn.microsoft.com/azure/api-management/migrate-stv1-to-stv2')
    Frameworks    = @{ MCSB = @('PV-2', 'AM-2') }
    Defender      = @{ 'e5f60ef8-3fcc-4fb5-bee7-7aaeb44c1509' = 'Azure API Management platform version should be stv2' }
    Policy        = @{ '1dc2fc00-2245-4143-99f4-874c937f13ef' = 'Azure API Management platform version should be stv2' }
    ResourceTypes = $apimType
    Evaluate      = {
        param($Record)
        $version = $Record.resource.properties.platformVersion
        $evidence = [ordered]@{ platformVersion = $version }
        if ($version -eq 'stv1') { return New-Fail 'Retired stv1 platform' $evidence }
        New-Pass "Platform $version" $evidence
    }
}

$automationType = @('Microsoft.Automation/automationAccounts')

Add-AzTest @{
    Id            = 'AZ-AUTO-001'
    Title         = 'Automation account variables are encrypted'
    Category      = 'Data protection'
    Service       = 'Automation'
    Severity      = 'High'
    Description   = 'Finds Automation variables that are not encrypted. The values of unencrypted variables are readable by anyone with Reader access.'
    Rationale     = 'Unencrypted variables are frequently used for passwords, keys and connection strings, which are then exposed through the Azure Resource Manager API and exports.'
    Remediation   = 'Recreate sensitive variables as encrypted variables (encryption cannot be added later) or move them to Key Vault, then delete the unencrypted ones.'
    References    = @('https://learn.microsoft.com/azure/automation/shared-resources/variables')
    Frameworks    = @{ MCSB = @('DP-4', 'IM-8'); WAF = 'SE:09'; ALZ = 'Enforce-GR-Automation0' }
    Defender      = @{ 'b12bc79e-4f12-44db-acda-571820191ddc' = 'Automation account variables should be encrypted' }
    Policy        = @{ '3657f5a0-770e-44a3-b44e-9431ba1e9735' = 'Automation account variables should be encrypted' }
    ResourceTypes = $automationType
    Evaluate      = {
        param($Record)
        if (-not (Test-ChildCollected $Record 'variables')) { return New-Unknown 'Variables could not be listed' }
        $plain = @(Get-Child $Record 'variables' | Where-Object { $_ -and -not $_.properties.isEncrypted } | ForEach-Object name | Sort-Object)
        $evidence = [ordered]@{ unencryptedVariables = $plain }
        if ($plain) { return New-Fail "Unencrypted variable(s): $($plain -join ', ')" $evidence }
        New-Pass 'All variables encrypted' $evidence
    }
}

Add-AzTest @{
    Id            = 'AZ-AUTO-002'
    Version       = 2
    Title         = 'Automation accounts use managed identities instead of Run As accounts'
    Category      = 'Identity management'
    Service       = 'Automation'
    Severity      = 'Medium'
    Description   = 'Checks that Automation accounts have a managed identity and no legacy Run As (AzureServicePrincipal) connections or certificates.'
    Rationale     = 'Run As accounts were retired on 30 September 2023; they rely on self-signed certificates stored in the account and usually hold Contributor on the whole subscription.'
    Remediation   = 'Enable a managed identity, grant it the minimal roles, update runbooks to Connect-AzAccount -Identity, then delete the Run As connection, certificate and application.'
    References    = @('https://learn.microsoft.com/azure/automation/migrate-run-as-accounts-managed-identity')
    Frameworks    = @{ MCSB = @('IM-3', 'IM-8'); WAF = 'SE:09' }
    Policy        = @{ 'dea83a72-443c-4292-83d5-54a2f98749c0' = 'Automation Account should have Managed Identity' }
    ResourceTypes = $automationType
    Evaluate      = {
        param($Record)
        $identity = $Record.resource.identity.type
        if (-not (Test-ChildCollected $Record 'connections') -or -not (Test-ChildCollected $Record 'certificates')) { return New-Unknown 'Automation connections or certificates could not be read' }
        $runAsConnections = @(Get-Child $Record 'connections' | Where-Object { $_ -and $_.properties.connectionType.name -eq 'AzureServicePrincipal' } | ForEach-Object name)
        $runAsCertificates = @(Get-Child $Record 'certificates' | Where-Object { $_ -and $_.name -match 'RunAs' } | ForEach-Object name)
        $evidence = [ordered]@{ identityType = $identity; runAsConnections = $runAsConnections; runAsCertificates = $runAsCertificates }
        if ($runAsConnections -or $runAsCertificates) { return New-Fail 'Legacy Run As account present' $evidence }
        if (-not $identity -or $identity -eq 'None') { return New-Fail 'No managed identity' $evidence }
        New-Pass "Managed identity ($identity)" $evidence
    }
}