Modules/M365DSCTelemetryEngine.psm1

<#
.Description
This function gets the Application Insights key to be used for storing telemetry
 
.Functionality
Internal, Hidden
#>

function Get-M365DSCApplicationInsightsTelemetryClient
{
    [CmdletBinding()]
    param()

    if ($null -eq $Global:M365DSCTelemetryEngine)
    {
        $AI = "$PSScriptRoot/../Dependencies/Microsoft.ApplicationInsights.dll"
        [Reflection.Assembly]::LoadFile($AI) | Out-Null

        $TelClient = [Microsoft.ApplicationInsights.TelemetryClient]::new()

        $connectionString = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryConnectionString', `
            [System.EnvironmentVariableTarget]::Machine)
        if (-not [System.String]::IsNullOrEmpty($connectionString))
        {
            $TelClient.TelemetryConfiguration.ConnectionString = $connectionString
        }
        else
        {
            $InstrumentationKey = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryInstrumentationKey', `
                    [System.EnvironmentVariableTarget]::Machine)

            if ($null -eq $InstrumentationKey)
            {
                $InstrumentationKey = 'e670af5d-fd30-4407-a796-8ad30491ea7a'
            }
            $TelClient.InstrumentationKey = $InstrumentationKey
        }

        $Global:M365DSCTelemetryEngine = $TelClient
    }
    return $Global:M365DSCTelemetryEngine
}

<#
.Description
This function sends telemetry information to Application Insights
 
.Functionality
Internal
#>

