Framework/Helpers/ComplianceReportHelper.ps1
Set-StrictMode -Version Latest class ComplianceReportHelper: ComplianceBase { hidden [string] $ScanSource hidden [System.Version] $ScannerVersion hidden [string] $ScanKind static [ComplianceReportHelper] $Instance ComplianceReportHelper([SubscriptionContext] $subscriptionContext,[System.Version] $ScannerVersion): Base([SubscriptionContext] $subscriptionContext) { $this.ScanSource = [RemoteReportHelper]::GetScanSource(); $this.ScannerVersion = $ScannerVersion $this.ScanKind = [ServiceScanKind]::Partial; } #Get cached instance for compliance. This is to avoid repeatative calls for base constructor which fetch details of AzSK resources on every resource static [ComplianceReportHelper] GetInstance([SubscriptionContext] $subscriptionContext,[System.Version] $ScannerVersion) { if ( $null -eq [ComplianceReportHelper]::Instance) { [ComplianceReportHelper]::Instance = [ComplianceReportHelper]::new($subscriptionContext, $ScannerVersion) } return [ComplianceReportHelper]::Instance } hidden [ComplianceStateTableEntity[]] GetSubscriptionComplianceReport() { return $this.GetSubscriptionComplianceReport($null,$null); } hidden [ComplianceStateTableEntity[]] GetSubscriptionComplianceReport([string[]] $PartitionKeys) { [ComplianceStateTableEntity[]] $finalResults = @(); if($PartitionKeys.Length -gt 0) { $limit = 15; if($PartitionKeys.Length -le $limit) { $queryStringParam = $this.ConstructPartitionKeysFilterQueryString($PartitionKeys); return $this.GetSubscriptionComplianceReport($queryStringParam, $null); } else { $counter = 1; $subPartitionKeys = @(); $totalCount = $PartitionKeys.Length; foreach($partitionKey in $PartitionKeys) { $subPartitionKeys += $partitionKey; if($counter % $limit -eq 0 -or $totalCount -eq $counter) { $queryStringParam = $this.ConstructPartitionKeysFilterQueryString($subPartitionKeys); $finalResults += $this.GetSubscriptionComplianceReport($queryStringParam, $null); $subPartitionKeys = @(); } $counter += 1; } } } return $finalResults; } hidden [ComplianceStateTableEntity[]] GetSubscriptionComplianceReport($currentScanResults,$selectColumns) { $filterStringParams = ""; $selectStringParams = ""; $partitionKeys = @(); if(($currentScanResults | Measure-Object).Count -gt 0) { $currentScanResults | ForEach-Object { $currentScanResult = $_; $resourceId = $currentScanResult.SubscriptionContext.Scope; if($currentScanResult.IsResource()) { $resourceId = $currentScanResult.ResourceContext.ResourceId; } $controlsToProcess = @(); if(($currentScanResult.ControlResults | Measure-Object).Count -gt 0) { $controlsToProcess += $currentScanResult.ControlResults; } $controlsToProcess | ForEach-Object { $cScanResult = $_; $currentResultHashId_p = [Helpers]::ComputeHash($resourceId.ToLower()); $partitionKeys += $currentResultHashId_p; } } $partitionKeys = $partitionKeys | Select -Unique $filterStringParams = $this.ConstructPartitionKeysFilterQueryString($partitionKeys); } if(($selectColumns | Measure-Object).Count -gt 0) { $selectStringParams =[String]::Join(",",$selectColumns) } return $this.GetSubscriptionComplianceReport($filterStringParams,$selectStringParams); } hidden [ComplianceStateTableEntity[]] GetSubscriptionComplianceReport([string] $filterStringParams, [string] $selectStringParams) { [ComplianceStateTableEntity[]] $complianceData = @() try { $storageInstance = $this.azskStorageInstance; $TableName = $this.ComplianceTableName $AccountName = $storageInstance.StorageAccountName $AccessKey = $storageInstance.AccessKey $queryStringParams = "?`$filter=IsActive%20eq%20true" if(-not [string]::IsNullOrWhiteSpace($filterStringParams)) { $queryStringParams += "%20and%20(" + $filterStringParams + ")" } if(-not [string]::IsNullOrWhiteSpace($selectStringParams)) { $queryStringParams += "&`$select=" + $selectStringParams; } $Uri="https://$AccountName.table.core.windows.net/$TableName()$queryStringParams" $Verb = "GET" $ContentMD5 = "" $ContentType = "" $Date = [DateTime]::UtcNow.ToString('r') $CanonicalizedResource = "/$AccountName/$TableName()" $SigningParts=@($Verb,$ContentMD5,$ContentType,$Date,$CanonicalizedResource) $StringToSign = [String]::Join("`n",$SigningParts) $sharedKey = [StorageHelper]::CreateStorageAccountSharedKey($StringToSign,$AccountName,$AccessKey) $xmsdate = $Date $headers = @{"Accept"="application/json";"x-ms-date"=$xmsdate;"Authorization"="SharedKey $sharedKey";"x-ms-version"="2018-03-28"} $tempComplianceData = ([WebRequestHelper]::InvokeGetWebRequest($Uri,$headers)) $newEntity = [ComplianceStateTableEntity]::new(); $props = @(); $item = $null; if(($tempComplianceData | Measure-Object).Count -gt 0) { $item = $tempComplianceData[0]; } if($null -ne $item) { foreach($Property in $newEntity | Get-Member -type NoteProperty, Property) { if([Helpers]::CheckMember($item, $Property.Name, $false)) { $props += $Property.Name } } if("IsControlInGrace" -notin $props) { $props += "IsControlInGrace" } if("IsPreviewBaselineControl" -notin $props) { $props += "IsPreviewBaselineControl" } if(($props | Measure-Object).Count -gt 0) { foreach($item in $tempComplianceData) { $newEntity = [ComplianceStateTableEntity]::new() foreach($Property in $props){ if([Helpers]::CheckMember($item, $Property, $false)) { $newEntity.$($Property) = $item.$($Property) } } if(-not [string]::IsNullOrWhiteSpace($newEntity.PartitionKey) -and -not [string]::IsNullOrWhiteSpace($newEntity.RowKey)) { $complianceData+=$newEntity } } } } } catch { Write-Host $_; return $null; } return $complianceData; } hidden [ComplianceStateTableEntity] ConvertScanResultToSnapshotResult($currentSVTResult, $persistedSVTResult, $svtEventContext, $partitionKey, $rowKey, $resourceId) { [ComplianceStateTableEntity] $scanResult = $null; if($null -ne $persistedSVTResult) { $scanResult = $persistedSVTResult; } $isLegitimateResult = ($currentSVTResult.CurrentSessionContext.IsLatestPSModule -and $currentSVTResult.CurrentSessionContext.Permissions.HasRequiredAccess -and $currentSVTResult.CurrentSessionContext.Permissions.HasAttestationReadPermissions -and $currentSVTResult.ActualVerificationResult -ne [VerificationResult]::Error -and $currentSVTResult.ActualVerificationResult -ne [VerificationResult]::Disabled) if($isLegitimateResult) { $controlItem = $svtEventContext.ControlItem; if($null -eq $scanResult) { $scanResult = [ComplianceStateTableEntity]::new(); $scanResult.PartitionKey = $partitionKey; $scanResult.RowKey = $rowKey; } $scanResult.ResourceId = $resourceId; $scanResult.FeatureName = $svtEventContext.FeatureName; if($svtEventContext.IsResource()) { $scanResult.ResourceName = $svtEventContext.ResourceContext.ResourceName; $scanResult.ResourceGroupName = $svtEventContext.ResourceContext.ResourceGroupName; } if($scanResult.VerificationResult -ne $currentSVTResult.VerificationResult.ToString()) { $scanResult.LastResultTransitionOn = [System.DateTime]::UtcNow.ToString("s"); $scanResult.PreviousVerificationResult = $scanResult.VerificationResult; } if($scanResult.FirstScannedOn -eq [Constants]::AzSKDefaultDateTime -or ([datetime] $scanResult.FirstScannedOn) -gt ([datetime] $currentSVTResult.FirstScannedOn) ) { if($currentSVTResult.FirstScannedOn -eq [Constants]::AzSKDefaultDateTime) { $scanResult.FirstScannedOn = [System.DateTime]::UtcNow.ToString("s"); } else { $scanResult.FirstScannedOn = (get-date $currentSVTResult.FirstScannedOn).ToString("s"); } } if($scanResult.FirstFailedOn -eq [Constants]::AzSKDefaultDateTime -and $currentSVTResult.ActualVerificationResult -ne [VerificationResult]::Passed) { if($currentSVTResult.FirstFailedOn -eq [Constants]::AzSKDefaultDateTime) { $scanResult.FirstFailedOn = [System.DateTime]::UtcNow.ToString("s"); } else { $scanResult.FirstFailedOn = $currentSVTResult.FirstFailedOn.ToString("s"); } } $scanResult.IsControlInGrace=$currentSVTResult.IsControlInGrace $scanResult.ScannedBy = [ContextHelper]::GetCurrentRMContext().Account $scanResult.ScanSource = $this.ScanSource $scanResult.ScannerVersion = $this.ScannerVersion #TODO check in the case sub control $scanResult.ChildResourceName = $currentSVTResult.ChildResourceName $scanResult.ControlId = $controlItem.ControlId $scanResult.ControlIntId = $controlItem.Id $scanResult.ControlSeverity = $controlItem.ControlSeverity.ToString() $scanResult.ActualVerificationResult = $currentSVTResult.ActualVerificationResult.ToString(); $scanResult.AttestationStatus = $currentSVTResult.AttestationStatus.ToString(); if($scanResult.AttestationStatus.ToString() -ne [AttestationStatus]::None -and $null -ne $currentSVTResult.StateManagement -and $null -ne $currentSVTResult.StateManagement.AttestedStateData) { if($scanResult.FirstAttestedOn -eq [Constants]::AzSKDefaultDateTime) { $scanResult.FirstAttestedOn = $currentSVTResult.StateManagement.AttestedStateData.AttestedDate.ToString("s"); } if($currentSVTResult.StateManagement.AttestedStateData.AttestedDate -gt $scanResult.AttestedDate) { $scanResult.AttestationCounter = $scanResult.AttestationCounter + 1 } $scanResult.AttestedBy = $currentSVTResult.StateManagement.AttestedStateData.AttestedBy $scanResult.AttestedDate = $currentSVTResult.StateManagement.AttestedStateData.AttestedDate.ToString("s"); $scanResult.Justification = $currentSVTResult.StateManagement.AttestedStateData.Justification } else { $scanResult.AttestedBy = "" $scanResult.AttestedDate = [Constants]::AzSKDefaultDateTime.ToString("s") ; $scanResult.Justification = "" } if($currentSVTResult.VerificationResult -ne [VerificationResult]::Manual) { $scanResult.VerificationResult = $currentSVTResult.VerificationResult } else { $scanResult.VerificationResult = $currentSVTResult.ActualVerificationResult.ToString(); } $scanResult.ScannerModuleName = [Constants]::AzSKModuleName $scanResult.IsLatestPSModule = $currentSVTResult.CurrentSessionContext.IsLatestPSModule $scanResult.HasRequiredPermissions = $currentSVTResult.CurrentSessionContext.Permissions.HasRequiredAccess $scanResult.HasAttestationWritePermissions = $currentSVTResult.CurrentSessionContext.Permissions.HasAttestationWritePermissions $scanResult.HasAttestationReadPermissions = $currentSVTResult.CurrentSessionContext.Permissions.HasAttestationReadPermissions $scanResult.UserComments = $currentSVTResult.UserComments $scanResult.IsBaselineControl = $controlItem.IsBaselineControl $scanResult.IsPreviewBaselineControl = $controlItem.IsPreviewBaselineControl if($controlItem.Tags.Contains("OwnerAccess") -or $controlItem.Tags.Contains("GraphRead")) { $scanResult.HasOwnerAccessTag = $true } $scanResult.LastScannedOn = [DateTime]::UtcNow.ToString('s') } return $scanResult } #new functions hidden [ComplianceStateTableEntity[]] MergeSVTScanResult($currentScanResults) { if($currentScanResults.Count -lt 1) { return $null} [ComplianceStateTableEntity[]] $finalScanData = @() #TODO $SVTEventContextFirst = $currentScanResults[0] #TODO get specific data $complianceReport = $this.GetSubscriptionComplianceReport($currentScanResults, $null); # $inActiveRecords = @(); # $complianceReport | ForEach-Object { # $record = $_; # if($_.RowKey -eq "EmptyResource") # { # $record.IsActive = $false; # $inActiveRecords += $record; # } # } $foundPersistedData = ($complianceReport | Measure-Object).Count -gt 0 $currentScanResults | ForEach-Object { $currentScanResult = $_ $resourceId = $currentScanResult.SubscriptionContext.Scope; if($currentScanResult.IsResource()) { $resourceId = $currentScanResult.ResourceContext.ResourceId; } if($currentScanResult.FeatureName -ne "AzSKCfg") { $controlsToProcess = @(); if(($currentScanResult.ControlResults | Measure-Object).Count -gt 0) { $controlsToProcess += $currentScanResult.ControlResults; } $controlsToProcess | ForEach-Object { $cScanResult = $_; $partsToHash = $currentScanResult.ControlItem.Id; if(-not [string]::IsNullOrWhiteSpace($cScanResult.ChildResourceName)) { $partsToHash = $partsToHash + ":" + $cScanResult.ChildResourceName; } $currentResultHashId_r = [Helpers]::ComputeHash($partsToHash.ToLower()); $currentResultHashId_p = [Helpers]::ComputeHash($resourceId.ToLower()); $persistedScanResult = $null; if($foundPersistedData) { $persistedScanResult = $complianceReport | Where-Object { $_.PartitionKey -eq $currentResultHashId_p -and $_.RowKey -eq $currentResultHashId_r } # if(($persistedScanResult | Measure-Object).Count -le 0) # { # $foundPersistedData = $false; # } } $mergedScanResult = $this.ConvertScanResultToSnapshotResult($cScanResult, $persistedScanResult, $currentScanResult, $currentResultHashId_p, $currentResultHashId_r, $resourceId) if($null -ne $mergedScanResult) { $finalScanData += $mergedScanResult; } } } } # $finalScanData += $inActiveRecords; return $finalScanData } hidden [void] SetLocalSubscriptionScanReport([ComplianceStateTableEntity[]] $scanResultForStorage) { $storageInstance = $this.azskStorageInstance; $groupedScanResultForStorage = $scanResultForStorage | Group-Object { $_.PartitionKey} $groupedScanResultForStorage | ForEach-Object { $group = $_; $results = $_.Group; #MERGE batch req sample [WebRequestHelper]::InvokeTableStorageBatchWebRequest($storageInstance.ResourceGroupName,$storageInstance.StorageAccountName,$this.ComplianceTableName,$results,$true, $storageInstance.AccessKey) #POST batch req sample #[WebRequestHelper]::InvokeTableStorageBatchWebRequest($storageInstance.ResourceGroupName,$storageInstance.StorageAccountName,$this.ComplianceTableName,$results,$false) } } hidden [void] StoreComplianceDataInUserSubscription([SVTEventContext[]] $currentScanResult) { $finalScanReport = $this.MergeSVTScanResult($currentScanResult) $this.SetLocalSubscriptionScanReport($finalScanReport) } hidden [string] ConstructPartitionKeysFilterQueryString([string[]] $PartitionKeys) { if($PartitionKeys.Length -gt 0) { $template = "PartitionKey%20eq%20'{0}'"; $tempQS = "" $havePartitionKeys = $false; $PartitionKeys | ForEach-Object { $pKey = $_ $tempQS = $tempQS + ($template -f $pKey) + "%20or%20"; $havePartitionKeys = $true; } if($havePartitionKeys) { $tempQS = $tempQS.Substring(0,$tempQS.Length - 8); } return $tempQS; } else { return ""; } } hidden [string] FetchComplianceStateFromDb() { if($null -eq $this.azskStorageInstance) { throw "Unable to find storage account in the subscription. Please scan the complete subscription with co-administrator/owner access atleast once before running this command." } $result = [RemoteAPIHelper]::GetComplianceSnapshot($this.SubscriptionContext.SubscriptionId) $Complianceinfo = ConvertFrom-Json -InputObject $result if(($Complianceinfo | Measure-Object).Count -gt 0) { $ComplianceState = New-Object -TypeName "System.Collections.Generic.List[SVTEventContext]"; $subContext= $this.SubscriptionContext; foreach ($item in $Complianceinfo) { $CResult= New-Object -TypeName ControlResult $StateData = New-Object -TypeName StateData $SVTEvent= New-Object -TypeName SVTEventContext $controlDetails = New-Object -TypeName ControlItem $resourceDetails=[ResourceContext]::new() $CResult.ChildResourceName = ""; $CResult.VerificationResult = $item.VerificationResult $CResult.ActualVerificationResult = $item.ActualVerificationResult; if(-not [string]::IsNullOrEmpty($item.FirstFailedOn)) { $CResult.FirstFailedOn = get-date $item.FirstFailedOn } if(-not [string]::IsNullOrEmpty($item.FirstScannedOn)) { $CResult.FirstScannedOn = get-date $item.FirstScannedOn } $CResult.MaximumAllowedGraceDays=$item.MaximumAllowedGraceDays; $scanFromDays = [System.DateTime]::UtcNow.Subtract($CResult.FirstScannedOn) # Setting isControlInGrace Flag if($scanFromDays.Days -le $CResult.MaximumAllowedGraceDays) { $CResult.IsControlInGrace = $true } else { $CResult.IsControlInGrace = $false } $StateData.AttestedBy = $item.attestedBy; $StateData.AttestedDate = $item.attestedDate $StateData.Justification = $item.Justification $CResult.StateManagement.AttestedStateData=$StateData $controlDetails.ControlId=$item.ControlId $CResult.CurrentSessionContext.IsLatestPSModule = $this.azskStorageInstance.RunningLatestPSModule $CResult.CurrentSessionContext.Permissions.HasRequiredAccess = $true $CResult.CurrentSessionContext.Permissions.HasAttestationWritePermissions = $this.azskStorageInstance.HaveWritePermissions $CResult.CurrentSessionContext.Permissions.HasAttestationReadPermissions = $this.azskStorageInstance.HaveWritePermissions $controlDetails.Id=$item.ControlIntId $controlDetails.ControlSeverity=$item.ControlSeverity $SVTEvent.ControlResults = $CResult; if($item.IsBaselineControl) { $controlDetails.IsBaselineControl=$true } else { $controlDetails.IsBaselineControl=$false } #<TODO: Currently remote API does not return PreviewBaselineControl flag. Disabling below code> if($item.IsPreviewBaselineControl) { $controlDetails.IsPreviewBaselineControl=$true } else { $controlDetails.IsPreviewBaselineControl=$false } $SVTEvent.ControlItem=$controlDetails; $resourceDetails.ResourceName=$item.resourceName; $SVTEvent.FeatureName=$item.FeatureName; $resourceDetails.ResourceGroupName=$item.ResourceGroupName; $SVTEvent.SubscriptionContext = $subContext $resourceDetails.ResourceId= $SVTEvent.SubscriptionContext.Scope; if(-not [string]::IsNullOrEmpty($item.ResourceId)) { $resourceDetails.ResourceId = $item.ResourceId; } $SVTEvent.ResourceContext=$resourceDetails $ComplianceState.Add($SVTEvent); } $this.StoreComplianceDataInUserSubscription($ComplianceState); return "Compliance data has been successfully stored in storage " } else { return "No records found for this subscription. Make sure to scan the complete subscription with co-administrator/owner access atleast once before running this command." } } } |