Framework/Core/SVT/Services/AppService.ps1

#using namespace Microsoft.Azure.Commands.AppService.Models
Set-StrictMode -Version Latest
class AppService: SVTBase
{
    hidden [PSObject] $ResourceObject;
    hidden [PSObject] $WebAppDetails;
    hidden [PSObject] $AuthenticationSettings;
    hidden [bool] $IsReaderRole;

    AppService([string] $subscriptionId, [string] $resourceGroupName, [string] $resourceName):
        Base($subscriptionId, $resourceGroupName, $resourceName)
    {
        $this.GetResourceObject();
    }

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

    hidden [PSObject] GetResourceObject()
    {
        if (-not $this.ResourceObject)
        {
            # Get App Service details
            $this.ResourceObject = Get-AzureRmResource -ResourceName $this.ResourceContext.ResourceName  `
                                        -ResourceType $this.ResourceContext.ResourceType `
                                        -ResourceGroupName $this.ResourceContext.ResourceGroupName

            if(-not $this.ResourceObject)
            {
                throw ("Resource '$($this.ResourceContext.ResourceName)' not found under Resource Group '$($this.ResourceContext.ResourceGroupName)'")
            }

            # Get web sites details
            $this.WebAppDetails = Get-AzureRmWebApp -Name $this.ResourceContext.ResourceName `
                                    -ResourceGroupName $this.ResourceContext.ResourceGroupName

            try
            { 
                $this.AuthenticationSettings = Invoke-AzureRmResourceAction -ResourceType "Microsoft.Web/sites/config/authsettings" `
                                                                                    -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                                                                    -ResourceName $this.ResourceContext.ResourceName `
                                                                                    -Action list `
                                                                                    -ApiVersion $this.ControlSettings.AppService.AADAuthAPIVersion `
                                                                                    -Force `
                                                                                    -ErrorAction Stop
                $this.IsReaderRole = $false;
            }
            catch
            {
                if(($_.Exception | Get-Member -Name "HttpStatus" ) -and $_.Exception.HttpStatus -eq "Forbidden")
                {
                    $this.IsReaderRole = $true;
                }    
            }
        }

