Framework/Abstracts/AzSVTBase.ps1
class AzSVTBase: SVTBase{ AzSVTBase() { } AzSVTBase([string] $subscriptionId): Base($subscriptionId) { $this.CreateInstance(); } AzSVTBase([string] $subscriptionId, [SVTResource] $svtResource): Base($subscriptionId) { $this.CreateInstance($svtResource); } #Create instance for subscription scan hidden [void] CreateInstance() { [Helpers]::AbstractClass($this, [SVTBase]); $this.LoadSvtConfig([SVTMapping]::SubscriptionMapping.JsonFileName); $this.ResourceId = $this.SubscriptionContext.Scope; } #Add PreviewBaselineControls hidden [bool] CheckBaselineControl($controlId) { if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"BaselineControls.ResourceTypeControlIdMappingList")) { $baselineControl = $this.ControlSettings.BaselineControls.ResourceTypeControlIdMappingList | Where-Object {$_.ControlIds -contains $controlId} if(($baselineControl | Measure-Object).Count -gt 0 ) { return $true } } if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"BaselineControls.SubscriptionControlIdList")) { $baselineControl = $this.ControlSettings.BaselineControls.SubscriptionControlIdList | Where-Object {$_ -eq $controlId} if(($baselineControl | Measure-Object).Count -gt 0 ) { return $true } } return $false } hidden [bool] CheckPreviewBaselineControl($controlId) { if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"PreviewBaselineControls.ResourceTypeControlIdMappingList")) { $PreviewBaselineControls = $this.ControlSettings.PreviewBaselineControls.ResourceTypeControlIdMappingList | Where-Object {$_.ControlIds -contains $controlId} if(($PreviewBaselineControls | Measure-Object).Count -gt 0 ) { return $true } } if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"PreviewBaselineControls.SubscriptionControlIdList")) { $PreviewBaselineControls = $this.ControlSettings.PreviewBaselineControls.SubscriptionControlIdList | Where-Object {$_ -eq $controlId} if(($PreviewBaselineControls | Measure-Object).Count -gt 0 ) { return $true } } return $false } hidden [void] GetResourceId() { try { if ([FeatureFlightingManager]::GetFeatureStatus("EnableResourceGroupTagTelemetry","*") -eq $true -and $this.ResourceId -and $this.ResourceContext -and $this.ResourceTags.Count -eq 0) { $tags = (Get-AzResourceGroup -Name $this.ResourceContext.ResourceGroupName).Tags if( $tags -and ($tags | Measure-Object).Count -gt 0) { $this.ResourceTags = $tags } } } catch { # flow shouldn't break if there are errors in fetching tags eg. locked resource groups. <TODO: Add exception telemetry> } } <# TODO: Remove this block if new logic for policy complaince is working seemlessly hidden [ControlResult] CheckPolicyCompliance([ControlItem] $controlItem, [ControlResult] $controlResult) { $initiativeName = [ConfigurationManager]::GetAzSKConfigData().AzSKInitiativeName $defnResourceId = $this.ResourceId + $controlItem.PolicyDefnResourceIdSuffix $policyState = Get-AzPolicyState -ResourceId $defnResourceId -Filter "PolicyDefinitionId eq '/providers/microsoft.authorization/policydefinitions/$($controlItem.PolicyDefinitionGuid)' and PolicySetDefinitionName eq '$initiativeName'" if($policyState) { $policyStateObject = $policyState | Select-Object ResourceId, PolicyAssignmentId, PolicyDefinitionId, PolicyAssignmentScope, PolicyDefinitionAction, PolicySetDefinitionName, IsCompliant if($policyState.IsCompliant) { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Policy compliance data:", $policyStateObject)); } else { #$controlResult.EnableFixControl = $true; $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Policy compliance data:", $policyStateObject)); } return $controlResult; } return $null; } #> hidden [void] CheckPolicyCompliance([ControlItem] $controlItem, [ControlResult] $controlResult) { try { $controlResult.PolicyState = [PolicyState]::new() $initiativeName = [ConfigurationManager]::GetAzSKConfigData().AzSKInitiativeName $securityCenterInitiativeName = [ConfigurationManager]::GetAzSKConfigData().AzSKSecurityCenterInitiativeName.Replace("{0}", $this.SubscriptionContext.SubscriptionId) $defnResourceId = $this.ResourceId + $controlItem.PolicyDefnResourceIdSuffix $policyState = Get-AzPolicyState -ResourceId $defnResourceId -Filter "((PolicyDefinitionId eq '/providers/microsoft.authorization/policydefinitions/$($controlItem.PolicyDefinitionGuid)') or (PolicyDefinitionId eq '/subscriptions/$($this.SubscriptionContext.SubscriptionId)/providers/microsoft.authorization/policydefinitions/$($controlItem.PolicyDefinitionGuid)')) and (PolicySetDefinitionName eq '$initiativeName' or PolicySetDefinitionName eq '$securityCenterInitiativeName')" -ErrorAction Stop if ($policyState) { $groupResultByComplianceState = $policyState | Group-Object -Property ComplianceState if (($groupResultByComplianceState | Measure-Object).Count -eq 1) { # Select first when multiple assignment are found for the same definition at subscription scope $policyState = $policyState | Select-Object -First 1 $policyStateObject = $policyState | Select-Object ResourceId, PolicyAssignmentId, PolicyAssignmentScope, PolicyDefinitionAction, IsCompliant if ($policyState.IsCompliant) { $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Passed; $controlResult.PolicyState.DataObject = $policyStateObject; } else { $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Failed; $controlResult.PolicyState.DataObject = $policyStateObject; } } else { $policyStateObject = @() $policyState | ForEach-Object { $AssignmentDetails = "" | Select-Object PolicyAssignmentId, PolicyAssignmentScope, PolicyDefinitionAction, IsCompliant, Parameters $assignmentdetails.PolicyAssignmentId = $_.PolicyAssignmentId $assignmentdetails.PolicyAssignmentScope = $_.PolicyAssignmentScope $assignmentdetails.PolicyDefinitionAction = $_.PolicyDefinitionAction $assignmentdetails.IsCompliant = $_.IsCompliant $assignment = Get-AzPolicyAssignment -Id $($_.PolicyAssignmentId) -ErrorAction SilentlyContinue if ($assignment) { $assignmentdetails.Parameters = $assignment.parameters } $policyStateObject += $assignmentdetails } # Mark policy verification result as Verify if control is found to be both compliance and non-compliant $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Verify $controlResult.PolicyState.DataObject = $policyStateObject; } } else { #Check if definition is created in portal for this respective control $definition = Get-AzPolicyDefinition -Name $($controlItem.PolicyDefinitionGuid) -ErrorAction Stop $assignment = $null # TODO: Move this to a common place, where it is called only once $initiative = @() # Get azsk policy initiative $azskpolicyinitiative = Get-AzPolicySetDefinition -Name $initiativeName -ErrorAction Stop if ($azskpolicyinitiative) { $assignment = Get-AzPolicyAssignment -PolicyDefinitionId $($azskpolicyinitiative.PolicySetDefinitionId) -ErrorAction Stop $initiative += $azskpolicyinitiative } # Get security center initiative $initiative += Get-AzPolicySetDefinition -Name $securityCenterInitiativeName -ErrorAction Stop # Definition is present, and compliance result not found if ($definition) { # Definition is present, and is added to the initiative if ( ($initiative | Measure-Object).Count -gt 0 ` -and [Helpers]::CheckMember($initiative[0], "Properties.policyDefinitions.policyDefinitionId") ` -and $initiative.Properties.policyDefinitions.policyDefinitionId -contains $definition.PolicyDefinitionId) { # Assignment is present; compliance state not found for this resource if ($assignment) { $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::NoResponse } # Assignment not found else { $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::AssignmentNotFound } } # Definition is present, and is not added to the initiative else { $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::DefinitionNotInInitiative } } # Definition is not present else { $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::DefinitionNotFound } } } catch { $controlResult.PolicyState.PolicyVerificationResult = [PolicyVerificationResult]::Error if ([Helpers]::CheckMember($_, "Exception.Message")) { $ErrorDetails = "" | Select-Object OuterMessage $ErrorDetails.OuterMessage = $_.Exception.Message $controlResult.PolicyState.DataObject = $ErrorDetails } else { $ErrorDetails = "" | Select-Object StackTrace $ErrorDetails.StackTrace = $_ $controlResult.PolicyState.DataObject = $ErrorDetails } } } # Policy compliance methods end hidden [ControlResult] CheckDiagnosticsSettings([ControlResult] $controlResult) { $diagnostics = $Null try { $diagnostics = Get-AzDiagnosticSetting -ResourceId $this.ResourceId -ErrorAction Stop -WarningAction SilentlyContinue } catch { if([Helpers]::CheckMember($_.Exception, "Response") -and ($_.Exception).Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { $controlResult.AddMessage([VerificationResult]::Failed, "Diagnostics setting is disabled for resource - [$($this.ResourceContext.ResourceName)]."); return $controlResult } else { $this.PublishException($_); } } if($Null -ne $diagnostics -and ($diagnostics.Logs | Measure-Object).Count -ne 0) { $nonCompliantLogs = $diagnostics.Logs | Where-Object { -not ($_.Enabled -and ($_.RetentionPolicy.Days -eq $this.ControlSettings.Diagnostics_RetentionPeriod_Forever -or $_.RetentionPolicy.Days -ge $this.ControlSettings.Diagnostics_RetentionPeriod_Min))}; $selectedDiagnosticsProps = $diagnostics | Select-Object -Property Logs, Metrics, StorageAccountId, EventHubName, Name; if(($nonCompliantLogs | Measure-Object).Count -eq 0) { $controlResult.AddMessage([VerificationResult]::Passed, "Diagnostics settings are correctly configured for resource - [$($this.ResourceContext.ResourceName)]", $selectedDiagnosticsProps); } else { $failStateDiagnostics = $nonCompliantLogs | Select-Object -Property Logs, Metrics, StorageAccountId, EventHubName, Name; $controlResult.SetStateData("Non compliant resources are:", $failStateDiagnostics); $controlResult.AddMessage([VerificationResult]::Failed, "Diagnostics settings are either disabled OR not retaining logs for at least $($this.ControlSettings.Diagnostics_RetentionPeriod_Min) days for resource - [$($this.ResourceContext.ResourceName)]", $selectedDiagnosticsProps); } } else { $controlResult.AddMessage([VerificationResult]::Failed, "Diagnostics setting is disabled for resource - [$($this.ResourceContext.ResourceName)]."); } return $controlResult; } hidden [ControlResult] CheckRBACAccess([ControlResult] $controlResult) { $accessList = [RoleAssignmentHelper]::GetAzSKRoleAssignmentByScope($this.ResourceId, $false, $true); return $this.CheckRBACAccess($controlResult, $accessList) } hidden [ControlResult] CheckRBACAccess([ControlResult] $controlResult, [PSObject] $accessList) { $resourceAccessList = $accessList | Where-Object { $_.Scope -eq $this.ResourceId }; $controlResult.VerificationResult = [VerificationResult]::Verify; if(($resourceAccessList | Measure-Object).Count -ne 0) { $controlResult.SetStateData("Identities having RBAC access at resource level", ($resourceAccessList | Select-Object -Property ObjectId,RoleDefinitionId,RoleDefinitionName,Scope)); $controlResult.AddMessage("Validate that the following identities have explicitly provided with RBAC access to resource - [$($this.ResourceContext.ResourceName)]"); $controlResult.AddMessage([MessageData]::new($this.CreateRBACCountMessage($resourceAccessList), $resourceAccessList)); } else { $controlResult.AddMessage("No identities have been explicitly provided with RBAC access to resource - [$($this.ResourceContext.ResourceName)]"); } $inheritedAccessList = $accessList | Where-Object { $_.Scope -ne $this.ResourceId }; if(($inheritedAccessList | Measure-Object).Count -ne 0) { $controlResult.AddMessage("Note: " + $this.CreateRBACCountMessage($inheritedAccessList) + " have inherited RBAC access to resource. It's good practice to keep the RBAC access to minimum."); } else { $controlResult.AddMessage("No identities have inherited RBAC access to resource"); } return $controlResult; } hidden [string] CreateRBACCountMessage([array] $resourceAccessList) { $nonNullObjectTypes = $resourceAccessList | Where-Object { -not [string]::IsNullOrEmpty($_.ObjectType) }; if(($nonNullObjectTypes | Measure-Object).Count -eq 0) { return "$($resourceAccessList.Count) identities"; } else { $countBreakupString = [string]::Join(", ", ($nonNullObjectTypes | Group-Object -Property ObjectType -NoElement | ForEach-Object { "$($_.Name): $($_.Count)" } )); return "$($resourceAccessList.Count) identities ($countBreakupString)"; } } hidden [bool] CheckMetricAlertConfiguration([PSObject[]] $metricSettings, [ControlResult] $controlResult, [string] $extendedResourceName) { $result = $false; if($metricSettings -and $metricSettings.Count -ne 0) { $resIdMessageString = ""; if(-not [string]::IsNullOrWhiteSpace($extendedResourceName)) { $resIdMessageString = "for nested resource [$extendedResourceName]"; } $resourceGrpAlerts = @() $resourceAlerts = @() $resourceGrpAlerts += Get-AzMetricAlertRuleV2 -ResourceGroup $this.ResourceContext.ResourceGroupName -WarningAction SilentlyContinue $resourceAlerts += $resourceGrpAlerts | Where-Object { ($_.Scopes -eq $this.ResourceId) -and ( $_.Enabled -eq '$true' ) } $alertsConfiguration = @(); $nonConfiguredMetrices = @(); $misConfiguredMetrices = @(); $metricSettings | ForEach-Object { $currentMetric = $_; $matchedMetrices = @(); $alertsConfiguration = @(); $matchedMetrices += $resourceAlerts | Where-Object { ($_.Criteria.MetricName -eq $currentMetric.Condition.DataSource.MetricName) } if($matchedMetrices.Count -eq 0) { $nonConfiguredMetrices += $currentMetric; } else { $misConfigured = @(); $matchedMetrices | ForEach-Object { if (($_.Criteria | Measure-Object).Count -gt 0 ) { $condition = New-Object -TypeName PSObject Add-Member -InputObject $condition -Name "OperatorProperty" -MemberType NoteProperty -Value $_.Criteria.OperatorProperty Add-Member -InputObject $condition -Name "Threshold" -MemberType NoteProperty -Value $_.Criteria.Threshold Add-Member -InputObject $condition -Name "TimeAggregation" -MemberType NoteProperty -Value $_.Criteria.TimeAggregation Add-Member -InputObject $condition -Name "WindowSize" -MemberType NoteProperty -Value $_.WindowSize.ToString() $obj= [PSCustomObject]@{MetricName = $_.Criteria.MetricName} Add-Member -InputObject $condition -Name "DataSource" -MemberType NoteProperty -Value $obj $alert = New-Object -TypeName PSObject Add-Member -InputObject $alert -Name "Condition" -MemberType NoteProperty -Value $condition $actions=@(); if([Helpers]::CheckMember($_,"Actions.actionGroupId")) { $_.Actions | ForEach-Object { $actionGroupTemp = $_.actionGroupId.Split("/") $actionGroup = Get-AzActionGroup -ResourceGroupName $actionGroupTemp[4] -Name $actionGroupTemp[-1] -WarningAction SilentlyContinue if([Helpers]::CheckMember($actionGroup,"EmailReceivers.Status")) { if($actionGroup.EmailReceivers.Status -eq [Microsoft.Azure.Management.Monitor.Models.ReceiverStatus]::Enabled) { if([Helpers]::CheckMember($actionGroup,"EmailReceivers.EmailAddress")) { $actions += $actionGroup } } } } } Add-Member -InputObject $alert -Name "Actions" -MemberType NoteProperty -Value $actions Add-Member -InputObject $alert -Name "AlertName" -MemberType NoteProperty -Value $_.Name Add-Member -InputObject $alert -Name "AlertType" -MemberType NoteProperty -Value $_.Type if(($alert|Measure-Object).Count -gt 0) { $alertsConfiguration += $alert } } } } if(($alertsConfiguration|Measure-Object).Count -gt 0) { $alertsConfiguration | ForEach-Object { if([Helpers]::CompareObject($currentMetric.Condition, $_.Condition)) { $isActionConfigured = $false; if (($_.Actions | Measure-Object).Count -gt 0 ) { $isActionConfigured = $true; } if(-not $isActionConfigured) { $misConfigured += $_.Condition; } } else { $misConfigured += $_.Condition } }; if($misConfigured.Count -eq $matchedMetrices.Count) { $misConfiguredMetrices += $misConfigured; } } } $controlResult.AddMessage("Following metric alerts must be configured $resIdMessageString with settings mentioned below:", $metricSettings); $controlResult.VerificationResult = [VerificationResult]::Failed; if($nonConfiguredMetrices.Count -ne 0) { $controlResult.AddMessage("Following metric alerts are not configured $($resIdMessageString):", $nonConfiguredMetrices); } if($misConfiguredMetrices.Count -ne 0) { $controlResult.AddMessage("Following metric alerts are not correctly configured $resIdMessageString. Please update the metric settings in order to comply.", $misConfiguredMetrices); } if($nonConfiguredMetrices.Count -eq 0 -and $misConfiguredMetrices.Count -eq 0) { $result = $true; $controlResult.AddMessage([VerificationResult]::Passed , "All mandatory metric alerts are correctly configured $resIdMessageString."); } } else { throw [System.ArgumentException] ("The argument 'metricSettings' is null or empty"); } return $result; } hidden [void] GetDataFromSubscriptionReport($singleControlResult) { try { $azskConfig = [ConfigurationManager]::GetAzSKConfigData(); $settingStoreComplianceSummaryInUserSubscriptions = [ConfigurationManager]::GetAzSKSettings().StoreComplianceSummaryInUserSubscriptions; #return if feature is turned off at server config if(-not $azskConfig.StoreComplianceSummaryInUserSubscriptions -and -not $settingStoreComplianceSummaryInUserSubscriptions) {return;} if(($this.ComplianceStateData | Measure-Object).Count -gt 0) { $ResourceData = @(); $PersistedControlScanResult=@(); #$ResourceScanResult=$ResourceData.ResourceScanResult [ControlResult[]] $controlsResults = @(); $singleControlResult.ControlResults | ForEach-Object { $currentControl=$_ $partsToHash = $singleControlResult.ControlItem.Id; if(-not [string]::IsNullOrWhiteSpace($currentControl.ChildResourceName)) { $partsToHash = $partsToHash + ":" + $currentControl.ChildResourceName; } $rowKey = [Helpers]::ComputeHash($partsToHash.ToLower()); $matchedControlResult = $this.ComplianceStateData | Where-Object { $_.RowKey -eq $rowKey} # initialize default values $currentControl.FirstScannedOn = [DateTime]::UtcNow if($currentControl.ActualVerificationResult -ne [VerificationResult]::Passed) { $currentControl.FirstFailedOn = [DateTime]::UtcNow } if($null -ne $matchedControlResult -and ($matchedControlResult | Measure-Object).Count -gt 0) { $currentControl.UserComments = $matchedControlResult.UserComments $currentControl.FirstFailedOn = [datetime] $matchedControlResult.FirstFailedOn $currentControl.FirstScannedOn = [datetime] $matchedControlResult.FirstScannedOn } $scanFromDays = [System.DateTime]::UtcNow.Subtract($currentControl.FirstScannedOn) $currentControl.MaximumAllowedGraceDays = $this.CalculateGraceInDays($singleControlResult); # Setting isControlInGrace Flag if($scanFromDays.Days -le $currentControl.MaximumAllowedGraceDays) { $currentControl.IsControlInGrace = $true } else { $currentControl.IsControlInGrace = $false } $controlsResults+=$currentControl } $singleControlResult.ControlResults=$controlsResults } } catch { $this.PublishException($_); } } } |