ITGlue integration

Adding a request on behalf of a community member for ITGlue integration.
They would like to see Immy being able to create configurations in ITGlue from Immy.

Some example code that may assist for this:

$TestResults = [Ordered]@{}
$verbosepreference = 'continue'
$ITGlueAPIEndpoint = 'https://api.itglue.com'
$ITGlueAPIKey = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # password access
#$ITGConfigID = 'XXXXXXXXX'

Import-ImmyITGlueModule
Add-ITGlueBaseURI -base_uri $ITGlueAPIEndpoint
Add-ITGlueAPIKey -Api_Key $ITGlueAPIKey

#Get-ITGlueUsers | %{$_.data}
$PrimaryITGlueOrg = Get-ITGlueOrganizations| %{$_.data} | ?{$_.Attributes.Primary} #| ?{$_.Attributes."organization-type-name" -eq 'Owner'}
#$PrimaryITGlueOrg.Attributes

Function Get_Existing_Config_ID
{  
    $computer = Get-ImmyComputer -InventoryKeys WindowsSystemInfo
    $SerialNumber = $Computer | %{$_.Inventory.WindowsSystemInfo.SerialNumber} 
    if(!$SerialNumber)
    {
        Write-Error "Unable to determine SerialNumber for computer" -ErrorAction Stop
    }
    Write-Verbose "SerialNumber: $($SerialNumber | fl * | Out-String)"
    $ComputerConfiguration = Get-ITGlueConfigurations -filter_serial_number $SerialNumber | Select-Object -Expand Data | Select-Object -Last 1
    Write-Verbose ($ComputerConfiguration | ConvertTo-Json -depth 5)
    Return ($ComputerConfiguration).ID    
}

Function Get-Config-Type-ID
{if($computer.OperatingSystemName -match "Windows.*Server") {

  # Set to server type
  $config_type_id = 11332 

}
else {
 
  # Set to workstation type
  $config_type_id = 11333

} 
return $config_type_id
}

Function Get-Org-ID
{
    $params = @{
    filter_name = $ImmyTenant
    }

    Get-ITGlueOrganizations @params | ForEach-Object { 
    $_.data.id
}
}

Function New_or_Update_Config
{
$ConfigData = @{} 

# Add required attributes
$ConfigData['type'] = 'configurations'
$ConfigData['attributes'] = @{}
$ConfigData['attributes']['name'] = (Get-ImmyComputer).Name 
$ConfigData['attributes']['configuration-type-id'] = Get-Config-Type-ID # Managed Workstation = 11333 and Manage Server = 11332

# Add additional attributes
#$ConfigData['attributes']['operating-system-id'] = 225 # Windows 11
#$ConfigData['attributes']['manufacturer-id'] = 12345 # Dell
#$ConfigData['attributes']['model-id'] = 67890 # OptiPlex 7090
$ConfigData['attributes']['serial-number'] = $SerialNumber
#$ConfigData['attributes']['location-id'] = 3485940 # Main
#$ConfigData['attributes']['contact-id'] = 8823386
$ConfigData['attributes']['organization-id'] = Get-Org-ID

if($NewConfig)
{New-ITGlueConfigurations -data $ConfigData}
else
{Set-ITGlueConfigurations -data $ConfigData}
}

$computer = Get-ImmyComputer -InventoryKeys WindowsSystemInfo
$ImmyTenant = ($Computer).TenantName

Adding device configurations would be sweet. I bet the reverse could be useful, too (ie. importing software licenses)

3 Likes

I could see this being helpful in a number of ways. Yes!

Was just thinking that, in addition to device configurations, if a task could further set BitLocker keys as embedded passwords on those configurations…

Chefs Kiss Meme - Music Used

1 Like

@Dakota_Lewis we have a maintenance task that backs up Bitlocker keys to the matched Configuration in IT Glue. Glad to share.

That would honestly be amazing! Does it (or can it) also create the configuration if one doesn’t exist?

