Framework/Core/SVT/Services/Storage.ps1

using namespace Microsoft.Azure.Management.Storage.Models
using namespace Microsoft.WindowsAzure.Storage.Shared.Protocol
Set-StrictMode -Version Latest 
class Storage: AzSVTBase
{       
    hidden [PSObject] $ResourceObject;
    hidden [bool] $LockExists = $false;

    Storage([string] $subscriptionId, [SVTResource] $svtResource): 
        Base($subscriptionId, $svtResource) 
    { 
        $this.GetResourceObject();
    }

    hidden [PSObject] GetResourceObject()
    {
        if (-not $this.ResourceObject) {
            $this.ResourceObject = Get-AzStorageAccount -Name $this.ResourceContext.ResourceName -ResourceGroupName $this.ResourceContext.ResourceGroupName -ErrorAction Stop
                                                         
            if(-not $this.ResourceObject)
            {
                throw ([SuppressedException]::new(("Resource '{0}' not found under Resource Group '{1}'" -f ($this.ResourceContext.ResourceName), ($this.ResourceContext.ResourceGroupName)), [SuppressedExceptionType]::InvalidOperation))
            }
        }
        return $this.ResourceObject;
    }

    [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls)
    {
        if($controls.Count -eq 0)
        {
            return $controls;
        }

        $result = @();

        if([Helpers]::CheckMember($this.ResourceObject, "Sku.Tier") -and $this.ResourceObject.Sku.Tier -eq "Premium")
        {
            $result += $controls | Where-Object {$_.Tags -contains "PremiumSku" }
        }
        else{
            $result += $controls | Where-Object {$_.Tags -contains "StandardSku" }
        }

        
        if([Helpers]::CheckMember($this.ResourceObject, "Kind") -and ($this.ResourceObject.Kind -eq "BlobStorage"))
        {
            $result = $result | Where-Object {$_.Tags -contains "BlobStorage" }
        }
        else{
            $result = $result | Where-Object {$_.Tags -contains "GeneralPurposeStorage" }
        }
        
        $recourcelocktype = Get-AzResourceLock -ResourceName $this.ResourceContext.ResourceName -ResourceGroupName $this.ResourceContext.ResourceGroupName -ResourceType $this.ResourceContext.ResourceType
        if($recourcelocktype)
        {
            $this.LockExists = $true;
            $this.ControlSettings.LockedResourcesTags | ForEach-Object{
                 if($this.ResourceObject.Tags.ContainsKey($_.TagName) -and $this.ResourceObject.Tags[$_.TagName] -eq $_.TagValue)
                 {
                    $result = $result | Where-Object {$_.Tags -notcontains "ResourceLocked" }
                 }
            }
        }

        #Disabling the control 'Azure_Storage_AuthN_Dont_Allow_Anonymous' for FileShare type available in Premium storage account as blobs and containers are not supported in it.
        if([Helpers]::CheckMember($this.ResourceObject, "Kind") -and ($this.ResourceObject.Kind -eq "FileStorage"))
        {
            $result = $result | Where-Object {$_.Tags -contains "PremiumFileShareStorage"}
        }

        $resource = Get-AzResource -ResourceId $this.ResourceContext.ResourceId;
        #Disabling the control 'Azure_Storage_AuthN_Dont_Allow_Anonymous' for Data Lake Storage Gen2 resources with hierarchical namespace accounts enabled as blob storage is not currently supported.

        if(([Helpers]::CheckMember($resource.Properties, "isHnsEnabled") -and ($resource.Properties.isHnsEnabled -eq $true)))
        {
            $result = $result | Where-Object {$_.Tags -notcontains "HNSDisabled"}
        }

        return $result;
    }

