IvantiPS.psm1
### --- PUBLIC FUNCTIONS --- ### #Region - Connect-IvantiTenant.ps1 function Connect-IvantiTenant { <# .SYNOPSIS Connect to an Ivanti Service Manager tenant .DESCRIPTION Connect to an Ivanti Service Manager tenant .PARAMETER Credential Credential object to use to authenticate with the ISM tenant .PARAMETER SessionID Existing session value to use instead of credentials .EXAMPLE Connect-IvantiTenant -Credential (Get-Credential) .NOTES https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Session-ID-Log-In.htm #> [CmdletBinding()] #[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] param( [Parameter(ParameterSetName="Credential")] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [Parameter(ParameterSetName="Session")] [string]$SessionID ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function started" $config = Get-IvantiPSConfig -ErrorAction Stop $tenant = $config.IvantiTenantID $LoginURL = "https://$($tenant)/api/rest/authentication/login" } process { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] ParameterSetName: $($PsCmdlet.ParameterSetName)" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] PSBoundParameters: $($PSBoundParameters | Out-String)" if ($SessionID) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Existing SessionID passed in, using it" # If existing session id was passed in, use it # $result = $SessionID } elseif ($Credential) { # Create payload for call to login endpoint # $Payload = @{ tenant = $tenant username = $Credential.username password = $Credential.GetNetworkCredential().password role = $config.DefaultRole } try { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Invoking RestMethod on $LoginURL" $result = Invoke-RestMethod -Uri $LoginURL -Body $Payload -Method POST } catch { Write-Warning "[$($MyInvocation.MyCommand.Name)] Problem calling $LoginURL" $_ } } else { Write-Warning "[$($MyInvocation.MyCommand.Name)] No Credentials or SessionID passed in. Exiting..." break } # The resulting session value from a valid call to the login url will be # saved in the module private data # if ($MyInvocation.MyCommand.Module.PrivateData) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Adding session result to existing module PrivateData" $MyInvocation.MyCommand.Module.PrivateData.Session = $result } else { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Creating module PrivateData" $MyInvocation.MyCommand.Module.PrivateData = @{ 'Session' = $result } } Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] SessionID: $result" } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Complete" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Complete" } } Export-ModuleMember -Function Connect-IvantiTenant #EndRegion - Connect-IvantiTenant.ps1 #Region - Get-IvantiAgency.ps1 function Get-IvantiAgency { <# .SYNOPSIS Get Agency business objects from Ivanti. Defaults to all. .DESCRIPTION Get Agency business objects from Ivanti. Defaults to all. .PARAMETER RecID Ivanti Record ID for a specific Agency business object .PARAMETER Agency Full agency name to filter on .PARAMETER AgencyShortName Agency short name to filter on. Usually an abbreviation .EXAMPLE Get-IvantiAgency -AgencyShortName ACM .EXAMPLE Get-IvantiAgency -Agency 'Advanced Computer Machines' .EXAMPLE Get-IvantiAgency -RecID DC218F83EC504222B148EF1344E15BCB .NOTES https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Get-Business-Object-by-Filter.htm #> [CmdletBinding()] param( [string]$RecID, [string]$Agency, [string]$AgencyShortName ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Function Started. PSBoundParameters: $($PSBoundParameters | Out-String)" # If one or more parameters are passed in, use only one of them # Order of preference is RecID, Agency, then AgencyShortName # if no parameters are passed in, then do not set any get parameters # if ($RecID) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] RecID [$RecID] passed in, setting filter" $GetParameter = @{'$filter' = "RecID eq '$($RecID)'"} } elseif ($Agency) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Agency [$Agency] passed in, setting filter" $GetParameter = @{'$filter' = "Agency eq '$($Agency)'"} } elseif ($AgencyShortName) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] AgencyShortName [$AgencyShortName] passed in, setting filter" $GetParameter = @{'$filter' = "AgencyShortName eq '$($AgencyShortName)'"} } else { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] No parameters passed in" } # Build the URL. It will look something like the below for agency business objects # Note the 's' at the end # https://tenant.ivanticloud.com/api/odata/businessobject/agencys # $IvantiTenantID = (Get-IvantiPSConfig).IvantiTenantID $uri = "https://$IvantiTenantID/api/odata/businessobject/agencys" } # end begin process { Invoke-IvantiMethod -URI $uri -GetParameter $GetParameter } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function ended" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function ended" } } # end function Export-ModuleMember -Function Get-IvantiAgency #EndRegion - Get-IvantiAgency.ps1 #Region - Get-IvantiBusinessObject.ps1 function Get-IvantiBusinessObject { <# .SYNOPSIS Get business objects from Ivanti. Defaults to all. .DESCRIPTION Get business objects from Ivanti. Defaults to all. .PARAMETER BusinessObject Ivanti Business Object to return .EXAMPLE Get-IvantiBusinessObject -BusinessObject agency .EXAMPLE Get-IvantiBusinessObject -BusinessObject change #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$BusinessObject, [string]$RecID ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Function Started. PSBoundParameters: $($PSBoundParameters | Out-String)" # Build the URL. It will look something like the below for agency business objects # Note the 's' at the end # https://tenant.ivanticloud.com/api/odata/businessobject/agencys # $IvantiTenantID = (Get-IvantiPSConfig).IvantiTenantID $uri = "https://{0}/api/odata/businessobject/{1}s" -f $IvantiTenantID,$BusinessObject if ($RecID) { $uri = "{0}('{1}')" -f $uri,$RecID } } # end begin process { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] $uri" Invoke-IvantiMethod -URI $uri } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function ended" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function ended" } } # end function Export-ModuleMember -Function Get-IvantiBusinessObject #EndRegion - Get-IvantiBusinessObject.ps1 #Region - Get-IvantiBusinessObjectMetadata.ps1 function Get-IvantiBusinessObjectMetadata { <# .SYNOPSIS Get the metadata for an Ivanti Business Object .DESCRIPTION Get the metadata for an Ivanti Business Object .PARAMETER BusinessObject A business object to get meta data for. Example value: agency .PARAMETER MetaDatatype The type of meta data to return. May be Fields, Relationships, Actions, or SavedSearch .NOTES https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Metadata.htm https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Saved-Search-API.htm https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Quick-Actions-API.htm #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='Yes, Metadata is plural. technically. but really?!?!?')] param( [Parameter(Mandatory)] [string]$BusinessObject, [Parameter(Mandatory)] [ValidateSet('Fields','Relationships','Actions','SavedSearch')] [string]$MetaDataType ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function started PSBoundParameters: $($PSBoundParameters | Out-String)" $tenant = (Get-IvantiPSConfig).IvantiTenantID $session = Get-IvantiSession if (-not $session) { Write-Warning "[$($MyInvocation.MyCommand.Name)] No Ivanti session available. Exiting..." break } $headers = @{Authorization = $Session} # like this https://tenant.ivanticloud.com/api/odata/agencys/$metadata # $uri = "https://{0}/api/odata/{1}s/`$metadata" -f $tenant, $BusinessObject $splatParameters = @{ Uri = $Uri Method = 'GET' Headers = $headers ErrorAction = "Stop" } try { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Invoke-RestMethod with `$splatParameters: $($splatParameters | Out-String)" # Invoke rest method # $Response = Invoke-RestMethod @splatParameters } catch { Write-Warning "[$($MyInvocation.MyCommand.Name)] Failed to get answer" Write-Warning "[$($MyInvocation.MyCommand.Name)] URI: $($splatParameters.Uri)" $_ } Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Executed RestMethod" } # End begin process { if ($Response) { switch ($MetaDataType) { 'Fields' { $Response.Edmx.DataServices.Schema.EntityType | Where-Object {$_.Name -eq $BusinessObject} | Select-Object -ExpandProperty Property } 'Relationships' { $Response.Edmx.DataServices.Schema.EntityType | Where-Object {$_.Name -eq $BusinessObject} | Select-Object -ExpandProperty NavigationProperty | Select-Object Name,Type } 'Actions' { $Response.Edmx.DataServices.Schema.Action | Select-Object Name,@{ Name='ActionID'; Expression={ $null = $_.InnerXML -match 'String="([\w|-]*)" />'; $matches[1] } } } 'SavedSearch' { # More information on Saved Search # https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Saved-Search-API.htm $Response.Edmx.DataServices.Schema.Function | Select-Object Name,@{ Name='ActionID'; Expression={ $null = $_.InnerXML -match 'String="([\w|-]*)" />'; $matches[1] } } } Default { $Response } } # end switch } else { Write-Verbose "[$($MyInvocation.MyCommand.Name)] No results were returned" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] No results were returned" } } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function ended" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function ended" } } Export-ModuleMember -Function Get-IvantiBusinessObjectMetadata #EndRegion - Get-IvantiBusinessObjectMetadata.ps1 #Region - Get-IvantiCI.ps1 function Get-IvantiCI { <# .SYNOPSIS Get CI (assets) business objects from Ivanti. Defaults to all. .DESCRIPTION Get CI (assets) business objects from Ivanti. Defaults to all. .PARAMETER RecID Ivanti Record ID for a specific CI business object .PARAMETER Name CI (Asset) Name to filter on .PARAMETER IPAddress IP Address to filter on .EXAMPLE Get-IvantiCI -Name wpdotsqll42 .NOTES https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Get-Business-Object-by-Filter.htm #> [CmdletBinding()] [Alias('Get-IvantiAsset')] param( [string]$RecID, [string]$Name, [string]$IPAddress ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Function Started. PSBoundParameters: $($PSBoundParameters | Out-String)" # If one or more parameters are passed in, use only one of them # Order of preference is RecID, Agency, then AgencyShortName # if no parameters are passed in, then do not set any get parameters # if ($RecID) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] RecID [$RecID] passed in, setting filter" $GetParameter = @{'$filter' = "RecID eq '$($RecID)'"} } elseif ($Name) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Agency [$Agency] passed in, setting filter" $GetParameter = @{'$filter' = "Name eq '$($Name)'"} } elseif ($IPAddress) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] AgencyShortName [$AgencyShortName] passed in, setting filter" $GetParameter = @{'$filter' = "IPAddress eq '$($IPAddress)'"} } else { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] No parameters passed in" } # Select only specific fields because there are way too many # $GetParameter += @{'$select' = 'RecId, Name, IPAddress, Category, CIType, ivnt_AssetFullType, Status, AssignedDescription, ivnt_SelfServiceDescription, AssignedOS, ivnt_AssignedManufacturer, Model, SerialNumber, AssetTag, OSSWPatchMgtMethod, ScheduleRebootInterval, ivnt_Location, BuildingAndFloor, EquipmentLocation, LocationRegion, BillableCpu, BillableMemory'} # Build the URL. It will look something like the below for ci business objects # Note the 's' at the end # https://tenant.ivanticloud.com/api/odata/businessobject/cis # $IvantiTenantID = (Get-IvantiPSConfig).IvantiTenantID $uri = "https://$IvantiTenantID/api/odata/businessobject/cis" } # end begin process { Invoke-IvantiMethod -URI $uri -GetParameter $GetParameter } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function ended" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function ended" } } # end function Export-ModuleMember -Function Get-IvantiCI -Alias Get-IvantiAsset #EndRegion - Get-IvantiCI.ps1 #Region - Get-IvantiEmployee.ps1 function Get-IvantiEmployee { <# .SYNOPSIS Get Employee business objects from Ivanti. Defaults to all. .DESCRIPTION Get Employee business objects from Ivanti. Defaults to all. .PARAMETER RecID Ivanti Record ID for a specific Employee business object .PARAMETER Name Employee name, will filter against DisplayName property .PARAMETER Email Employee email to filter on. .PARAMETER AllFields Set this parameter if returning all fields is desired .EXAMPLE Get-IvantiEmployee -Email john.smith@domain.name .EXAMPLE Get-IvantiEmployee -Name 'John Smith' .EXAMPLE Get-IvantiEmployee -RecID DC218F83EC504222B148EF1344E15BCB -AllFields .NOTES https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Get-Business-Object-by-Filter.htm #> [CmdletBinding()] param( [string]$RecID, [string]$Name, [string]$Email, [switch]$AllFields ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Function Started. PSBoundParameters: $($PSBoundParameters | Out-String)" # If one or more parameters are passed in, use only one of them # Order of preference is RecID, Name, then Email # if no parameters are passed in, then do not set any get parameters # if ($RecID) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] RecID [$RecID] passed in, setting filter" $GetParameter = @{'$filter' = "RecID eq '$($RecID)'"} } elseif ($Name) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Name [$Name] passed in, setting filter" $GetParameter = @{'$filter' = "DisplayName eq '$($Name)'"} } elseif ($Email) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Email [$Email] passed in, setting filter" $GetParameter = @{'$filter' = "PrimaryEmail eq '$($Email)'"} } else { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] No parameters passed in" } if (-not $AllFields) { # Select only specific fields because there are way too many # $GetParameter += @{'$select' = 'RecId, DisplayName, OREmployeeID, PrimaryEmail, ManagerEmail, Agency, IsManager, IsLDAPUserAccountEnabled, LockDate, LockType, LoginAttemptCount, Disabled, LastModBy, LastModDateTime, CreatedDateTime'} } # Build the URL. It will look something like the below for Employee business objects # Note the 's' at the end # https://tenant.ivanticloud.com/api/odata/businessobject/employees # $IvantiTenantID = (Get-IvantiPSConfig).IvantiTenantID $uri = "https://$IvantiTenantID/api/odata/businessobject/employees" } # end begin process { Invoke-IvantiMethod -URI $uri -GetParameter $GetParameter } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function ended" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function ended" } } # end function Export-ModuleMember -Function Get-IvantiEmployee #EndRegion - Get-IvantiEmployee.ps1 #Region - Get-IvantiPSConfig.ps1 function Get-IvantiPSConfig { <# .SYNOPSIS Get default configurations for IvantiPS from config.json file .DESCRIPTION Get default configurations for IvantiPS from config.json file .EXAMPLE Get-IvantiPSConfig #> [CmdletBinding()] [OutputType([PSCustomObject])] Param () begin { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function started" $config = "$([Environment]::GetFolderPath('ApplicationData'))\IvantiPS\config.json" } process { if ($config) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Getting config from [$config]" [PSCustomObject](Get-Content -Path "$config" -ErrorAction Stop | ConvertFrom-Json) } else { Write-Warning "[$($MyInvocation.MyCommand.Name)] No config found at [$config]" Write-Warning "[$($MyInvocation.MyCommand.Name)] Use Set-IvantiPSConfig first!" break } } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function complete" } } # end function Export-ModuleMember -Function Get-IvantiPSConfig #EndRegion - Get-IvantiPSConfig.ps1 #Region - Get-IvantiSession.ps1 function Get-IvantiSession { <# .SYNOPSIS Get the session id from module's privatedata .DESCRIPTION Get the session id from module's privatedata .EXAMPLE Get-IvantiSession #> [CmdletBinding()] param() begin { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function started" } process { if ($MyInvocation.MyCommand.Module.PrivateData.Session) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Using Session saved in PrivateData" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Using Session saved in PrivateData" Write-Output $MyInvocation.MyCommand.Module.PrivateData.Session } else { Write-Warning "[$($MyInvocation.MyCommand.Name)] No session found in PrivateData. Use Connect-IvantiTenant first!" } } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function complete" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Function complete" } } Export-ModuleMember -Function Get-IvantiSession #EndRegion - Get-IvantiSession.ps1 #Region - Set-IvantiPSConfig.ps1 function Set-IvantiPSConfig { <# .SYNOPSIS Set the URL, default Role, and Auth Type to use when connecting to Ivanti Service Manager .DESCRIPTION Set the URL, default Role, and Auth Type to use when connecting to Ivanti Service Manager. Saves the information to IvantiPS/config.json file in user profile .PARAMETER IvantiTenantID Ivanti Tenant ID for IvantiCloud tenant. example: tenantname.ivanticloud.com .PARAMETER DefaultRole Default role to use to connect. Example values: Admin, SelfService, SelfServiceViewer .PARAMETER AuthType Type of authentication to use to access ISM. SessionID, APIKey, or OIDC .EXAMPLE Set-IvantiPSConfig -IvantiTenantID tenantname.ivanticloud.com -DefaultRole SelfService -AuthType SessionID .NOTES https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Authentication_of_APIs.htm #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='This function is trivial enough that we do not need ShouldProcess')] Param ( [parameter(Mandatory)] [ValidateNotNullOrEmpty()] [Uri]$IvantiTenantID, [parameter(Mandatory)] [string]$DefaultRole, [parameter(Mandatory)] [string]$AuthType ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started" #region configuration $configPath = "$([Environment]::GetFolderPath('ApplicationData'))\IvantiPS\config.json" Write-Verbose "Configuration will be stored in $($configPath)" #endregion configuration if (-not (Test-Path $configPath)) { # If the config file doesn't exist, created it $null = New-Item -Path $configPath -ItemType File -Force } } process { $config = [ordered]@{ IvantiTenantID = $IvantiTenantID DefaultRole = $DefaultRole AuthType = $AuthType } $config | ConvertTo-Json | Set-Content -Path "$configPath" } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Complete" } } # end function Export-ModuleMember -Function Set-IvantiPSConfig #EndRegion - Set-IvantiPSConfig.ps1 ### --- PRIVATE FUNCTIONS --- ### #Region - ConvertTo-GetParameter.ps1 function ConvertTo-GetParameter { <# .SYNOPSIS Generate the GET parameter string for an URL from a hashtable #> [CmdletBinding()] param ( [Parameter( Position = 0, Mandatory = $true, ValueFromPipeline = $true )] [hashtable]$InputObject ) process { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Making HTTP get parameter string out of a hashtable" Write-Verbose ($InputObject | Out-String) [string]$parameters = "?" foreach ($key in $InputObject.Keys) { $value = $InputObject[$key] $parameters += "$key=$($value)&" } $parameters -replace ".$" } } #EndRegion - ConvertTo-GetParameter.ps1 #Region - ConvertTo-ParameterHash.ps1 function ConvertTo-ParameterHash { [CmdletBinding( DefaultParameterSetName = 'ByString' )] param ( # URI from which to use the query [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByUri' )] [Uri]$Uri, # Query string [Parameter( Position = 0, Mandatory, ParameterSetName = 'ByString' )] [String]$Query ) process { $GetParameter = @{} if ($Uri) { $Query = $Uri.Query } if ($Query -match "^\?.+") { $Query.TrimStart("?").Split("&") | ForEach-Object { $key, $value = $_.Split("=") $GetParameter.Add($key, $value) } } Write-Output $GetParameter } } #EndRegion - ConvertTo-ParameterHash.ps1 #Region - Invoke-IvantiMethod.ps1 function Invoke-IvantiMethod { <# .SYNOPSIS Call the Ivanti Service Manager (ISM) end point. This is used by other functions, and is not meant to be called directly. .DESCRIPTION Call the Ivanti Service Manager (ISM) end point. This is used by other functions, and is not meant to be called directly. .PARAMETER URI URI for the ISM end point .PARAMETER Method Method to use. Must be a valid Web Request Method. Defaults to GET .PARAMETER Headers Headers to include with the request .PARAMETER GetParameter Get parameters to append to the URI. .PARAMETER Level Indicates level of recursion .NOTES https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Authentication_of_APIs.htm #> [CmdletBinding()] param( [Parameter(Mandatory)] [Uri]$URI, [Microsoft.PowerShell.Commands.WebRequestMethod]$Method = "GET", [Hashtable]$Headers, [Hashtable]$GetParameter = @{}, [int]$Level = 1 ) begin { Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] Function started" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Function started. PSBoundParameters: $($PSBoundParameters | Out-String)" #region Headers # Construct the Headers with the following priority: # - Headers passes as parameters # - Module's default Headers # $session = Get-IvantiSession if (-not $session) { Write-Warning "[$($MyInvocation.MyCommand.Name) $Level] Must first establish session with Connect-IvantiTenant. Exiting..." break } $AuthHeader = @{Authorization = $Session} # If headers hash was passed in, join with auth header # if ($Headers) { $_headers = Join-Hashtable -Hashtable $Headers, $AuthHeader } else { $_headers = $AuthHeader } #endregion Headers #region Manage URI # Amend query from URI with GetParameter $uriQuery = ConvertTo-ParameterHash -Uri $Uri $internalGetParameter = Join-Hashtable $uriQuery, $GetParameter # Use default 100 for top records, unless top parm is used # if (-not $internalGetParameter.ContainsKey('$top')) { $internalGetParameter['$top'] = 100 } # remove URL from from URI # [Uri]$Uri = $Uri.GetLeftPart("Path") Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Left portion of URI: [$uri]" $PaginatedUri = $Uri [Uri]$PaginatedUri = "{0}{1}" -f $PaginatedUri, (ConvertTo-GetParameter $internalGetParameter) #endregion Manage URI #region Construct IRM Parameter $splatParameters = @{ Uri = $PaginatedUri Method = $Method Headers = $_headers ErrorAction = "Stop" Verbose = $false } #endregion Constructe IRM Parameter #region Execute the actual query try { Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] $($splatParameters.Method) $($splatParameters.Uri)" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Invoke-RestMethod with `$splatParameters: $($splatParameters | Out-String)" # Invoke the API # $RestResponse = Invoke-RestMethod @splatParameters } catch { Write-Warning "[$($MyInvocation.MyCommand.Name) $Level] Failed to get answer" Write-Warning "[$($MyInvocation.MyCommand.Name) $Level] URI: $($splatParameters.Uri)" $_ } Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Executed RestMethod" # Test to see if there was an error code in the # response from invoke-restmethod # Test-ServerResponse -InputObject $Response #endregion Execute the actual query } # End begin process { if ($RestResponse) { # Value should have the data. If not, then just dump whatever was returned to pipeline # if (-not $RestResponse.Value) { $RestResponse } else { $result = $RestResponse.Value Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] (`$response).Count: $(($result).Count)" # The @odata.count property has the total number records # $ODataCount = $RestResponse."@odata.count" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] ODataCount: $ODataCount" if ($GetParameter['$top']) { $top = $GetParameter['$top'] } if ($GetParameter['$skip']) { $skip = $GetParameter['$skip'] } $TopPlusSkip = $top + $skip Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] TopPlusSkip: $TopPlusSkip" if ($TopPlusSkip -lt $ODataCount) { $GetMore = $true if (-not $top) { $top = 100 } } else { $GetMore = $false } Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] `$GetMore: $GetMore" #region paging if ($GetMore -eq $true) { # Remove Parameters that don't need propagation $null = $PSBoundParameters.Remove('$top') $null = $PSBoundParameters.Remove('$skip') if (-not $PSBoundParameters["GetParameter"]) { $PSBoundParameters["GetParameter"] = $internalGetParameter } $total = 0 do { $total += $result.Count Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Invoking pagination, [`$Total: $Total]" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Output results [Level: $Level] [Results count: $(($result|Measure-Object).Count)]" # Output results from this loop $result if ($Total -ge $ODataCount) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Stopping paging, as [`$Total: $Total] reached [`$ODataAcount: $ODataCount]" break } else { Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Continuing paging, as [`$Total: $Total] has not reached [`$ODataAcount: $ODataCount]" } # calculate the size of the next page $PSBoundParameters["GetParameter"]['$skip'] = $Total + $skip $expectedTotal = $PSBoundParameters["GetParameter"]['$skip'] + $top if ($expectedTotal -gt $ODataCount) { $reduceBy = $expectedTotal - $ODataCount $PSBoundParameters["GetParameter"]['$top'] = $top - $reduceBy } Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] URI: $($PSBoundParameters["Uri"])" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] GetParameter top: $($PSBoundParameters["GetParameter"]['$top'])" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] GetParameter skip: $($PSBoundParameters["GetParameter"]['$skip'])" # increment the recursion level for debugging $PSBoundParameters["Level"] = $Level + 1 # Inquire the next page $result = Invoke-IvantiMethod @PSBoundParameters } while ($result.Count -gt 0) #endregion paging } else { Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Final output results [$(($result | Measure-Object).Count)]" $result } } } else { Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] No Web result object was returned" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] `$RestResponse was empty" } } end { #Set-TlsLevel -Revert Write-Verbose "[$($MyInvocation.MyCommand.Name) $Level] Function ended" Write-DebugMessage "[$($MyInvocation.MyCommand.Name) $Level] Function ended" } } #EndRegion - Invoke-IvantiMethod.ps1 #Region - Join-Hashtable.ps1 function Join-Hashtable { <# .SYNOPSIS Combines multiple hashtables into a single table. .DESCRIPTION Combines multiple hashtables into a single table. On multiple identic keys, the last wins. .EXAMPLE PS C:\> Join-Hashtable -Hashtable $Hash1, $Hash2 Merges the hashtables contained in $Hash1 and $Hash2 into a single hashtable. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] Param ( # The tables to merge. [Parameter( Mandatory, ValueFromPipeline )] [AllowNull()] [System.Collections.IDictionary[]] $Hashtable ) begin { $table = @{ } } process { foreach ($item in $Hashtable) { foreach ($key in $item.Keys) { $table[$key] = $item[$key] } } } end { $table } } #EndRegion - Join-Hashtable.ps1 #Region - Test-ServerResponse.ps1 function Test-ServerResponse { [CmdletBinding()] <# .SYNOPSIS Evaluate the response of the API call .LINK https://help.ivanti.com/ht/help/en_US/ISM/2020/admin/Content/Configure/API/Session-ID-Log-In.htm .NOTES Thanks to Lipkau: https://github.com/AtlassianPS/JiraPS/blob/master/JiraPS/Private/Test-ServerResponse.ps1 #> param ( # Response of Invoke-WebRequest [Parameter( ValueFromPipeline )] [PSObject]$InputObject ) begin { } process { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Checking response for error" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Checking response for error" if ($InputObject.Code) { Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Error code found, throwing error" throw ("{0}: {1}: {2}" -f $InputObject.Code,$InputObject.description,($InputObject.message -join ',')) } } end { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Done checking response for error" Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] Done checking response for error" } } #EndRegion - Test-ServerResponse.ps1 #Region - Write-DebugMessage.ps1 function Write-DebugMessage { <# .SYNOPSIS Utility to write out debug message .NOTES Thanks Atlassian! #> [CmdletBinding()] param( [Parameter( ValueFromPipeline )] [String]$Message ) begin { $oldDebugPreference = $DebugPreference if (-not ($DebugPreference -eq "SilentlyContinue")) { $DebugPreference = 'Continue' } } process { Write-Debug $Message } end { $DebugPreference = $oldDebugPreference } } #EndRegion - Write-DebugMessage.ps1 # SIG # Begin signature block # MIIFjQYJKoZIhvcNAQcCoIIFfjCCBXoCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUSm+oGlGbBdA7LbhdkgQN+/Fj # AhugggMnMIIDIzCCAgugAwIBAgIQfxlXoOWZRbhMi6xrw92ZDzANBgkqhkiG9w0B # AQsFADAbMRkwFwYDVQQDDBBzZWxmLnNpZ25lZC5jZXJ0MB4XDTIyMDQzMDAyNDYw # MFoXDTIzMDQzMDAzMDYwMFowGzEZMBcGA1UEAwwQc2VsZi5zaWduZWQuY2VydDCC # ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALUk2x84obgu2AqpvrBQ47LK # T01imCRSOYU3wIVEyd0H0WE/gZkc/Aji68mojxlKcdLKGRNiEFDsXbWX2fWM6KC2 # PVS/txe1fgCpz5eeq9CyHqTuUz8m3XDRMtAX91R8xyiLQSFWgrfDPJWRBWHv3sNv # ZD4c00hle+YHLhuw76oc2z22ikMhCND8GfVlSWxoIiI2hcNN5oCkqiNjxYs3fWD5 # sUcYTDWj62AL00Zml+FI6CvRO2XSdKVRMvAqN2vskwwO6ayMASYrEcJ4WTcQj1dp # WjCZdgqMrBGVxvc6wg6YWVDzlHRCg4AxVZjt12LGkqYwkjROHN2wzE/4/0svU00C # AwEAAaNjMGEwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsG # A1UdEQQUMBKCEHNlbGYuc2lnbmVkLmNlcnQwHQYDVR0OBBYEFMYfeufU5fggCTcR # +V0v1lQC6bOkMA0GCSqGSIb3DQEBCwUAA4IBAQAsI+0UFKIeVPZ36nodt9JqhyQK # yC1SsuvIwGew0QsdeDaM2mLwbZfmKumAPq27tOIhozaydme4vnbKyDv4MShR6MLk # UwquAJ70k+mMrivol7SL+I208iO0itt5Kxxd+cNKl2G1tRIfGvE9qoZ02WIjvNQH # BazJHjldlW/QXkoy0Ip/02mR3KvnGGRiipU8DjLi/lUbAUJAVO3zZ99iiXg2w5Be # 2gOt7n7Csx10Fe2KJfKrbXVYShcim6wMbrHtvYBrtagFaAT2RXzZyQfCoGYtP2Vw # w8fHDmJ8yFy3fRCHL53A7FsJ499gJDbj0afH0AE7uDShNoc2F8WoHaLLe7UfMYIB # 0DCCAcwCAQEwLzAbMRkwFwYDVQQDDBBzZWxmLnNpZ25lZC5jZXJ0AhB/GVeg5ZlF # uEyLrGvD3ZkPMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAA # MBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgor # BgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBQUXpiFOSyUwYD0uRs8gQGZhZadvjAN # BgkqhkiG9w0BAQEFAASCAQATgJTUQjSntBcJQbDqQpL1Yxp/El806SI/M7wN4BVM # uL8TKSPZpBUIHdzD0irCozE0wAinP4eGHnHUjS3CQxiEjPfVDMGKNZJvxumu6pXm # sHYC8UsIvhDTegFlt/EJkygFFdQDVBwF4K6CcANwSYPvyqO5ThOo2cklgx4GU3yc # sI7G+heOadviq3e4D225ZRNowmuX8Oz+ZS8UHZq4PB6xHXaccc1Iv23c2VxRnXEQ # Ao5+gZN5W1Wfyii9eGIeqrd+rXJbRGOaEIZycjAr7pC9lmSvLcintD8PRQORT2LJ # zvKgK+50uYPoH55retBDW64qwxiD1nEhYrWRkbeLVtte # SIG # End signature block |