function Add-M365DSCTelemetryEvent
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [System.String]
        $Type,

        [Parameter()]
        [System.Collections.Generic.Dictionary[[System.String], [System.String]]]
        $Data,

        [Parameter()]
        [System.Collections.Generic.Dictionary[[System.String], [System.Double]]]
        $Metrics
    )
    $TelemetryEnabled = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryEnabled', `
            [System.EnvironmentVariableTarget]::Machine)

    if ($null -eq $TelemetryEnabled -or $TelemetryEnabled -eq $true)
    {
        if ($Type -eq 'DriftEvaluation')
        {
            try
            {
                $hostId = (Get-Host).InstanceId
                if ($null -eq $Script:M365DSCCountResourceInstance -or $hostId -ne $Script:M365DSCExecutionContextId)
                {
                    $Script:M365DSCCountResourceInstance = 1
                }
                else
                {
                    $Script:M365DSCCountResourceInstance++
                }
                if ($null -eq $Script:M365DSCOperationStartTime -or $hostId -ne $Script:M365DSCExecutionContextId)
                {
                    $Script:M365DSCOperationStartTime = [System.DateTime]::Now
                }

                $Script:M365DSCOperationTimeTaken = [System.DateTime]::Now.Subtract($Script:M365DSCOperationStartTime)

                if ($hostId -ne $Script:M365DSCExecutionContextId)
                {
                    $Script:M365DSCExecutionContextId = $hostId
                }
                $Data.Add('ResourceInstancesCount', $Script:M365DSCCountResourceInstance)
                $Data.Add('M365DSCExecutionContextId', $hostId)
                $Data.Add('M365DSCOperationTotalTime', $Script:M365DSCOperationTimeTaken.TotalSeconds)
            }
            catch
            {
                Write-Verbose -Message $_
            }
        }

        try
        {
            if ($null -ne $Data.ConnectionMode -and $Data.ConnectionMode.StartsWith('Credential'))
            {
                if ($null -eq $Script:M365DSCCurrentRoles -or $Script:M365DSCCurrentRoles.Length -eq 0)
                {
                    try
                    {
                        Connect-M365Tenant -Workload 'MicrosoftGraph' @Global:M365DSCTelemetryConnectionToGraphParams -ErrorAction SilentlyContinue
                    }
                    catch
                    {
                        Write-Verbose -Message $_
                    }
                    $Script:M365DSCCurrentRoles = @()

                    $uri = $Global:MSCloudLoginConnectionProfile.MicrosoftGraph.ResourceUrl + 'v1.0/me?$select=id'
                    $currentUser = Invoke-MgGraphRequest -Uri $uri -Method GET
                    $currentUserId = $currentUser.id

                    $assignments = Get-MgBetaRoleManagementDirectoryRoleAssignment -Filter "principalId eq '$currentUserId'" `
                                    -Property @('RoleDefinitionId', 'DirectoryScopeId') -All -ErrorAction 'SilentlyContinue'

                    if ($null -ne $assignments)
                    {
                        $roles = Get-MgBetaRoleManagementDirectoryRoleDefinition -All `
                                    -Property @('Id', 'DisplayName')
                        foreach ($assignment in $assignments)
                        {
                            $role = $roles | Where-Object -FilterScript {$_.Id -eq $assignment.RoleDefinitionId}
                            if ($null -ne $role)
                            {
                                $Script:M365DSCCurrentRoles += $role.DisplayName + '|' + $assignment.DirectoryScopeId
                            }
                        }
                        $Data.Add('M365DSCCurrentRoles', $Script:M365DSCCurrentRoles -join ',')
                    }
                }
                else
                {
                    $Data.Add('M365DSCCurrentRoles', $Script:M365DSCCurrentRoles -join ',')
                }
            }
            elseif ($null -ne $Data.ConnectionMode -and $Data.ConnectionMode.StartsWith('ServicePrincipal'))
            {
                if ($null -eq $Script:M365DSCCurrentRoles -or $Script:M365DSCCurrentRoles.Length -eq 0)
                {
                    try
                    {
                        Connect-M365Tenant -Workload 'MicrosoftGraph' @Global:M365DSCTelemetryConnectionToGraphParams -ErrorAction Stop
                        $Script:M365DSCCurrentRoles = @()

                        $sp = Get-MgServicePrincipal -Filter "AppId eq '$($Global:M365DSCTelemetryConnectionToGraphParams.ApplicationId)'" `
                                -ErrorAction 'SilentlyContinue'
                        if ($null -ne $sp)
                        {
                            $assignments = Get-MgBetaRoleManagementDirectoryRoleAssignment -Filter "principalId eq '$($sp.Id)'" `
                                        -Property @('RoleDefinitionId', 'DirectoryScopeId') -All -ErrorAction 'SilentlyContinue'
                            if ($null -ne $assignments)
                            {
                                $roles = Get-MgBetaRoleManagementDirectoryRoleDefinition -All `
                                            -Property @('Id', 'DisplayName')
                                foreach ($assignment in $assignments)
                                {
                                    $role = $roles | Where-Object -FilterScript {$_.Id -eq $assignment.RoleDefinitionId}
                                    if ($null -ne $role)
                                    {
                                        $Script:M365DSCCurrentRoles += $role.DisplayName + '|' + $assignment.DirectoryScopeId
                                    }
                                }
                                $Data.Add('M365DSCCurrentRoles', $Script:M365DSCCurrentRoles -join ',')
                            }
                        }
                    }
                    catch
                    {
                        Write-Verbose -Message $_
                    }
                }
                else
                {
                    $Data.Add('M365DSCCurrentRoles', $Script:M365DSCCurrentRoles -join ',')
                }
            }
        }
        catch
        {
            Write-Verbose -Message $_
        }

        $TelemetryClient = Get-M365DSCApplicationInsightsTelemetryClient

        try
        {
            $ProjectName = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryProjectName', `
                    [System.EnvironmentVariableTarget]::Machine)

            if ($null -ne $ProjectName)
            {
                $Data.Add('ProjectName', $ProjectName)
            }

            if (-not $Data.ContainsKey('Tenant'))
            {
                if (-not [System.String]::IsNullOrEmpty($Data.Principal))
                {
                    if ($Data.Principal -like '*@*.*')
                    {
                        $principalValue = $Data.Principal.Split('@')[1]
                        $Data.Add('Tenant', $principalValue)
                    }
                }
                elseif (-not [System.String]::IsNullOrEmpty($Data.TenantId))
                {
                    $principalValue = $Data.TenantId
                    $Data.Add('Tenant', $principalValue)
                }
            }

            $Data.Remove('TenandId') | Out-Null
            $Data.Remove('Principal') | Out-Null

            # Capture PowerShell Version Info
            if (-not $Data.Keys.Contains('PSMainVersion'))
            {
                $Data.Add('PSMainVersion', $PSVersionTable.PSVersion.Major.ToString() + '.' + $PSVersionTable.PSVersion.Minor.ToString())
            }
            if (-not $Data.Keys.Contains('PSVersion'))
            {
                $Data.Add('PSVersion', $PSVersionTable.PSVersion.ToString())
            }
            if (-not $Data.Keys.Contains('PSEdition'))
            {
                $Data.Add('PSEdition', $PSVersionTable.PSEdition.ToString())
            }

            if ($null -ne $PSVersionTable.BuildVersion -and -not $Data.Keys.Contains('PSBuildVersion'))
            {
                $Data.Add('PSBuildVersion', $PSVersionTable.BuildVersion.ToString())
            }

            if ($null -ne $PSVersionTable.CLRVersion -and -not $Data.Keys.Contains('PSCLRVersion'))
            {
                $Data.Add('PSCLRVersion', $PSVersionTable.CLRVersion.ToString())
            }

            # Capture Console/Host Information
            if ($host.Name -eq 'ConsoleHost' -and $null -eq $env:WT_SESSION -and -not $Data.Keys.Contains('PowerShellAgent'))
            {
                $Data.Add('PowerShellAgent', 'Console')
            }
            elseif ($host.Name -eq 'Windows PowerShell ISE Host' -and -not $Data.Keys.Contains('PowerShellAgent'))
            {
                $Data.Add('PowerShellAgent', 'ISE')
            }
            elseif ($host.Name -eq 'ConsoleHost' -and $null -ne $env:WT_SESSION -and -not $Data.Keys.Contains('PowerShellAgent'))
            {
                $Data.Add('PowerShellAgent', 'Windows Terminal' -and -not $Data.Keys.Contains('PowerShellAgent'))
            }
            elseif ($host.Name -eq 'ConsoleHost' -and $null -eq $env:WT_SESSION -and `
                    $null -ne $env:BUILD_BUILDID -and $env:SYSTEM -eq 'build' -and -not $Data.Keys.Contains('PowerShellAgent'))
            {
                $Data.Add('PowerShellAgent', 'Azure DevOPS')
                $Data.Add('AzureDevOPSPipelineType', 'Build')
                $Data.Add('AzureDevOPSAgent', $env:POWERSHELL_DISTRIBUTION_CHANNEL)
            }
            elseif ($host.Name -eq 'ConsoleHost' -and $null -eq $env:WT_SESSION -and `
                    $null -ne $env:BUILD_BUILDID -and $env:SYSTEM -eq 'release' -and -not $Data.Keys.Contains('PowerShellAgent'))
            {
                $Data.Add('PowerShellAgent', 'Azure DevOPS')
                $Data.Add('AzureDevOPSPipelineType', 'Release')
                $Data.Add('AzureDevOPSAgent', $env:POWERSHELL_DISTRIBUTION_CHANNEL)
            }
            elseif ($host.Name -eq 'Default Host' -and `
                    $null -ne $env:APPSETTING_FUNCTIONS_EXTENSION_VERSION -and -not $Data.Keys.Contains('PowerShellAgent'))
            {
                $Data.Add('PowerShellAgent', 'Azure Function')
                $Data.Add('AzureFunctionWorkerVersion', $env:FUNCTIONS_WORKER_RUNTIME_VERSION)
            }
            elseif ($host.Name -eq 'CloudShell' -and -not $Data.Keys.Contains('PowerShellAgent'))
            {
                $Data.Add('PowerShellAgent', 'Cloud Shell')
            }

            if ($null -ne $Data.Resource -and $Data.Keys.Contains('Resource'))
            {
                if ($Data.Resource.StartsWith('MSFT_AAD') -or $Data.Resource.StartsWith('AAD'))
                {
                    $Data.Add('Workload', 'Azure Active Directory')
                }
                elseif ($Data.Resource.StartsWith('MSFT_Intune') -or $Data.Resource.StartsWith('Defender'))
                {
                    $Data.Add('Workload', 'Defender')
                }
                elseif ($Data.Resource.StartsWith('MSFT_EXO') -or $Data.Resource.StartsWith('EXO'))
                {
                    $Data.Add('Workload', 'Exchange Online')
                }
                elseif ($Data.Resource.StartsWith('MSFT_Intune') -or $Data.Resource.StartsWith('Intune'))
                {
                    $Data.Add('Workload', 'Intune')
                }
                elseif ($Data.Resource.StartsWith('MSFT_O365') -or $Data.Resource.StartsWith('O365'))
                {
                    $Data.Add('Workload', 'Office 365 Admin')
                }
                elseif ($Data.Resource.StartsWith('MSFT_OD') -or $Data.Resource.StartsWith('OD'))
                {
                    $Data.Add('Workload', 'OneDrive for Business')
                }
                elseif ($Data.Resource.StartsWith('MSFT_Planner') -or $Data.Resource.StartsWith('Planner'))
                {
                    $Data.Add('Workload', 'Planner')
                }
                elseif ($Data.Resource.StartsWith('MSFT_PP') -or $Data.Resource.StartsWith('PP'))
                {
                    $Data.Add('Workload', 'Power Platform')
                }
                elseif ($Data.Resource.StartsWith('MSFT_SC') -or $Data.Resource.StartsWith('SC'))
                {
                    $Data.Add('Workload', 'Security and Compliance Center')
                }
                elseif ($Data.Resource.StartsWith('MSFT_SPO') -or $Data.Resource.StartsWith('SPO'))
                {
                    $Data.Add('Workload', 'SharePoint Online')
                }
                elseif ($Data.Resource.StartsWith('MSFT_Teams') -or $Data.Resource.StartsWith('Teams'))
                {
                    $Data.Add('Workload', 'Teams')
                }
                $Data.Resource = $Data.Resource.Replace('MSFT_', '')
            }

            if ($Type -eq "ExportCompleted")
            {
                if ($null -ne $Global:M365DSCExportResourceInstancesCount)
                {
                    $Data.Add("ExportedResourceInstancesCount", $Global:M365DSCExportResourceInstancesCount)
                }
                if ($null -ne $Global:M365DSCExportResourceTypes)
                {
                    $Data.Add("ExportedResourceTypes", $Global:M365DSCExportResourceTypes)
                    $Data.Add("ExportedResourceTypesCount", $Global:M365DSCExportResourceTypes.Length)
                }
                if($null -ne $Global:M365DSCExportContentSize)
                {
                    $Data.Add("ExportedContentSize", $Global:M365DSCExportContentSize)
                }
            }

            [array]$version = (Get-Module 'Microsoft365DSC').Version | Sort-Object -Descending
            $Data.Add('M365DSCVersion', $version[0].ToString())

            # OS Version
            try
            {
                if ($null -eq $Global:M365DSCOSInfo)
                {
                    $Global:M365DSCOSInfo = (Get-CimInstance -ClassName Win32_OperatingSystem -Property Caption -ErrorAction SilentlyContinue).Caption
                }
                $Data.Add('M365DSCOSVersion', $Global:M365DSCOSInfo)
            }
            catch
            {
                Write-Verbose -Message $_
            }

            # LCM Metadata Information
            try
            {
                if ($null -eq $Script:LCMInfo)
                {
                    $Script:LCMInfo = Get-DscLocalConfigurationManager -ErrorAction Stop
                }

                $certificateConfigured = $false
                if (-not [System.String]::IsNullOrEmpty($LCMInfo.CertificateID))
                {
                    $certificateConfigured = $true
                }

                $partialConfiguration = $false
                if (-not [System.String]::IsNullOrEmpty($Script:LCMInfo.PartialConfigurations))
                {
                    $partialConfiguration = $true
                }
                $Data.Add('LCMUsesPartialConfigurations', $partialConfiguration)
                $Data.Add('LCMCertificateConfigured', $certificateConfigured)
                $Data.Add('LCMConfigurationMode', $Script:LCMInfo.ConfigurationMode)
                $Data.Add('LCMConfigurationModeFrequencyMins', $Script:LCMInfo.ConfigurationModeFrequencyMins)
                $Data.Add('LCMRefreshMode', $Script:LCMInfo.RefreshMode)
                $Data.Add('LCMState', $Script:LCMInfo.LCMState)
                $Data.Add('LCMStateDetail', $Script:LCMInfo.LCMStateDetail)

                if ([System.String]::IsNullOrEmpty($Type))
                {
                    if ($Global:M365DSCExportInProgress)
                    {
                        $Type = 'Export'
                    }
                    elseif ($Script:LCMInfo.LCMStateDetail -eq 'LCM is performing a consistency check.' -or `
                            $Script:LCMInfo.LCMStateDetail -eq 'LCM exécute une vérification de cohérence.' -or `
                            $Script:LCMInfo.LCMStateDetail -eq 'LCM führt gerade eine Konsistenzüberprüfung durch.')
                    {
                        $Type = 'MonitoringScheduled'
                    }
                    elseif ($Script:LCMInfo.LCMStateDetail -eq 'LCM is testing node against the configuration.')
                    {
                        $Type = 'MonitoringManual'
                    }
                    elseif ($Script:LCMInfo.LCMStateDetail -eq 'LCM is applying a new configuration.' -or `
                            $Script:LCMInfo.LCMStateDetail -eq 'LCM applique une nouvelle configuration.')
                    {
                        $Type = 'ApplyingConfiguration'
                    }
                    else
                    {
                        $Type = 'Undetermined'
                    }
                }
            }
            catch
            {
                Write-Verbose -Message $_
            }

            $M365DSCTelemetryEventId = (New-GUID).ToString()
            $Data.Add('M365DSCTelemetryEventId', $M365DSCTelemetryEventId)

            if ([System.String]::IsNullOrEMpty($Type))
            {
                if ((-not [System.String]::IsNullOrEmpty($Data.Method) -and $Data.Method -eq 'Export-TargetResource') -or $Global:M365DSCExportInProgress)
                {
                    $Type = 'Export'
                }
            }

            $TelemetryClient.TrackEvent($Type, $Data, $Metrics)
        }
        catch
        {
            try
            {
                $TelemetryClient.TrackEvent('Error', $Data, $Metrics)
            }
            catch
            {
                Write-Error $_
            }
        }
    }
    else
    {
        return
    }
}

