Framework/Core/AzSKInfo/PersistedStateInfo.ps1

using namespace System.Management.Automation
Set-StrictMode -Version Latest 

class PersistedStateInfo: CommandBase
{    
    
    hidden [PSObject] $AzSKRG = $null
    hidden [String] $AzSKRGName = ""


    PersistedStateInfo([string] $subscriptionId, [InvocationInfo] $invocationContext): 
        Base($subscriptionId, $invocationContext) 
    { 
        #$this.DoNotOpenOutputFolder = $true;
        $this.AzSKRGName = [ConfigurationManager]::GetAzSKConfigData().AzSKRGName;
        $this.AzSKRG = Get-AzureRmResourceGroup -Name $this.AzSKRGName -ErrorAction SilentlyContinue
    }
    

    [MessageData[]] UpdatePersistedState([string] $filePath)
    {    
        [string] $errorMessages="";
        $customErrors=@();
        [MessageData[]] $messages = @();
       
        try
        {
            $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 $messages;
            } 
            #Check for file path exist
            if(-not (Test-Path -path $filePath))
            {  
                $this.PublishCustomMessage("Could not find file: [$filePath]. `nPlease rerun the command with correct path.", [MessageType]::Error);
                return $messages;
            }
            # Read Local CSV file
            [CsvOutputItem[]] $controlResultSet  =@();
            $controlResultSet = Get-ChildItem -Path $filePath -Filter '*.csv' -Force | Get-Content | Convertfrom-csv
            $controlResultSet = $controlResultSet | Where-Object {$_.FeatureName -ne "AzSKCfg"}
            $totalCount = ($controlResultSet | Measure-Object).Count
            if($totalCount -eq 0)
            {
                $this.PublishCustomMessage("Could not find any control in file: [$filePath].",[MessageType]::Error);
                return $messages;
            }
            $resultsGroups = $controlResultSet | Group-Object -Property ResourceId 
            # Read file from Storage
            $complianceReportHelper = [ComplianceReportHelper]::new($this.SubscriptionContext.SubscriptionId); 
            $StorageReportJson =$null;
            # Check for write access
            if($complianceReportHelper.azskStorageInstance.HaveWritePermissions -eq 1)
            {
                [LSRSubscription] $StorageReportJson = $complianceReportHelper.GetLocalSubscriptionScanReport($this.SubscriptionContext.SubscriptionId);
            }
            else
            {
                $this.PublishCustomMessage("You don't have the required permissions to update user comments. If you'd like to update user comments, please request your subscription owner to grant you 'Contributor' access to the DevOps Kit resource group.", [MessageType]::Warning);
                return $messages;
            }
    
            $erroredControls=@();
            $PersistedControlScanResult=@();
            $ResourceData=@();
            $successCount=0;
        
            if($null -ne $StorageReportJson -and $null -ne $StorageReportJson.ScanDetails)
            {
                $this.PublishCustomMessage("Updating user comments in AzSK control data for $totalCount controls... ", [MessageType]::Warning);

                foreach ($resultGroup in $resultsGroups) {
                    #count check has been done before itself. Here it is safe to assume atleast one group. resultGroup data is populated from CSV
                    if($resultGroup.Group[0].FeatureName -eq "SubscriptionCore" -and ($StorageReportJson.ScanDetails.SubscriptionScanResult | Measure-Object).Count -gt 0)
                    {                        
                        $startIndex = $resultGroup.Name.lastindexof("/")
                        $lastIndex = $resultGroup.Name.length - $startIndex-1
                        $localSubID = $resultGroup.Name.substring($startIndex+1,$lastIndex)
                        if($localSubID -eq $this.SubscriptionContext.SubscriptionId)
                        {
                            $PersistedControlScanResult=$StorageReportJson.ScanDetails.SubscriptionScanResult
                        }                        
                    }
                    elseif($resultGroup.Group[0].FeatureName -ne "SubscriptionCore" -and $resultGroup.Group[0].FeatureName -ne "AzSKCfg" -and ($StorageReportJson.ScanDetails.Resources | Measure-Object).Count -gt 0)
                    {                         
                        $ResourceData = $StorageReportJson.ScanDetails.Resources | Where-Object { $_.ResourceId -eq $resultGroup.Name }     
                        if($null -ne $ResourceData -and ($ResourceData.ResourceScanResult | Measure-Object).Count -gt 0 )
                        {
                            $PersistedControlScanResult=$ResourceData.ResourceScanResult
                        }
                    }
                    if(($PersistedControlScanResult | Measure-Object).Count -gt 0)
                    {
                        $resultGroup.Group | ForEach-Object{
                            try
                            {
                                $currentItem=$_
                                $matchedControlResult = $PersistedControlScanResult | Where-Object {        
                                    ($_.ControlID -eq $currentItem.ControlID -and (($currentItem.FeatureName -ne "SubscriptionCore" -and $_.ChildResourceName -eq $currentItem.ChildResourceName) -or $currentItem.FeatureName -eq "SubscriptionCore"))
                                }
                                $encoder = [System.Text.Encoding]::UTF8
                                $encUserComments= $encoder.GetBytes($currentItem.UserComments)
                                $decUserComments= $encoder.GetString($encUserComments)
                                if($decUserComments.length -le 255)
                                {
                                    if(($matchedControlResult|Measure-Object).Count -eq 1)
                                    {
                                        $successCount+=1;
                                        $matchedControlResult.UserComments= $decUserComments
                                    }
                                    else
                                    {
                                        $erroredControls+=$this.CreateCustomErrorObject($currentItem,"Could not find previous persisted state.")         
                                    }
                                }
                                else
                                {    
                                    $erroredControls+=$this.CreateCustomErrorObject($currentItem,"User Comment's length should not exceed 255 characters.")
                                }
                            }
                            catch
                            {
                                $this.PublishException($_);
                                $erroredControls+=$this.CreateCustomErrorObject($currentItem,"Could not find previous persisted state.")
                            }        
                        }        
                    }
                    else
                    {
                        $resultGroup.Group| ForEach-Object{
                            $erroredControls+=$this.CreateCustomErrorObject($_,"Could not find previous persisted state.")
                        }
                    }
                }
                if($successCount -gt 0)
                {
                    [LocalSubscriptionReport] $complianceReport = [LocalSubscriptionReport]::new();
                    $complianceReport.Subscriptions += $StorageReportJson;
                    $complianceReportHelper.SetLocalSubscriptionScanReport($complianceReport);
                }
                # If updation failed for any control, genearte error file
                if(($erroredControls | Measure-Object).Count -gt 0)
                {
                    $nonNullProps=@();
                    [CsvOutputItem].GetMembers() | Where-Object { $_.MemberType -eq [System.Reflection.MemberTypes]::Property } | ForEach-Object {
                      $propName = $_.Name;
                      if(($erroredControls | Where-object { -not [string]::IsNullOrWhiteSpace($_.$propName) } | Measure-object).Count -ne 0)
                        {
                            $nonNullProps += $propName;
                        }
                     };
                    $nonNullProps += "ErrorDetails"
                    $csvItems = $erroredControls | Select-Object -Property $nonNullProps
                    $controlCSV = New-Object -TypeName WriteCSVData
                    $controlCSV.FileName = "Controls_NotUpdated_" + $this.RunIdentifier
                    $controlCSV.FileExtension = 'csv'
                    $controlCSV.FolderPath = ''
                    $controlCSV.MessageData = $csvItems
                    $this.PublishAzSKRootEvent([AzSKRootEvent]::WriteCSV, $controlCSV);
                    $this.PublishCustomMessage("[$successCount/$totalCount] user comments have been updated successfully.", [MessageType]::Update);
                    $this.PublishCustomMessage("[$(($erroredControls | Measure-Object).Count)/$totalCount] user comments could not be updated due to an error. See the log file for details.", [MessageType]::Warning);
                }
                else
                {
                    $this.PublishCustomMessage("All User Comments have been updated successfully.", [MessageType]::Update);
                }
            }
            else
            {
                $this.PublishEvent([AzSKGenericEvent]::Exception, "Unable to update user comments. Could not find previous persisted state in DevOps Kit storage.");
            }
        }
        catch
        {
            $this.PublishException($_);
        }
        return $messages;
    }

    hidden [PSObject] CreateCustomErrorObject($currentItem,$reason)
    {
        $currentItem | Add-Member -NotePropertyName ErrorDetails -NotePropertyValue $reason
        return $currentItem;
    }
}