Currently it doesn’t but it would be pretty easy to adapt. We’re relying on Manage/Automate to create the Configurations since those ITG integrations don’t match on manually created ones and then we’d have duplicates.

Fair enough, we use N-Central for configurations, but it doesn’t always seem to work. Regardless, even having something like that for available configurations would be great!

@Stephen_Moody I would be interested in that task if you wouldn’t mind sharing. We also have a EDF in Automate with the keys that I am looking to throw to ITGlue.

1 Like

@Stephen_Moody I would also be interested if you don’t mind sharing the script? let me know! thanks!

If it helps anyone, here’s what we ended up doing. It’s a bit rough and could be improved with some more error handling and dupication checks. We create/update the configs in CW Manage since if you do this from the IT Glue side there are limitations with renaming computernames and updating other fields when IT Glue and CW Manage are syncing. We then update the BitLocker keys as embedded PWs in IT Glue with the second script below.

Update/Create configs in CW Manage:

# Import CWManageAPI PowerShell Module
Import-Module CWManageAPI

# Connect to ConnectWise Manage
$CWManageHeaders = Connect-CWManage -URL $CWMUri -Company $CWMCompany -PublicKey $CWMPublicKey -PrivateKey $CWMPrivateKey -ClientID $CWMClientID
Write-Host "Connected to ConnectWise Manage."

# Retrieve the mapping of TenantId to ConnectWise Manage company IDs
$PsaInfo = Get-RmmInfo -ProviderType "CWManage" -IncludeClients
[int]$TenantId = $TenantId
$PsaCompanyID = $PsaInfo.Clients | Where-Object { $_.TenantId -eq $TenantId } | Select-Object -ExpandProperty ClientID
Write-Host "Retrieved CW company ID: $PsaCompanyID for ImmyBot TenantId: $TenantId"

$script:success = $true
function New-CWMCompanyConfiguration {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$ConfigurationDetails
    )
    
    $jsonBody = ConvertTo-Json -InputObject $ConfigurationDetails -Depth 10
    $endpoint = "/company/configurations"
    
    Invoke-CWMRestMethod -Endpoint $endpoint -Method 'Post' -Body $jsonBody
}

function Update-CWMCompanyConfiguration {
    param(
        [Parameter(Mandatory = $true)]
        [int]$id,
        [Parameter(Mandatory = $true)]
        [string]$Operation,
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        $Value
    )
    
    # Ensure the update details are formatted as an array of patch operations
    $updateDetails = @(
        @{
            op    = $Operation
            path  = $Path
            value = $Value
        }
    )
    
    $jsonBody = ConvertTo-Json -InputObject $updateDetails -Depth 10
    $endpoint = "/company/configurations/$id"
    
    # Invoke the REST method for the PATCH request
    Invoke-CWMRestMethod -Endpoint $endpoint -Method 'Patch' -Body $jsonBody
}

