Framework/Core/SVT/Services/AppService.ps1
#using namespace Microsoft.Azure.Commands.AppService.Models Set-StrictMode -Version Latest class AppService: AzSVTBase { hidden [PSObject] $ResourceObject; hidden [PSObject] $WebAppDetails; hidden [PSObject] $SiteConfigs; hidden [PSObject] $AuthenticationSettings; hidden [bool] $IsReaderRole; AppService([string] $subscriptionId, [SVTResource] $svtResource): Base($subscriptionId, $svtResource) { $this.GetResourceObject(); $this.AddResourceMetadata($this.ResourceObject.Properties) } hidden [string] FormatURL([string] $AppURL) { $ind = $AppURL.IndexOf('.') $AppURL = $AppURL.Substring($ind+1) return $AppURL } hidden [PSObject] GetResourceObject() { if (-not $this.ResourceObject) { # Get App Service details $this.ResourceObject = Get-AzResource -Name $this.ResourceContext.ResourceName ` -ResourceType $this.ResourceContext.ResourceType ` -ResourceGroupName $this.ResourceContext.ResourceGroupName if(-not $this.ResourceObject) { throw ([SuppressedException]::new(("Resource '$($this.ResourceContext.ResourceName)' not found under Resource Group '$($this.ResourceContext.ResourceGroupName)'"), [SuppressedExceptionType]::InvalidOperation)) } # Get web sites details $this.WebAppDetails = Get-AzWebApp -Name $this.ResourceContext.ResourceName ` -ResourceGroupName $this.ResourceContext.ResourceGroupName try { $this.AuthenticationSettings = Invoke-AzResourceAction -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; } } try{ $this.SiteConfigs = Get-AzResource -ResourceGroupName $this.ResourceContext.ResourceGroupName -ResourceType Microsoft.Web/sites/config -ResourceName $this.ResourceContext.ResourceName -ApiVersion 2018-02-01 # Append SiteConfig to ResourceObject if($null -ne $this.SiteConfigs -and [Helpers]::CheckMember($this.SiteConfigs,"Properties") -and [Helpers]::CheckMember($this.ResourceObject.Properties,"siteConfig", $false)){ $this.ResourceObject.Properties.siteConfig = $this.SiteConfigs.Properties } }catch{ $this.SiteConfigs = $null # No need to break execution , null object is handled in respective controls } } return $this.ResourceObject; } [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls) { $serviceFilterTag = "AppService"; $osFilterTag = "Windows"; if([Helpers]::CheckMember($this.ResourceObject, "Kind")) { if($this.ResourceObject.Kind -like "*functionapp*") { $serviceFilterTag = "FunctionApp"; } if($this.ResourceObject.Kind -like "*linux*") { $osFilterTag = "Linux"; } } $result = @(); $result += $controls | Where-Object { $_.Tags -contains $serviceFilterTag } | Where-Object { $_.Tags -contains $osFilterTag }; return $result; } hidden [ControlResult] CheckAppServiceCustomDomainWithSSLConfig([ControlResult] $controlResult) { # Get custom domain URLs $customHostNames = $this.ResourceObject.Properties.HostNames | Where-Object { -not $_.Contains(".azurewebsites.") }; # 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 -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) { if($this.IsReaderRole) { #Setting this property ensures that this control result wont be considered for the central telemetry. As control doesnt have the required permissions $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false; $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Control can not be validated due to insufficient access permission on resource")); } else { #Checks if functions app present if([Helpers]::CheckMember($this.ResourceObject, "Kind") -and ($this.ResourceObject.Kind -eq "functionapp")) { $ResourceAppIdURI = [WebRequestHelper]::GetServiceManagementUrl() $accessToken = [ContextHelper]::GetAccessToken($ResourceAppIdURI) $authorisationToken = "Bearer " + $accessToken $headers = @{"Authorization"=$authorisationToken;"Content-Type"="application/json"} if([Helpers]::CheckMember($this.WebAppDetails,"EnabledHostNames")) { if((($this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('scm') }) | Measure-Object).Count -eq 1) { $scmURL = $this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('scm') } $apiFunctionsUrl = [string]::Format("https://{0}/api/functions",$scmURL) } else { $temp = @($this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('.azurewebsites.') }) $AppURL = $this.FormatURL($temp[0]) $apiFunctionsUrl = [string]::Format("https://{0}.scm.{1}/api/functions",$this.ResourceContext.ResourceName,$AppURL) } } else { $temp = @($this.ResourceObject.Properties.HostNames | where-object { $_.Contains('.azurewebsites.') }) $AppURL = $this.FormatURL($temp[0]) $apiFunctionsUrl = [string]::Format("https://{0}.scm.{1}/api/functions",$this.ResourceContext.ResourceName,$AppURL) } try { $functionDetail = [WebRequestHelper]::InvokeGetWebRequest($apiFunctionsUrl, $headers) #check if functions are present in FunctionApp if([Helpers]::CheckMember($functionDetail,"config")) { $bindingsDetail =$functionDetail.config.bindings $ishttpTriggerFunction=$false if(($bindingsDetail| Measure-Object).Count -gt 0) { $bindingsDetail | ForEach-Object{ if($_.type -eq "httpTrigger" ) { $ishttpTriggerFunction=$true } } #if HTTP trigger function is not present, then AAD authentication is not required if(!$ishttpTriggerFunction) { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("AAD Authentication for resource " + $this.ResourceContext.ResourceName + " is not required.")); return $controlResult; } } } #if no function is present in Functions App, then AAD authentication is not required else { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("AAD Authentication for resource " + $this.ResourceContext.ResourceName + " is not required.")); return $controlResult; } } catch { $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Unable to fetch details of functions.")); return $controlResult; } } $AADEnabled = $false if([Helpers]::CheckMember($this.AuthenticationSettings,"Properties")) { $aadSettings = New-Object PSObject if([Helpers]::CheckMember($this.AuthenticationSettings.Properties,"enabled")) { Add-Member -InputObject $aadSettings -MemberType NoteProperty -Name "Enabled" -Value $this.AuthenticationSettings.Properties.enabled } if([Helpers]::CheckMember($this.AuthenticationSettings.Properties,"ClientId")) { Add-Member -InputObject $aadSettings -MemberType NoteProperty -Name "ClientId" -Value $this.AuthenticationSettings.Properties.ClientId if(([Helpers]::CheckMember($this.AuthenticationSettings.Properties,"enabled")) -and ($this.AuthenticationSettings.Properties.enabled -eq $True)) { if($null -ne $this.AuthenticationSettings.Properties.ClientId) { $AADEnabled = $True; } } } #Check if any non AAD authenticaion is also enabled if([Helpers]::CheckMember($this.ControlSettings,"AppService.NonAADAuthProperties")){ $nonAadSettings = New-Object PSObject $nonAADAuthEnabled = $false $NonAADAuthProperties = $this.ControlSettings.AppService.NonAADAuthProperties ForEach($authProperty in $NonAADAuthProperties){ if([Helpers]::CheckMember($this.AuthenticationSettings.Properties,$authProperty)){ $nonAADAuthEnabled = $true Add-Member -InputObject $nonAadSettings -MemberType NoteProperty -Name $authProperty -Value $this.AuthenticationSettings.Properties.$($authProperty) } } if($nonAADAuthEnabled ){ if($AADEnabled) { $controlResult.AddMessage("AAD Authentication for resource " + $this.ResourceContext.ResourceName + " is enabled "+ $aadSettings) } #Fail the control if non AAD authentication is enabled, irrespective of AAD is enabled or not $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Authentication mechanism other than AAD is enabled for " + $this.ResourceContext.ResourceName )); $controlResult.AddMessage($nonAadSettings); $controlResult.SetStateData("App Service authentication settings", $nonAadSettings); return $controlResult; } } if($AADEnabled) { if([FeatureFlightingManager]::GetFeatureStatus("EnableAppServiceAADAuthAllowAnonymousCheck",$($this.SubscriptionContext.SubscriptionId)) -eq $true) { if(([Helpers]::CheckMember($this.AuthenticationSettings.Properties,"unauthenticatedClientAction"))) { Add-Member -InputObject $aadSettings -MemberType NoteProperty -Name "UnauthenticatedClientAction" -Value $this.AuthenticationSettings.Properties.unauthenticatedClientAction if( $this.AuthenticationSettings.Properties.unauthenticatedClientAction -eq 'AllowAnonymous') { $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("AAD Authentication for resource " + $this.ResourceContext.ResourceName + " is enabled.")); $controlResult.AddMessage("Action to take when request is not authenticated is set to $($this.AuthenticationSettings.Properties.unauthenticatedClientAction)") $controlResult.AddMessage($aadSettings); return $controlResult; } } } $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); } } else { $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Verify that AAD Authentication for resource " + $this.ResourceContext.ResourceName + " is enabled")); } } return $controlResult; } hidden [ControlResult] CheckAppServiceRemoteDebuggingConfiguration([ControlResult] $controlResult) { if([Helpers]::CheckMember($this.WebAppDetails,"SiteConfig")) { if([Helpers]::CheckMember($this.WebAppDetails.SiteConfig,"RemoteDebuggingEnabled") -and $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))); } } else { $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Could not validate remote debugging settings on the AppService: " + $this.ResourceContext.ResourceName + ".")); } return $controlResult; } hidden [ControlResult] CheckAppServiceWebSocketsConfiguration([ControlResult] $controlResult) { if($this.WebAppDetails.SiteConfig.WebSocketsEnabled) { $controlResult.EnableFixControl = $true; $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.EnableFixControl = $true; $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.EnableFixControl = $true; $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 $resource = Get-AzResource -ResourceId $this.ResourceObject.Properties.ServerFarmId if(($resource|Measure-Object).Count -gt 0 -and [Helpers]::CheckMember($resource,"Sku")) { $sku = $resource.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.EnableFixControl = $true; $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("SKU for resource " + $this.ResourceContext.ResourceName + " is :", $sku)); } } else { $controlResult.EnableFixControl = $true; $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Resource not found. Please verify that the app service exists.")); } return $controlResult; } hidden [ControlResult] CheckAppServiceBackupConfiguration([ControlResult] $controlResult) { try { if($this.IsReaderRole) { #Setting this property ensures that this control result wont be considered for the central telemetry. As control doesnt have the required permissions $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false; $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Control can not be validated due to insufficient access permission on resource")); } else { $backupConfiguration = Get-AzWebAppBackupConfiguration ` -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>=" + $this.ControlSettings.AppService.Backup_RetentionPeriod_Min +", 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.EnableFixControl = $true; $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) { $isHttpsEnabled = $this.ResourceObject.Properties.httpsOnly if($isHttpsEnabled) { $controlResult.VerificationResult = [VerificationResult]::Passed } else { $controlResult.EnableFixControl = $true; $controlResult.VerificationResult = [VerificationResult]::Failed } return $controlResult; } hidden [ControlResult] CheckFunctionsAppHttpCertificateSSL([ControlResult] $controlResult) { if($this.IsReaderRole) { #Setting this property ensures that this control result wont be considered for the central telemetry. As control doesnt have the required permissions $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false; $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Control can not be validated due to insufficient access permission on resource")); } else { $ResourceAppIdURI = [WebRequestHelper]::GetServiceManagementUrl() $accessToken = [ContextHelper]::GetAccessToken($ResourceAppIdURI) $authorisationToken = "Bearer " + $accessToken $headers = @{"Authorization"=$authorisationToken;"Content-Type"="application/json"} if([Helpers]::CheckMember($this.WebAppDetails,"EnabledHostNames")) { if((($this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('scm') }) | Measure-Object).Count -eq 1) { $scmURL = $this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('scm') } $apiFunctionsUrl = [string]::Format("https://{0}/api/functions",$scmURL) } else { $temp = @($this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('.azurewebsites.') }) $AppURL = $this.FormatURL($temp[0]) $apiFunctionsUrl = [string]::Format("https://{0}.scm.{1}/api/functions",$this.ResourceContext.ResourceName,$AppURL) } } else { $temp = @($this.ResourceObject.Properties.HostNames | where-object { $_.Contains('.azurewebsites.') }) $AppURL = $this.FormatURL($temp[0]) $apiFunctionsUrl = [string]::Format("https://{0}.scm.{1}/api/functions",$this.ResourceContext.ResourceName,$AppURL) } try { #$apiFunctionsUrl = [string]::Format("https://{0}.scm.azurewebsites.net/api/functions",$this.ResourceContext.ResourceName) $functionDetail = [WebRequestHelper]::InvokeGetWebRequest($apiFunctionsUrl, $headers) #check if functions are present in FunctionApp if([Helpers]::CheckMember($functionDetail,"config")) { $bindingsDetail =$functionDetail.config.bindings $ishttpTriggerFunction=$false if(($bindingsDetail| Measure-Object).Count -gt 0) { $bindingsDetail | ForEach-Object{ if($_.type -eq "httpTrigger" ) { $ishttpTriggerFunction=$true } } #if HTTP trigger function is not present, then Http check is not required if(!$ishttpTriggerFunction) { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Enabling 'HttpsOnly' is not required for resource " + $this.ResourceContext.ResourceName + ".")); } else { $isHttpsEnabled = $this.ResourceObject.Properties.httpsOnly if($isHttpsEnabled) { $controlResult.VerificationResult = [VerificationResult]::Passed } else { $controlResult.VerificationResult = [VerificationResult]::Failed } } } } #if no function is present in Functions App, then Http check is not required else { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Enabling 'HttpsOnly' is not required for this resource " + $this.ResourceContext.ResourceName + ".")); } } catch { $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Unable to fetch details of functions.")); } } 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; } hidden [ControlResult] CheckFunctionsEditMode([ControlResult] $controlResult) { $appSettings = $this.WebAppDetails.SiteConfig.AppSettings $editModeReadOnly = $null if(($appSettings| Measure-Object).Count -gt 0) { $editModeReadOnly = $appSettings | Where-Object { $_.Name -eq "FUNCTION_APP_EDIT_MODE" -and $_.Value -eq "readonly"} } if($null -ne $editModeReadOnly) { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Functions app edit mode is defined as 'readonly' for resource " + $this.ResourceContext.ResourceName)); } else { $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Functions app edit mode is defined as 'readwrite' for resource " + $this.ResourceContext.ResourceName)); } return $controlResult; } hidden [ControlResult] CheckFunctionsAuthorizationLevel([ControlResult] $controlResult) { if($this.IsReaderRole) { #Setting this property ensures that this control result wont be considered for the central telemetry. As control doesnt have the required permissions $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false; $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Control can not be validated due to insufficient access permission on resource")); } elseif([Helpers]::CheckMember($this.WebAppDetails,"State") -and ($this.WebAppDetails.State -eq "Stopped")) { $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Control can not be validated as the resource is in 'Stopped' state.")); } else { $ResourceAppIdURI = [WebRequestHelper]::GetServiceManagementUrl() $accessToken = [ContextHelper]::GetAccessToken($ResourceAppIdURI) $authorisationToken = "Bearer " + $accessToken $headers = @{"Authorization"=$authorisationToken;"Content-Type"="application/json"} if([Helpers]::CheckMember($this.WebAppDetails,"EnabledHostNames")) { if((($this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('scm') }) | Measure-Object).Count -eq 1) { $scmURL = $this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('scm') } $apiFunctionsUrl = [string]::Format("https://{0}/api/functions",$scmURL) } else { $temp = @($this.WebAppDetails.EnabledHostNames | where-object { $_.Contains('.azurewebsites.') }) $AppURL = $this.FormatURL($temp[0]) $apiFunctionsUrl = [string]::Format("https://{0}.scm.{1}/api/functions",$this.ResourceContext.ResourceName,$AppURL) } } else { $temp = @($this.ResourceObject.Properties.HostNames | where-object { $_.Contains('.azurewebsites.') }) $AppURL = $this.FormatURL($temp[0]) $apiFunctionsUrl = [string]::Format("https://{0}.scm.{1}/api/functions",$this.ResourceContext.ResourceName,$AppURL) } #$apiFunctionsUrl = [string]::Format("https://{0}.scm.azurewebsites.net/api/functions",$this.ResourceContext.ResourceName) $functionDetail = [WebRequestHelper]::InvokeGetWebRequest($apiFunctionsUrl, $headers) #check if functions are present in FunctionApp if([Helpers]::CheckMember($functionDetail,"config")) { $bindingsDetail =$functionDetail.config.bindings $authorizationLevel = $null if(($bindingsDetail| Measure-Object).Count -gt 0) { $bindingsDetail | ForEach-Object{ if($_.type -eq "httpTrigger" ) { if([Helpers]::CheckMember($_,"authLevel")) { if($_.authLevel -ne 'function') { $authorizationLevel=$_.authLevel } } } } } if($null -ne $authorizationLevel) { $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Authorization level for all functions in a Functions app is not defined as 'Function' for resource " + $this.ResourceContext.ResourceName)); } else { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Authorization level for all functions in a Functions app is defined as 'Function' for resource " + $this.ResourceContext.ResourceName)); } } else { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("No functions are found in Functions app resource " + $this.ResourceContext.ResourceName)); } } return $controlResult; } hidden [ControlResult] CheckAppServiceCORSAllowed([ControlResult] $controlResult) { if($this.IsReaderRole) { $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("Control can not be validated due to insufficient access permission on resource")); } else { $corsSettings=$this.WebAppDetails.SiteConfig.Cors if([Helpers]::CheckMember($corsSettings,"AllowedOrigins") -and ($corsSettings.AllowedOrigins | Measure-Object).Count -ne 0) { if($corsSettings.AllowedOrigins.Contains("*")) { $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("CORS is enabled in app service with access from all domains ('*') " + $this.ResourceContext.ResourceName, $corsSettings.AllowedOrigins)); $controlResult.SetStateData("CORS setting Allowed Origins",$corsSettings.AllowedOrigins); } else { $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("CORS is enabled in app service with access from custom domains " , $corsSettings.AllowedOrigins)); $controlResult.SetStateData("CORS setting Allowed Origins",$corsSettings.AllowedOrigins); } } else { $controlResult.AddMessage([VerificationResult]::Manual, [MessageData]::new("No CORS settings found for "+$this.ResourceContext.ResourceName)); } } return $controlResult; } hidden [ControlResult] CheckAppServiceMsiEnabled([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 $msiObject = $null if(($appSettings| Measure-Object).Count -gt 0) { $msiObject = $appSettings | Where-Object { $_.Name -eq "WEBSITE_DISABLE_MSI"} } if($msiObject -eq $null) { $ResourceAppIdURI = [WebRequestHelper]::GetResourceManagerUrl() $uri=[system.string]::Format($ResourceAppIdURI+"subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Web/sites/{2}?api-version=2016-08-01",$this.SubscriptionContext.SubscriptionId,$this.ResourceContext.ResourceGroupName,$this.ResourceContext.ResourceName) $json=$null; try { $json=[WebRequestHelper]::InvokeGetWebRequest($uri); } catch { $json=$null; } if(($null -ne $json) -and (($json | Measure-Object).Count -gt 0)) { if(([Helpers]::CheckMember($json[0],"Identity")) -and ($json[0].Identity.type -eq "SystemAssigned")) { $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Your app service is using Managed Service Identity(MSI). It is specifically turned On.")); } else { $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Your app service is not using Managed Service Identity(MSI). It is specifically turned Off.")); } } else { $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Unable to validate control.Please verify from portal, Managed Service Identity(MSI) is On or Off.")); } } elseif($msiObject.Value -eq "false") { $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Your app service is using Managed Service Identity(MSI). Found the value below in the app settings",$msiObject)); $controlResult.SetStateData("Managed Service Identity Settings",$msiObject); } else { $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Your app service is not using Managed Service Identity(MSI). It is specifically turned Off. Found the value below in the app settings",$msiObject)); $controlResult.SetStateData("Managed Service Identity Settings",$msiObject); } } return $controlResult; } hidden [ControlResult] CheckAppServiceTLSVersion([ControlResult] $controlResult) { $requiredVersion = [System.Version] $this.ControlSettings.AppService.TLS_Version if($null -ne $this.SiteConfigs -and [Helpers]::CheckMember($this.SiteConfigs.Properties,"minTlsVersion")){ $minTlsVersion = [System.Version] $this.SiteConfigs.Properties.minTlsVersion if($minTlsVersion -ge $requiredVersion) { $controlResult.VerificationResult = [VerificationResult]::Passed } else { $controlResult.VerificationResult = [VerificationResult]::Failed $controlResult.AddMessage("Current Minimum TLS Version: $($minTlsVersion), Required Minimum TLS Version: $($requiredVersion)"); } }else{ $controlResult.VerificationResult = [VerificationResult]::Manual $controlResult.AddMessage("Unable to fetch TLS settings."); } return $controlResult; } hidden [ControlResult] CheckAppServiceInstalledExtensions([ControlResult] $controlResult) { try{ $installedExtensions = Get-AzResource -ResourceGroupName $this.ResourceContext.ResourceGroupName -ResourceType Microsoft.Web/sites/siteextensions -ResourceName $this.ResourceContext.ResourceName -ApiVersion 2018-02-01 -ErrorAction silentlycontinue } catch{ $installedExtensions = $null } if($installedExtensions -ne $null -and ($installedExtensions | Measure-Object).Count -gt 0) { $extensions = $installedExtensions | Select-Object "Name", "ResourceId" $controlResult.AddMessage([VerificationResult]::Verify, [MessageData]::new("Following extensions are installed on resource:",$installedExtensions)); $controlResult.SetStateData("Installed extensions",$extensions); } else { $controlResult.AddMessage([VerificationResult]::Passed,[MessageData]::new("No extension is installed on resource " +$this.ResourceContext.ResourceName)); } return $controlResult; } hidden [ControlResult] CheckAppServiceAccessRestriction([ControlResult] $controlResult) { $ipSecurityRestrictions = $false $scmIpSecurityRestrictions = $false $controlResult.VerificationResult = [VerificationResult]::Verify $scmIpSecurityRestrictionsUseMain = $this.SiteConfigs.Properties.scmIpSecurityRestrictionsUseMain # Check IP restrictions for main website if($null -eq $this.SiteConfigs.Properties.ipSecurityRestrictions){ $controlResult.AddMessage("IP rule based access restriction is not set up for app: " +$this.ResourceContext.ResourceName); }else{ $ipSecurityRestrictions = $true $controlResult.AddMessage("Following IP rule based access restriction is cofigured for app: "+$this.ResourceContext.ResourceName); $controlResult.AddMessage($this.SiteConfigs.Properties.ipSecurityRestrictions); } # Check IP restrictions for scm website if($scmIpSecurityRestrictionsUseMain -eq $true){ $scmIpSecurityRestrictions = $ipSecurityRestrictions $controlResult.AddMessage("IP based access restriction rules are same for both scm site and main app."); }elseif($null -eq $this.SiteConfigs.Properties.scmIpSecurityRestrictions){ $scmIpSecurityRestrictions = $false $controlResult.AddMessage("IP based access restriction is not set up for scm site used by app."); }else{ $ipSecurityRestrictions = $true $controlResult.AddMessage("Following IP based access restriction is configured for scm site used by app:"); $controlResult.AddMessage($this.SiteConfigs.Properties.scmIpSecurityRestrictions); } return $controlResult; } hidden [ControlResult] CheckAppServiceCORSCredential([ControlResult] $controlResult) { $supportCredentials = $false $controlResult.VerificationResult = [VerificationResult]::Verify if($null -ne $this.SiteConfigs -and [Helpers]::CheckMember( $this.SiteConfigs.Properties,"cors.supportCredentials") -and $this.SiteConfigs.Properties.cors.supportCredentials){ $supportCredentials = $true $controlResult.AddMessage("CORS Response header 'Access-Control-Allow-Credentials' is enabled for resource."); }else{ $controlResult.VerificationResult = [VerificationResult]::Passed $controlResult.AddMessage("CORS Response header 'Access-Control-Allow-Credentials' is disabled for resource."); } $controlResult.SetStateData("Response header 'Access-Control-Allow-Credentials' is set to: ",$supportCredentials); return $controlResult; } } |