Framework/Core/AzSKInfo/ComplianceInfo.ps1
using namespace System.Management.Automation Set-StrictMode -Version Latest class ComplianceInfo: CommandBase { hidden [ComplianceMessageSummary[]] $ComplianceMessageSummary = @(); hidden [ComplianceResult[]] $ComplianceScanResult = @(); hidden [string] $SubscriptionId hidden [bool] $Full hidden $SVTConfig = @{} hidden $baselineControls = @(); hidden [PSObject] $ControlSettings hidden [PSObject] $EmptyResource = @(); ComplianceInfo([string] $subscriptionId, [InvocationInfo] $invocationContext, [bool] $full): Base($subscriptionId, $invocationContext) { $this.SubscriptionId = $subscriptionId $this.Full = $full } hidden [void] GetComplianceScanData() { $ComplianceRptHelper = [ComplianceReportHelper]::new($this.SubscriptionContext.SubscriptionId); $ComplianceReportData = $ComplianceRptHelper.GetLocalSubscriptionScanReport($this.SubscriptionContext.SubscriptionId) if($null -ne $ComplianceReportData -and $null -ne $ComplianceReportData.ScanDetails) { if(($ComplianceReportData.ScanDetails.SubscriptionScanResult | Measure-Object).Count -gt 0) { $ComplianceReportData.ScanDetails.SubscriptionScanResult | ForEach-Object { $subScanRes = $_ $tmpCompRes = [ComplianceResult]::new([SVTMapping]::SubscriptionMapping.ClassName, "/subscriptions/"+$this.SubscriptionId, "", "", "", [VerificationResult]::Manual, $false, [ControlSeverity]::High, [VerificationResult]::NotScanned) $this.MapScanResultToComplianceResult($subScanRes, $tmpCompRes) $this.ComplianceScanResult += $tmpCompRes } } if(($ComplianceReportData.ScanDetails.Resources | Measure-Object).Count -gt 0) { $ComplianceReportData.ScanDetails.Resources | ForEach-Object { $resource = $_ if($null -ne $resource -and ($resource.ResourceScanResult | Measure-Object).Count -gt 0) { $resource.ResourceScanResult | ForEach-Object { $resourceScanRes = $_ $tmpCompRes = [ComplianceResult]::new($resource.FeatureName, $resource.ResourceId, $resource.ResourceGroupName, $resource.ResourceName, "", [VerificationResult]::Manual, $false, [ControlSeverity]::High, [VerificationResult]::NotScanned) $this.MapScanResultToComplianceResult($resourceScanRes, $tmpCompRes) $this.ComplianceScanResult += $tmpCompRes } } else { $this.EmptyResource += $resource | Select-Object FeatureName, ResourceId, ResourceGroupName, ResourceName } } } } } #This function is responsible to convert the persisted compliance data to the required report format hidden [void] MapScanResultToComplianceResult([LSRControlResultBase] $scannedControlResult, [ComplianceResult] $complianceResult) { $complianceResult.PSObject.Properties | ForEach-Object { $property = $_ try { #need to handle the enums case specifically, as checkmember fails to recognize enums if([Helpers]::CheckMember($scannedControlResult,$property.Name) -or $property.Name -eq "VerificationResult" -or $property.Name -eq "AttestationStatus" -or $property.Name -eq "ControlSeverity" -or $property.Name -eq "ScanSource") { $propValue= $scannedControlResult | Select-Object -ExpandProperty $property.Name if([Constants]::AzSKDefaultDateTime -eq $propValue) { $_.Value = "" } else { $_.Value = $propValue } } } catch { # need to add detail in catch block #$currentInstance.PublishException($_); } } } hidden [void] GetComplianceInfo() { $this.PublishCustomMessage([Constants]::DoubleDashLine, [MessageType]::Default); $azskConfig = [ConfigurationManager]::GetAzSKConfigData(); $settingPersistScanReportInSubscription = [ConfigurationManager]::GetAzSKSettings().PersistScanReportInSubscription; #return if feature is turned off at server config if(-not $azskConfig.PersistScanReportInSubscription -and -not $settingPersistScanReportInSubscription) { $this.PublishCustomMessage("NOTE: This feature is currently disabled in your environment. Please contact the cloud security team for your org. ", [MessageType]::Warning); return; } #Below code is commented as CA can be configured in multiple ways apart from AzSKRG # $this.PublishCustomMessage("`r`nChecking if the subscription ["+ $this.SubscriptionId +"] is setup for Continuous Assurance (CA) scanning...", [MessageType]::Default); # $AutomationAccount=[Constants]::AutomationAccount # $AzSKRGName=[ConfigurationManager]::GetAzSKConfigData().AzSKRGName # $caAutomationAccount = Get-AzureRmAutomationAccount -Name $AutomationAccount -ResourceGroupName $AzSKRGName -ErrorAction SilentlyContinue # if($caAutomationAccount) # { # $this.PublishCustomMessage("`r`nCA setup found in the subscription ["+ $this.SubscriptionId +"].", [MessageType]::Default); # } # else # { # $this.PublishCustomMessage("`r`nCA setup not found in the subscription ["+ $this.SubscriptionId +"].", [MessageType]::Default); # $this.PublishCustomMessage("`r`nCompliance data may be inaccurate when CA is not setup or is unhealthy.", [MessageType]::Default); # } $this.PublishCustomMessage([Constants]::DoubleDashLine, [MessageType]::Default); $this.PublishCustomMessage("`r`nFetching compliance info for subscription "+ $this.SubscriptionId +" ...", [MessageType]::Default); $this.PublishCustomMessage([Constants]::SingleDashLine, [MessageType]::Default); $this.GetComplianceScanData(); $this.GetControlDetails(); $this.ComputeCompliance(); $this.GetComplianceSummary() $this.ExportComplianceResultCSV() } #ToDo Where is this function called hidden [void] GetControlDetails() { $resourcetypes = @() $resourcetypes += ([SVTMapping]::SubscriptionMapping | Select-Object JsonFileName) $resourcetypes += ([SVTMapping]::Mapping | Sort-Object ResourceTypeName | Select-Object JsonFileName ) # Fetch control Setting data $this.ControlSettings = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); # Filter control for baseline controls if($null -ne $this.ControlSettings) { if([Helpers]::CheckMember($this.ControlSettings,"BaselineControls.ResourceTypeControlIdMappingList")) { $this.baselineControls += $this.ControlSettings.BaselineControls.ResourceTypeControlIdMappingList | Select-Object ControlIds | ForEach-Object { $_.ControlIds } } if([Helpers]::CheckMember($this.ControlSettings,"BaselineControls.SubscriptionControlIdList")) { $this.baselineControls += $this.ControlSettings.BaselineControls.SubscriptionControlIdList | ForEach-Object { $_ } } } $resourcetypes | ForEach-Object{ $controls = [ConfigurationManager]::GetSVTConfig($_.JsonFileName); # Filter control for enable only $controls.Controls = ($controls.Controls | Where-Object { $_.Enabled -eq $true }) if ([Helpers]::CheckMember($controls, "Controls") -and $controls.Controls.Count -gt 0) { $this.SVTConfig.Add($controls.FeatureName, @($controls.Controls)) } } } hidden [void] ComputeCompliance() { $this.ComplianceScanResult | ForEach-Object { # ToDo: Add condition to check whether control in grace if($_.FeatureName -eq "AzSKCfg" -or $_.VerificationResult -eq [VerificationResult]::Disabled) { $_.EffectiveResult = [VerificationResult]::Skipped } else { if($_.VerificationResult -eq [VerificationResult]::Passed) { $_.EffectiveResult = [VerificationResult]::Passed $lastScannedDate = [datetime] $_.LastScannedOn $days = [DateTime]::UtcNow.Subtract($lastScannedDate).Days [int]$allowedDays = [Constants]::ControlResultComplianceInDays if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"ResultComplianceInDays.DefaultControls")) { [int32]::TryParse($this.ControlSettings.ResultComplianceInDays.DefaultControls, [ref]$allowedDays) } if($_.HasOwnerAccessTag) { if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"ResultComplianceInDays.OwnerAccessControls")) { [int32]::TryParse($this.ControlSettings.ResultComplianceInDays.OwnerAccessControls, [ref]$allowedDays) } } #revert back to actual result if control result is stale if($days -ge $allowedDays) { $_.EffectiveResult = [VerificationResult]::Failed } } else { $_.EffectiveResult = [VerificationResult]::Failed } } } # Append missing controls for scanned resources $groupedResult = $this.ComplianceScanResult | Group-Object { $_.ResourceId } foreach($group in $groupedResult){ $featureControls = $this.SVTConfig[$group.Group[0].FeatureName] if(($group.Group).Count -ne $featureControls.Count) { $featureControls | ForEach-Object { $singleControl = $_ if(($group.Group | Where-Object { $_.ControlID -eq $singleControl.ControlId } | Measure-Object).Count -eq 0) { $isControlInBaseline = $false if($this.baselineControls -contains $singleControl.ControlID) { $isControlInBaseline = $true } $controlToAdd = [ComplianceResult]::new($group.Group[0].FeatureName, $group.Group[0].ResourceId, $group.Group[0].ResourceGroupName, $group.Group[0].ResourceName, $singleControl.ControlID, [VerificationResult]::Manual, $isControlInBaseline, $singleControl.ControlSeverity, [VerificationResult]::Failed) $this.ComplianceScanResult += $controlToAdd } } } } # Add controls for resource inventory $this.EmptyResource | ForEach-Object { $resource = $_ $featureControls = $this.SVTConfig[$resource.FeatureName] $featureControls | ForEach-Object { $singleControl = $_ $isControlInBaseline = $false if($this.baselineControls -contains $singleControl.ControlID) { $isControlInBaseline = $true } $controlToAdd = [ComplianceResult]::new($resource.FeatureName, $resource.ResourceId, $resource.ResourceGroupName, $resource.ResourceName, $singleControl.ControlID, [VerificationResult]::Manual, $isControlInBaseline, $singleControl.ControlSeverity, [VerificationResult]::Failed) $this.ComplianceScanResult += $controlToAdd } } # Extra Check for subscription security if((($this.ComplianceScanResult | Where-Object { $_.FeatureName -eq [SVTMapping]::SubscriptionMapping.ClassName }) | Measure-Object).Count -eq 0) { $subControls = $this.SVTConfig[[SVTMapping]::SubscriptionMapping.ClassName] # When no subscription control available in json then flow comes here. Adding all subscription controls $subControls | ForEach-Object { $singleControl = $_ $isControlInBaseline = $false if($this.baselineControls -contains $singleControl.ControlID) { $isControlInBaseline = $true } $controlToAdd = [ComplianceResult]::new([SVTMapping]::SubscriptionMapping.ClassName, "/subscriptions/"+$this.SubscriptionId, "", "", $singleControl.ControlID, [VerificationResult]::Manual, $isControlInBaseline, $singleControl.ControlSeverity, [VerificationResult]::Failed) $this.ComplianceScanResult += $controlToAdd } } } hidden [void] GetComplianceSummary() { $totalCompliance = 0.0 $baselineCompliance = 0.0 $passControlCount = 0 $failedControlCount = 0 $baselinePassedControlCount = 0 $baselineFailedControlCount = 0 $attestedControlCount = 0 $gracePeriodControlCount = 0 $totalControlCount = 0 $baselineControlCount = 0 $attestedControlCount = 0 $gracePeriodControlCount = 0 if(($this.ComplianceScanResult | Measure-Object).Count -gt 0) { $this.ComplianceScanResult | ForEach-Object { $result = $_ #ideally every proper control should fall under effective result in passed/failed/skipped if($result.EffectiveResult -eq [VerificationResult]::Passed -or $result.EffectiveResult -eq [VerificationResult]::Failed) { # total count has been kept inside to exclude not-scanned and skipped controls $totalControlCount++ if($result.EffectiveResult -eq [VerificationResult]::Passed) { $passControlCount++ #baseline controls condition shouldnot increment if it wont fall in passed/ failed state if($_.IsBaselineControl.ToLower() -eq "true") { $baselineControlCount++ $baselinePassedControlCount++ } } elseif($result.EffectiveResult -eq [VerificationResult]::Failed) { $failedControlCount++ if($_.IsBaselineControl.ToLower() -eq "true") { $baselineControlCount++ $baselineFailedControlCount++ } } if(-not [string]::IsNullOrEmpty($result.AttestationStatus) -and ($result.AttestationStatus -ne [AttestationStatus]::None)) { $attestedControlCount++ } if($result.IsControlInGrace) { $gracePeriodControlCount++ } } } $totalCompliance = (100 * $passControlCount)/($passControlCount + $failedControlCount) $baselineCompliance = (100 * $baselinePassedControlCount)/($baselinePassedControlCount + $baselineFailedControlCount) $ComplianceStats = @(); $ComplianceStat = "" | Select-Object "ComplianceType", "Pass-%", "No. of Passed Controls", "No. of Failed Controls" $ComplianceStat.ComplianceType = "Baseline" $ComplianceStat."Pass-%"= [math]::Round($baselineCompliance,2) $ComplianceStat."No. of Passed Controls" = $baselinePassedControlCount $ComplianceStat."No. of Failed Controls" = $baselineFailedControlCount $ComplianceStats += $ComplianceStat $ComplianceStat = "" | Select-Object "ComplianceType", "Pass-%", "No. of Passed Controls", "No. of Failed Controls" $ComplianceStat.ComplianceType = "Full" $ComplianceStat."Pass-%"= [math]::Round($totalCompliance,2) $ComplianceStat."No. of Passed Controls" = $passControlCount $ComplianceStat."No. of Failed Controls" = $failedControlCount $ComplianceStats += $ComplianceStat $this.PublishCustomMessage(($ComplianceStats | Format-Table | Out-String), [MessageType]::Default) $this.PublishCustomMessage([Constants]::SingleDashLine, [MessageType]::Default); $this.PublishCustomMessage("`r`nAttested control count: "+ $attestedControlCount , [MessageType]::Default); $this.PublishCustomMessage("`r`nControl in grace period count: "+ $gracePeriodControlCount , [MessageType]::Default); $this.PublishCustomMessage([Constants]::DoubleDashLine, [MessageType]::Default); $this.PublishCustomMessage("`r`n`r`n`r`nDisclaimer: Compliance summary/control counts may differ slightly from the central telemetry/dashboard due to various timing/sync lags.", [MessageType]::Default); } } hidden [void] GetControlsInGracePeriod() { $this.PublishCustomMessage("List of control in grace period", [MessageType]::Default); } hidden [void] ExportComplianceResultCSV() { $this.ComplianceScanResult | ForEach-Object { if($_.IsBaselineControl.ToLower() -eq "true") { $_.IsBaselineControl = "Yes" } else { $_.IsBaselineControl = "No" } if($_.IsControlInGrace.ToLower() -eq "true") { $_.IsControlInGrace = "Yes" } else { $_.IsControlInGrace = "No" } if($_.AttestationStatus.ToLower() -eq "none") { $_.AttestationStatus = "" } if($_.HasOwnerAccessTag.ToLower() -eq "true") { $_.HasOwnerAccessTag = "Yes" } else { $_.HasOwnerAccessTag = "No" } } $objectToExport = $this.ComplianceScanResult if(-not $this.Full) { $objectToExport = $this.ComplianceScanResult | Select-Object "ControlId", "VerificationResult", "ActualVerificationResult", "FeatureName", "ResourceGroupName", "ResourceName", "ChildResourceName", "IsBaselineControl", ` "ControlSeverity", "AttestationStatus", "AttestedBy", "Justification", "LastScannedOn", "ScanSource", "ScannedBy", "ScannerModuleName", "ScannerVersion" } $controlCSV = New-Object -TypeName WriteCSVData $controlCSV.FileName = 'ComplianceDetails_' + $this.RunIdentifier $controlCSV.FileExtension = 'csv' $controlCSV.FolderPath = '' $controlCSV.MessageData = $objectToExport $this.PublishAzSKRootEvent([AzSKRootEvent]::WriteCSV, $controlCSV); } AddComplianceMessage([string] $ComplianceType, [string] $ComplianceCount, [string] $ComplianceComment) { $ComplianceMessage = New-Object -TypeName ComplianceMessageSummary $ComplianceMessage.ComplianceType = $ComplianceType $ComplianceMessage.ComplianceCount = $ComplianceCount $this.ComplianceMessageSummary += $ComplianceMessage } } class ComplianceMessageSummary { [string] $ComplianceType = "" [string] $ComplianceCount = "" #[string] $ComplianceComment = "" } class ComplianceResult { [string] $ControlId = "" [VerificationResult] $VerificationResult = [VerificationResult]::Manual [VerificationResult] $ActualVerificationResult= [VerificationResult]::Manual; [string] $FeatureName = "" [string] $ResourceGroupName = "" [string] $ResourceName = "" [string] $ChildResourceName = "" [string] $IsBaselineControl = "" [ControlSeverity] $ControlSeverity = [ControlSeverity]::High [string] $AttestationCounter = "" [string] $AttestationStatus = "" [string] $AttestedBy = "" [string] $AttestedDate = "" [string] $Justification = "" [String] $UserComments = "" [string] $LastScannedOn = "" [string] $FirstScannedOn = "" [string] $FirstFailedOn = "" [string] $FirstAttestedOn = "" [string] $LastResultTransitionOn = "" [string] $ScanSource = "" [string] $ScannedBy = "" [string] $ScannerModuleName = "" [string] $ScannerVersion = "" [string] $IsControlInGrace = "" [string] $HasOwnerAccessTag = "" [string] $ResourceId = "" [VerificationResult] $EffectiveResult = [VerificationResult]::NotScanned ComplianceResult($featureName, $resourceId, $resourceGroupName, $resourceName, $controlId, $verificationResult, $isBaselineControl, $controlSeverity, $effectiveResult) { $this.ControlId = $controlId $this.FeatureName = $featureName $this.VerificationResult = $verificationResult $this.ResourceGroupName = $resourceGroupName $this.ResourceName = $resourceName $this.IsBaselineControl = $isBaselineControl $this.ControlSeverity = $controlSeverity $this.ResourceId = $resourceId $this.EffectiveResult = $effectiveResult } } |