<#
.Description
This function configures the telemetry feature of M365DSC
 
.Parameter Enabled
Enables or disables telemetry collection.
 
.Parameter InstrumentationKey
Specifies the Instrumention Key to be used to send the telemetry to.
 
.Parameter ProjectName
Specifies the name of the project to store the telemetry data under.
 
.Example
Set-M365DSCTelemetryOption -Enabled $false
 
.Functionality
Public
#>

function Set-M365DSCTelemetryOption
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [System.Boolean]
        $Enabled,

        [Parameter()]
        [System.String]
        $InstrumentationKey,

        [Parameter()]
        [System.String]
        $ProjectName,

        [Parameter()]
        [System.String]
        $ConnectionString
    )

    if ($null -ne $Enabled)
    {
        [System.Environment]::SetEnvironmentVariable('M365DSCTelemetryEnabled', $Enabled, `
                [System.EnvironmentVariableTarget]::Machine)
    }

    if ($null -ne $InstrumentationKey)
    {
        [System.Environment]::SetEnvironmentVariable('M365DSCTelemetryInstrumentationKey', $InstrumentationKey, `
                [System.EnvironmentVariableTarget]::Machine)
    }

    if ($null -ne $ProjectName)
    {
        [System.Environment]::SetEnvironmentVariable('M365DSCTelemetryProjectName', $ProjectName, `
                [System.EnvironmentVariableTarget]::Machine)
    }

    if ($null -ne $ConnectionString)
    {
        [System.Environment]::SetEnvironmentVariable('M365DSCTelemetryConnectionString', $ConnectionString, `
                [System.EnvironmentVariableTarget]::Machine)
    }
}

