Private/DuoPrivateFunctions.ps1
Function New-DuoRequest{ <# .SYNOPSIS Formats hashtables to payloads for Duo web requst .DESCRIPTION Creates request to send to Duo to preform requested function .PARAMETER Uri The child path to the api that follows the Duo API host name .PARAMETER Methods The method type of the request [GET], [POST], [DELETE] .PARAMETER Arguments The parameters that will be sent within the Duo request .OUTPUTS [PSCustomObject]DuoRequest .EXAMPLE New-DuoRequest -UriPath "/admin/v1/users" -Method Post -Arguments @{username,"username"} .LINK https://github.com/jyates2006/PSDuo https://jaredyatesit.com/Documentation/PSDuo .NOTES Version: 1.0 Author: Jared Yates Creation Date: 10/5/2022 Purpose/Change: Initial script development #> PARAM( [Parameter(Mandatory = $true)]$UriPath, [Parameter(Mandatory = $true)] $Method, [Parameter(Mandatory = $true)] $Arguments ) #Decrypt our keys from our config $skey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Script:DuoConfig.SecretKey)) $iKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Script:DuoConfig.IntergrationKey)) $apiHost = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Script:DuoConfig.apiHost)) $Date = (Get-Date).ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss -0000") $DuoParamsParamsString = ($Arguments.Keys | Sort-Object | ForEach-Object { $_ + "=" + [uri]::EscapeDataString($Arguments.$_) }) -join "&" $DuoParams = (@( $Date.Trim(), $method.ToUpper().Trim(), $apiHost.ToLower().Trim(), $Uri.Trim(), $DuoParamsParamsString.trim() ).trim() -join "`n").ToCharArray().ToByte([System.IFormatProvider]$UTF8) $Secret = [System.Security.Cryptography.HMACSHA1]::new($skey.ToCharArray().ToByte([System.IFormatProvider]$UTF8)) $Secret.ComputeHash($DuoParams) | Out-Null $Secret = [System.BitConverter]::ToString($Secret.Hash).Replace("-", "").ToLower() $AuthHeader = $ikey + ":" + $Secret [byte[]]$AuthHeader = [System.Text.Encoding]::ASCII.GetBytes($AuthHeader) $WebReqest = @{ URI = ('Https://{0}{1}' -f $apiHost, $UriPath) Headers = @{ "X-Duo-Date" = $Date "Authorization" = ('Basic: {0}' -f [System.Convert]::ToBase64String($AuthHeader)) } Body = $Arguments Method = $method ContentType = 'application/x-www-form-urlencoded' } $WebReqest } Function ConvertTo-UnixTime($Time){ <# .Synopsis Converts time to epox time format .DESCRIPTION Converts time to epox time format copatibale for unix systems .EXAMPLE ConvertTo-UnixTime .INPUTS .OUTPUTS [int]$Timespan .NOTES .COMPONENT .FUNCTIONALITY Time conversion #> $Epox = Get-Date -Date '01/01/1970' $Timespan = New-Timespan -Start $Epox -End $Time | Select-Object -ExpandProperty TotalSeconds Write-Output $Timespan } Function Get-DuoDirectoryKey{ Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true )] [String]$DirectoryName ) If($DirectoryName){ $Directories = $DirectoryName } Else{ $Directories = Get-DuoDirectoryNames } $DuoConfig = Get-DuoConfig ForEach($Directory in $Directories){ $Output = $DuoConfig.GetEnumerator() | Where-Object Name -EQ $DirectoryName $Output.Value } } Function Get-AllDuoGroups{ #Base Claim [String]$Method = "GET" [String]$Uri = "/admin/v1/groups" [Hashtable]$DuoParams = @{} $DuoParams.Add("limit","100") $DuoParams.Add("offset","0") #Duo has a 100 group limit in their api. Loop to return all groups $Offset=0 Do{ $DuoParams.Offset = $Offset $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output #Increment offset to return the next 100 groups $Offset += 100 } }Until($Output.Count -lt 100) } Function ConvertTo-EpochTimeStamp { PARAM( [datetime]$DateTime ) $EpochTimestamp = [Int][Double]::Parse((Get-Date -Date $DateTime -UFormat %s)) Return $EpochTimestamp } Function Get-Base64Image { PARAM( [String]$ImagePath ) $File = Get-Item -Path $ImagePath $Image = [System.Drawing.Image]::FromFile($ImagePath) $ImageBytes = [System.IO.File]::ReadAllBytes($ImagePath) $Base64String = [Convert]::ToBase64String($ImageBytes) $ImgObj = [PSCustomObject]@{ Name = $File.Name Width = $Image.Width Height = $Image.Height Size = $File.Length Base64String = $Base64String } Return $ImgObj } Function Test-DuoConnection{ <# .Synopsis Ping Duo Endpoints .DESCRIPTION The /ping endpoint acts as a "liveness check" that can be called to verify that Duo is up before trying to call other endpoints. Unlike the other endpoints, this one does not have to be signed with the Authorization header. .EXAMPLE Get-DuoUser .EXAMPLE Test-DuoConnection .INPUTS .OUTPUTS [PSCustomObject]DuoRequest .NOTES DUO API Method GET Path /auth/v2/ping PARAMETERS None RESPONSE CODES Response Meaning 200 Success. RESPONSE FORMAT Key Value time Current server time. Formatted as a epoch timestamp Int. .COMPONENT DUO Auth .FUNCTIONALITY Sends a webrequest to DUO, verifying the service is available. #> [CmdletBinding( )] PARAM() [String]$method = "GET" [String]$path = "/auth/v2/ping" $apiHost = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Script:DuoConfig.apiHost)) $DUORestRequest = @{ URI = ('Https://{0}{1}' -f $apiHost, $path) Method = $method ContentType = 'application/x-www-form-urlencoded' } $Response = Invoke-RestMethod @DUORestRequest If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "APiParams:"+($APiParams | Out-String) Write-Warning "Method:$method Path:$path" } #$Output = $Response | Select-Object -ExpandProperty Response #Write-Output $Output Write-Output "Successfully connected" Try{ $DuoUsers = Get-DuoUser } Catch{ Write-Warning "User Check: Failed" Write-Warning "Cannot pull user information" } Finally{ If($DuoUsers.Count -gt 1){ Write-Output "User Check: Passed" } } } Function Test-DuoUser{ <# .Synopsis Validates if a user exist in Duo .DESCRIPTION Test if user exist within Duo .EXAMPLE Test-DuoUser -Username TestUser .EXAMPLE Test-DuoUser -UserID ABCDEF12G34567HIJKLM .INPUTS .OUTPUTS [bool]$true/$false .NOTES .COMPONENT .FUNCTIONALITY Time conversion #> [CmdletBinding(DefaultParameterSetName="Uname")] Param( [Parameter( ParameterSetName="Uname", Mandatory=$true )] $Username, [Parameter( ParameterSetName="UID", Mandatory=$true )] $UserID ) If($Username){$UserID = (Get-DuoUser -Username $Username -ErrorAction Ignore).user_id} If([String]::IsNullOrEmpty($UserID)){$UserID="null"} Try{ Get-DuoUser -UserID $UserID | Out-Null Return $true } Catch{ Return $false } } Function Test-DuoGroup{ [CmdletBinding(DefaultParameterSetName="Gname")] Param( [Parameter( ParameterSetName="Gname", Mandatory=$true, ValueFromPipelin=$true, Position=0 )] $GroupName, [Parameter( ParameterSetName="GID", Mandatory=$true, ValueFromPipelin=$true, Position=0 )] $GroupID ) If($GroupName){ Try{ Get-DuoGroup -GroupName $GroupName | Out-Null Return $true } Catch{ Return $false } } ElseIf($GroupID){ Try{ Get-DuoGroup -GroupID $GroupID | Out-Null Return $true } Catch{ Return $false } } } Function Test-DuoPhone{ Param( [String]$Name, [String]$PhoneID, [String]$Number, [String]$Extension ) #Base claim [String]$Method = "GET" [String]$Uri = "/admin/v1/phones" [Hashtable]$DuoParams = @{} $DuoParams.Add("limit","500") $DuoParams.Add("offset","0") $Offset = 0 #Duo has a 300 user limit in their api. Loop to return all users $AllPhones = Do{ $DuoParams.Offset = $Offset $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output #Increment offset to return the next 300 users $Offset += 500 } }Until($Output.Count -lt 500) If($Name){ If(($AllPhones | Where-Object Name -EQ $Name)){ Return $true } Else{ Return $false } } ElseIf($PhoneID){ If(($AllPhones | Where-Object Phone_ID -EQ $PhoneID)){ Return $true } Else{ Return $false } } ElseIf($Number -and $Extension){ If(($AllPhones | Where-Object ($_.Number -EQ $Number -and $_.extension -eq $Extension))){ Return $true } Else{ Return $false } } ElseIf($Number){ If(($AllPhones | Where-Object Number -EQ $Number)){ Return $true } Else{ Return $false } } } Function Test-DuoBypassCode{ Param( [String]$BypassCodeID ) #Base claim [String]$Method = "GET" [String]$Uri = "/admin/v1/bypass_codes/$($BypassCodeID)" [Hashtable]$DuoParams = @{} Try{ $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output } Return $true } Catch{ Return $false } } Function Validate-PhoneNumber { Param ( [Parameter(Mandatory=$true)] [String]$PhoneNumber ) # Regular expression pattern for phone number in the format +18144554545 $Pattern = '^\+\d{11}$' If($PhoneNumber -match $pattern) { Return $true } Else { Return $false } } Function Test-DuoTokens{ Param( [String]$TokenID ) #Base claim [String]$Method = "GET" [String]$Uri = "/admin/v1/tokens/$($TokenID)" [Hashtable]$DuoParams = @{} Try{ $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output } Return $true } Catch{ Return $false } } Function Test-WebAuthnKey { PARAMS( [Parameter( Mandatory = $true, ValueFromPipeLine = $true, Position=0 )] [String]$WebAuthnKey ) #Base claim [String]$Method = "GET" [String]$Uri = "/admin/v1/webauthncredentials/$($WebAuthnKey)" [Hashtable]$DuoParams = @{} Try{ $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output } Return $true } Catch{ Return $false } } Function Test-DuoDesktop { PARAMS( [Parameter( Mandatory = $true, ValueFromPipeLine = $true, Position=0 )] [ValidateScript({ If(Test-DuoWEbAuthnKey -TokenID $_){$true} Else{Throw "Token: $($_) doesn't exist in Duo"} })] [String]$DesktopKey ) #Base claim [String]$Method = "GET" [String]$Uri = "/admin/v1/desktop_authenticators/$($DesktopKey.dakey)" [Hashtable]$DuoParams = @{} Try{ $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output } Return $true } Catch{ Return $false } } Function Test-DuoIntegrations { [CmdletBinding(DefaultParameterSetName="IKey")] PARAM( [Parameter(ParameterSetName="IKey", Mandatory = $true, ValueFromPipeLine = $true, Position=0 )] [String]$IntegrationKey ) #Base claim [String]$Method = "GET" [String]$Uri = "/admin/v1/integrations/$($IntegrationKey)" [Hashtable]$DuoParams = @{} Try{ $DuoParams.Offset = $Offset $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output } Return $true } Catch{ Return $false } } Function Test-EpochTimestamp { PARAM ( [Parameter(Mandatory=$true)] [string]$Timestamp ) # Check if the timestamp is a number If($Timestamp -match '^\d+$'){ # Convert the timestamp to an integer $TimeStampInt = [Int64]$Timestamp # Define the range for valid epoch timestamps $EpochStart = [DateTime]::New(1970, 1, 1, 0, 0, 0, [DateTimeKind]::UTC) $EpochEnd = [DateTime]::UTCnow # Convert the timestamp to a DateTime object $TimeStampDate = $EpochStart.AddSeconds($TimeStampInt) # Check if the timestamp falls within the valid range If($TimeStampDate -ge $EpochStart -and $TimeStampDate -le $EpochEnd){ Return $true } } Return $false } Function Test-DuoAdmin { PARAM( [Parameter( Mandatory = $true, ValueFromPipeLine = $true, Position=0 )] [String]$AdminID ) #Base claim [String]$Method = "GET" [String]$Uri = "/admin/v1/admins/$($AdminID)" [Hashtable]$DuoParams = @{} Try{ $Request = New-DuoRequest -UriPath $Uri -Method $Method -Arguments $DuoParams $Response = Invoke-RestMethod @Request If($Response.stat -ne 'OK'){ Write-Warning 'DUO REST Call Failed' Write-Warning "Arguments:"+($DuoParams | Out-String) Write-Warning "Method:$Method Path:$Uri" } Else{ $Output = $Response | Select-Object -ExpandProperty Response $Output } Return $true } Catch{ Return $false } } |