Framework/Core/ARMChecker/ARMCheckerStatus.ps1

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

class ARMCheckerStatus: EventBase
{
    hidden [string] $ARMControls;
    hidden [string] $PSLogPath;
    [bool] $DoNotOpenOutputFolder = $false;

    ARMCheckerStatus([InvocationInfo] $invocationContext) 
    {
        if (-not $invocationContext)
        {
            throw [System.ArgumentException] ("The argument 'invocationContext' is null. Pass the `$PSCmdlet.MyInvocation from PowerShell command.");
        }
        $this.InvocationContext = $invocationContext;

        #load config file here.
        $this.ARMControls = [ConfigurationHelper]::LoadOfflineConfigFile("ARMControls.json", $false);
        if([string]::IsNullOrWhiteSpace($this.ARMControls))
        {
            throw ([SuppressedException]::new(("There are no controls to evaluate in ARM checker. Please contact support team."), [SuppressedExceptionType]::InvalidOperation))
        }

        if($null -ne $this.InvocationContext.BoundParameters["DoNotOpenOutputFolder"])
        {
            $this.DoNotOpenOutputFolder = $this.InvocationContext.BoundParameters["DoNotOpenOutputFolder"];
        }
    }

    hidden [void] CommandStartedAction()
    {
        $currentVersion = $this.GetCurrentModuleVersion();
        $moduleName = $this.GetModuleName();
        $methodName = $this.InvocationContext.InvocationName;
        
        $this.WriteMessage([Constants]::DoubleDashLine + "`r`n$moduleName Version: $currentVersion `r`n" + [Constants]::DoubleDashLine , [MessageType]::Info);      
        $this.WriteMessage("Method Name: $methodName `r`nInput Parameters: $(($this.InvocationContext.BoundParameters | Out-String).TrimEnd()) `r`n" + [Constants]::DoubleDashLine , [MessageType]::Info);                           
    }

