Framework/Listeners/RemoteReports/UsageTelemetry.ps1

Set-StrictMode -Version Latest

class UsageTelemetry: ListenerBase {
    [Microsoft.ApplicationInsights.TelemetryClient] $TelemetryClient;

    hidden UsageTelemetry() {
        $this.TelemetryClient = [Microsoft.ApplicationInsights.TelemetryClient]::new()
        $this.TelemetryClient.InstrumentationKey = [Constants]::UsageTelemetryKey
    }

    hidden static [UsageTelemetry] $Instance = $null;

    static [UsageTelemetry] GetInstance() {
        if ( $null  -eq [UsageTelemetry]::Instance -or  $null  -eq [UsageTelemetry]::Instance.TelemetryClient) {
            [UsageTelemetry]::Instance = [UsageTelemetry]::new();
        }
        return [UsageTelemetry]::Instance
    }

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

        $this.RegisterEvent([AzSdkRootEvent]::GenerateRunIdentifier, {
            $currentInstance = [UsageTelemetry]::GetInstance();
            try
            {
                $runIdentifier = [AzSdkRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)
                $currentInstance.SetRunIdentifier($runIdentifier);
            }
            catch
            {
                $currentInstance.PublishException($_);
            }
        });

        $this.RegisterEvent([SVTEvent]::EvaluationCompleted, {
            $azsdkSettings = [ConfigurationManager]::GetAzSdkSettings();
            if($azsdkSettings.UsageTelemetryLevel -ne "Anonymous") { return; }
            $currentInstance = [UsageTelemetry]::GetInstance();
            try
            {
                $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext
                $featureGroup = [RemoteReportHelper]::GetFeatureGroup($invocationContext.MyCommand.Name)
                $SVTEventContexts = [SVTEventContext[]] $Event.SourceArgs
                if($featureGroup -eq [FeatureGroup]::Subscription){
                    [UsageTelemetry]::PushSubscriptionScanResults($currentInstance, $SVTEventContexts)
                }elseif($featureGroup -eq [FeatureGroup]::Service){
                    [UsageTelemetry]::PushServiceScanResults($currentInstance, $SVTEventContexts)
                }else{

                }
            }
            catch
            {
                $currentInstance.PublishException($_);
            }
            $currentInstance.TelemetryClient.Flush()
        });

        $this.RegisterEvent([AzSdkGenericEvent]::Exception, {
            $currentInstance = [UsageTelemetry]::GetInstance();
            try
            {
                [System.Management.Automation.ErrorRecord] $er = ($Event.SourceArgs | Select-Object -First 1)
                [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er);
            }
            catch
            {
            }
        });