<#
.Description
This function gets the configuration for the M365DSC telemetry feature
 
.Example
Get-M365DSCTelemetryOption
 
.Functionality
Public
#>

function Get-M365DSCTelemetryOption
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param()

    try
    {
        return @{
            Enabled            = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryEnabled', `
                    [System.EnvironmentVariableTarget]::Machine)
            InstrumentationKey = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryInstrumentationKey', `
                    [System.EnvironmentVariableTarget]::Machine)
            ProjectName        = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryProjectName', `
                    [System.EnvironmentVariableTarget]::Machine)
            ConnectionString   = [System.Environment]::GetEnvironmentVariable('M365DSCTelemetryConnectionString', `
                    [System.EnvironmentVariableTarget]::Machine)
        }
    }
    catch
    {
        throw $_
    }
}

<#
.Description
This function converts the data which is send to Application Insights to the correct format.
 
.Functionality
Internal
#>

function Format-M365DSCTelemetryParameters
{
    [CmdletBinding()]
    [OutputType([System.Collections.Generic.Dictionary[[String], [String]]])]
    param(
        [parameter(Mandatory = $true)]
        [System.String]
        $ResourceName,

        [parameter(Mandatory = $true)]
        [System.String]
        $CommandName,

        [parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $Parameters
    )
    $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new()

    try
    {
        $data.Add('Resource', $ResourceName)
        $data.Add('Method', $CommandName)
        if ($Parameters.Credential)
        {
            try
            {
                $data.Add('Principal', $Parameters.Credential.UserName)
                $data.Add('Tenant', $Parameters.Credential.UserName.Split('@')[1])
            }
            catch
            {
                Write-Verbose -Message $_
            }
        }
        elseif ($Parameters.ApplicationId)
        {
            $data.Add('Principal', $Parameters.ApplicationId)
            $data.Add('Tenant', $Parameters.TenantId)
        }
        elseif (-not [System.String]::IsNullOrEmpty($TenantId))
        {
            $data.Add('Tenant', $Parameters.TenantId)
        }
        $connectionMode = Get-M365DSCAuthenticationMode -Parameters $Parameters
        $data.Add('ConnectionMode', $connectionMode)
    }
    catch
    {
        Write-Verbose -Message $_
    }
    return $data
}

Export-ModuleMember -Function @(
    'Add-M365DSCTelemetryEvent',
    'Format-M365DSCTelemetryParameters',
    'Get-M365DSCTelemetryOption',
    'Set-M365DSCTelemetryOption'
)