    hidden [void] CommandCompletedAction($resultsFolder)
    {
        $this.WriteMessage([Constants]::SingleDashLine, [MessageType]::Info);
        $this.WriteMessage("Status and detailed logs have been exported to path - $($resultsFolder)", [MessageType]::Info);
        $this.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info);
    }


    [string] EvaluateStatus([string] $armTemplatePath,[Boolean]  $isRecurse)
    {
        if(-not (Test-Path -path $armTemplatePath))
        {
            $this.WriteMessage("ARMTemplate file path or folder path is empty, verify that the path is correct and try again", [MessageType]::Error);
            return $null;
        }
        $this.PSLogPath = "";
        $baseDirectory = [System.IO.Path]::GetDirectoryName($armTemplatePath);
        if($isRecurse -eq $true)
        {
            $ARMTemplates = Get-ChildItem -Path $armTemplatePath -Recurse -Filter '*.json' 
        }
        else
        {
            $ARMTemplates = Get-ChildItem -Path $armTemplatePath -Filter '*.json' 
        }

        $armEvaluator = [AzSK.ARMChecker.Lib.ArmTemplateEvaluator]::new([string] $this.ARMControls);
        $skippedFiles = @();
        $timeMarker = [datetime]::Now.ToString("yyyyMMdd_HHmmss")
        $resultsFolder = [Constants]::AzSKLogFolderPath + [Constants]::AzSKModuleName + "Logs\ARMChecker\" + $timeMarker + "\";
        $csvFilePath = $resultsFolder + "ARMCheckerResults_" + $timeMarker + ".csv";
        [System.IO.Directory]::CreateDirectory($resultsFolder) | Out-Null
        $this.PSLogPath = $resultsFolder + "PowerShellOutput.LOG";
        $this.CommandStartedAction();
        $csvResults = @();
        $armcheckerscantelemetryEvents = [System.Collections.ArrayList]::new()
        $scannedFileCount = 0

        foreach($armTemplate in $ARMTemplates)
        {
            $armFileName = $armTemplate.FullName.Replace($baseDirectory, "");
            try
            {
                $results = @();
                $armTemplateContent = Get-Content $armTemplate.FullName -Raw        
                $libResults = $armEvaluator.Evaluate($armTemplateContent, $null);
                $results += $libResults | Where-Object {$_.VerificationResult -ne "NotSupported"} | Select-Object -ExcludeProperty "IsEnabled"        
        
                $this.WriteMessage(([Constants]::DoubleDashLine + "`r`nStarting analysis: [FileName: $armFileName] `r`n" + [Constants]::SingleDashLine), [MessageType]::Info);
                $scannedFileCount += 1;
                if($results.Count -gt 0)
                {
                    foreach($result in $results)
                    {
                        $csvResultItem = "" | Select-Object "ControlId", "Status", "ResourceType",  "Severity", `
                                                            "PropertyPath", "LineNumber", "CurrentValue", "ExpectedValue", `
                                                            "ResourcePath", "ResourceLineNumber", "Description","FilePath"

                        $csvResultItem.ResourceType = $result.ResourceType
                        $csvResultItem.ControlId = $result.ControlId
                        $csvResultItem.Description = $result.Description
                        $csvResultItem.ExpectedValue = $result.ExpectedValue
                        $csvResultItem.Severity = $result.Severity.ToString()
                        $csvResultItem.Status = $result.VerificationResult
                        $csvResultItem.FilePath = $armFileName

                        if($result.ResultDataMarkers.Count -gt 0)
                        {
                            $csvResultItem.LineNumber = $result.ResultDataMarkers[0].LineNumber
                            $csvResultItem.PropertyPath = $result.ResultDataMarkers[0].JsonPath
                            $data = $result.ResultDataMarkers[0].DataMarker
                            if($data -ieq "true" -or $data -ieq "false") {
                                $csvResultItem.CurrentValue = $data.ToLower()
                            }
                            else
                            {
                                $csvResultItem.CurrentValue = $data
                            }
                        }
                        else
                        {
                            $csvResultItem.LineNumber = -1
                            $csvResultItem.PropertyPath = "Not found"
                            $csvResultItem.CurrentValue = ""
                        }
                        $csvResultItem.ResourceLineNumber = $result.ResourceDataMarker.LineNumber
                        $csvResultItem.ResourcePath = $result.ResourceDataMarker.JsonPath
                        $csvResults += $csvResultItem;

                        $this.WriteResult($result);

                        $properties = @{};
                        $properties.Add("ResourceType", $result.ResourceType)
                        $properties.Add("ControlId", $result.ControlId)
                        $properties.Add("VerificationResult", $result.VerificationResult);

                        $telemetryEvent = "" | Select-Object Name, Properties, Metrics
                        $telemetryEvent.Name = "ARMChecker Control Scanned"
                        $telemetryEvent.Properties = $properties
                        $armcheckerscantelemetryEvents.Add($telemetryEvent)
                    }
                    $this.WriteMessage([Constants]::SingleDashLine, [MessageType]::Info);
                    $this.WriteSummary($results, "Severity", "VerificationResult");
                }
                else
                {
                    $this.WriteMessage("No controls have been evaluated for file: $armFileName", [MessageType]::Info);
                }
            }
            catch
            {
                #Write-Host ([Helpers]::ConvertObjectToString($_, $false)) -ForegroundColor Red
                $skippedFiles += $armFileName;
            }        
        }
        
        $csvResults | Export-Csv $csvFilePath -NoTypeInformation -Force

        if($skippedFiles.Count -ne 0)
        {
            $this.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Warning);
            $this.WriteMessage("Skipped file(s): $($skippedFiles.Count)", [MessageType]::Warning);
            $skippedFiles | ForEach-Object {
                $this.WriteMessage($_, [MessageType]::Warning);
            };
            $this.WriteMessage([Constants]::SingleDashLine, [MessageType]::Warning);
        }

        $teleEvent = "" | Select-Object Name, Properties, Metrics
        $teleEvent.Name = "ARMChecker Command Completed"
        $teleEvent.Properties = @{};
        $teleEvent.Properties.Add("Total", $csvResults.Count);

        if($csvResults.Count -ne 0)
        {
            $this.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info);
            $this.WriteSummary($csvResults, "Severity", "Status");
            $this.WriteMessage("Total scanned file(s): $scannedFileCount", [MessageType]::Info);

            $resultsGroup = $csvResults | Group-Object Status | ForEach-Object {
                $teleEvent.Properties.Add($_.Name, $_.Count);
            };
        }
        else
        {
            $this.WriteMessage("No controls have been evaluated for ARM Template(s).", [MessageType]::Error);
        }

        $this.CommandCompletedAction($resultsFolder);    

        $armcheckerscantelemetryEvents.Add($teleEvent)
        [AIOrgTelemetryHelper]::PublishARMCheckerEvent($armcheckerscantelemetryEvents)

        if((-not $this.DoNotOpenOutputFolder) -and (-not [string]::IsNullOrEmpty($resultsFolder)))
        {
            try
            {
                Invoke-Item -Path $resultsFolder;
            }
            catch
            {
                #ignore if any exception occurs
            }
        }
        return $resultsFolder
    }

    hidden [void] WriteResult($result)
    {
        $messageType = [MessageType]::Info;
        switch ($result.VerificationResult)
        {
            Passed
            { 
                $messageType = [MessageType]::Update;
            }
            Verify
            {
                $messageType = [MessageType]::Warning;
            }
            Failed
            {
                $messageType = [MessageType]::Error;
            }
        }
        $this.WriteMessage("$($result.VerificationResult): [$($result.ControlId)]", $messageType);
    }
    hidden [void] WriteSummary($summary, $severityPropertyName, $resultPropertyName)
    {
        if($summary.Count -ne 0)
        {
            $summaryResult = @();

            $severities = @();
            $severities += $summary | Select-Object -Property $severityPropertyName | Select-Object -ExpandProperty $severityPropertyName -Unique;

            $verificationResults = @();
            $verificationResults += $summary | Select-Object -Property $resultPropertyName | Select-Object -ExpandProperty $resultPropertyName -Unique;

            if($severities.Count -ne 0)
            {
                # Create summary matrix
                $totalText = "Total";
                $MarkerText = "MarkerText";
                $rows = @();
                $rows += [Enum]::GetNames([ControlSeverity]) | Where-Object { $severities -contains $_ };
                $rows += $MarkerText;
                $rows += $totalText;
                $rows += $MarkerText;
                $rows | ForEach-Object {
                    $result = [PSObject]::new();
                    Add-Member -InputObject $result -Name "Summary" -MemberType NoteProperty -Value $_.ToString()
                    Add-Member -InputObject $result -Name $totalText -MemberType NoteProperty -Value 0

                    [Enum]::GetNames([VerificationResult]) | Where-Object { $verificationResults -contains $_ } |
                    ForEach-Object {
                        Add-Member -InputObject $result -Name $_.ToString() -MemberType NoteProperty -Value 0
                    };
                    $summaryResult += $result;
                };

                $totalRow = $summaryResult | Where-Object { $_.Summary -eq $totalText } | Select-Object -First 1;

                $summary | Group-Object -Property $severityPropertyName | ForEach-Object {
                    $item = $_;
                    $summaryItem = $summaryResult | Where-Object { $_.Summary -eq $item.Name } | Select-Object -First 1;
                    if($summaryItem)
                    {
                        $summaryItem.Total = $_.Count;
                        if($totalRow)
                        {
                            $totalRow.Total += $_.Count
                        }
                        $item.Group | Group-Object -Property $resultPropertyName | ForEach-Object {
                            $propName = $_.Name;
                            $summaryItem.$propName += $_.Count;
                            if($totalRow)
                            {
                                $totalRow.$propName += $_.Count
                            }
                        };
                    }
                };
                $markerRows = $summaryResult | Where-Object { $_.Summary -eq $MarkerText } 
                $markerRows | ForEach-Object { 
                    $markerRow = $_
                    Get-Member -InputObject $markerRow -MemberType NoteProperty | ForEach-Object {
                            $propName = $_.Name;
                            $markerRow.$propName = "------";                
                        }
                    };
                if($summaryResult.Count -ne 0)
                {        
                    $this.WriteMessage(($summaryResult | Format-Table | Out-String), [MessageType]::Info)
                }
            }
        }
    }
    hidden [void] WriteMessage([PSObject] $message, [MessageType] $messageType)
    {
        if(-not $message)
        {
            return;
        }
        
        $colorCode = [System.ConsoleColor]::White

        switch($messageType)
        {
            ([MessageType]::Critical) {  
                $colorCode = [System.ConsoleColor]::Red              
            }
            ([MessageType]::Error) {
                $colorCode = [System.ConsoleColor]::Red             
            }
            ([MessageType]::Warning) {
                $colorCode = [System.ConsoleColor]::Yellow              
            }
            ([MessageType]::Info) {
                $colorCode = [System.ConsoleColor]::Cyan
            }  
            ([MessageType]::Update) {
                $colorCode = [System.ConsoleColor]::Green
            }
            ([MessageType]::Deprecated) {
                $colorCode = [System.ConsoleColor]::DarkYellow
            }
            ([MessageType]::Default) {
                $colorCode = [System.ConsoleColor]::White
            }           
        }   

        $formattedMessage = [Helpers]::ConvertObjectToString($message, (-not [string]::IsNullOrEmpty($this.PSLogPath)));        
        Write-Host $formattedMessage -ForegroundColor $colorCode

        $this.AddOutputLog([Helpers]::ConvertObjectToString($message, $false));
    }

    hidden [void] AddOutputLog([string] $message)   
    {
        if([string]::IsNullOrEmpty($message) -or [string]::IsNullOrEmpty($this.PSLogPath))
        {
            return;
        }
             
        Add-Content -Value $message -Path $this.PSLogPath        
    } 
}