HybridHealthServices_utils.ps1

# Gets ADHybridHealthService member credentials
# May 26th 2021
function Get-HybridHealthServiceMemberCredentials
{
<#
    .SYNOPSIS
    Gets ADHybridHealthService member credentials
 
    .DESCRIPTION
    Creates and returns new ADHybridHealthService member credentials
 
    .Parameter AccessToken
    The access token used to get ADHybridHealthService members.
 
    .Parameter ServiceName
    Name of the ADHybridHealthService
 
    .Parameter ServiceMemberId
    Guid of the service member.
 
    .Example
    Get-AADIntHybridHealthServiceMemberCredentials -ServiceName AdFederationService-sts.company.com -MemberId 0fce7ce0-81a0-4bf7-87fb-fc787dfe13c2
 
    lastReboot : 2021-03-16T08:17:19.0912Z
    lastDisabled :
    lastUpdated : 2021-05-06T06:04:20.6537234Z
    activeAlerts : 2
    resolvedAlerts : 1
    createdDate : 0001-01-01T00:00:00
    disabled : False
    dimensions :
    additionalInformation :
    tenantId : 5b53828e-8e7b-42d1-a5f0-9b34bbd1844a
    serviceId : 50abc8f3-243a-4ac1-a3fb-712054d7334b
    serviceMemberId : bec07a23-dd4a-4c80-8c92-9b9dc089f75c
    machineId : 0cf2774f-a188-4bd3-b4b3-3a690374325d
    machineName : STS01
    role : AdfsServer_2016
    status : Error
    properties :
    installedQfes :
    recommendedQfes :
    monitoringConfigurationsComputed :
    monitoringConfigurationsCustomized :
    osVersion : 10.0.17763.0
    osName : Microsoft Windows Server 2019 Standard
    disabledReason : 0
    serverReportedMonitoringLevel :
    lastServerReportedMonitoringLevelChange :
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$AccessToken,
        [Parameter(Mandatory=$True)]
        [String]$ServiceName,
        [Parameter(Mandatory=$True)]
        [guid]$ServiceMemberId

    )
    Process
    {
        # Get from cache if not provided
        $AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://management.core.windows.net/" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c"

        # Set the headers
        $headers=@{
            "Authorization" = "Bearer $AccessToken"
        }

        # Invoke the command
        $response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://management.azure.com/providers/Microsoft.ADHybridHealthService/services/$ServiceName/servicemembers/$($ServiceMemberId.toString())/credentials?api-version=2014-01-01" -Headers $headers

        # Return credentials
        $creds = [ordered]@{}
        foreach($cred in $response)
        {
            # Decode the certificate
            if($cred.identifier -eq "tenant.cert")
            {
                $bCert = Convert-B64ToByteArray -B64 $cred.credentialData

                # Strip the header if exists
                if($bcert[0] -eq 0x01)
                {
                    # First 4 bytes = format
                    # Next 4 bytes = length of the string like "policykeyservice.dc.ad.msft.net"
                    # Total header length = 4 + 4 + length
                    $length = [bitconverter]::ToInt32($bCert[4..7],0)
                    $bCert = $bCert[(4+4+$length)..($bcert.length)]
                }

                $creds[$cred.identifier] = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$bCert)
            }
            else
            {
                $creds[$cred.identifier] = $cred.credentialData
            }
        }

        

        return New-Object psobject -Property $creds
               
    }
}

# Gets ADHybridHealthService access token
# May 26th 2021
function Get-HybridHealthServiceAccessToken
{
<#
    .SYNOPSIS
    Gets ADHybridHealthService access token
 
    .DESCRIPTION
    Returns ADHybridHealthService access token
 
    .Parameter AgentKey
    AgentKey of the ADHybridHealthService agent
 
    .Parameter MachineID
    MachineID of the computer running the ADHybridHealthService agent
 
    .Parameter TenantID
    Tenant ID.
 
    .Example
    $at = Get-AADIntHybridHealthServiceAccessToken -AgentKey $agentKey -TenantId $tenantId -MachineId $machineId
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$AgentKey,
        [Parameter(Mandatory=$True)]
        [guid]$MachineId,
        [Parameter(Mandatory=$True)]
        [guid]$TenantId
    )
    Process
    {
        # Build a body
        $body=@{
            "grant_type" = "client_credentials"
            "client_secret" = $AgentKey
            "client_id"     = "$($TenantId.ToString())_$($MachineId.ToString())"
        }

        # Invoke the command
        $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://s1.adhybridhealth.azure.com/oauth2/token" -Body $body

        return $response.access_token
               
    }
}