        return $this.ResourceObject;
    }
    
    [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls)
    {
        $serviceFilterTag = "AppService";
        if([Helpers]::CheckMember($this.ResourceObject, "Kind"))
        {
            if($this.ResourceObject.Kind -eq "functionapp")
            {
                $serviceFilterTag = "FunctionApp";
            }
        }
        
        $result = @();
        $result += $controls | Where-Object { $_.Tags -contains $serviceFilterTag };
        return $result;
    }

    hidden [ControlResult] CheckAppServiceCustomDomainWithSSLConfig([ControlResult] $controlResult)
    {
        # Get custom domain URLs
        $customHostNames = $this.ResourceObject.Properties.HostNames |
                                Where-Object {
                                     -not $_.EndsWith(".azurewebsites.net")
                                };

        # Combine custom domain name and SSL configuration TCP

        if(($customHostNames | Measure-Object).Count -gt 0)
        {
            $controlResult.AddMessage([MessageData]::new("Custom domains are configured for resource " + $this.ResourceContext.ResourceName), $customHostNames);

            $SSLStateNotEnabled = $this.ResourceObject.Properties.hostNameSslStates | Where-Object { (($customHostNames | Measure-Object) -contains $_.name) -and  ($_.sslState -eq 'Disabled')} | Select-Object -Property Name
            if($null -eq $SSLStateNotEnabled)
            {
                $controlResult.AddMessage([VerificationResult]::Passed,
                                            [MessageData]::new("SSL configuration for resource " + $this.ResourceContext.ResourceName + " is enabled for all custom domains", $this.ResourceObject.Properties.hostNameSslStates));
            }
            else
            {
                $controlResult.AddMessage([VerificationResult]::Failed,
                                            [MessageData]::new("SSL configuration for resource " + $this.ResourceContext.ResourceName + " is not enabled for all custom domains", $this.ResourceObject.Properties.hostNameSslStates));
            }
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Failed,
                                    [MessageData]::new("Custom domains are not configured for resource " + $this.ResourceContext.ResourceName, $this.ResourceObject.Properties.HostNames));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceADAuthentication([ControlResult] $controlResult)
    {
        try
        {
            if($this.IsReaderRole)
            {
                $controlResult.AddMessage([VerificationResult]::Manual,
                                        [MessageData]::new("Control can not be validated due to insufficient access permission on resource"));
            }
            else
            {
                $aadSettings = New-Object PSObject
                Add-Member -InputObject $aadSettings -MemberType NoteProperty -Name "Enabled" -Value $this.AuthenticationSettings.Properties.enabled
                Add-Member -InputObject $aadSettings -MemberType NoteProperty -Name "ClientId" -Value $this.AuthenticationSettings.Properties.ClientId

                $AADEnabled = $false
                if($this.AuthenticationSettings.Properties.enabled -eq $True)
                {
                    if($null -ne $this.AuthenticationSettings.Properties.ClientId)
                    {
                        $AADEnabled = $True;
                    }
                }

                if($AADEnabled)
                {
                    $controlResult.AddMessage([VerificationResult]::Passed,
                                            [MessageData]::new("AAD Authentication for resource " + $this.ResourceContext.ResourceName + " is enabled", $aadSettings));
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Verify,
                                            [MessageData]::new("Verify that AAD Authentication for resource " + $this.ResourceContext.ResourceName + " is enabled", $aadSettings));
                    $controlResult.SetStateData("App Service AAD settings", $aadSettings);
                }
            }
        }
        catch
        {
           throw;
        }

      return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceRemoteDebuggingConfiguration([ControlResult] $controlResult)
    {
        if($this.WebAppDetails.SiteConfig.RemoteDebuggingEnabled)
        {
            $controlResult.AddMessage([VerificationResult]::Failed,
                                        [MessageData]::new("Remote debugging for resource " + $this.ResourceContext.ResourceName + " is turned on", ($this.WebAppDetails.SiteConfig | Select-Object RemoteDebuggingEnabled)));
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Passed,
                                        [MessageData]::new("Remote debugging for resource " + $this.ResourceContext.ResourceName + " is turned off", ($this.WebAppDetails.SiteConfig | Select-Object RemoteDebuggingEnabled)));
        }

      return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceWebSocketsConfiguration([ControlResult] $controlResult)
    {
        if($this.WebAppDetails.SiteConfig.WebSocketsEnabled)
        {
           $controlResult.AddMessage([VerificationResult]::Failed,
                                     [MessageData]::new("Web sockets for resource " + $this.ResourceContext.ResourceName + " is enabled", ($this.WebAppDetails.SiteConfig | Select-Object WebSocketsEnabled)));
        }
        else
        {
           $controlResult.AddMessage([VerificationResult]::Passed,
                                     [MessageData]::new("Web sockets for resource " + $this.ResourceContext.ResourceName + " is not enabled", ($this.WebAppDetails.SiteConfig | Select-Object WebSocketsEnabled)));
        }

      return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceAlwaysOnConfiguration([ControlResult] $controlResult)
    {
        if($this.WebAppDetails.SiteConfig.AlwaysOn)
        {
           $controlResult.AddMessage([VerificationResult]::Passed,
                                     [MessageData]::new("Always On feature for resource " + $this.ResourceContext.ResourceName + " is enabled", ($this.WebAppDetails.SiteConfig | Select-Object AlwaysOn)));
        }
        else
        {
           $controlResult.AddMessage([VerificationResult]::Failed,
                                     [MessageData]::new("Always On feature for resource " + $this.ResourceContext.ResourceName + " is not enabled", ($this.WebAppDetails.SiteConfig | Select-Object AlwaysOn)));
        }

      return $controlResult;
    }

    hidden [ControlResult] CheckAppService64BitPlatformConfiguration([ControlResult] $controlResult)
    {
        if($this.WebAppDetails.SiteConfig.Use32BitWorkerProcess)
        {
           $controlResult.AddMessage([VerificationResult]::Failed,
                                     [MessageData]::new("32-bit platform is used for resource " + $this.ResourceContext.ResourceName, ($this.WebAppDetails.SiteConfig | Select-Object Use32BitWorkerProcess)));
        }
        else
        {
           $controlResult.AddMessage([VerificationResult]::Passed,
                                     [MessageData]::new("64-bit platform is used for resource " + $this.ResourceContext.ResourceName, ($this.WebAppDetails.SiteConfig | Select-Object Use32BitWorkerProcess)));
        }

      return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceDotNetFrameworkVersion([ControlResult] $controlResult)
    {
        $dotNetFrameworkVersion = $this.WebAppDetails.SiteConfig.NetFrameworkVersion
        $splitVersionNumber = $this.ControlSettings.AppService.LatestDotNetFrameworkVersionNumber.split(".")

        # Compare App Service Net Framework version with latest Net Framework version from configuration
        $isCompliant =  $dotNetFrameworkVersion.CompareTo($splitVersionNumber[0] + "." + $splitVersionNumber[1]) -eq 0

        if($isCompliant)
        {
            $controlResult.AddMessage([VerificationResult]::Passed,
                                        [MessageData]::new("Latest .Net Framework version is used for resource " + $this.ResourceContext.ResourceName, ($this.WebAppDetails.SiteConfig | Select-Object NetFrameworkVersion)));
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Failed,
                                        [MessageData]::new("Latest .Net Framework version is not used for resource " + $this.ResourceContext.ResourceName, ($this.WebAppDetails.SiteConfig | Select-Object NetFrameworkVersion)));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceInstanceCount([ControlResult] $controlResult)
    {
        # Get number of instances
        $sku = (Get-AzureRmResource -ResourceId $this.ResourceObject.Properties.ServerFarmId).Sku

        if($sku.Capacity -ge $this.ControlSettings.AppService.Minimum_Instance_Count)
        {
            $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("SKU for resource " + $this.ResourceContext.ResourceName + " is :", $sku));
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("SKU for resource " + $this.ResourceContext.ResourceName + " is :", $sku));
        }

      return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceBackupConfiguration([ControlResult] $controlResult)
    {
        try
        {
            if($this.IsReaderRole)
            {
                $controlResult.AddMessage([VerificationResult]::Manual,
                                        [MessageData]::new("Control can not be validated due to insufficient access permission on resource"));
            }
            else
            {
                $backupConfiguration = Get-AzureRmWebAppBackupConfiguration `
                                                    -ResourceGroupName $this.ResourceContext.ResourceGroupName `
                                                    -Name $this.ResourceContext.ResourceName `
                                                    -ErrorAction Stop

                $isCompliant = $False
                If ($null -ne $backupConfiguration)
                {
                    # Backup must be enabled and retention period days must be more than 365 and backup start date is less than current time (backup has been already started)
                    # and at least one backup is available
                    If($backupConfiguration.Enabled -eq $True -and `
                    ($backupConfiguration.RetentionPeriodInDays -eq $this.ControlSettings.AppService.Backup_RetentionPeriod_Forever -or $backupConfiguration.RetentionPeriodInDays -ge $this.ControlSettings.AppService.Backup_RetentionPeriod_Min) -and`
                        $backupConfiguration.StartTime -le $(Get-Date) -and $backupConfiguration.KeepAtLeastOneBackup -eq $True)
                    {
                        $isCompliant = $True
                    }
                }

                if(-not $isCompliant)
                {
                    $controlResult.VerificationResult = [VerificationResult]::Failed;
                    if($null -ne $backupConfiguration)
                    {
                        $controlResult.AddMessage([MessageData]::new("Configured backup for resource " + $this.ResourceContext.ResourceName + " is not as per the security guidelines. Please make sure that the configured backup is inline with below settings:-"));
                        $controlResult.AddMessage([MessageData]::new("Enabled=True, StorageAccountEncryption=Enabled, RetentionPeriodInDays=0 or RetentionPeriodInDays>=365, BackupStartTime<=CurrentTime, KeepAtLeastOneBackup=True", $backupConfiguration));
                    }
                    else
                    {
                        $controlResult.AddMessage([MessageData]::new("Backup for resource " + $this.ResourceContext.ResourceName + " is not configured", $backupConfiguration));
                    }
                }
                else
                {
                    $controlResult.AddMessage([VerificationResult]::Passed,
                                        [MessageData]::new("Backup for resource " + $this.ResourceContext.ResourceName + " is enabled", $backupConfiguration));
                }
            }
        }
        catch
        {
            if(($_.Exception).Response.StatusCode.value__ -eq 404)
            {
                    $controlResult.AddMessage([VerificationResult]::Failed,
                                        [MessageData]::new("Backup for resource " + $this.ResourceContext.ResourceName + " is not configured"));
            }
            else
            {
                    throw $_
            }
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceDiagnosticLogsConfiguration([ControlResult] $controlResult)
    {
        $diagnosticLogsConfig = New-Object PSObject
        Add-Member -InputObject $diagnosticLogsConfig -MemberType NoteProperty -Name "HttpLoggingEnabled" -Value $this.WebAppDetails.SiteConfig.HttpLoggingEnabled
        Add-Member -InputObject $diagnosticLogsConfig -MemberType NoteProperty  -Name "DetailedErrorLoggingEnabled" -Value $this.WebAppDetails.SiteConfig.DetailedErrorLoggingEnabled
        Add-Member -InputObject $diagnosticLogsConfig -MemberType NoteProperty  -Name "RequestTracingEnabled" -Value $this.WebAppDetails.SiteConfig.RequestTracingEnabled

        $isCompliant =  $diagnosticLogsConfig.HttpLoggingEnabled -eq $true -and `
                        $diagnosticLogsConfig.DetailedErrorLoggingEnabled -eq $true -and `
                        $diagnosticLogsConfig.RequestTracingEnabled -eq $true

        if($isCompliant)
        {
           $controlResult.AddMessage([VerificationResult]::Passed,
                                     [MessageData]::new("Diagnostics logs for resource " + $this.ResourceContext.ResourceName + " are enabled", $diagnosticLogsConfig));
        }
        else
        {
            $controlResult.AddMessage([VerificationResult]::Failed,
                                     [MessageData]::new("All configurations of diagnostics logs for resource " + $this.ResourceContext.ResourceName + " are not enabled", $diagnosticLogsConfig));
        }

        return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceHttpCertificateSSL([ControlResult] $controlResult)
    {
        $hostNames = $this.ResourceObject.Properties.HostNames |
                                Where-Object {
                                     -not $_.EndsWith(".scm.azurewebsites.net")
                                };

        $isPass = 0
        if(($hostNames | Measure-Object).Count -gt 0)
        {
            $hostNames | ForEach-Object{
                try
                {       
                    $request = [System.Net.WebRequest]::Create('http://'+$_)
                    $response = $request.GetResponse()
                    $responceURL = $response.ResponseUri.OriginalString
                    $response.Close()

                    $scheme = ([System.Uri]$responceURL).Scheme

                    if($scheme -eq "https")
                    {
                        $controlResult.AddMessage([MessageData]::new("HTTP requests are not allowed for resource " + $this.ResourceContext.ResourceName, $responceURL));
                    }
                    else
                    {
                        $isPass = If ($isPass -lt 2) { 2 } Else { $isPass }
                        $controlResult.AddMessage([MessageData]::new("HTTP requests are allowed for resource " + $this.ResourceContext.ResourceName, $responceURL));
                    }
                }
                catch [System.Management.Automation.MethodInvocationException]
                {
                    $isPass = If ($isPass -lt 1) { 1 } Else { $isPass }
                    $controlResult.AddMessage([MessageData]::new("Default Host URL is not reachable or self-signed certificate is used", $request.Address.OriginalString));
                }
            }
        }

        if($isPass -eq 0)
        {
            $controlResult.VerificationResult = [VerificationResult]::Passed
        }
        ElseIf ($isPass -eq 1)
        {
            $controlResult.VerificationResult = [VerificationResult]::Verify
            $controlResult.SetStateData("Host urls", $hostNames);
        }
        else
        {
            $controlResult.VerificationResult = [VerificationResult]::Failed
            $controlResult.SetStateData("Host urls", $hostNames);
        }
        

        return $controlResult;
    }

    hidden [ControlResult] CheckAppServiceLoadCertAppSettings([ControlResult] $controlResult)
    {
        if($this.IsReaderRole)
        {
            $controlResult.AddMessage([VerificationResult]::Manual,
                                        [MessageData]::new("Control can not be validated due to insufficient access permission on resource"));
        }
        else
        {
            $appSettings = $this.WebAppDetails.SiteConfig.AppSettings
               $appSettingParameterList = $null

            if(($appSettings| Measure-Object).Count -gt 0)
            {
                $appSettingParameterList = $appSettings | Where-Object { $_.Name -eq $this.ControlSettings.AppService.LoadCertAppSettings -and $_.Value -eq "*"}
            }

            if($null -ne $appSettingParameterList)
            {
               $controlResult.AddMessage([VerificationResult]::Failed,
                                         [MessageData]::new("'WEBSITE_LOAD_CERTIFICATES' parameter defined equal to '*' is found in App Settings for resource " + $this.ResourceContext.ResourceName));
            }
            else
            {
               $controlResult.AddMessage([VerificationResult]::Passed,
                                         [MessageData]::new("'WEBSITE_LOAD_CERTIFICATES' parameter defined equal to '*' is not found in App Settings for resource " + $this.ResourceContext.ResourceName));
            }
        }
        return $controlResult;
    }
}