        $this.RegisterEvent([AzSdkRootEvent]::CommandError, {
            $currentInstance = [UsageTelemetry]::GetInstance();
            try
            {
                [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage
                [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er);
            }
            catch
            {
            }
        });

        $this.RegisterEvent([SVTEvent]::CommandError, {
            $currentInstance = [UsageTelemetry]::GetInstance();
            try
            {
                [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage
                [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er);
            }
            catch
            {
            }
        });

        $this.RegisterEvent([SVTEvent]::EvaluationError, {
            $currentInstance = [UsageTelemetry]::GetInstance();
            try
            {
                [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage
                [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er);
            }
            catch
            {
            }
        });

        $this.RegisterEvent([SVTEvent]::ControlError, {
            $currentInstance = [UsageTelemetry]::GetInstance();
            try
            {
                [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage
                [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er);
            }
            catch
            {
            }
        });
    }

    static [void] PushSubscriptionScanResults(
        [UsageTelemetry] $Publisher, `
        [SVTEventContext[]] $SVTEventContexts)
    {
        $eventData = @{
            [TelemetryKeys]::FeatureGroup = [FeatureGroup]::Subscription;
            "ScanKind" = [RemoteReportHelper]::GetSubscriptionScanKind(
                $Publisher.InvocationContext.MyCommand.Name,
                $Publisher.InvocationContext.BoundParameters);
        }
        $SVTEventContexts | ForEach-Object {
            $context = $_
            [hashtable] $eventDataClone = $eventData.Clone();
            $eventDataClone.Add("ControlIntId", $context.ControlItem.Id);
            $eventDataClone.Add("ControlId", $context.ControlItem.ControlID);
            $eventDataClone.Add("ControlSeverity", $context.ControlItem.ControlSeverity);
            if ($context.ControlItem.Enabled) {
                $eventDataClone.Add("ActualVerificationResult", $context.ControlResults[0].ActualVerificationResult)
                $eventDataClone.Add("AttestationStatus", $context.ControlResults[0].AttestationStatus)
                $eventDataClone.Add("VerificationResult", $context.ControlResults[0].VerificationResult)
            }
            else {
                $eventDataClone.Add("ActualVerificationResult", [VerificationResult]::Disabled)
                $eventDataClone.Add("AttestationStatus", [AttestationStatus]::None)
                $eventDataClone.Add("VerificationResult", [VerificationResult]::Disabled)
            }
            [UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{})
        }
    }

    static [void] PushServiceScanResults(
        [UsageTelemetry] $Publisher, `
        [SVTEventContext[]] $SVTEventContexts)
    {
        $NA = "NA"
        $SVTEventContextFirst = $SVTEventContexts[0]
        $eventData = @{
            [TelemetryKeys]::FeatureGroup = [FeatureGroup]::Service;
            "ScanKind" = [RemoteReportHelper]::GetServiceScanKind(
                $Publisher.InvocationContext.MyCommand.Name,
                $Publisher.InvocationContext.BoundParameters);
            "Feature" = $SVTEventContextFirst.FeatureName;
            "ResourceGroup" = [RemoteReportHelper]::Mask($SVTEventContextFirst.ResourceContext.ResourceGroupName);
            "ResourceName" = [RemoteReportHelper]::Mask($SVTEventContextFirst.ResourceContext.ResourceName);
            "ResourceId" = [RemoteReportHelper]::Mask($SVTEventContextFirst.ResourceContext.ResourceId);
        }
        $SVTEventContexts | ForEach-Object {
            $SVTEventContext = $_
            [hashtable] $eventDataClone = $eventData.Clone()
            $eventDataClone.Add("ControlIntId", $SVTEventContext.ControlItem.Id);
            $eventDataClone.Add("ControlId", $SVTEventContext.ControlItem.ControlID);
            $eventDataClone.Add("ControlSeverity", $SVTEventContext.ControlItem.ControlSeverity);
            if (!$SVTEventContext.ControlItem.Enabled) {
                $eventDataClone.Add("ActualVerificationResult", [VerificationResult]::Disabled)
                $eventDataClone.Add("AttestationStatus", [AttestationStatus]::None)
                $eventDataClone.Add("VerificationResult", [VerificationResult]::Disabled)
                [UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{})
            }
            elseif ($SVTEventContext.ControlResults.Count -eq 1 -and `
                ($SVTEventContextFirst.ResourceContext.ResourceName -eq $SVTEventContext.ControlResults[0].ChildResourceName -or `
                    [string]::IsNullOrWhiteSpace($SVTEventContext.ControlResults[0].ChildResourceName)))
            {
                $eventDataClone.Add("ActualVerificationResult", $SVTEventContext.ControlResults[0].ActualVerificationResult)
                $eventDataClone.Add("AttestationStatus", $SVTEventContext.ControlResults[0].AttestationStatus)
                $eventDataClone.Add("VerificationResult", $SVTEventContext.ControlResults[0].VerificationResult)
                $eventDataClone.Add("IsNestedResource", 'No')
                $eventDataClone.Add("NestedResourceName", $NA)
                [UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{})
            }
            elseif ($SVTEventContext.ControlResults.Count -eq 1 -and `
                $SVTEventContextFirst.ResourceContext.ResourceName -ne $SVTEventContext.ControlResults[0].ChildResourceName)
            {
                $eventDataClone.Add("ActualVerificationResult", $SVTEventContext.ControlResults[0].ActualVerificationResult)
                $eventDataClone.Add("AttestationStatus", $SVTEventContext.ControlResults[0].AttestationStatus)
                $eventDataClone.Add("VerificationResult", $SVTEventContext.ControlResults[0].VerificationResult)
                $eventDataClone.Add("IsNestedResource", 'Yes')
                $eventDataClone.Add("NestedResourceName", [RemoteReportHelper]::Mask($SVTEventContext.ControlResults[0].ChildResourceName))
                [UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{})
            }
            elseif ($SVTEventContext.ControlResults.Count -gt 1)
            {
                $eventDataClone.Add("IsNestedResource", 'Yes')
                $SVTEventContext.ControlResults | Foreach-Object {
                    [hashtable] $eventDataCloneL2 = $eventDataClone.Clone()
                    $eventDataCloneL2.Add("ActualVerificationResult", $_.ActualVerificationResult)
                    $eventDataCloneL2.Add("AttestationStatus", $_.AttestationStatus)
                    $eventDataCloneL2.Add("VerificationResult", $_.VerificationResult)
                    $eventDataCloneL2.Add("NestedResourceName", [RemoteReportHelper]::Mask($_.ChildResourceName))
                    [UsageTelemetry]::PushEvent($Publisher, $eventDataCloneL2, @{})
                }
            }
        }
    }

    static [void] PushEvent([UsageTelemetry] $Publisher, `
                            [hashtable] $Properties, [hashtable] $Metrics)
    {
        try{
            [UsageTelemetry]::SetCommonProperties($Publisher, $Properties);
            $event = [Microsoft.ApplicationInsights.DataContracts.EventTelemetry]::new()
            $event.Name = "Control Scanned"
            $Properties.Keys | ForEach-Object {
                try{
                    $event.Properties.Add($_, $Properties[$_].ToString());
                }
                catch{
                }
            }
            $Metrics.Keys | ForEach-Object {
                try{
                    $event.Metrics.Add($_, $Metrics[$_]);
                }
                catch{
                }
            }
            $Publisher.TelemetryClient.TrackEvent($event);
        }
        catch{
        }
    }

    static [void] PushException([UsageTelemetry] $Publisher, `
                            [hashtable] $Properties, [hashtable] $Metrics, `
                            [System.Management.Automation.ErrorRecord] $ErrorRecord)
    {
        try{
            [UsageTelemetry]::SetCommonProperties($Publisher, $Properties);
            $ex = [Microsoft.ApplicationInsights.DataContracts.ExceptionTelemetry]::new()
            $ex.Exception = $ErrorRecord.Exception
            try{
                $ex.Properties.Add("ScriptStackTrace", [UsageTelemetry]::AnonScriptStackTrace($ErrorRecord.ScriptStackTrace))
            }catch{}
            $Properties.Keys | ForEach-Object {
                try{
                    $ex.Properties.Add($_, $Properties[$_].ToString());
                }
                catch{
                }
            }
            $Metrics.Keys | ForEach-Object {
                try{
                    $ex.Metrics.Add($_, $Metrics[$_]);
                }
                catch{
                }
            }
            $Publisher.TelemetryClient.TrackException($ex)
            $Publisher.TelemetryClient.Flush()
        }
        catch{
        }
    }

    hidden static [void] SetCommonProperties([UsageTelemetry] $Publisher, [hashtable] $Properties)
    {
        try{
            $NA = "NA";
            $Properties.Add("InfoVersion", "V1");
            try{
                $Properties.Add("ScanSource", [RemoteReportHelper]::GetScanSource());
            }catch{}
            try{
                $Properties.Add("ScannerVersion", $Publisher.GetCurrentModuleVersion());
            }catch{}
            try{
                $Properties.Add("ControlVersion", $Publisher.GetCurrentModuleVersion());
            }catch{}
            try{
                $azureContext = Get-AzureRmContext
                try{
                    $Properties.Add([TelemetryKeys]::SubscriptionId, [RemoteReportHelper]::Mask($azureContext.Subscription.Id))
                }catch{}
                try{
                    $Properties.Add([TelemetryKeys]::SubscriptionName, [RemoteReportHelper]::Mask($azureContext.Subscription.Name))
                }catch{}
                try{
                    $Properties.Add("AzureEnv", $azureContext.Environment.Name)
                }catch{}
                try{
                    $Properties.Add("TenantId", [RemoteReportHelper]::Mask($azureContext.Tenant.Id))
                }catch{}
                try{
                    $Properties.Add("AccountId", [RemoteReportHelper]::Mask($azureContext.Account.Id))
                }catch{}
                try{
                    $Properties.Add("RunIdentifier",  [RemoteReportHelper]::Mask($azureContext.Account.Id + '##' + $Publisher.RunIdentifier));
                }catch{
                    $Properties.Add("RunIdentifier",  $Publisher.RunIdentifier);
                }
                try{
                    $Properties.Add("AccountType", $azureContext.Account.Type)
                }catch{}
            }catch{
            }
        }
        catch{
        }
    }

    hidden static [string] AnonScriptStackTrace([string] $ScriptStackTrace)
    {
        try{
            $ScriptStackTrace = $ScriptStackTrace.Replace($env:USERNAME, "USERNAME")
            $lines = $ScriptStackTrace.Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries)
            $newLines = $lines | ForEach-Object {
                $line = $_
                $lineSplit = $line.Split(@(", "), [System.StringSplitOptions]::RemoveEmptyEntries);
                if($lineSplit.Count -eq 2){
                    $filePath = $lineSplit[1];
                    $startMarker = $filePath.IndexOf("AzSDK")
                    if($startMarker -gt 0){
                        $anonFilePath = $filePath.Substring($startMarker, $filePath.Length - $startMarker)
                        $newLine = $lineSplit[0] + ", " + $anonFilePath
                        $newLine
                    }
                    else{
                        $line
                    }
                }
                else{
                    $line
                }
            }
            return ($newLines | Out-String)
        }
        catch{
            return $ScriptStackTrace
        }
    }
}