    hidden [ControlResult] CheckStorageContainerPublicAccessTurnOff([ControlResult] $controlResult)
    {
        if([FeatureFlightingManager]::GetFeatureStatus("EnableAnonymousAccessCheckUsingAPI",$($this.SubscriptionContext.SubscriptionId)) -eq $true)
        {
            $allContainersFromAPI = $null;
            $publicContainersFromAPI = @();
            $AzureManagementUri = [WebRequestHelper]::GetResourceManagerUrl()
            $uri = [system.string]::Format($AzureManagementUri+"subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}/blobServices/default/containers?api-version=2018-07-01",$this.SubscriptionContext.SubscriptionId,$this.ResourceContext.ResourceGroupName,$this.ResourceContext.ResourceName)

            try 
            {    
                $allContainersFromAPI = [WebRequestHelper]::InvokeGetWebRequest($uri);

                foreach($item in $allContainersFromAPI)
                {
                    #To check if it is not an Empty object.
                    if([Helpers]::CheckMember($item,"id"))
                    {
                        if(-not ($item.properties.publicAccess -eq "None"))
                        {
                            $publicContainersFromAPI += $item
                        }
                    }
                }
            }
            catch
            {
                throw $_
            }            

            if($publicContainersFromAPI.Count -eq 0)
            {
                $controlResult.AddMessage([VerificationResult]::Passed, "No containers were found that have public (anonymous) access in this storage account.");
            }
            else
            {
                $controlResult.EnableFixControl = $true;
                $controlResult.AddMessage([VerificationResult]::Failed  , 
                                        [MessageData]::new("Remove public access from following containers. Total - $($publicContainersFromAPI.Count)", ($publicContainersFromAPI.name, $publicContainersFromAPI.properties.publicAccess)));                                
            }
        }
        else
        {
            $allContainers = @();
            try
            {
                $allContainers += Get-AzureStorageContainer -Context $this.ResourceObject.Context -ErrorAction Stop
            }
            catch
            {
                if(([Helpers]::CheckMember($_.Exception,"Response") -and  ($_.Exception).Response.StatusCode -eq [System.Net.HttpStatusCode]::Forbidden) -or $this.LockExists)
                {
                    #Setting this property ensures that this control result will not be considered for the central telemetry, as control does not have the required permissions.
                    $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false;
                    $controlResult.AddMessage([VerificationResult]::Manual, ($_.Exception).Message);    
                    return $controlResult
                }
                else
                {
                    throw $_
                }
            }

            #Containers other than private
            $publicContainers = $allContainers | Where-Object { $_.PublicAccess -ne  [Microsoft.Azure.Storage.Blob.BlobContainerPublicAccessType]::Off }
                
            if(($publicContainers | Measure-Object ).Count -eq 0)
            {
                $controlResult.AddMessage([VerificationResult]::Passed, "No containers were found that have public (anonymous) access in this storage account.");
            }                 
            else
            {
                $controlResult.EnableFixControl = $true;
                $controlResult.AddMessage([VerificationResult]::Failed  , 
                                        [MessageData]::new("Remove public access from following containers. Total - $(($publicContainers | Measure-Object ).Count)", ($publicContainers | Select-Object -Property Name, PublicAccess)));  
            }
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckStorageEnableDiagnosticsLog([ControlResult] $controlResult)
        {
            #Checking for storage kind
            $serviceMapping = $this.ControlSettings.StorageKindMapping | Where-Object { $_.Kind -eq $this.ResourceObject.Kind } | Select-Object -First 1;
             if(-not $serviceMapping)
             {
                #Currently only 'General purpose' or 'Blob storage' account kind is present
                #If new storage kind is introduced code needs to be updated as per new storage kind
                $controlResult.AddMessage("Storage Account kind is not supported");
                return $controlResult; 
             }

            #Checking for applicable sku
            $daignosticsSkuMapping = $this.ControlSettings.StorageDiagnosticsSkuMapping | Where-Object { $_ -eq $this.ResourceObject.Sku.Name } | Select-Object -First 1;
            if(-not $daignosticsSkuMapping)
            {
                #Diagnostics settings are not available for premium storage.
                $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Diagnostics settings are not supported for Sku Tier - [$($this.ResourceObject.Sku.Name)]")); 
                return $controlResult; 
            }

            try{
                    $result = $true
                    #Check Metrics diagnostics log property
                    $serviceMapping.DiagnosticsLogServices | 
                    ForEach-Object {
                            #Diagnostic logging is not available for File service.
                            $result = $this.GetServiceLoggingProperty($_, $controlResult) -and $result ;
                    }

                    #Check Metrics logging property
                    $serviceMapping.Services | 
                    ForEach-Object {
                            $result = $this.GetServiceMetricsProperty($_, $controlResult) -and $result ;
                    }

                    if($result){
                          $controlResult.VerificationResult = [VerificationResult]::Passed
                    }
                    else{
                        $controlResult.EnableFixControl = $true;
                        $controlResult.VerificationResult = [VerificationResult]::Failed
                    }
            }
            catch{
                     #With Reader Role exception will be thrown.
                    if(([Helpers]::CheckMember($_.Exception,"Response") -and  ($_.Exception).Response.StatusCode -eq [System.Net.HttpStatusCode]::Forbidden) -or $this.LockExists)
                    {
                        #Setting this property ensures that this control result will not be considered for the central telemetry, as control does not have the required permissions.
                        $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false;
                        $controlResult.AddMessage(($_.Exception).Message);
                        return $controlResult
                    }
                    else
                    {
                        throw $_
                    }
            }    
            return $controlResult;
        }

    hidden [ControlResult] CheckStorageGeoRedundantReplication([ControlResult] $controlResult)
     {
         if($null -ne $this.ResourceObject.Sku.Tier -and $null -ne $this.ResourceObject.Sku.Name){
              $controlResult.AddMessage("Current storage sku tier is - [$($this.ResourceObject.Sku.Tier)] and sku name is - [$($this.ResourceObject.Sku.Name)]"); 
         }
         else{
              $controlResult.AddMessage("Unable to get sku details for - [$($this.ResourceContext.ResourceName)]"); 
              return $controlResult
         }
         
         if($this.ResourceObject.Sku.Tier -eq [SkuTier]::Standard){
             
             $isGeoRedundantSku = $this.ControlSettings.StorageGeoRedundantSku | Where-Object { $_ -eq $this.ResourceObject.Sku.Name } | Select-Object -First 1;

             if($isGeoRedundantSku){
                   $controlResult.VerificationResult = [VerificationResult]::Passed
             }
             else {
                    $controlResult.EnableFixControl = $true;
                    $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Geo-Replication is turned OFF for this storage account. GRS ensures availability in the face of regional catastrophes. You should review its applicability to your business data and storage scenario."));                     
             }
         }
         else{
               $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("A premium storage account supports only locally redundant storage as the replication option"));  
         }
         return $controlResult;  
     }

    hidden [ControlResult] CheckStorageBlobEncryptionEnabled([ControlResult] $controlResult)
     {
         if($null -ne $this.ResourceObject.Encryption)
         {
        if([Helpers]::CheckMember($this.ResourceObject,"Encryption.services.blob.Enabled"))
            {
                        if($null -eq $this.ResourceObject.Encryption.Services.Blob.Enabled ){
                            $controlResult.EnableFixControl = $true;
                            $controlResult.AddMessage([MessageData]::new("Unable to get blob encryption settings"))
                            return $controlResult;
                        }
                        if($this.ResourceObject.Encryption.Services.Blob.Enabled -eq $true){
                            $controlResult.VerificationResult = [VerificationResult]::Passed
                        }
                        else{
                            $controlResult.EnableFixControl = $true;
                            $controlResult.VerificationResult = [VerificationResult]::Failed
                        }
                }
            else{
                            $controlResult.EnableFixControl = $true;
                            $controlResult.VerificationResult = [VerificationResult]::Failed
                        }    
         }
         else
         {
             $controlResult.EnableFixControl = $true;
             $controlResult.AddMessage([MessageData]::new("Storage blob encryption is not enabled"))
             $controlResult.VerificationResult = [VerificationResult]::Failed
         }
         return $controlResult;  
     }

    hidden [ControlResult] CheckStorageFileEncryptionEnabled([ControlResult] $controlResult)
     {
         if($null -ne $this.ResourceObject.Sku.Tier -and $null -ne $this.ResourceObject.Sku.Name){
              $controlResult.AddMessage("Current storage sku tier is - [$($this.ResourceObject.Sku.Tier)] and sku name is - [$($this.ResourceObject.Sku.Name)]"); 
         }
         else{
              $controlResult.AddMessage("Unable to get sku details for - [$($this.ResourceContext.ResourceName)]"); 
              return $controlResult
         }

         if($this.ResourceObject.Sku.Tier -eq [SkuTier]::Standard){
                 if($null -ne $this.ResourceObject.Encryption)
                 {
                    if([Helpers]::CheckMember($this.ResourceObject.Encryption, "Services")){
                      if([Helpers]::CheckMember($this.ResourceObject.Encryption.Services, "File")){
                        if($null -eq $this.ResourceObject.Encryption.Services.File )
                        {
                        $controlResult.EnableFixControl = $true;
                        $controlResult.AddMessage([VerificationResult]::Failed, "Unable to get file encryption settings")
                        return $controlResult;
                        }
                        else
                        {
                        if($this.ResourceObject.Encryption.Services.File.Enabled -eq $true)
                        {
                            $controlResult.VerificationResult = [VerificationResult]::Passed
                        }
                        else
                        {
                            $controlResult.EnableFixControl = $true;
                            $controlResult.VerificationResult = [VerificationResult]::Failed
                        }
                    }
                }
                 else{
                            $controlResult.EnableFixControl = $true;
                            $controlResult.VerificationResult = [VerificationResult]::Failed
                 }
             }
             else{
                     $controlResult.EnableFixControl = $true;
                    $controlResult.VerificationResult = [VerificationResult]::Failed
             }
         }
         else
         {
             $controlResult.EnableFixControl = $true;
             $controlResult.AddMessage([MessageData]::new("Storage file encryption is not enabled"))
             $controlResult.VerificationResult = [VerificationResult]::Failed
         }
         
         
         }
         else{
            $controlResult.AddMessage([VerificationResult]::Passed, "File type encryption is not applicable for premium storage acccount.");  
         }
         
         return $controlResult;  
     }

    hidden [ControlResult] CheckStorageMetricAlert([ControlResult] $controlResult)
    {
        $serviceMapping = $this.ControlSettings.StorageKindMapping | Where-Object { $_.Kind -eq $this.ResourceObject.Kind } | Select-Object -First 1;
        
        if(-not $serviceMapping)
        {
            #Currently only 'General purpose' or 'Blob storage' account kind is present
            #If new storage kind is introduced code needs to be updated as per new storage kind
            $controlResult.AddMessage("Storage Account kind is not supported");
            return $controlResult; 
        }

        #Checking for applicable sku
        $daignosticsSkuMapping = $this.ControlSettings.StorageAlertSkuMapping | Where-Object { $_ -eq $this.ResourceObject.Sku.Name } | Select-Object -First 1;
        if(-not $daignosticsSkuMapping)
        {
            #Metrics or logging capability not enabled for premium storage and zone redundant storage account.
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Diagnostics settings are not supported for Sku Tier - [$($this.ResourceObject.Sku.Name)]")); 
            return $controlResult; 
        }

        $result = $true;
        
        try {
            $serviceMapping.Services | 
            ForEach-Object {
                $result = $this.CheckMetricAlertConfiguration($this.ControlSettings.MetricAlert.Storage, $controlResult, ("/services/" + $_)) -and $result ;
            }    
        }
        catch {
            if(([Helpers]::CheckMember($_.Exception,"Response") -and  ($_.Exception).Response.StatusCode -eq [System.Net.HttpStatusCode]::Forbidden) -or $this.LockExists)
            {
                #Setting this property ensures that this control result will not be considered for the central telemetry, as control does not have the required permissions.
                $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false;
                $controlResult.AddMessage(($_.Exception).Message);
                return $controlResult
            }
            else
            {
                throw $_
            }
        }        

        if($result)
        {
            $controlResult.VerificationResult = [VerificationResult]::Passed
        }
        else
        {
            $controlResult.EnableFixControl = $true;
            $controlResult.VerificationResult = [VerificationResult]::Manual
            $controlResult.AddMessage([MessageData]::new("Configure 'AnonymousSuccess' metric alert on your storage account to track anonymous activity. Threshold count and window duration should be minimum according to your business use case."))
        }

        return $controlResult;  
     }

    hidden [boolean] GetServiceLoggingProperty([string] $serviceType, [ControlResult] $controlResult)
        {
            $loggingProperty = Get-AzStorageServiceLoggingProperty -ServiceType $ServiceType -Context $this.ResourceObject.Context -ErrorAction Stop
            if($null -ne $loggingProperty){
                #Check For Retention day's
                if($loggingProperty.LoggingOperations -eq [LoggingOperations]::All -and (($loggingProperty.RetentionDays -eq $this.ControlSettings.Diagnostics_RetentionPeriod_Forever) -or ($loggingProperty.RetentionDays -ge $this.ControlSettings.Diagnostics_RetentionPeriod_Min))){
                        return $True
                } 
                else{
                        $controlResult.AddMessage("Diagnostics settings($($serviceType) logs) is either disabled OR not retaining logs for at least $($this.ControlSettings.Diagnostics_RetentionPeriod_Min) days for service type - [$($serviceType)]")
                        return $false
                }
            }
            else
            {
                 $controlResult.AddMessage("Diagnostics settings($($serviceType) logs) is disabled for service type - [$($serviceType)]")
                 return $false
            }
        }

    hidden [boolean] GetServiceMetricsProperty([string] $serviceType,[ControlResult] $controlResult)
        {
            $serviceMetricsProperty= Get-AzStorageServiceMetricsProperty -MetricsType Hour -ServiceType $ServiceType -Context $this.ResourceObject.Context  -ErrorAction Stop
            if($null -ne $serviceMetricsProperty){
                #Check for Retention day's
                if($serviceMetricsProperty.MetricsLevel -eq [MetricsLevel]::ServiceAndApi -and (($serviceMetricsProperty.RetentionDays -ge $this.ControlSettings.Diagnostics_RetentionPeriod_Min) -or ($serviceMetricsProperty.RetentionDays -eq $this.ControlSettings.Diagnostics_RetentionPeriod_Forever)))
                {
                    return $True
                }
                else
                {
                    $controlResult.AddMessage("Diagnostics settings($($serviceType) aggregate metrics, $($serviceType) per API metrics) is either disabled OR not retaining logs for at least $($this.ControlSettings.Diagnostics_RetentionPeriod_Min) days for service type - [$($serviceType)]")
                    return $false                       
                }
            }
            else
            {
                 $controlResult.AddMessage("Diagnostics settings($($serviceType) aggregate metrics, $($serviceType) per API metrics) is disabled for service type - [$($serviceType)]")
                 return $false
            }
        }

    hidden [ControlResult] CheckStorageEncryptionInTransit([ControlResult] $controlResult)
        {

            if($null -ne $this.ResourceObject.EnableHttpsTrafficOnly){
                if($this.ResourceObject.EnableHttpsTrafficOnly -eq $true){
                $controlResult.VerificationResult = [VerificationResult]::Passed
                $controlResult.AddMessage([MessageData]::new("Storage secure transfer is enabled"))
                }
                else{
                $controlResult.EnableFixControl = $true;
                $controlResult.VerificationResult = [VerificationResult]::Failed
                $controlResult.AddMessage([MessageData]::new("Storage secure transfer is not enabled"))
                }
            }
            else{
                $controlResult.EnableFixControl = $true;
                $controlResult.AddMessage([MessageData]::new("Storage secure transfer is not enabled"))
                $controlResult.VerificationResult = [VerificationResult]::Failed
            }
            return $controlResult;
        }
    hidden [ControlResult] CheckStorageCORSAllowed([ControlResult] $controlResult)
        {         
         $corsRules = @();    
          try
          {
            #Currently only 'General purpose' or 'Blob storage' account kind is present
            #If new storage kind is introduced code needs to be updated as per new storage kind
            if($this.ResourceObject.Kind -eq "BlobStorage"){
                $corsRules+= Get-AzStorageCORSRule -Context $this.ResourceObject.Context -ServiceType Blob -ErrorAction Stop
            }
            else{
                "Blob","File","Table","Queue"|ForEach-Object {$corsRules +=Get-AzStorageCORSRule -Context $this.ResourceObject.Context -ServiceType $_ -ErrorAction Stop}
            }                                     
            if($corsRules.Count -eq 0){
                $controlResult.AddMessage([VerificationResult]::Passed,[MessageData]::new("The CORS feature has not been enabled on this storage account."));
                }
           else{
               
                $allowAllOrigins = @($corsRules | ForEach-Object{$_.AllowedOrigins.Contains("*")}).Contains($true)
                $allowAllMethods = @($corsRules | ForEach-Object{$_.AllowedMethods.Count}).Contains(7)    
                $controlResult.SetStateData("Following CORS rule(s) are defined in storage:",$corsRules);
                if($allowAllOrigins){
                    $controlResult.AddMessage([VerificationResult]::Failed,[MessageData]::new("CORS rule is defined in storage with access from all origins ('*')"));
                    }
                elseif(-not $allowAllOrigins -and $allowAllMethods){
                    $controlResult.AddMessage([VerificationResult]::Verify,[MessageData]::new("CORS rule is defined in storage with all type of request methods(verbs) and access from specific origins"));
                    }
                elseif(-not $allowAllOrigins -and -not $allowAllMethods){
                    $controlResult.AddMessage([VerificationResult]::Verify,[MessageData]::new("CORS rule is defined in storage with specific request methods(verbs) and access from specific origins"));
                    }
                }                                   
          }
          catch
          {
             #With Reader Role exception will be thrown.
                    if(([Helpers]::CheckMember($_.Exception,"Response") -and  ($_.Exception).Response.StatusCode -eq [System.Net.HttpStatusCode]::Forbidden) -or $this.LockExists)
                    {
                        #As control does not have the required permissions
                        $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false;
                        $controlResult.AddMessage(($_.Exception).Message);
                        return $controlResult
                    }
                    else
                    {
                        throw $_
                    }
          }
        return $controlResult;
        }
}