Function Get-CWConfigTypeId {
    param (
        [Parameter(Mandatory=$true)]
        [string]$OperatingSystemName
    )
    
    Write-Host "Determining config type ID for OS: $OperatingSystemName"
    $workstationTypeId = 24  # Managed Workstation
    $serverTypeId = 25  # Managed Server

    if ($OperatingSystemName -match "Windows") {
        if ($OperatingSystemName -match "Server") {
            Write-Host "Detected Windows Server OS."
            return $serverTypeId
        } else {
            Write-Host "Detected Windows Workstation OS."
            return $workstationTypeId
        }
    } else {
        Write-Host "Non-Windows OS detected, skipping configuration."
        return $null
    }
}
Function New_or_Update_CWMConfiguration {
    param (
        [Parameter(Mandatory = $true)]
        $computer
    )

    Write-Host "Processing computer: $($computer.Name)"
    $lastLoginName = $computer.Inventory.LoggedOnUser

    try {
        $configTypeId = Get-CWConfigTypeId -OperatingSystemName $computer.OperatingSystemName

        if (-not $configTypeId) {
            Write-Host "Skipping configuration for $($computer.Name) due to incompatible OS type or unsupported config type."
            return
        }

        $filterConditions = "serialNumber = '$($computer.SerialNumber)' and company/id = $PsaCompanyID"
        $existingConfigs = Get-CWMObjects -Header $CWManageHeaders -EndPoint "/company/configurations" -Conditions $filterConditions
        
        if ($existingConfigs.Count -gt 0) {
            # Sort configurations by dateEntered within the _info property
            $sortedConfigs = $existingConfigs | Sort-Object { $_._info.dateEntered }

            # First configuration will be the oldest
            $oldestConfig = $sortedConfigs | Select-Object -First 1

            foreach ($config in $sortedConfigs) {
                if ($config.id -eq $oldestConfig.id) {
                    # Update only the oldest configuration
                    if ($config.name -ne $computer.Name) {
                        Update-CWMCompanyConfiguration -id $config.id -Operation "replace" -Path "/name" -Value $computer.Name
                    }

                    if ($config.tagNumber -ne $computer.DeviceId) {
                        Update-CWMCompanyConfiguration -id $config.id -Operation "replace" -Path "/tagNumber" -Value $computer.DeviceId
                    }

                    if (![string]::IsNullOrWhiteSpace($lastLoginName) -and $config.lastLoginName -ne $lastLoginName) {
                        Update-CWMCompanyConfiguration -id $config.id -Operation "replace" -Path "/lastLoginName" -Value $lastLoginName
                    }

                    # Set oldest configuration to Active (status id 2)
                    if ($config.status.id -ne 2) {
                        Update-CWMCompanyConfiguration -id $config.id -Operation "replace" -Path "/status/id" -Value 2
                    }
                } else {
                    # Set other configurations to Inactive (status id 3)
                    if ($config.status.id -ne 3) {
                        Update-CWMCompanyConfiguration -id $config.id -Operation "replace" -Path "/status/id" -Value 3
                    }
                }
            }
        } else {
            $newConfigParams = @{
                name = $computer.Name
                type = @{ id = $configTypeId }
                company = @{ id = $PsaCompanyID }
                serialNumber = $computer.SerialNumber
                tagNumber = $computer.DeviceId
                lastLoginName = if (![string]::IsNullOrWhiteSpace($lastLoginName)) { $lastLoginName } else { $null }
                status = @{ id = 2 } # Assuming new configurations are Active by default
            }
            New-CWMCompanyConfiguration -ConfigurationDetails $newConfigParams
        }
    } catch {
        Write-Host "Error processing $($computer.Name): $_"
        $script:success = $false
    }
}

# Add a switch to control whether to process all computers or just a limited number for testing
$ProcessAll = $true  # Set to $false to limit processing based on $TestLimit

$TestLimit = 1
$processedCount = 0

Write-Host "Retrieving computers from ImmyBot..."
$computers = Get-ImmyComputer -IncludeOffline -InventoryKeys LoggedOnUser

foreach ($computer in $computers) {
    # Check if processing all configs or if within the test limit
    if ($ProcessAll -or $processedCount -lt $TestLimit) {
        New_or_Update_CWMConfiguration -computer $computer
        $processedCount++
    } else {
        break  # Exit the loop if not processing all and the test limit is reached
    }
}

Write-Host "Processing completed. Total computers processed: $processedCount"

# Return the overall success status
return $script:success

Create/Update BitLocker keys as embedded PWs in IT Glue:

param(
    [Parameter(HelpMessage='Enter the API URI here...')]
    [Uri]$ITGlueAPIEndpoint = "https://api.itglue.com",
    [Parameter(Mandatory)]
    [string]$ITGlueAPIKey
)

# Initialization
$script:OperationSuccess = $true  # Assume success at the start

# Import ITGlueAPI PowerShell Module
Import-Module ITGlueAPI