# Gets ADHybridHealthService Blob upload key
# May 26th 2021
function Get-HybridHealthServiceBlobUploadKey
{
<#
    .SYNOPSIS
    Gets ADHybridHealthService blob upload key
 
    .DESCRIPTION
    Gets ADHybridHealthService blob upload key. The key is an url used to upload events to Azure blob. Url contains pre-calcuated SAS token.
 
    .Parameter AccessToken
    The access token used to get ADHybridHealthService blob upload key.
 
    .Parameter ServiceID
    ServiceID
     
    .Example
    Get-HybridHealthServiceBlobUploadKey -AccessToken $at -ServiceId $serviceId
 
    https://adhsprodweuaadsynciadata.blob.core.windows.net/adfederationservice-8c11e4fb-299c-42c0-b79a-555c33964b58?sv=2018-03-28&sr=c&sig=pZ056YDtl8iK9PjiNoQED6tLHd3h0EkwDlHY%2Bxf8Znc%3D&se=2021-05-24T06%3A32%3A18Z&sp=w
 
    .Example
    $blobKey = Get-AADIntHybridHealthServiceBlobUploadKey -AccessToken $at -ServiceId $serviceId
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [String]$AccessToken,
        [Parameter(Mandatory=$True)]
        [guid]$ServiceId
    )
    Process
    {
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
        }

        # Invoke the command
        $response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/$($ServiceId.ToString())/keys/BlobUploadKey" -Headers $headers

        # Return upload key
        $response
    }
}

# Gets ADHybridHealthService event hub publisher key
# May 29th 2021
function Get-HybridHealthServiceEventHubPublisherKey
{
<#
    .SYNOPSIS
    Gets ADHybridHealthService event hub publisher key
 
    .DESCRIPTION
    Gets ADHybridHealthService event hub publisher key. The key includes Service Bus endpoint and SharedAccessSignature.
 
    .Parameter AccessToken
    The access token used to get ADHybridHealthService event hub publisher key.
 
    .Parameter ServiceID
    ServiceID
     
    .Example
    Get-AADIntHybridHealthServiceEventHubPublisherKey -AccessToken $at -ServiceId $serviceId
 
    Endpoint=sb://adhsprodweuehadfsia.servicebus.windows.net/;SharedAccessSignature=SharedAccessSignature sr=sb%3a%2f%2fadhsprodweuehadfsia.servicebus.windows.net%2fadhsprodweuehadfsia%2fPublishers%2f8c77dad6-9932-4bfe-bf9e-58734ccb3e2c&sig=XRKxI%2bR7LEe4pBxe4OZt86dzFxIvSsyqs0UPmlO3hFM%3d&se=1622788339&skn=RootManageSharedAccessKey;EntityPath=adhsprodweuehadfsia;Publisher=8c77dad6-9932-4bfe-bf9e-58734ccb3e2c
 
    .Example
    $eventKey = Get-AADIntHybridHealthServiceEventHubPublisherKey -AccessToken $at -ServiceId $serviceId
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [String]$AccessToken,
        [Parameter(Mandatory=$True)]
        [guid]$ServiceId
    )
    Process
    {
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
        }

        # Invoke the command
        $response = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/$($ServiceId.ToString())/keys/EventHubPublisherKey" -Headers $headers

        # Return upload key
        $response
    }
}

