Framework/Listeners/OMS/OMSOutput.ps1

Set-StrictMode -Version Latest 

class OMSOutput: ListenerBase
{        
    hidden static [OMSOutput] $Instance = $null;  
    #Default source is kept as SDL / PowerShell.
    #This value must be set in respective environment i.e. CICD,CC
    [string] $OMSSource;
    hidden static [bool] $IsIssueLogged = $false
    OMSOutput()
    {
        
    }


    static [OMSOutput] GetInstance()
    {
        if($null -eq [OMSOutput]::Instance)
        {
            [OMSOutput]::Instance = [OMSOutput]::new();
        }
        return [OMSOutput]::Instance;
    }

    [void] RegisterEvents()
    {
        $this.UnregisterEvents();

        # Mandatory: Generate Run Identifier Event
        $this.RegisterEvent([AzSdkRootEvent]::GenerateRunIdentifier, {
            $currentInstance = [OMSOutput]::GetInstance();
            try 
            {
                $currentInstance.SetRunIdentifier([AzSdkRootEventArgument] ($Event.SourceArgs | Select-Object -First 1));                         
                [OMSOutput]::IsIssueLogged = $false
            }
            catch 
            {
                $currentInstance.PublishException($_);
            }
        });        
        $this.RegisterEvent([SVTEvent]::EvaluationCompleted, {
            $currentInstance = [OMSOutput]::GetInstance();
            try
            {
                $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext
                $SVTEventContexts = [SVTEventContext[]] $Event.SourceArgs
                foreach($svtEventContext in $SVTEventContexts)
                {
                    $currentInstance.WriteControlResult($svtEventContext);
                }
            }
            catch
            {
                $currentInstance.PublishException($_);
            }
        });
    }

    hidden [void] WriteControlResult([SVTEventContext] $eventContext)
    {
        try
        {
            $settings = [ConfigurationManager]::GetAzSdkSettings()
            if(-not [string]::IsNullOrWhiteSpace($settings.OMSSource))
            {
                $this.OMSSource = $settings.OMSSource
            }

            if(-not [string]::IsNullOrWhiteSpace($settings.OMSWorkspaceId))
            {
                $tempBodyObjects = $this.GetOMSBodyObjects($this.OMSSource,$eventContext) #need to prioritize this
                $tempBodyObjects | ForEach-Object{
                    Set-Variable -Name tempBody -Value $_ -Scope Local
                    $body = $tempBody | ConvertTo-Json
                    $this.PostOMSData($settings.OMSWorkspaceId, $settings.OMSSharedKey, ([System.Text.Encoding]::UTF8.GetBytes($body)),$settings.OMSType)
                }            
            }
        }
        catch
        {
            [Exception] $ex = [Exception]::new(("Invalid OMS Settings: " + $_.Exception.ToString()), $_.Exception)
            throw [SuppressedException] $ex
        }
    }

    hidden [PSObject[]] GetOMSBodyObjects([string] $Source,[SVTEventContext] $eventContext)
    {
        [PSObject[]] $output = @();
        [array] $eventContext.ControlResults | ForEach-Object{
            Set-Variable -Name ControlResult -Value $_ -Scope Local
            $out = "" | Select-Object ResourceType, ResourceGroup, Reference, ResourceName, ChildResourceName, ControlStatus, ActualVerificationResult, ControlId, SubscriptionName, SubscriptionId, FeatureName, Source, Recommendation, ControlSeverity, TimeTakenInMs, AttestationStatus, AttestedBy, Justification
            if($eventContext.IsResource())
            {
                $out.ResourceType=$eventContext.ResourceContext.ResourceType
                $out.ResourceGroup=$eventContext.ResourceContext.ResourceGroupName            
                $out.ResourceName=$eventContext.ResourceContext.ResourceName
                $out.ChildResourceName=$ControlResult.ChildResourceName
            }

            $out.Reference=$eventContext.Metadata.Reference
            $out.ControlStatus=$ControlResult.VerificationResult.ToString()
            $out.ActualVerificationResult=$ControlResult.ActualVerificationResult.ToString()
            $out.ControlId=$eventContext.ControlItem.ControlID
            $out.SubscriptionName=$eventContext.SubscriptionContext.SubscriptionName
            $out.SubscriptionId=$eventContext.SubscriptionContext.SubscriptionId
            $out.FeatureName=$eventContext.FeatureName
            $out.Recommendation=$eventContext.ControlItem.Recommendation
            $out.ControlSeverity=$eventContext.ControlItem.ControlSeverity.ToString()
            $out.Source=$Source

            #mapping the attestation properties
            if($null -ne $ControlResult -and $null -ne $ControlResult.StateManagement -and $null -ne $ControlResult.StateManagement.AttestedStateData)
            {
                $attestedData = $ControlResult.StateManagement.AttestedStateData;
                $out.AttestationStatus = $ControlResult.AttestationStatus.ToString();
                $out.AttestedBy = $attestedData.AttestedBy;
                $out.Justification = $attestedData.Justification;
            }
            
            #$out.TimeTakenInMs=[int] $Metrics["TimeTakenInMs"]
            $output += $out
        }
        return $output    
    }

    hidden [string] GetOMSSignature ($OMSWorkspaceID, $SharedKey, $Date, $ContentLength, $Method, $ContentType, $Resource)
    {
            [string] $xHeaders = "x-ms-date:" + $Date
            [string] $stringToHash = $Method + "`n" + $ContentLength + "`n" + $ContentType + "`n" + $xHeaders + "`n" + $Resource
        
            [byte[]]$bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
            
            [byte[]]$keyBytes = [Convert]::FromBase64String($SharedKey)

            [System.Security.Cryptography.HMACSHA256] $sha256 = New-Object System.Security.Cryptography.HMACSHA256
            $sha256.Key = $keyBytes
            [byte[]]$calculatedHash = $sha256.ComputeHash($bytesToHash)
            $encodedHash = [Convert]::ToBase64String($calculatedHash)
            $authorization = 'SharedKey {0}:{1}' -f $OMSWorkspaceID,$encodedHash
            return $authorization   
    }


# Create the function to create and post the request
    hidden  PostOMSData([string] $OMSWorkspaceID, [string] $SharedKey, $Body, $LogType)
    {            
        try{
        [string] $method = "POST"
        [string] $contentType = "application/json"
        [string] $resource = "/api/logs"
        $rfc1123date = [System.DateTime]::UtcNow.ToString("r")
        [int] $contentLength = $Body.Length
        [string] $signature = $this.GetOMSSignature($OMSWorkspaceID , $SharedKey , $rfc1123date ,$contentLength ,$method ,$contentType ,$resource)
        [string] $uri = "https://" + $OMSWorkspaceID + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01"
        [DateTime] $TimeStampField = [System.DateTime]::UtcNow
        $headers = @{
            "Authorization" = $signature;
            "Log-Type" = $LogType;
            "x-ms-date" = $rfc1123date;
            "time-generated-field" = $TimeStampField;
        }

        $response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $Body -UseBasicParsing
        }
        catch
        {
            if(-not [OMSOutput]::IsIssueLogged)
            {
                $this.PublishCustomMessage("An error occurred while pushing data to OMS. Please check logs for more details. AzSDK control evaluation results will not be sent to the configured OMS workspace from this environment until the error is resolved.", [MessageType]::Error);
                $this.PublishException($_);
                [OMSOutput]::IsIssueLogged = $true
            }
        }
    }

}