# Add IT Glue Base URI and API Key
Add-ITGlueBaseURI -base_uri $ITGlueAPIEndpoint
Add-ITGlueAPIKey -Api_Key $ITGlueAPIKey
Function Get-Org-ID {
    param (
        [Parameter(Mandatory=$true)]
        [string]$SerialNumber,
        [string]$DeviceID
    )

    $configurations = Get-ITGlueConfigurations -filter_serial_number $SerialNumber

    $activeConfigurations = $configurations.data | Where-Object {
        $_.attributes.'configuration-status-id' -eq 704 -and
        $_.attributes.'asset-tag' -eq $DeviceID
    }

    if ($activeConfigurations.Count -eq 0) {
        Write-Host "No matching active IT Glue configuration found for the provided criteria."
        return $null
    } else {
        $orgIDs = $activeConfigurations | ForEach-Object { $_.attributes.'organization-id' } | Sort-Object -Unique

        if ($orgIDs.Count -gt 1) {
            throw "Error: More than one organization ID found for the provided serial number ($SerialNumber) and device ID ($DeviceID). Operation cannot proceed."
        } else {
            return $orgIDs[0]
        }
    }
}

$Computer = Get-ImmyComputer
$SerialNumber = $Computer.SerialNumber
$ComputerName = $Computer.Name
$DeviceID = $Computer.DeviceID

$orgID = Get-Org-ID -SerialNumber $SerialNumber -DeviceID $DeviceID

if (-not $orgID) {
    Write-Host "Unable to proceed without a valid organization ID."
    $script:OperationSuccess = $false
    return $false
}

$allConfigurations = Get-ITGlueConfigurations -organization_id $orgID -filter_serial_number $SerialNumber
$TaggedResource = $allConfigurations.data | Where-Object {
    $_.attributes.'configuration-status-id' -eq 704 -and
    $_.attributes.'asset-tag' -eq $DeviceID
} | Select-Object -First 1

if (-not $TaggedResource) {
    Write-Host "No matching active IT Glue configuration found for serial number: $SerialNumber and asset tag: $DeviceID."
    $script:OperationSuccess = $false
    return $false
}

$BitLockerVolumes = Invoke-ImmyCommand {
    Get-BitLockerVolume
}

foreach ($Volume in $BitLockerVolumes) {
    $MountPoint = $Volume.MountPoint
    foreach ($KeyProtector in $Volume.KeyProtector | Where-Object {$_.KeyProtectorType -eq 'RecoveryPassword'}) {
        $RecoveryPassword = $KeyProtector.RecoveryPassword
        $PasswordObjectName = "BitLocker Key - $ComputerName ($DeviceID) - $MountPoint"

        # Prepare the password object payload
        $PasswordData = @{
            type = 'passwords'
            attributes = @{
                name = $PasswordObjectName
                password = $RecoveryPassword
                notes = "Bitlocker key for mount point $MountPoint on $ComputerName. Serial: $SerialNumber"
                resource_id = $TaggedResource.id
                resource_type = "Configuration"
            }
        }

        # Logic to check and update/create password entry
        $existingPassword = Get-ITGluePasswords -filter_name $PasswordObjectName -organization_id $orgID
        $ExistingPasswordAsset = $existingPassword | Where-Object { $_ -ne $null } | Select-Object -First 1
        if ($ExistingPasswordAsset.data) {
            $PasswordID = $ExistingPasswordAsset.data.id
            Write-Host "Attempting to update existing BitLocker password object for $PasswordObjectName."
            try {
                Set-ITGluePasswords -id $PasswordID -data $PasswordData -organization_id $orgID
                Write-Host "Successfully updated BitLocker password object for $PasswordObjectName."
            } catch {
                Write-Error "Error updating BitLocker password for PasswordObjectName: $_"
                $script:OperationSuccess = $false
            }
        } else {
            Write-Host "Creating new BitLocker password object for $PasswordObjectName."
            # Assuming New-ITGluePasswords is correctly implemented to handle new password creation.
            New-ITGluePasswords -data $PasswordData -organization_id $orgID
        }
    }
}

Write-Host "Successfully processed IT Glue configurations for BitLocker keys."

return $script:OperationSuccess