# Send the signature to Azure using service bus
# May 26th 2021
function Send-ADFSServiceBusMessage
{

    [cmdletbinding()]

    param(
        [Parameter(Mandatory=$True)]
        [String]$EventHubPublisherKey,
        [Parameter(Mandatory=$True)]
        [String]$BlobAbsoluteUri,
        [Parameter(Mandatory=$True)]
        [guid]$MachineId,
        [Parameter(Mandatory=$True)]
        [guid]$TenantId,
        [Parameter(Mandatory=$True)]
        [guid]$ServiceId,
        [Parameter(Mandatory=$True)]
        [datetime]$SigningTime,
        [Parameter(Mandatory=$True)]
        [String]$HMACSignature
        )
    Try
    {
        # Define some needed variables
        $keyParts   = $EventHubPublisherKey.Split(";")
        $endpoint   = $keyParts[0].Substring($keyParts[0].IndexOf("=")+1).Replace("sb:","wss:")
        $url        = "$endPoint$`servicebus/websocket"

        $SAS        = $keyParts[1].Substring($keyParts[1].IndexOf("=")+1)
        $entityPath = $keyParts[2].Substring($keyParts[2].IndexOf("=")+1)
        $publisher  = $keyParts[3].Substring($keyParts[3].IndexOf("=")+1)
        $SASName    = "$($endpoint.Replace("wss:","amqp:"))$entitypath/Publishers/$publisher"

        # Define headers for the first request
        $headers = @{
            "Connection"             = "Upgrade"
            "Upgrade"                = "websocket"
            "Sec-WebSocket-Key"      = [convert]::ToBase64String((New-Guid).ToByteArray())
            "Sec-WebSocket-Version"  = "13"
            "Sec-WebSocket-Protocol" = "AMQPWSB10"
            "User-Agent"             = ""
        }

        # Create the socket
        $socket = New-Object System.Net.WebSockets.ClientWebSocket
            
        # Add AMQPWSB10 as sub protocol
        $socket.Options.AddSubProtocol("AMQPWSB10")

        # Create the token and open the connection
        $token = New-Object System.Threading.CancellationToken                                                   

        Write-Verbose "Opening websocket: $url"

        $connection = $socket.ConnectAsync($url, $token)
        While (!$connection.IsCompleted) { Start-Sleep -Milliseconds 100 }

        if($connection.IsFaulted -eq "True")
        {
            Write-Error $connection.Exception
            return
        }

        # Send the first AMQP message
        SendToSocket -Socket $socket -Token $token -Bytes @(
            0x41, 0x4D, 0X51, 0X50, # AMQP
            0x03, # Protocol = SASL
            0x01, # Major
            0x00, # Minor
            0x00)

        # Receive response for the first AMQP message
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)

        # Receive SASL mechanism
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)

        # Send SASL Init (external)
        SendToSocket -Socket $socket -Token $token -Bytes (New-SASLInit)

        # Receive Welcome!
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)

        # Send AMQP init
        SendToSocket -Socket $socket -Token $token -Bytes @(
            0x41, 0x4D, 0X51, 0X50, # AMQP
            0x00, # Protocol
            0x01, # Major
            0x00, # Minor
            0x00) # Revision

        # Receive AMQP init response
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)

        # Send AMQP Open
        $id = Convert-ByteArrayToHex (Get-RandomBytes -Bytes 16)
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPOpen -ContainerId $id -HostName ($endpoint.Split("/")[2]))

        # Receive AMQP Open response
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)

        # Send AMQP Begin
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPBegin)

        # Receive AMQP Begin response
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)

        # Generate the name for Attach message
        $targetNumber = Get-Random -Minimum 1000 -Maximum 65000
        $name = "duplex$($targetNumber):$($targetNumber+2):$($targetNumber+3):"
        $source = '$cbs'
        
        # Send AMQP Attach (out)
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttachADFS -Handle 0 -Direction out -Name "$($name)sender" -Target $source)

        # Receive AMQP Attach (out)
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 250)

        # Receive AMQP Flow
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 100)

        # Send AMQP Attach (in)
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttachADFS -Handle 1 -Direction in -Name "$($name)receiver" -Target $id -Source $source)

        # Receive AMQP Attach (in)
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 250)

        # Send AMQP Flow
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPFlow)

        # Send SAS key
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPTransferADFSSAS -Name $SASName -Handle 0 -Id $id -SharedAccessSignature $SAS)
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 1000)

        # Send another attach for the insights
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPAttachADFS2 -Handle 2 -Name "$((New-Guid).ToString().replace('-',''));$($targetNumber):$($targetNumber+1):$($targetNumber+6)" -Target "$entitypath/Publishers/$publisher")
        $message = Parse-BusMessage (ReadFromSocket -Socket $socket -Token $token -ArraySize 1000)
                
        # Send the Insights message signature
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPTransferADFSInsights -Handle 2 -TenantId $TenantId -MachineId $MachineId -ServiceId $ServiceId -BlobUri $BlobAbsoluteUri -SigningTime $SigningTime -HmacSignature "$HMACSignature")

        # Close the channels
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPDetach -Handle 0)
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPDetach -Handle 1)
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPDetach -Handle 2)

        # Close the bus
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPEnd)
        SendToSocket -Socket $socket -Token $token -Bytes (New-AMQPClose)

            
    }Finally{

        If ($socket) { 
            Write-Verbose "Closing websocket"
            $socket.Dispose()
        }

    }
}

