Framework/Abstracts/SVTBase.ps1
<#
.Description # SVTBase class for all service classes. # Provides functionality to create context object for resources, load controls for resource, #> Set-StrictMode -Version Latest class SVTBase: AzSKRoot { #Region: Properties hidden [string] $ResourceId = "" [ResourceContext] $ResourceContext = $null; hidden [SVTConfig] $SVTConfig hidden [PSObject] $ControlSettings hidden [ControlStateExtension] $ControlStateExt; hidden [ControlState[]] $ResourceState; hidden [ControlState[]] $DirtyResourceStates; hidden [ControlItem[]] $ApplicableControls = $null; hidden [ControlItem[]] $FeatureApplicableControls = $null; [string[]] $ChildResourceNames = $null; [System.Net.SecurityProtocolType] $currentSecurityProtocol; #User input parameters for controls [string[]] $FilterTags = @(); [string[]] $ExcludeTags = @(); [string[]] $ControlIds = @(); [string[]] $Severity = @(); [string[]] $ExcludeControlIds = @(); [hashtable] $ResourceTags = @{} [bool] $GenerateFixScript = $false; [bool] $IncludeUserComments = $false; [string] $PartialScanIdentifier = [string]::Empty [ComplianceStateTableEntity[]] $ComplianceStateData = @(); [PSObject[]] $ChildSvtObjects = @(); [bool] $IsLocalComplianceStoreEnabled = $false; [bool] $IsControlExclusionByOrgPolicyEnabled = $false; [bool] $IsResourceScan = $false; # To identify whether its resource scan or subscription scan #EndRegion SVTBase([string] $subscriptionId): Base($subscriptionId) { # Intialize control exclusion feature flag $this.IsControlExclusionByOrgPolicyEnabled = [FeatureFlightingManager]::GetFeatureStatus("EnableControlExclusionByOrgPolicy",$($this.SubscriptionContext.SubscriptionId)); } SVTBase([string] $subscriptionId, [SVTResource] $svtResource): Base($subscriptionId, [SVTResource] $svtResource) { $this.CreateInstance($svtResource); $this.IsControlExclusionByOrgPolicyEnabled = [FeatureFlightingManager]::GetFeatureStatus("EnableControlExclusionByOrgPolicy",$($this.SubscriptionContext.SubscriptionId)); } #Create instance for resource scan hidden [void] CreateInstance([SVTResource] $svtResource) { $this.IsResourceScan = $true; #Flag used to indentifying whether its Resource scan or Subscription Scan [Helpers]::AbstractClass($this, [SVTBase]); #Region: validation for resource object if (-not $svtResource) { throw [System.ArgumentException] ("The argument 'svtResource' is null"); } if ([string]::IsNullOrEmpty($svtResource.ResourceGroupName)) { throw [System.ArgumentException] ("The argument 'ResourceGroupName' is null or empty"); } if ([string]::IsNullOrEmpty($svtResource.ResourceName)) { throw [System.ArgumentException] ("The argument 'ResourceName' is null or empty"); } #EndRegion #<TODO Framework: ResourceTypeMapping is already part of svtResource and populated from Resolver. Below validation is redudant. if (-not $svtResource.ResourceTypeMapping) { $svtResource.ResourceTypeMapping = [SVTMapping]::Mapping | Where-Object { $_.ClassName -eq $this.GetType().Name } | Select-Object -First 1 } if (-not $svtResource.ResourceTypeMapping) { throw [System.ArgumentException] ("No ResourceTypeMapping found"); } if ([string]::IsNullOrEmpty($svtResource.ResourceTypeMapping.JsonFileName)) { throw [System.ArgumentException] ("JSON file name is null or empty"); } $this.ResourceId = $svtResource.ResourceId; $this.LoadSvtConfig($svtResource.ResourceTypeMapping.JsonFileName); $this.ResourceContext = [ResourceContext]@{ ResourceGroupName = $svtResource.ResourceGroupName; ResourceName = $svtResource.ResourceName; ResourceType = $svtResource.ResourceTypeMapping.ResourceType; ResourceTypeName = $svtResource.ResourceTypeMapping.ResourceTypeName; ResourceId = $svtResource.ResourceId ResourceDetails = $svtResource.ResourceDetails }; #<TODO Framework: Fetch resource group details from resolver itself> $this.ResourceContext.ResourceGroupTags = $this.ResourceTags; } hidden [void] LoadSvtConfig([string] $controlsJsonFileName) { $this.ControlSettings = $this.LoadServerConfigFile("ControlSettings.json"); if (-not $this.SVTConfig) { #Check if SVTConfig is present in cache and fetch the same if present if ([ConfigurationHelper]::PolicyCacheContent.ContainsKey($controlsJsonFileName)) { [Policy]$policy = [ConfigurationHelper]::PolicyCacheContent[$controlsJsonFileName] $this.SVTConfig = $policy.Content #If policy is already in Final State simply return if ($policy.State -eq [PolicyCacheStatus]::Final) { return } } # If Policy is not present in the Cache if(-not $this.SVTConfig) { $this.SVTConfig = [ConfigurationManager]::GetSVTConfig($controlsJsonFileName); } # Code proceeds here if either the policy was not in the cache and we fetched it from policy server or it was present in cache but in Raw state $this.SVTConfig.Controls | Foreach-Object { #Expand description and recommendation string if any dynamic values defined field using control settings $_.Description = $global:ExecutionContext.InvokeCommand.ExpandString($_.Description) $_.Recommendation = $global:ExecutionContext.InvokeCommand.ExpandString($_.Recommendation) $ControlSeverity = $_.ControlSeverity #Check if ControlSeverity is customized/overridden using controlsettings configurations if ([Helpers]::CheckMember($this.ControlSettings, "ControlSeverity.$ControlSeverity")) { $_.ControlSeverity = $this.ControlSettings.ControlSeverity.$ControlSeverity } #<TODO Framework: Do we really need to trim method name as it is defined by developer> if (-not [string]::IsNullOrEmpty($_.MethodName)) { $_.MethodName = $_.MethodName.Trim(); } #Check if if ($this.CheckBaselineControl($_.ControlID)) { $_.IsBaselineControl = $true } #AddPreviewBaselineFlag if ($this.CheckPreviewBaselineControl($_.ControlID)) { $_.IsPreviewBaselineControl = $true } # Check for control exclusion if ($this.CheckForControlExclusion($_.ControlID)) { $_.IsControlExcluded = $true } } #Save the final SVTConfig in cache $policy = [Policy]@{ State = [PolicyCacheStatus]::Final Content = $this.SVTConfig } [ConfigurationHelper]::PolicyCacheContent[$controlsJsonFileName] = $policy } } #stub to be used when Baseline configuration exists hidden [bool] CheckBaselineControl($controlId) { return $false } #stub to be used when PreviewBaseline configuration exists hidden [bool] CheckPreviewBaselineControl($controlId) { return $false } #Check if service is under mentainance and display maintenance warning message [bool] ValidateMaintenanceState() { if ($this.SVTConfig.IsMaintenanceMode) { $this.PublishCustomMessage(([ConfigurationManager]::GetAzSKConfigData().MaintenanceMessage -f $this.SVTConfig.FeatureName), [MessageType]::Warning); } return $this.SVTConfig.IsMaintenanceMode; } hidden [ControlResult] CreateControlResult([string] $childResourceName, [VerificationResult] $verificationResult) { [ControlResult] $control = [ControlResult]@{ VerificationResult = $verificationResult; }; if (-not [string]::IsNullOrEmpty($childResourceName)) { $control.ChildResourceName = $childResourceName; } [SessionContext] $sc = [SessionContext]::new(); $sc.IsLatestPSModule = $this.RunningLatestPSModule; $control.CurrentSessionContext = $sc; return $control; } [ControlResult] CreateControlResult() { return $this.CreateControlResult("", [VerificationResult]::Manual); } hidden [ControlResult] CreateControlResult([FixControl] $fixControl) { $control = $this.CreateControlResult(); if ($this.GenerateFixScript -and $fixControl -and $fixControl.Parameters -and ($fixControl.Parameters | Get-Member -MemberType Properties | Measure-Object).Count -ne 0) { $control.FixControlParameters = $fixControl.Parameters | Select-Object -Property *; } return $control; } [ControlResult] CreateControlResult([string] $childResourceName) { return $this.CreateControlResult($childResourceName, [VerificationResult]::Manual); } [ControlResult] CreateChildControlResult([string] $childResourceName, [ControlResult] $controlResult) { $control = $this.CreateControlResult($childResourceName, [VerificationResult]::Manual); if ($controlResult.FixControlParameters -and ($controlResult.FixControlParameters | Get-Member -MemberType Properties | Measure-Object).Count -ne 0) { $control.FixControlParameters = $controlResult.FixControlParameters | Select-Object -Property *; } return $control; } hidden [SVTEventContext] CreateSVTEventContextObject() { return [SVTEventContext]@{ FeatureName = $this.SVTConfig.FeatureName; Metadata = [Metadata]@{ Reference = $this.SVTConfig.Reference; }; SubscriptionContext = $this.SubscriptionContext; ResourceContext = $this.ResourceContext; PartialScanIdentifier = $this.PartialScanIdentifier; IsLocalComplianceStoreEnabled = $this.IsLocalComplianceStoreEnabled; }; } hidden [SVTEventContext] CreateErrorEventContext([System.Management.Automation.ErrorRecord] $exception) { [SVTEventContext] $arg = $this.CreateSVTEventContextObject(); $arg.ExceptionMessage = $exception; return $arg; } hidden [void] ControlStarted([SVTEventContext] $arg) { $this.PublishEvent([SVTEvent]::ControlStarted, $arg); } hidden [void] ControlDisabled([SVTEventContext] $arg) { $this.PublishEvent([SVTEvent]::ControlDisabled, $arg); } hidden [void] ControlCompleted([SVTEventContext] $arg) { $this.PublishEvent([SVTEvent]::ControlCompleted, $arg); } hidden [void] ControlError([ControlItem] $controlItem, [System.Management.Automation.ErrorRecord] $exception) { $arg = $this.CreateErrorEventContext($exception); $arg.ControlItem = $controlItem; $this.PublishEvent([SVTEvent]::ControlError, $arg); } hidden [void] EvaluationCompleted([SVTEventContext[]] $arguments) { $this.PublishEvent([SVTEvent]::EvaluationCompleted, $arguments); } hidden [void] EvaluationStarted() { $this.PublishEvent([SVTEvent]::EvaluationStarted, $this.CreateSVTEventContextObject()); } hidden [void] EvaluationError([System.Management.Automation.ErrorRecord] $exception) { $this.PublishEvent([SVTEvent]::EvaluationError, $this.CreateErrorEventContext($exception)); } [SVTEventContext[]] EvaluateAllControls() { [SVTEventContext[]] $resourceSecurityResult = @(); if (-not $this.ValidateMaintenanceState()) { $ControlsApplicableForScan = @(); $ControlsApplicableForScan = $this.GetApplicableControls(); if ($ControlsApplicableForScan.Count -eq 0) { if ($this.ResourceContext) { $this.PublishCustomMessage("No controls have been found to evaluate for Resource [$($this.ResourceContext.ResourceName)]", [MessageType]::Warning); $this.PublishCustomMessage("$([Constants]::SingleDashLine)"); # Marking resource scan completed status in ResourceScanTracker file if no controls found to be applicable # This will avoid scanning resource repetitively in case of CA and unblock further scan if ($this.invocationContext.BoundParameters["UsePartialCommits"]) { [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); $partialScanMngr.UpdateResourceStatus( $this.ResourceContext.ResourceId, "COMP"); } } else { $this.PublishCustomMessage("No controls have been found to evaluate for Subscription", [MessageType]::Warning); } } else { $this.PostTelemetry(); $this.EvaluationStarted(); $resourceSecurityResult += $this.GetAutomatedSecurityStatus(); $resourceSecurityResult += $this.GetManualSecurityStatus(); $this.PostEvaluationCompleted($resourceSecurityResult); $this.EvaluationCompleted($resourceSecurityResult); $EnabledApplicableControlForScan = @(); $EnabledApplicableControlForScan = $ControlsApplicableForScan | Where-Object { $_.Enabled -eq $true }; # Marking resource scan completed status in ResourceScanTracker file if no controls found to be Enabled # This scenario has been observed in case of Org Policy overwriting Control jsons # This will avoid scanning resource repetitively in case of CA and unblock further scan if ($this.invocationContext.BoundParameters["UsePartialCommits"] -and $EnabledApplicableControlForScan -eq $null) { [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); $partialScanMngr.UpdateResourceStatus( $this.ResourceContext.ResourceId, "COMP"); } } } return $resourceSecurityResult; } [SVTEventContext[]] RescanAndPostAttestationData() { [SVTEventContext[]] $resourceScanResult = @(); [SVTEventContext[]] $stateResult = @(); [ControlItem[]] $controlsToBeEvaluated = @(); $this.PostTelemetry(); #Publish event to display host message to indicate start of resource scan $this.EvaluationStarted(); #Fetch attested controls list from Blob $stateResult = $this.GetControlsStateResult() If (($stateResult | Measure-Object).Count -gt 0 ) { #Get controls list which were attested in last 24 hours $attestedControlsinBlob = $stateResult | Where-Object { $_.ControlResults.StateManagement.AttestedStateData.AttestedDate -gt ((Get-Date).AddDays(-1)) } if (($attestedControlsinBlob | Measure-Object).Count -gt 0 ) { $attestedControlsinBlob | ForEach-Object { $controlsToBeEvaluated += $_.ControlItem }; $this.ApplicableControls = @($controlsToBeEvaluated); $resourceScanResult += $this.GetAutomatedSecurityStatus(); $resourceScanResult += $this.GetManualSecurityStatus(); $this.PostEvaluationCompleted($resourceScanResult); $this.EvaluationCompleted($resourceScanResult); } else { Write-Host "No attested control found.`n$([Constants]::SingleDashLine)" } } else { Write-Host "No attested control found.`n$([Constants]::SingleDashLine)" } return $resourceScanResult; } [SVTEventContext[]] ComputeApplicableControlsWithContext() { [SVTEventContext[]] $contexts = @(); if (-not $this.ValidateMaintenanceState()) { $controls = $this.GetApplicableControls(); if ($controls.Count -gt 0) { foreach ($control in $controls) { [SVTEventContext] $singleControlResult = $this.CreateSVTEventContextObject(); $singleControlResult.ControlItem = $control; $contexts += $singleControlResult; } } } return $contexts; } [void] PostTelemetry() { # Setting the protocol for databricks if ([Helpers]::CheckMember($this.ResourceContext, "ResourceType") -and $this.ResourceContext.ResourceType -eq "Microsoft.Databricks/workspaces") { $this.currentSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } $this.PostFeatureControlTelemetry() } [void] PostFeatureControlTelemetry() { #todo add check for latest module version if ($this.RunningLatestPSModule -and ($this.FeatureApplicableControls | Measure-Object).Count -gt 0) { [CustomData] $customData = [CustomData]::new(); $customData.Name = "FeatureControlTelemetry"; $ResourceObject = "" | Select ResourceContext, Controls, ChildResourceNames; $ResourceObject.ResourceContext = $this.ResourceContext; $ResourceObject.Controls = $this.FeatureApplicableControls; $ResourceObject.ChildResourceNames = $this.ChildResourceNames; $customData.Value = $ResourceObject; $this.PublishCustomData($customData); } } [SVTEventContext[]] FetchStateOfAllControls() { [SVTEventContext[]] $resourceSecurityResult = @(); if (-not $this.ValidateMaintenanceState()) { if ($this.GetApplicableControls().Count -eq 0) { $this.PublishCustomMessage("No security controls match the input criteria specified", [MessageType]::Warning); } else { $this.EvaluationStarted(); $resourceSecurityResult += $this.GetControlsStateResult(); if (($resourceSecurityResult | Measure-Object).Count -gt 0) { $this.EvaluationCompleted($resourceSecurityResult); } } } return $resourceSecurityResult; } [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls) { return $controls; } hidden [ControlItem[]] GetApplicableControls() { #Lazy load the list of the applicable controls if ($null -eq $this.ApplicableControls) { $this.ApplicableControls = @(); $this.FeatureApplicableControls = @(); $filterControlsById = @(); $filteredControls = @(); #Apply service filters based on default set of controls $this.FeatureApplicableControls += $this.ApplyServiceFilters($this.SVTConfig.Controls); if ($this.ControlIds.Count -ne 0) { $filterControlsById += $this.FeatureApplicableControls | Where-Object { $this.ControlIds -Contains $_.ControlId }; } else { $filterControlsById += $this.FeatureApplicableControls } if ($this.ExcludeControlIds.Count -ne 0) { $filterControlsById = $filterControlsById | Where-Object { $this.ExcludeControlIds -notcontains $_.ControlId }; } #Filter controls based on filterstags and excludetags $filterTagsCount = ($this.FilterTags | Measure-Object).Count $excludeTagsCount = ($this.ExcludeTags | Measure-Object).Count #filters controls based on Severity if ($this.Severity.Count -ne 0 -and ($filterControlsById | Measure-Object).Count -gt 0) { $filterControlsById = $filterControlsById | Where-Object { $_.ControlSeverity -in $this.Severity }; } $unfilteredControlsCount = ($filterControlsById | Measure-Object).Count if ($unfilteredControlsCount -gt 0) { #If we have any controls at this point... #If FilterTags are specified, limit the candidate set to matching controls if ($filterTagsCount -gt 0) { #Look at each candidate control's tags and see if there's a match in FilterTags $filterControlsById | ForEach-Object { Set-Variable -Name control -Value $_ -Scope Local Set-Variable -Name filterMatch -Value $false -Scope Local $filterMatch = $false $control.Tags | ForEach-Object { Set-Variable -Name cTag -Value $_ -Scope Local if ( ($this.FilterTags | Where-Object { $_ -like $cTag } | Measure-Object).Count -ne 0) { $filterMatch = $true } } #Add if this control has a tag that matches FilterTags if ($filterMatch) { $filteredControls += $control } } } else { #No FilterTags specified, so all controls qualify $filteredControls = $filterControlsById } #Note: Candidate controls list is now in $filteredControls...we will use that to calculate $filteredControlsFinal $filteredControlsFinal = @() if ($excludeTagsCount -eq 0) { #If exclude tags are not specified, then not much to do. $filteredControlsFinal = $filteredControls } else { #ExludeTags _are_ specified, we need to check if candidate set has to be reduced... #Look at each candidate control's tags and see if there's a match in ExcludeTags $filteredControls | ForEach-Object { Set-Variable -Name control -Value $_ -Scope Local Set-Variable -Name excludeMatch -Value $false -Scope Local $excludeMatch = $false $control.Tags | ForEach-Object { Set-Variable -Name cTag -Value $_ -Scope Local if (($this.ExcludeTags | Where-Object { $_ -like $cTag } | Measure-Object).Count -ne 0) { $excludeMatch = $true } } #Add to final list if this control *does-not* have a tag that matches ExcludeTags if (-not $excludeMatch) { $filteredControlsFinal += $control } } $filteredControls = $filteredControlsFinal } } $this.ApplicableControls = $filteredControls; #this filtering has been done as the first step it self; #$this.ApplicableControls += $this.ApplyServiceFilters($filteredControls); } return $this.ApplicableControls; } hidden [SVTEventContext[]] GetManualSecurityStatus() { [SVTEventContext[]] $manualControlsResult = @(); try { $this.GetApplicableControls() | Where-Object { $_.Automated -eq "No" -and $_.Enabled -eq $true } | ForEach-Object { $controlItem = $_; [SVTEventContext] $arg = $this.CreateSVTEventContextObject(); $arg.ControlItem = $controlItem; [ControlResult] $control = [ControlResult]@{ VerificationResult = [VerificationResult]::Manual; }; [SessionContext] $sc = [SessionContext]::new(); $sc.IsLatestPSModule = $this.RunningLatestPSModule; $control.CurrentSessionContext = $sc; $arg.ControlResults += $control $this.PostProcessData($arg); $manualControlsResult += $arg; } } catch { $this.EvaluationError($_); } return $manualControlsResult; } hidden [SVTEventContext[]] GetAutomatedSecurityStatus() { [SVTEventContext[]] $automatedControlsResult = @(); $this.DirtyResourceStates = @(); try { $this.GetApplicableControls() | Where-Object { $_.Automated -ne "No" -and (-not [string]::IsNullOrEmpty($_.MethodName)) } | ForEach-Object { $eventContext = $this.RunControl($_); if ($null -ne $eventContext -and $eventcontext.ControlResults.Length -gt 0) { $automatedControlsResult += $eventContext; } }; } catch { $this.EvaluationError($_); } return $automatedControlsResult; } hidden [SVTEventContext[]] GetControlsStateResult() { [SVTEventContext[]] $automatedControlsResult = @(); $this.DirtyResourceStates = @(); try { $this.GetApplicableControls() | ForEach-Object { $eventContext = $this.FetchControlState($_); #filter controls if there is no state found if ($eventContext) { $eventContext.ControlResults = $eventContext.ControlResults | Where-Object { $_.AttestationStatus -ne [AttestationStatus]::None } if ($eventContext.ControlResults) { $automatedControlsResult += $eventContext; } } }; } catch { $this.EvaluationError($_); } return $automatedControlsResult; } hidden [SVTEventContext] RunControl([ControlItem] $controlItem) { [SVTEventContext] $singleControlResult = $this.CreateSVTEventContextObject(); $singleControlResult.ControlItem = $controlItem; $this.ControlStarted($singleControlResult); if ($controlItem.Enabled -eq $false) { $this.ControlDisabled($singleControlResult); } else { $azskScanResult = $this.CreateControlResult($controlItem.FixControl); try { $methodName = $controlItem.MethodName; #$this.CurrentControlItem = $controlItem; $singleControlResult.ControlResults += $this.$methodName($azskScanResult); } catch { $azskScanResult.VerificationResult = [VerificationResult]::Error $azskScanResult.AddError($_); $singleControlResult.ControlResults += $azskScanResult $this.ControlError($controlItem, $_); } $this.PostProcessData($singleControlResult); # Check for the control which requires elevated permission to modify 'Recommendation' so that user can know it is actually automated if they have the right permission if ($singleControlResult.ControlItem.Automated -eq "Yes") { $singleControlResult.ControlResults | ForEach-Object { $currentItem = $_; if ($_.VerificationResult -eq [VerificationResult]::Manual -and $singleControlResult.ControlItem.Tags.Contains([Constants]::OwnerAccessTagName)) { $singleControlResult.ControlItem.Recommendation = [Constants]::RequireOwnerPermMessage + $singleControlResult.ControlItem.Recommendation } } } } $this.ControlCompleted($singleControlResult); return $singleControlResult; } # Policy compliance methods begin hidden [ControlResult] ComputeFinalScanResult([ControlResult] $azskScanResult, [ControlResult] $policyScanResult) { if ($policyScanResult.VerificationResult -ne [VerificationResult]::Failed -and $azskScanResult.VerificationResult -ne [VerificationResult]::Passed) { return $azskScanResult } else { return $policyScanResult; } } hidden [SVTEventContext] FetchControlState([ControlItem] $controlItem) { [SVTEventContext] $singleControlResult = $this.CreateSVTEventContextObject(); $singleControlResult.ControlItem = $controlItem; $controlState = @(); $controlStateValue = @(); try { $resourceStates = $this.GetResourceState(); if (($resourceStates | Measure-Object).Count -ne 0) { $controlStateValue += $resourceStates | Where-Object { $_.InternalId -eq $singleControlResult.ControlItem.Id }; $controlStateValue | ForEach-Object { $currentControlStateValue = $_; if ($null -ne $currentControlStateValue) { #assign expiry date $expiryIndays = $this.CalculateExpirationInDays($singleControlResult, $currentControlStateValue); if ($expiryIndays -ne -1) { $currentControlStateValue.State.ExpiryDate = ($currentControlStateValue.State.AttestedDate.AddDays($expiryIndays)).ToString("MM/dd/yyyy"); } $controlState += $currentControlStateValue; } } } } catch { $this.EvaluationError($_); } if (($controlState | Measure-Object).Count -gt 0) { if (!(Get-Variable AttestationValue -Scope Global)) { $this.ControlStarted($singleControlResult); } if ($controlItem.Enabled -eq $false) { $this.ControlDisabled($singleControlResult); } else { $controlResult = $this.CreateControlResult($controlItem.FixControl); $singleControlResult.ControlResults += $controlResult; $singleControlResult.ControlResults | ForEach-Object { try { $currentItem = $_; if ($controlState.Count -ne 0) { # Process the state if it's available $childResourceState = $controlState | Where-Object { $_.ChildResourceName -eq $currentItem.ChildResourceName } | Select-Object -First 1; if ($childResourceState) { $currentItem.StateManagement.AttestedStateData = $childResourceState.State; $currentItem.AttestationStatus = $childResourceState.AttestationStatus; $currentItem.ActualVerificationResult = $childResourceState.ActualVerificationResult; $currentItem.VerificationResult = [VerificationResult]::NotScanned } } } catch { $this.EvaluationError($_); } }; } $this.ControlCompleted($singleControlResult); } return $singleControlResult; } hidden [void] PostEvaluationCompleted([SVTEventContext[]] $ControlResults) { # If ResourceType is Databricks, reverting security protocol if ([Helpers]::CheckMember($this.ResourceContext, "ResourceType") -and $this.ResourceContext.ResourceType -eq "Microsoft.Databricks/workspaces") { [Net.ServicePointManager]::SecurityProtocol = $this.currentSecurityProtocol } $this.UpdateControlStates($ControlResults); } hidden [void] UpdateControlStates([SVTEventContext[]] $ControlResults) { if ($null -ne $this.ControlStateExt -and $this.ControlStateExt.HasControlStateWriteAccessPermissions() -and ($ControlResults | Measure-Object).Count -gt 0 -and ($this.ResourceState | Measure-Object).Count -gt 0) { $effectiveResourceStates = @(); if (($this.DirtyResourceStates | Measure-Object).Count -gt 0) { $this.ResourceState | ForEach-Object { $controlState = $_; if (($this.DirtyResourceStates | Where-Object { $_.InternalId -eq $controlState.InternalId -and $_.ChildResourceName -eq $controlState.ChildResourceName } | Measure-Object).Count -eq 0) { $effectiveResourceStates += $controlState; } } } else { #If no dirty states found then no action needed. return; } #get the uniqueid from the first control result. Here we can take first as it would come here for each resource. $id = $ControlResults[0].GetUniqueId(); $this.ControlStateExt.SetControlState($id, $effectiveResourceStates, $true) } } hidden [void] PostProcessData([SVTEventContext] $eventContext) { $tempHasRequiredAccess = $true; $controlState = @(); $controlStateValue = @(); try { # Get policy compliance if org-level flag is enabled and policy is found #TODO: set flag in a variable once and reuse it if ([FeatureFlightingManager]::GetFeatureStatus("EnableAzurePolicyBasedScan", $($this.SubscriptionContext.SubscriptionId)) -eq $true) { if (-not [string]::IsNullOrWhiteSpace($eventContext.ControlItem.PolicyDefinitionGuid)) { #update with policy compliance state result; This result will be captured in AI telemetry data #todo: currently excluding child controls $policyScanResult = $this.CheckPolicyCompliance($eventContext.ControlItem, $eventContext.ControlResults[0]); #TODO: Remove this block if new logic of policy compliance check works as expected # This block can be reused if we want to replace control scanned result with policy complaince state <# #create default controlresult $policyScanResult = $this.CreateControlResult($eventContext.ControlItem.FixControl); #update default controlresult with policy compliance state $policyScanResult = $this.CheckPolicyCompliance($eventContext.ControlItem, $policyScanResult); #todo: currently excluding child controls if($eventContext.ControlResults.Count -eq 1 -and $Null -ne $policyScanResult) { $finalScanResult = $this.ComputeFinalScanResult($eventContext.ControlResults[0],$policyScanResult) $eventContext.ControlResults[0] = $finalScanResult } #> } } $this.GetDataFromSubscriptionReport($eventContext); $resourceStates = $this.GetResourceState() if (($resourceStates | Measure-Object).Count -ne 0) { $controlStateValue += $resourceStates | Where-Object { $_.InternalId -eq $eventContext.ControlItem.Id }; $controlStateValue | ForEach-Object { $currentControlStateValue = $_; if ($null -ne $currentControlStateValue) { if ($this.IsStateActive($eventContext, $currentControlStateValue)) { $controlState += $currentControlStateValue; } else { #add to the dirty state list so that it can be removed later $this.DirtyResourceStates += $currentControlStateValue; } } } } elseif ($null -eq $resourceStates) { $tempHasRequiredAccess = $false; } } catch { $this.EvaluationError($_); } $eventContext.ControlResults | ForEach-Object { try { $currentItem = $_; # Copy the current result to Actual Result field $currentItem.ActualVerificationResult = $currentItem.VerificationResult; #Logic to append the control result with the permissions metadata [SessionContext] $sc = $currentItem.CurrentSessionContext; $sc.Permissions.HasAttestationWritePermissions = $this.ControlStateExt.HasControlStateWriteAccessPermissions(); $sc.Permissions.HasAttestationReadPermissions = $this.ControlStateExt.HasControlStateReadAccessPermissions(); # marking the required access as false if there was any error reading the attestation data $sc.Permissions.HasRequiredAccess = $sc.Permissions.HasRequiredAccess -and $tempHasRequiredAccess; # Disable the fix control feature if (-not $this.GenerateFixScript) { $currentItem.EnableFixControl = $false; } if ($currentItem.StateManagement.CurrentStateData -and $currentItem.StateManagement.CurrentStateData.DataObject -and $eventContext.ControlItem.DataObjectProperties) { $currentItem.StateManagement.CurrentStateData.DataObject = [Helpers]::SelectMembers($currentItem.StateManagement.CurrentStateData.DataObject, $eventContext.ControlItem.DataObjectProperties); } if ($controlState.Count -ne 0) { # Process the state if its available $childResourceState = $controlState | Where-Object { $_.ChildResourceName -eq $currentItem.ChildResourceName } | Select-Object -First 1; if ($childResourceState) { $isControlExcludedByOrgPolicy = $this.IsControlExclusionByOrgPolicyEnabled -and $eventcontext.controlItem.IsControlExcluded # Skip passed ones (that are not marked excluded as per org policy) from State Management if (($currentItem.ActualVerificationResult -ne [VerificationResult]::Passed) -or $isControlExcludedByOrgPolicy) { #compare the states if control is not marked excluded as per org policy if (($childResourceState.ActualVerificationResult -eq $currentItem.ActualVerificationResult -or $isControlExcludedByOrgPolicy) -and $childResourceState.State) { $currentItem.StateManagement.AttestedStateData = $childResourceState.State; # Compare dataobject property of State if ($null -ne $childResourceState.State.DataObject) { if ($currentItem.StateManagement.CurrentStateData -and $null -ne $currentItem.StateManagement.CurrentStateData.DataObject) { $currentStateDataObject = [JsonHelper]::ConvertToJsonCustom($currentItem.StateManagement.CurrentStateData.DataObject) | ConvertFrom-Json try { # Objects match, change result based on attestation status if ($eventContext.ControlItem.AttestComparisionType -and $eventContext.ControlItem.AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) { if ([Helpers]::CompareObject($childResourceState.State.DataObject, $currentStateDataObject, $true, $eventContext.ControlItem.AttestComparisionType)) { $this.ModifyControlResult($currentItem, $childResourceState); } } else { if ([Helpers]::CompareObject($childResourceState.State.DataObject, $currentStateDataObject, $true)) { $this.ModifyControlResult($currentItem, $childResourceState); } } } catch { $this.EvaluationError($_); } } } else { if ($currentItem.StateManagement.CurrentStateData) { if ($null -eq $currentItem.StateManagement.CurrentStateData.DataObject) { # No object is persisted, change result based on attestation status $this.ModifyControlResult($currentItem, $childResourceState); } } else { # No object is persisted, change result based on attestation status $this.ModifyControlResult($currentItem, $childResourceState); } } #region: Prevent attestation drift due to dev changes if ( ([FeatureFlightingManager]::GetFeatureStatus("PreventAttestationStateDrift", $($this.SubscriptionContext.SubscriptionId)))) { # Check if drift is expected if ($eventContext.ControlItem.IsAttestationDriftExpected -eq $true) { if ($eventcontext.controlItem.OnAttestationDrift -and ` (-not [String]::IsNullOrEmpty($eventcontext.controlItem.OnAttestationDrift.ApplyToVersionsUpto))) { #check if drift is expected in all AzSK versions(* is specified in the control json file) if ($eventcontext.controlItem.OnAttestationDrift.ApplyToVersionsUpto -eq "*") { $IsDriftAllowedInCurrVer = $true } # Check if attested version is less than or equal to the last stable version (as specified in the control json file) elseif ([System.Version] $childResourceState.Version -le [System.Version] $eventcontext.controlItem.OnAttestationDrift.ApplyToVersionsUpto) { $IsDriftAllowedInCurrVer = $true } else { $IsDriftAllowedInCurrVer = $false } if ($IsDriftAllowedInCurrVer) { # Check action to be taken on drift # Repect attestation if attested with older version if (($eventcontext.controlItem.OnAttestationDrift.ActionOnAttestationDrift -eq [ActionOnAttestationDrift]::RespectExistingAttestationExpiryPeriod) -or ` ($eventcontext.controlItem.OnAttestationDrift.ActionOnAttestationDrift -eq [ActionOnAttestationDrift]::OverrideAttestationExpiryPeriod)) { $this.ModifyControlResult($currentItem, $childResourceState) } # Filter specific properties from state data object and compare result elseif ($eventcontext.controlItem.OnAttestationDrift.ActionOnAttestationDrift -eq [ActionOnAttestationDrift]::CheckOnlySelectPropertiesInDataObject) { #Filter select properties from dataobject if ($childResourceState.State -and $childResourceState.State.DataObject -and $eventContext.ControlItem.DataObjectProperties) { $childResourceState.State.DataObject = [Helpers]::SelectMembers($childResourceState.State.DataObject, $eventContext.ControlItem.DataObjectProperties); } # Compare dataobject property of State if ($null -ne $childResourceState.State.DataObject) { if ($currentItem.StateManagement.CurrentStateData -and $null -ne $currentItem.StateManagement.CurrentStateData.DataObject) { $currentStateDataObject = [JsonHelper]::ConvertToJsonCustom($currentItem.StateManagement.CurrentStateData.DataObject) | ConvertFrom-Json try { # Objects match, change result based on attestation status if ($eventContext.ControlItem.AttestComparisionType -and $eventContext.ControlItem.AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) { if ([Helpers]::CompareObject($childResourceState.State.DataObject, $currentStateDataObject, $true, $eventContext.ControlItem.AttestComparisionType)) { $this.ModifyControlResult($currentItem, $childResourceState); } } else { if ([Helpers]::CompareObject($childResourceState.State.DataObject, $currentStateDataObject, $true)) { $this.ModifyControlResult($currentItem, $childResourceState); } } } catch { $this.EvaluationError($_); } } } } # Don't fail attestation if current state data object is a subset of attested state data object elseif ($eventcontext.controlItem.OnAttestationDrift.ActionOnAttestationDrift -eq [ActionOnAttestationDrift]::CheckIfSubset) { # Compare dataobject property of State if ($null -ne $childResourceState.State.DataObject) { # Note: DataObjectProperties filter is not required here as the data object has already been filterd in above scan if ($currentItem.StateManagement.CurrentStateData -and $null -ne $currentItem.StateManagement.CurrentStateData.DataObject) { $currentStateDataObject = [JsonHelper]::ConvertToJsonCustom($currentItem.StateManagement.CurrentStateData.DataObject) | ConvertFrom-Json try { # Objects match, change result based on attestation status if ($eventContext.ControlItem.AttestComparisionType -and $eventContext.ControlItem.AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) { if ([Helpers]::CompareObject($childResourceState.State.DataObject, $currentStateDataObject, $false, $eventContext.ControlItem.AttestComparisionType)) { $this.ModifyControlResult($currentItem, $childResourceState); } } else { if ([Helpers]::CompareObject($currentStateDataObject, $childResourceState.State.DataObject, $false)) { $this.ModifyControlResult($currentItem, $childResourceState); } } } catch { $this.EvaluationError($_); } } } } } } } } #endregion: Prevent attestation drift due to dev changes #region: Respect attestation for controls excluded by org policy if($isControlExcludedByOrgPolicy){ $this.ModifyControlResult($currentItem, $childResourceState); } #endregion: Respect attestation for controls excluded by org policy } } else { #add to the dirty state list so that it can be removed later $this.DirtyResourceStates += $childResourceState } } } } catch { $this.EvaluationError($_); } }; } # State Machine implementation of modifying verification result hidden [void] ModifyControlResult([ControlResult] $controlResult, [ControlState] $controlState) { # No action required if Attestation status is None OR verification result is Passed if ($controlState.AttestationStatus -ne [AttestationStatus]::None -or $controlResult.VerificationResult -ne [VerificationResult]::Passed) { $controlResult.AttestationStatus = $controlState.AttestationStatus; $controlResult.VerificationResult = [Helpers]::EvaluateVerificationResult($controlResult.VerificationResult, $controlState.AttestationStatus); } } hidden [ControlState[]] GetResourceState() { if ($null -eq $this.ResourceState) { $this.ResourceState = @(); if ($this.ControlStateExt -and $this.ControlStateExt.HasControlStateReadAccessPermissions()) { $resourceStates = $this.ControlStateExt.GetControlState($this.ResourceId) if ($null -ne $resourceStates) { $this.ResourceState += $resourceStates } else { return $null; } } } return $this.ResourceState; } #Function to validate attestation data expiry validation hidden [bool] IsStateActive([SVTEventContext] $eventcontext, [ControlState] $controlState) { try { $expiryIndays = $this.CalculateExpirationInDays([SVTEventContext] $eventcontext, [ControlState] $controlState); #Validate if expiry period is passed #Added a condition so as to expire attested controls that were in 'Error' state. if (($expiryIndays -ne -1 -and $controlState.State.AttestedDate.AddDays($expiryIndays) -lt [DateTime]::UtcNow) -or ($controlState.ActualVerificationResult -eq [VerificationResult]::Error)) { return $false } else { $controlState.State.ExpiryDate = ($controlState.State.AttestedDate.AddDays($expiryIndays)).ToString("MM/dd/yyyy"); return $true } } catch { #if any exception occurs while getting/validating expiry period, return true. $this.EvaluationError($_); return $true } } hidden [int] CalculateExpirationInDays([SVTEventContext] $eventcontext, [ControlState] $controlState) { try { #For exempt controls, either the no. of days for expiry were provided at the time of attestation or a default of 6 motnhs was already considered, #therefore skipping this flow and calculating days directly using the expiry date already saved. if ($controlState.AttestationStatus -ne [AttestationStatus]::ApprovedException) { #Get controls expiry period. Default value is zero $controlAttestationExpiry = $eventcontext.controlItem.AttestationExpiryPeriodInDays $controlSeverity = $eventcontext.controlItem.ControlSeverity $controlSeverityExpiryPeriod = 0 $defaultAttestationExpiryInDays = [Constants]::DefaultControlExpiryInDays; $expiryInDays = -1; if (($eventcontext.ControlResults | Measure-Object).Count -gt 0) { $isControlInGrace = $eventcontext.ControlResults.IsControlInGrace; } else { $isControlInGrace = $true; } if ([Helpers]::CheckMember($this.ControlSettings, "AttestationExpiryPeriodInDays") ` -and [Helpers]::CheckMember($this.ControlSettings.AttestationExpiryPeriodInDays, "Default") ` -and $this.ControlSettings.AttestationExpiryPeriodInDays.Default -gt 0) { $defaultAttestationExpiryInDays = $this.ControlSettings.AttestationExpiryPeriodInDays.Default } #Below code is commented for future reference, if we want to make changes for customizing expiry duration for individual controls #if ([Helpers]::CheckMember($this.ControlSettings, "ControlsWithCustomExpiryPeriod")) #{ #$CustomExpiryControl = $this.ControlSettings.ControlsWithCustomExpiryPeriod| Where-Object{ #$controlState.ControlId -eq $_.ControlId } #} #if ($null -ne $CustomExpiryControl) #{ #$expiryInDays = $CustomExpiryControl.ExpiryPeriod #} #Json reference for above code, to be maintained in ControlSettings.json #"ControlsWithCustomExpiryPeriod":[ # { # "ControlId" : "Azure_AppService_AuthN_Use_AAD_for_Client_AuthN", #"ExpiryPeriod" : 180 #}, #{ #"ControlId" : "Azure_Storage_AuthN_Dont_Allow_Anonymous", #"ExpiryPeriod" : 150 #} #] #if control has ExtendedExpiry tag, set expiry duration to 180 for such controls. (Value of expiry days is taken from control settings) if(($eventcontext.ControlItem.Tags -contains "ExtendedExpiry") -and ([Helpers]::CheckMember($this.ControlSettings, "ExtendedAttestationExpiry"))) { $expiryInDays = $this.ControlSettings.ExtendedAttestationExpiry } #Expiry in the case of WillFixLater or StateConfirmed/Recurring Attestation state will be based on Control Severity. elseif ($controlState.AttestationStatus -eq [AttestationStatus]::NotAnIssue -or $controlState.AttestationStatus -eq [AttestationStatus]::NotApplicable) { $expiryInDays = $defaultAttestationExpiryInDays; } else { # Expire WillFixLater if GracePeriod has expired if ($this.IsLocalComplianceStoreEnabled -and -not($isControlInGrace) -and $controlState.AttestationStatus -eq [AttestationStatus]::WillFixLater) { $expiryInDays = 0; } else { if ($controlAttestationExpiry -ne 0) { $expiryInDays = $controlAttestationExpiry } elseif ([Helpers]::CheckMember($this.ControlSettings, "AttestationExpiryPeriodInDays")) { $controlsev = $this.ControlSettings.ControlSeverity.PSobject.Properties | Where-Object Value -eq $controlSeverity | Select-Object -First 1 $controlSeverity = $controlsev.name #Check if control severity has expiry period if ([Helpers]::CheckMember($this.ControlSettings.AttestationExpiryPeriodInDays.ControlSeverity, $controlSeverity) ) { $expiryInDays = $this.ControlSettings.AttestationExpiryPeriodInDays.ControlSeverity.$controlSeverity } #If control item and severity does not contain expiry period, assign default value else { $expiryInDays = $defaultAttestationExpiryInDays } } #Return -1 when expiry is not defined else { $expiryInDays = -1 } } } } else { #Calculating the expiry in days for exempt controls if (($controlState.AttestationStatus -eq "ApprovedException") -and [String]::IsNullOrEmpty($controlState.State.ExpiryDate)) { $expiryDate = ($controlState.State.AttestedDate).AddDays($this.ControlSettings.DefaultAttestationPeriodForExemptControl) } else { $expiryDate = [DateTime]$controlState.State.ExpiryDate } #Adding 1 explicitly to the days since the differnce below excludes the expiryDate and that also needs to be taken into account. $expiryInDays = ($expiryDate - $controlState.State.AttestedDate).Days + 1 } } catch { #if any exception occurs while getting/validating expiry period, return -1. $this.EvaluationError($_); $expiryInDays = -1 } # Check if attestation drift is expected if ( ([FeatureFlightingManager]::GetFeatureStatus("PreventAttestationStateDrift", $($this.SubscriptionContext.SubscriptionId)))) { if (($eventcontext.controlItem.IsAttestationDriftExpected -eq $true) -and ($expiryInDays -ne -1)) { # Check action to be taken on drift for a specific control if ($eventcontext.controlItem.OnAttestationDrift -and ` (-not [String]::IsNullOrEmpty($eventcontext.controlItem.OnAttestationDrift.ApplyToVersionsUpto)) -and ` (-not [String]::IsNullOrEmpty($eventcontext.controlItem.OnAttestationDrift.OverrideAttestationExpiryInDays)) -and ` ($eventcontext.controlItem.OnAttestationDrift.ActionOnAttestationDrift -eq [ActionOnAttestationDrift]::OverrideAttestationExpiryPeriod)) { # Check if drift is expected in all AzSK versions(* is specified in the control json file) if ($eventcontext.controlItem.OnAttestationDrift.ApplyToVersionsUpto -eq "*") { $IsDriftAllowedInCurrVer = $true } # Check if attested version is less than or equal to the last stable version (as specified in the control json file) elseif ([System.Version] $controlState.Version -le [System.Version] $eventcontext.controlItem.OnAttestationDrift.ApplyToVersionsUpto) { $IsDriftAllowedInCurrVer = $true } else { $IsDriftAllowedInCurrVer = $false } if ($IsDriftAllowedInCurrVer) { # Change attestation expiry period if number of days left for control to expire is greater than custom attestation expiry period # This sets the attestation to expire before the original expiry period if ($expiryInDays -gt $eventcontext.controlItem.OnAttestationDrift.OverrideAttestationExpiryInDays) { $expiryInDays = $eventcontext.controlItem.OnAttestationDrift.OverrideAttestationExpiryInDays } } } } } return $expiryInDays } hidden AddResourceMetadata([PSObject] $metadataObj) { [hashtable] $resourceMetadata = New-Object -TypeName Hashtable; $metadataObj.psobject.properties | ForEach-Object { $resourceMetadata.Add($_.name, $_.value) } if ([Helpers]::CheckMember($this.ControlSettings, 'AllowedResourceTypesForMetadataCapture') ) { if ( $this.ResourceContext.ResourceTypeName -in $this.ControlSettings.AllowedResourceTypesForMetadataCapture) { $this.ResourceContext.ResourceMetadata = $resourceMetadata } else { $this.ResourceContext.ResourceMetadata = $null } } else { $this.ResourceContext.ResourceMetadata = $resourceMetadata } } hidden [SVTResource] CreateSVTResource([string] $ConnectionResourceId, [string] $ResourceGroupName, [string] $ConnectionResourceName, [string] $ResourceType, [string] $Location, [string] $MappingName) { $svtResource = [SVTResource]::new(); $svtResource.ResourceId = $ConnectionResourceId; $svtResource.ResourceGroupName = $ResourceGroupName; $svtResource.ResourceName = $ConnectionResourceName $svtResource.ResourceType = $ResourceType; # $svtResource.Location = $Location; $svtResource.ResourceTypeMapping = ([SVTMapping]::Mapping | Where-Object { $_.ResourceTypeName -eq $MappingName } | Select-Object -First 1); return $svtResource; } #stub to be used when ComplianceState hidden [void] GetDataFromSubscriptionReport($singleControlResult) { } [int] hidden CalculateGraceInDays([SVTEventContext] $context) { $controlResult = $context.ControlResults; $computedGraceDays = 15; $ControlBasedGraceExpiryInDays = 0; $currentControlItem = $context.controlItem; $controlSeverity = $currentControlItem.ControlSeverity; if ([Helpers]::CheckMember($this.ControlSettings, "NewControlGracePeriodInDays")) { if ([Helpers]::CheckMember($this.ControlSettings, "ControlSeverity")) { $controlsev = $this.ControlSettings.ControlSeverity.PSobject.Properties | Where-Object Value -eq $controlSeverity | Select-Object -First 1 $controlSeverity = $controlsev.name $computedGraceDays = $this.ControlSettings.NewControlGracePeriodInDays.ControlSeverity.$ControlSeverity; } else { $computedGraceDays = $this.ControlSettings.NewControlGracePeriodInDays.ControlSeverity.$ControlSeverity; } } if ($null -ne $currentControlItem.GraceExpiryDate) { if ($currentControlItem.GraceExpiryDate -gt [DateTime]::UtcNow ) { $ControlBasedGraceExpiryInDays = $currentControlItem.GraceExpiryDate.Subtract($controlResult.FirstScannedOn).Days if ($ControlBasedGraceExpiryInDays -gt $computedGraceDays) { $computedGraceDays = $ControlBasedGraceExpiryInDays } } } return $computedGraceDays; } } |