src/collect/ConvertTo-ScoutStorageExposureEvidence.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <#+ .SYNOPSIS Normalizes storage account network and authentication exposure from retained ARM evidence. .DESCRIPTION This is a pure transform. It makes no Azure request and emits one evidence row per discovered storage account. Configured values are retained separately from effective defaults so an absent ARM property is never confused with a collector failure. Resource-group managedBy metadata and private-endpoint links are joined from the same raw inventory. #> function ConvertTo-ScoutStorageExposureEvidence { [CmdletBinding()] param( [Parameter()] [AllowNull()] [object[]] $Resources = @(), [Parameter()] [AllowNull()] [object[]] $ResourceContainers = @() ) function Get-StorageEvidenceValue { param([AllowNull()][object] $InputObject, [Parameter(Mandatory)][string] $Path) $current = $InputObject foreach ($segment in $Path.Split('.')) { if ($null -eq $current) { return $null } if ($current -is [System.Collections.IDictionary]) { $key = @($current.Keys | Where-Object { [string]$_ -ieq $segment } | Select-Object -First 1) if ($key.Count -eq 0) { return $null } $current = $current[$key[0]] continue } $property = @($current.PSObject.Properties | Where-Object Name -ieq $segment | Select-Object -First 1) if ($property.Count -eq 0) { return $null } $current = $property[0].Value } return $current } function Get-StorageEvidenceArray { param([AllowNull()][object] $Value) if ($null -eq $Value) { return @() } return @($Value | Where-Object { $null -ne $_ }) } function Get-StorageEffectiveBoolean { param([AllowNull()][object] $ConfiguredValue, [bool] $DefaultValue) if ($null -eq $ConfiguredValue -or [string]::IsNullOrWhiteSpace([string]$ConfiguredValue)) { return $DefaultValue } if ($ConfiguredValue -is [bool]) { return [bool]$ConfiguredValue } return [string]$ConfiguredValue -ieq 'true' } $allResources = @($Resources | Where-Object { $null -ne $_ }) $resourceGroupsById = @{} foreach ($container in @($ResourceContainers | Where-Object { $null -ne $_ })) { $type = [string](Get-StorageEvidenceValue -InputObject $container -Path 'type') if ($type -notin @('microsoft.resources/subscriptions/resourcegroups', 'microsoft.resources/resourcegroups')) { continue } $id = [string](Get-StorageEvidenceValue -InputObject $container -Path 'id') if (-not [string]::IsNullOrWhiteSpace($id)) { $resourceGroupsById[$id.ToLowerInvariant()] = $container } } $privateEndpointTargets = @{} foreach ($privateEndpoint in @($allResources | Where-Object { [string](Get-StorageEvidenceValue -InputObject $_ -Path 'type') -ieq 'microsoft.network/privateendpoints' })) { $privateEndpointId = [string](Get-StorageEvidenceValue -InputObject $privateEndpoint -Path 'id') $connections = @( Get-StorageEvidenceArray (Get-StorageEvidenceValue -InputObject $privateEndpoint -Path 'properties.privateLinkServiceConnections') Get-StorageEvidenceArray (Get-StorageEvidenceValue -InputObject $privateEndpoint -Path 'properties.manualPrivateLinkServiceConnections') ) foreach ($connection in $connections) { $targetId = [string](Get-StorageEvidenceValue -InputObject $connection -Path 'properties.privateLinkServiceId') if ([string]::IsNullOrWhiteSpace($targetId)) { continue } $targetKey = $targetId.ToLowerInvariant() if (-not $privateEndpointTargets.ContainsKey($targetKey)) { $privateEndpointTargets[$targetKey] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) } if (-not [string]::IsNullOrWhiteSpace($privateEndpointId)) { [void]$privateEndpointTargets[$targetKey].Add($privateEndpointId) } } } foreach ($storage in @($allResources | Where-Object { [string](Get-StorageEvidenceValue -InputObject $_ -Path 'type') -ieq 'microsoft.storage/storageaccounts' })) { $id = [string](Get-StorageEvidenceValue -InputObject $storage -Path 'id') $name = [string](Get-StorageEvidenceValue -InputObject $storage -Path 'name') $subscriptionId = [string](Get-StorageEvidenceValue -InputObject $storage -Path 'subscriptionId') $resourceGroupName = [string](Get-StorageEvidenceValue -InputObject $storage -Path 'resourceGroup') $resourceGroupId = if ($subscriptionId -and $resourceGroupName) { "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName" } else { $null } $resourceGroup = if ($resourceGroupId -and $resourceGroupsById.ContainsKey($resourceGroupId.ToLowerInvariant())) { $resourceGroupsById[$resourceGroupId.ToLowerInvariant()] } else { $null } $managedBy = [string](Get-StorageEvidenceValue -InputObject $resourceGroup -Path 'managedBy') if ([string]::IsNullOrWhiteSpace($managedBy)) { $managedBy = [string](Get-StorageEvidenceValue -InputObject $storage -Path 'managedBy') } $configuredPublicNetworkAccess = Get-StorageEvidenceValue -InputObject $storage -Path 'properties.publicNetworkAccess' $configuredDefaultAction = Get-StorageEvidenceValue -InputObject $storage -Path 'properties.networkAcls.defaultAction' $configuredBlobPublicAccess = Get-StorageEvidenceValue -InputObject $storage -Path 'properties.allowBlobPublicAccess' $configuredSharedKeyAccess = Get-StorageEvidenceValue -InputObject $storage -Path 'properties.allowSharedKeyAccess' $minimumTlsVersion = Get-StorageEvidenceValue -InputObject $storage -Path 'properties.minimumTlsVersion' $effectivePublicNetworkAccess = if ($null -eq $configuredPublicNetworkAccess -or [string]::IsNullOrWhiteSpace([string]$configuredPublicNetworkAccess)) { 'Enabled' } else { [string]$configuredPublicNetworkAccess } $effectiveDefaultAction = if ($null -eq $configuredDefaultAction -or [string]::IsNullOrWhiteSpace([string]$configuredDefaultAction)) { 'Allow' } else { [string]$configuredDefaultAction } $effectiveBlobPublicAccess = Get-StorageEffectiveBoolean -ConfiguredValue $configuredBlobPublicAccess -DefaultValue $true $effectiveSharedKeyAccess = Get-StorageEffectiveBoolean -ConfiguredValue $configuredSharedKeyAccess -DefaultValue $true $privateEndpointIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($connection in (Get-StorageEvidenceArray (Get-StorageEvidenceValue -InputObject $storage -Path 'properties.privateEndpointConnections'))) { $privateEndpointId = [string](Get-StorageEvidenceValue -InputObject $connection -Path 'properties.privateEndpoint.id') if ($privateEndpointId) { [void]$privateEndpointIds.Add($privateEndpointId) } } if ($id -and $privateEndpointTargets.ContainsKey($id.ToLowerInvariant())) { foreach ($privateEndpointId in $privateEndpointTargets[$id.ToLowerInvariant()]) { [void]$privateEndpointIds.Add($privateEndpointId) } } $publicOpen = $effectivePublicNetworkAccess -ieq 'Enabled' -and $effectiveDefaultAction -ieq 'Allow' $hasPrivateEndpoint = $privateEndpointIds.Count -gt 0 $exposure = if ($publicOpen -and $hasPrivateEndpoint) { 'Mixed' } elseif ($publicOpen) { 'Public' } elseif ($effectivePublicNetworkAccess -ieq 'Disabled' -and $hasPrivateEndpoint) { 'Private' } elseif ($effectivePublicNetworkAccess -ieq 'Enabled') { 'Restricted public' } else { 'Unknown' } $verdict = if (-not [string]::IsNullOrWhiteSpace($managedBy) -or $name -like 'dbstorage*') { 'Managed RG - by design, confirm' } elseif ($resourceGroupName -match '(?i)(migration-tmp|(^|-)mrg-)') { 'Deployment/temp - candidate to delete' } elseif ($publicOpen) { 'REAL exposure - remediate' } else { 'Needs owner review' } [pscustomobject][ordered]@{ id = "$id/providers/AzureScout/storageExposure" name = $name type = 'AZSC/Derived/StorageExposure' subscriptionId = $subscriptionId resourceGroup = $resourceGroupName location = [string](Get-StorageEvidenceValue -InputObject $storage -Path 'location') PARENTID = $id PARENTTYPE = 'microsoft.storage/storageaccounts' PARENTNAME = $name properties = [pscustomobject][ordered]@{ StorageAccountId = $id ManagedBy = $managedBy PublicNetworkAccessConfigured = $configuredPublicNetworkAccess PublicNetworkAccessEffective = $effectivePublicNetworkAccess NetworkDefaultActionConfigured = $configuredDefaultAction NetworkDefaultActionEffective = $effectiveDefaultAction AllowBlobPublicAccessConfigured = $configuredBlobPublicAccess AllowBlobPublicAccessEffective = $effectiveBlobPublicAccess AllowSharedKeyAccessConfigured = $configuredSharedKeyAccess AllowSharedKeyAccessEffective = $effectiveSharedKeyAccess MinimumTlsVersion = $minimumTlsVersion PrivateEndpointCount = $privateEndpointIds.Count PrivateEndpointIds = @($privateEndpointIds | Sort-Object) Exposure = $exposure Verdict = $verdict } AZSC = [pscustomobject][ordered]@{ Source = 'Azure Scout derived evidence' Dataset = 'StorageExposure' Operation = 'Normalize and join retained ARM evidence' SourceDatasets = @('resources:microsoft.storage/storageaccounts', 'resourcecontainers:resourcegroups', 'resources:microsoft.network/privateendpoints') CollectedAt = (Get-Date).ToString('o') } } } } |