# Gets ADHybridHealthService agent information from the local computer
# May 26th 2021
function Get-HybridHealthServiceAgentInfo
{
<#
    .SYNOPSIS
    Gets ADHybridHealthService agent information from the local computer.
 
    .DESCRIPTION
    Gets ADHybridHealthService agent information from the local computer.
 
    .Parameter Service
    Which service's agent information to return. Can be one of "ADFS" or "Sync". Defaults to ADFS.
 
    .Example
    PS C:\>Get-AADIntHybridHealthServiceAgentInfo
 
    AgentKey : 6Fk9SiL[redacted]Hw==
    TenantId : 5d898b21-4478-4ee0-a2be-ad4dfb540b09
    ServiceId : 59f626ab-92cd-4658-b12f-12a604f5f1c2
    ServiceMemberId : 0bfc0715-1ed2-44c7-89ec-bf7842cc4575
    ClientId : 0d88eaa8-08a4-4b2c-b5ce-49a9ea02d1d7
    MachineId : 279a0323-4647-494c-ac3a-fc13545f3c33
#>

    [cmdletbinding()]
    Param(
        [ValidateSet("ADFS","Sync")]
        [String]$Service="ADFS"
    )
    Begin
    {
        # Add the required assembly and entropy
        Add-Type -AssemblyName System.Security
        $entropy = [text.encoding]::Unicode.getBytes("ra4k1Q0qHdYSZfqGxgnFB3c6Z025w4IU")
    }
    Process
    {
        $attributes = [ordered]@{}
        try
        {
            # Decrypt the agent key
            $encAgentKey            = Convert-B64ToByteArray -B64   (Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent" -Name "AgentKey")
            $attributes["AgentKey"] = Convert-ByteArrayToB64 -Bytes ([Security.Cryptography.ProtectedData]::Unprotect([byte[]]$encAgentKey, $entropy, 'CurrentUser'))

            # Get other relevant agent information
            $attributes["TenantId"]        = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent"                              -Name "TenantId"
            $attributes["ServiceId"]       = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent\$Service"                     -Name "ServiceId"
            $attributes["ServiceMemberId"] = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent\$Service"                     -Name "ServiceMemberId"
            $attributes["ClientId"]        = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\ADHealthAgent\$Service"                     -Name "ClientId"
            $attributes["MachineId"]       = Get-ItemPropertyValue -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent" -Name "MachineIdentity"
            $attributes["Server"]          = $env:COMPUTERNAME
        }
        catch
        {
            Throw "Must be run as Local Administrator and on the computer where the agent is installed!`nGot error: $($_.Exception.Message)"
        }

        # Return
        New-Object -TypeName psobject -Property $attributes
    }
}

# Create a fake hybrid health service event
# May 26th 2021
function New-HybridHealtServiceEvent
{
<#
    .SYNOPSIS
    Creates a new ADHybridHealthService event.
 
    .DESCRIPTION
    Creates a new ADHybridHealthService event with the given parameters.
 
    .Parameter UniqueID
    Unique ID of the event. Provide existing Request ID from the sign-in log to overwrite.
 
    .Parameter Server
    Server name of the AD FS server.
         
    .Parameter EventType
    Event type. Can be one of "NotSet","AppToken","FreshCredential","System","Discovery","Signout","PwdChange","DeviceReg","Resource","Config","ExtranetLockout".
    Defaults to AppToken.
 
    .Parameter PrimaryAuthentication
    The list of authentication methods. Can be a combination of "NotSet","Forms","Windows","Certificate","Device","Multifactor","Sso","Federated"
    Seems to always require "Sso". Defaults to "Forms","Sso"
 
    .Parameter RequiredAuthType
    Requires authentication type. Can be one of "NotSet","Primary","Secondary".
    Defaults to Primary
 
    .Parameter RelayingParty
    Relaying party. Defaults to "urn:federation:MicrosoftOnline"
 
    .Parameter RelyingPartyName
    Display Name of the relaying party.
 
    .Parameter Result
    Was the authentication successful or not. Defaults to $True
 
    .Parameter DeviceAuthentication
    Was device authentication used or not. Defaults to $False
         
    .Parameter URL
    Url. Defaults to "/adfs/ls"
 
    .Parameter User
    Some user id number. Defaults to 666
 
    .Parameter UserId
    User Id. Defaults to empty.
         
    .Parameter UserIdType
    User Id type. Can be one of "AnchorID","UPN","WindowsAccountName","PrimarySID","NameID","Email","Name","NotSet"
    Defaults to AnchorID
 
    .Parameter UPN
    User Principal Name of the user.
 
    .Parameter Timestamp
    Login time. Defaults to current time.
 
    .Parameter Protocol
    Authentication protocol used. Can be one of "NotSet","WSFederation","WSFedSamlP","OAuth","SAMLP","WSTrust","MSAdfsPIP","MSISHTTP"
    Defaults to WSFederation. If OAuth is used, the client id can be provided.
 
    .Parameter NetworkLocationType
    Network type, can be one of "NotSet","Intranet","Extranet"
    Defaults to Intranet. If Extranet is used, Azure AD shows geo location.
 
    .Parameter AppTokenFailureType
    Reason for login failure. Can be one of "NotAFailure","UPError","LockoutError","ExpiredPassword","DisabledAccount","DeviceAuthError","UserCertAuthError","IssuanceAuthZError","MFAError","ExtranetLockoutError","LogoutError","CredentialValidationError","OtherCredentialError","IssuanceDelegationError","TokenAcceptanceError","ProtocolError","WsFedRequestFailure","InvalidRelyingPartyError","InvalidClientApplicationError","GenericError","OtherError"
    Defaults to NotAFailure
         
    .Parameter IPAddress
    Ip address of the user.
 
    .Parameter ClaimsProvider
    Claims provider.
    Defaults to empty.
         
    .Parameter OAuthClientID
    Client ID used in OAuth login.
    Defaults to empty.
 
    .Parameter OAuthTokenRetrievalMethod
    OAuth token retrieval method.
    Defaults to empty.
 
    .Parameter MFA
    MFA provider used to perform MFA.
    Defaults to empty.
 
    .Parameter MFAProviderErrorCode
    Error code provided by the MFA provider.
    Defaults to empty.
 
    .Parameter .ProxyServer
    Proxy server.
    Defaults to empty.
 
    .Parameter Endpoint
    AD FS endpoint.
    Defaults to "/adfs/ls/"
 
    .Parameter UserAgent
    UserAgent.
    Defaults to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
 
    .Parameter DeviceID
    Device ID used during the login. Can be any device from Azure AD.
    Defaults to empty.
 
    .Parameter ErrorHitCount
    Number of login errors.
    Defaults to 0.
 
    .Parameter X509CertificateType
    Type of X509 Certificate.
    Defaults to empty.
 
    .Parameter $MFAAuthenticationType
    MFA authentication type.
    Defaults to empty.
 
    .Parameter ActivityId
    Activity Id of the event.
    Defaults to random guid.
 
    .Parameter ActivityIdAutoGenerated
    Is Activity Id automatically generated or not. Defaults to $False
 
    .Parameter PrimarySid
    The primary SID of the user.
    Defaults to empty.
 
    .Parameter ImmutableId
    Immutable Id of the user. Base 64 encoded GUID of the user's AD object.
    Defaults to empty.
 
    .Example
    PS C:\>$events = @()
    PS C:\>$events += (New-AADIntHybridHealtServiceEvent -Server "Server" -UPN "user@company.com" -IPAddress "192.168.0.2")
 
    PS C:\>Send-AADIntHybridHealthServiceEventBlob -BlobKey $blobKey -TenantId $tenantId -MachineId $machineId -ServiceId $serviceId -EventPublisherKey $eventKey -Events $events
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [guid]$UniqueID = (New-Guid),

        [Parameter(Mandatory=$True)]
        [String]$Server,
        
        [Parameter(Mandatory=$False)]
        [ValidateSet("NotSet","AppToken","FreshCredential","System","Discovery","Signout","PwdChange","DeviceReg","Resource","Config","ExtranetLockout")]
        [String]$EventType = "AppToken",

        [Parameter(Mandatory=$False)]
        [ValidateSet("NotSet","Forms","Windows","Certificate","Device","Multifactor","Sso","Federated")]
        [String[]]$PrimaryAuthentication = @("Forms","Sso"),

        [Parameter(Mandatory=$False)]
        [ValidateSet("NotSet","Primary","Secondary")]
        [String]$RequiredAuthType = "Primary",

        [Parameter(Mandatory=$False)]
        [String]$RelyingParty = "urn:federation:MicrosoftOnline",

        [Parameter(Mandatory=$False)]
        [String]$RelyingPartyName = "",

        [Parameter(Mandatory=$False)]
        [bool]$Result = $True,

        [Parameter(Mandatory=$False)]
        [bool]$DeviceAuthentication = $False,
        
        [Parameter(Mandatory=$False)]
        [String]$URL = "/adfs/ls",

        [Parameter(Mandatory=$False)]
        [int]$User = 666,

        [Parameter(Mandatory=$False)]
        [String]$UserId,
        
        [Parameter(Mandatory=$False)]
        [ValidateSet("AnchorID","UPN","WindowsAccountName","PrimarySID","NameID","Email","Name","NotSet")]
        [String]$UserIdType = "AnchorID",

        [Parameter(Mandatory=$True)]
        [String]$UPN,

        [Parameter(Mandatory=$False)]
        [datetime]$Timestamp = (Get-Date),

        [Parameter(Mandatory=$False)]
        [ValidateSet("NotSet","WSFederation","WSFedSamlP","OAuth","SAMLP","WSTrust","MSAdfsPIP","MSISHTTP")]
        [String]$Protocol = "WSFederation",

        [Parameter(Mandatory=$False)]
        [ValidateSet("NotSet","Intranet","Extranet")]
        [String]$NetworkLocationType = "Intranet",

        [Parameter(Mandatory=$False)]
        [ValidateSet("NotAFailure","UPError","LockoutError","ExpiredPassword","DisabledAccount","DeviceAuthError","UserCertAuthError","IssuanceAuthZError","MFAError","ExtranetLockoutError","LogoutError","CredentialValidationError","OtherCredentialError","IssuanceDelegationError","TokenAcceptanceError","ProtocolError","WsFedRequestFailure","InvalidRelyingPartyError","InvalidClientApplicationError","GenericError","OtherError")]
        [String]$AppTokenFailureType = "NotAFailure",
        
        [Parameter(Mandatory=$True)]
        [String]$IPAddress,

        [Parameter(Mandatory=$False)]
        [String]$ClaimsProvider,
        
        [Parameter(Mandatory=$False)]
        [String]$OAuthClientID,

        [Parameter(Mandatory=$False)]
        [String]$OAuthTokenRetrievalMethod,

        [Parameter(Mandatory=$False)]
        [String]$MFA,

        [Parameter(Mandatory=$False)]
        [String]$MFAProviderErrorCode,

        [Parameter(Mandatory=$False)]
        [String]$ProxyServer,

        [Parameter(Mandatory=$False)]
        [String]$Endpoint = "/adfs/ls/",

        [Parameter(Mandatory=$False)]
        [String]$UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",

        [Parameter(Mandatory=$False)]
        [String]$DeviceID,

        [Parameter(Mandatory=$False)]
        [int]$ErrorHitCount = 0,

        [Parameter(Mandatory=$False)]
        [String]$X509CertificateType,

        [Parameter(Mandatory=$False)]
        [String]$MFAAuthenticationType,

        [Parameter(Mandatory=$False)]
        [guid]$ActivityId = (New-Guid),

        [Parameter(Mandatory=$False)]
        [bool]$ActivityIdAutoGenerated = $False,

        [Parameter(Mandatory=$False)]
        [String]$PrimarySid,

        [Parameter(Mandatory=$False)]
        [String]$ImmutableId
    )
    Begin
    {
        $ADFSEventTypes = [ordered]@{
            "NotSet"          =   0
            "AppToken"        =   1  # Normal login
            "FreshCredential" =   2
            "System"          =   4
            "Discovery"       =   8
            "Signout"         =  16
            "PwdChange"       =  32
            "DeviceReg"       =  64
            "Resource"        = 128
            "Config"          = 256
            "ExtranetLockout" = 512
        }
        $ADFSAuthTypes = [ordered]@{
            "NotSet"      =  0
            "Forms"       =  1
            "Windows"     =  2
            "Certificate" =  4
            "Device"      =  8
            "Multifactor" = 16
            "Sso"         = 32
            "Federated"   = 64
        }
        $ADFSRequiredAuthType = [ordered]@{
            "NotSet"    = 0
            "Primary"   = 1
            "Secondary" = 2
    }
        $UserIdTypes = [ordered]@{
            "AnchorID"           =   10
            "UPN"                =   20
            "WindowsAccountName" =   30
            "PrimarySID"         =   40
            "NameID"             =   50
            "Email"              =   60
            "Name"               =   70
            "NotSet"             = 1000
        }
        $ADFSProtocolTypes = [ordered]@{
            "NotSet"       =  0
            "WSFederation" =  2
            "WSFedSamlP"   =  4
            "OAuth"        = 10
            "SAMLP"        = 20
            "WSTrust"      = 30
            "MSAdfsPIP"    = 40
            "MSISHTTP"     = 50
        }
        $NetworkLocationTypes = [ordered]@{
            "NotSet"   = 0
            "Intranet" = 1
            "Extranet" = 2
        }
        $ADFSFailureTypes = [ordered]@{
            "NotAFailure"                   =    0
            "UPError"                       =    1
            "LockoutError"                  =    2
            "ExpiredPassword"               =    3
            "DisabledAccount"               =    4
            "DeviceAuthError"               =    5
            "UserCertAuthError"             =   10
            "IssuanceAuthZError"            =   20
            "MFAError"                      =   21
            "ExtranetLockoutError"          =   30
            "LogoutError"                   =   40
            "CredentialValidationError"     =   50
            "OtherCredentialError"          =   70
            "IssuanceDelegationError"       =   80
            "TokenAcceptanceError"          =   90
            "ProtocolError"                 =  100
            "WsFedRequestFailure"           =  110
            "InvalidRelyingPartyError"      =  111
            "InvalidClientApplicationError" =  112
            "GenericError"                  =  500
            "OtherError"                    = 1000
        }
    }
    Process
    {
        # Combine authentication types
        if($PrimaryAuthentication.Count -eq 1)
        {
            $combinedAuthType = $ADFSAuthTypes[$PrimaryAuthentication]
        }
        else
        {
            $combinedAuthType = 0
            foreach($type in $PrimaryAuthentication)
            {
                $combinedAuthType = $combinedAuthType -bor $ADFSAuthTypes[$type]
            }
        }

        $event = [ordered]@{
            "UniqueID"                  = $UniqueID.ToString()
            "Server"                    = $Server
            "EventType"                 = $ADFSEventTypes[$EventType]
            "PrimaryAuthentication"     = $combinedAuthType
            "RequiredAuthType"          = $ADFSRequiredAuthType[$RequiredAuthType]
            "RelyingParty"              = $RelyingParty
            "RelyingPartyName"          = $RelyingPartyName
            "Result"                    = $Result
            "DeviceAuthentication"      = $DeviceAuthentication
            "URL"                       = $URL
            "User"                      = $User
            "UserId"                    = $UserId
            "UserIdType"                = $UserIdTypes[$UserIdType]
            "UPN"                       = $UPN
            "Timestamp"                 = $Timestamp.ToUniversalTime().ToString("o", [cultureinfo]::InvariantCulture)
            "Protocol"                  = $ADFSProtocolTypes[$Protocol]
            "NetworkLocation"           = $NetworkLocationTypes[$NetworkLocationType]
            "AppTokenFailureType"       = $ADFSFailureTypes[$AppTokenFailureType]
            "IPAddress"                 = $IPAddress
            "ClaimsProvider"            = $ClaimsProvider
            "OAuthClientID"             = $OAuthClientID
            "OAuthTokenRetrievalMethod" = $OAuthTokenRetrievalMethod
            "MFA"                       = $MFA
            "MFAProviderErrorCode"      = $MFAProviderErrorCode
            "ProxyServer"               = $ProxyServer
            "Endpoint"                  = $Endpoint
            "UserAgent"                 = $UserAgent
            "DeviceID"                  = $DeviceID
            "ErrorHitCount"             = $ErrorHitCount
            "X509CertificateType"       = $X509CertificateType
            "MFAAuthenticationType"     = $MFAAuthenticationType
            "ActivityId"                = $ActivityId.ToString()
            "ActivityIdAutoGenerated"   = $ActivityIdAutoGenerated
            "PrimarySid"                = $PrimarySid
            "ImmutableId"               = $ImmutableId
        }

        return $event
    }
}