From c853772fdb3e8b94aa0abc29c8fffcb3119bea4a Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 14:16:20 -0400
Subject: [PATCH 01/17] Add recently deleted support
---
Logic.Monitor.Format.ps1xml | 53 +++++++++
Private/Update-LogicMonitorModule.ps1 | 93 ++++++++++-----
Public/Get-LMRecentlyDeleted.ps1 | 162 ++++++++++++++++++++++++++
Public/Remove-LMRecentlyDeleted.ps1 | 79 +++++++++++++
Public/Restore-LMRecentlyDeleted.ps1 | 79 +++++++++++++
5 files changed, 435 insertions(+), 31 deletions(-)
create mode 100644 Public/Get-LMRecentlyDeleted.ps1
create mode 100644 Public/Remove-LMRecentlyDeleted.ps1
create mode 100644 Public/Restore-LMRecentlyDeleted.ps1
diff --git a/Logic.Monitor.Format.ps1xml b/Logic.Monitor.Format.ps1xml
index bf321ec..4c95b50 100644
--- a/Logic.Monitor.Format.ps1xml
+++ b/Logic.Monitor.Format.ps1xml
@@ -1452,6 +1452,59 @@
+
+
+
+ LogicMonitorRecentlyDeleted
+
+ LogicMonitor.RecentlyDeleted
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
+ resourceType
+
+
+ resourceName
+
+
+ deletedOn
+
+
+ deletedBy
+
+
+ resourceId
+
+
+
+
+
LogicMonitorLMUptimeDevice
diff --git a/Private/Update-LogicMonitorModule.ps1 b/Private/Update-LogicMonitorModule.ps1
index 01f7078..afe31ad 100644
--- a/Private/Update-LogicMonitorModule.ps1
+++ b/Private/Update-LogicMonitorModule.ps1
@@ -42,48 +42,79 @@ function Update-LogicMonitorModule {
)
foreach ($Module in $Modules) {
- # Read the currently installed version
- $Installed = Get-Module -ListAvailable -Name $Module
+ try {
+ # Read the currently installed version
+ $Installed = Get-Module -ListAvailable -Name $Module -ErrorAction SilentlyContinue
- # There might be multiple versions
- if ($Installed -is [Array]) {
- $InstalledVersion = $Installed[0].Version
- }
- elseif ($Installed.Version) {
- $InstalledVersion = $Installed.Version
- }
- else {
- #Not installed or manually imported
- return
- }
+ if (-not $Installed) {
+ Write-Verbose "Module $Module is not installed; skipping update check."
+ continue
+ }
+
+ # There might be multiple versions
+ if ($Installed -is [Array]) {
+ $InstalledVersion = $Installed[0].Version
+ }
+ elseif ($Installed.Version) {
+ $InstalledVersion = $Installed.Version
+ }
+ else {
+ Write-Verbose "Unable to determine installed version for module $Module; skipping update check."
+ continue
+ }
+
+ # Lookup the latest version online
+ try {
+ $Online = Find-Module -Name $Module -Repository PSGallery -ErrorAction Stop
+ $OnlineVersion = $Online.Version
+ }
+ catch {
+ Write-Verbose "Unable to query online version for module $Module. $_"
+ continue
+ }
- # Lookup the latest version Online
- $Online = Find-Module -Name $Module -Repository PSGallery -ErrorAction Stop
- $OnlineVersion = $Online.Version
+ # Compare the versions
+ if ([System.Version]$OnlineVersion -le [System.Version]$InstalledVersion) {
+ Write-Information "[INFO]: Module $Module version $InstalledVersion is the latest version."
+ continue
+ }
- # Compare the versions
- if ([System.Version]$OnlineVersion -gt [System.Version]$InstalledVersion) {
+ Write-Information "[INFO]: You are currently using an outdated version ($InstalledVersion) of $Module."
- # Uninstall the old version
if ($CheckOnly) {
- Write-Information "[INFO]: You are currently using an outdated version ($InstalledVersion) of $Module, please consider upgrading to the latest version ($OnlineVersion) as soon as possible. Use the -AutoUpdateModule switch next time you connect to auto upgrade to the latest version."
+ Write-Information "[INFO]: Please consider upgrading to the latest version ($OnlineVersion) of $Module as soon as possible. Use the -AutoUpdateModule switch next time you connect to auto upgrade to the latest version."
+ continue
}
- elseif ($UninstallFirst -eq $true) {
- Write-Information "[INFO]: You are currently using an outdated version ($InstalledVersion) of $Module, uninstalling prior Module $Module version $InstalledVersion"
- Uninstall-Module -Name $Module -Force -Verbose:$False
- Write-Information "[INFO]: Installing newer Module $Module version $OnlineVersion."
- Install-Module -Name $Module -Force -AllowClobber -Verbose:$False -MinimumVersion $OnlineVersion
- Update-LogicMonitorModule -CheckOnly -Modules @($Module)
+ if ($UninstallFirst -eq $true) {
+ Write-Information "[INFO]: Uninstalling prior Module $Module version $InstalledVersion."
+ try {
+ Uninstall-Module -Name $Module -Force -Verbose:$False -ErrorAction Stop
+ }
+ catch {
+ Write-Verbose "Failed to uninstall module $Module version $InstalledVersion. $_"
+ continue
+ }
}
- else {
- Write-Information "[INFO]: You are currently using an outdated version ($InstalledVersion) of $Module. Installing newer Module $Module version $OnlineVersion."
- Install-Module -Name $Module -Force -AllowClobber -Verbose:$False -MinimumVersion $OnlineVersion
+
+ Write-Information "[INFO]: Installing newer Module $Module version $OnlineVersion."
+ try {
+ Install-Module -Name $Module -Force -AllowClobber -Verbose:$False -MinimumVersion $OnlineVersion -ErrorAction Stop
+ }
+ catch {
+ Write-Verbose "Failed to install module $Module version $OnlineVersion. $_"
+ continue
+ }
+
+ try {
Update-LogicMonitorModule -CheckOnly -Modules @($Module)
}
+ catch {
+ Write-Verbose "Post-installation verification failed for module $Module. $_"
+ }
}
- else {
- Write-Information "[INFO]: Module $Module version $InstalledVersion is the latest version."
+ catch {
+ Write-Verbose "Unexpected error encountered while updating module $Module. $_"
}
}
}
\ No newline at end of file
diff --git a/Public/Get-LMRecentlyDeleted.ps1 b/Public/Get-LMRecentlyDeleted.ps1
new file mode 100644
index 0000000..bdbcfa4
--- /dev/null
+++ b/Public/Get-LMRecentlyDeleted.ps1
@@ -0,0 +1,162 @@
+<#
+.SYNOPSIS
+Retrieves recently deleted resources from the LogicMonitor recycle bin.
+
+.DESCRIPTION
+The Get-LMRecentlyDeleted function queries the LogicMonitor recycle bin for deleted resources
+within a configurable time range. Results can be filtered by resource type and deleted-by user,
+and support paging through the API using size, offset, and sort parameters.
+
+.PARAMETER ResourceType
+Limits results to a specific resource type. Accepted values are All, device, and deviceGroup.
+Defaults to All.
+
+.PARAMETER DeletedAfter
+The earliest deletion timestamp (inclusive) to return. Defaults to seven days prior when not specified.
+
+.PARAMETER DeletedBefore
+The latest deletion timestamp (exclusive) to return. Defaults to the current time when not specified.
+
+.PARAMETER DeletedBy
+Limits results to items deleted by the specified user principal.
+
+.PARAMETER BatchSize
+The number of records to request per API call (1-1000). Defaults to 1000.
+
+.PARAMETER Sort
+Sort expression passed to the API. Defaults to -deletedOn.
+
+.EXAMPLE
+Get-LMRecentlyDeleted -ResourceType device -DeletedBy "lmsupport"
+
+Retrieves every device deleted by the user lmsupport over the past seven days.
+
+.EXAMPLE
+Get-LMRecentlyDeleted -DeletedAfter (Get-Date).AddDays(-1) -DeletedBefore (Get-Date) -BatchSize 100 -Sort "+deletedOn"
+
+Retrieves deleted resources from the past 24 hours in ascending order of deletion time.
+
+.NOTES
+You must establish a session with Connect-LMAccount prior to calling this function.
+#>
+function Get-LMRecentlyDeleted {
+
+ [CmdletBinding()]
+ param (
+ [ValidateSet('All', 'device', 'deviceGroup')]
+ [String]$ResourceType = 'All',
+
+ [Nullable[DateTime]]$DeletedAfter,
+
+ [Nullable[DateTime]]$DeletedBefore,
+
+ [String]$DeletedBy,
+
+ [ValidateRange(1, 1000)]
+ [Int]$BatchSize = 1000,
+
+ [String]$Sort = '-deletedOn'
+ )
+
+ if (-not $Script:LMAuth.Valid) {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ return
+ }
+
+ function Get-EpochMilliseconds {
+ param ([Parameter(Mandatory)][DateTime]$InputDate)
+ return [long][Math]::Round((New-TimeSpan -Start (Get-Date -Date '1/1/1970') -End $InputDate.ToUniversalTime()).TotalMilliseconds)
+ }
+
+ $now = Get-Date
+
+ if (-not $DeletedAfter -and -not $DeletedBefore) {
+ $DeletedBefore = $now
+ $DeletedAfter = $now.AddDays(-7)
+ }
+ else {
+ if (-not $DeletedAfter) {
+ $DeletedAfter = $now.AddDays(-7)
+ }
+ if (-not $DeletedBefore) {
+ $DeletedBefore = $now
+ }
+ }
+
+ if ($DeletedAfter -and $DeletedBefore -and $DeletedAfter -gt $DeletedBefore) {
+ Write-Error "The value supplied for DeletedAfter occurs after DeletedBefore. Please adjust the time range and try again."
+ return
+ }
+
+ $filterParts = @()
+
+ if ($DeletedAfter) {
+ $filterParts += ('deletedOn>:"{0}"' -f (Get-EpochMilliseconds -InputDate $DeletedAfter))
+ }
+
+ if ($DeletedBefore) {
+ $filterParts += ('deletedOn<:"{0}"' -f (Get-EpochMilliseconds -InputDate $DeletedBefore))
+ }
+
+ if ($ResourceType -ne 'All') {
+ $filterParts += ('resourceType:"{0}"' -f $ResourceType)
+ }
+
+ if ($DeletedBy) {
+ $filterParts += ('deletedBy:"{0}"' -f $DeletedBy)
+ }
+
+ $filterString = $null
+ if ($filterParts.Count -gt 0) {
+ $filterString = $filterParts -join ','
+ }
+
+ $resourcePath = '/recyclebin/recycles'
+ $results = @()
+ $currentOffset = 0
+ $total = $null
+
+ while ($true) {
+ $queryParams = "?size=$BatchSize&offset=$currentOffset&sort=$Sort"
+ if ($filterString) {
+ $queryParams += "&filter=$filterString"
+ }
+
+ $headers = New-LMHeader -Auth $Script:LMAuth -Method 'GET' -ResourcePath $resourcePath
+ $uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $resourcePath + $queryParams
+
+ Resolve-LMDebugInfo -Url $uri -Headers $headers[0] -Command $MyInvocation
+
+ $response = Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $uri -Method 'GET' -Headers $headers[0] -WebSession $headers[1]
+
+ $itemCount = 0
+ if ($response.items) {
+ $results += $response.items
+ $itemCount = ($response.items | Measure-Object).Count
+ }
+
+ if (-not $total -and $response.total) {
+ $total = $response.total
+ }
+
+ if ($itemCount -lt $BatchSize) {
+ break
+ }
+
+ if ($total -and (($currentOffset + $itemCount) -ge $total)) {
+ break
+ }
+
+ $currentOffset += $itemCount
+ }
+
+ if ($response.total) {
+ Write-Verbose "Retrieved $($results.Count) of $($response.total) recently deleted items."
+ }
+ else {
+ Write-Verbose "Retrieved $($results.Count) recently deleted items."
+ }
+
+ return (Add-ObjectTypeInfo -InputObject $results -TypeName 'LogicMonitor.RecentlyDeleted')
+}
+
diff --git a/Public/Remove-LMRecentlyDeleted.ps1 b/Public/Remove-LMRecentlyDeleted.ps1
new file mode 100644
index 0000000..946aa92
--- /dev/null
+++ b/Public/Remove-LMRecentlyDeleted.ps1
@@ -0,0 +1,79 @@
+<#
+.SYNOPSIS
+Permanently removes one or more resources from the LogicMonitor recycle bin.
+
+.DESCRIPTION
+The Remove-LMRecentlyDeleted function submits a batch delete request for the provided recycle
+identifiers, permanently removing the associated resources from the recycle bin.
+
+.PARAMETER RecycleId
+One or more recycle identifiers representing deleted resources. Accepts pipeline input and
+property names of Id.
+
+.EXAMPLE
+Get-LMRecentlyDeleted -ResourceType deviceGroup -DeletedBy "lmsupport" | Select-Object -First 3 -ExpandProperty id | Remove-LMRecentlyDeleted
+
+Permanently deletes the first three device groups currently in the recycle bin for the user lmsupport.
+
+.NOTES
+You must establish a session with Connect-LMAccount prior to calling this function.
+#>
+function Remove-LMRecentlyDeleted {
+
+ [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
+ param (
+ [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
+ [Alias('Id')]
+ [String[]]$RecycleId
+ )
+
+ begin {
+ $idBuffer = New-Object System.Collections.Generic.List[string]
+ }
+
+ process {
+ foreach ($id in $RecycleId) {
+ if ([string]::IsNullOrWhiteSpace($id)) {
+ continue
+ }
+ $idBuffer.Add($id)
+ }
+ }
+
+ end {
+ if (-not $Script:LMAuth.Valid) {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ return
+ }
+
+ if ($idBuffer.Count -eq 0) {
+ Write-Error "No recycle identifiers were supplied. Provide at least one identifier and try again."
+ return
+ }
+
+ $resourcePath = '/recyclebin/recycles/batchdelete'
+ $payload = ConvertTo-Json -InputObject $idBuffer.ToArray()
+ $headers = New-LMHeader -Auth $Script:LMAuth -Method 'POST' -ResourcePath $resourcePath -Data $payload
+ $uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $resourcePath
+
+ Resolve-LMDebugInfo -Url $uri -Headers $headers[0] -Command $MyInvocation -Payload $payload
+
+ $targetDescription = "RecycleId(s): $(($idBuffer.ToArray()) -join ', ')"
+
+ if ($PSCmdlet.ShouldProcess($targetDescription, 'Permanently delete recently deleted resources')) {
+ $response = Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $uri -Method 'POST' -Headers $headers[0] -WebSession $headers[1] -Body $payload
+
+ if ($null -ne $response) {
+ return (Add-ObjectTypeInfo -InputObject $response -TypeName 'LogicMonitor.RecentlyDeletedRemoveResult')
+ }
+
+ $summary = [PSCustomObject]@{
+ recycleIds = $idBuffer.ToArray()
+ message = 'Permanent delete request submitted successfully.'
+ }
+
+ return (Add-ObjectTypeInfo -InputObject $summary -TypeName 'LogicMonitor.RecentlyDeletedRemoveResult')
+ }
+ }
+}
+
diff --git a/Public/Restore-LMRecentlyDeleted.ps1 b/Public/Restore-LMRecentlyDeleted.ps1
new file mode 100644
index 0000000..21d3493
--- /dev/null
+++ b/Public/Restore-LMRecentlyDeleted.ps1
@@ -0,0 +1,79 @@
+<#
+.SYNOPSIS
+Restores one or more resources from the LogicMonitor recycle bin.
+
+.DESCRIPTION
+The Restore-LMRecentlyDeleted function issues a batch restore request for the provided recycle
+identifiers, returning the selected resources to their original state when possible.
+
+.PARAMETER RecycleId
+One or more recycle identifiers representing deleted resources. Accepts pipeline input and
+property names of Id.
+
+.EXAMPLE
+Get-LMRecentlyDeleted -ResourceType device -DeletedBy "lmsupport" | Select-Object -First 5 -ExpandProperty id | Restore-LMRecentlyDeleted
+
+Restores the five most recently deleted devices by lmsupport.
+
+.NOTES
+You must establish a session with Connect-LMAccount prior to calling this function.
+#>
+function Restore-LMRecentlyDeleted {
+
+ [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
+ param (
+ [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
+ [Alias('Id')]
+ [String[]]$RecycleId
+ )
+
+ begin {
+ $idBuffer = New-Object System.Collections.Generic.List[string]
+ }
+
+ process {
+ foreach ($id in $RecycleId) {
+ if ([string]::IsNullOrWhiteSpace($id)) {
+ continue
+ }
+ $idBuffer.Add($id)
+ }
+ }
+
+ end {
+ if (-not $Script:LMAuth.Valid) {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ return
+ }
+
+ if ($idBuffer.Count -eq 0) {
+ Write-Error "No recycle identifiers were supplied. Provide at least one identifier and try again."
+ return
+ }
+
+ $resourcePath = '/recyclebin/recycles/batchrestore'
+ $payload = ConvertTo-Json -InputObject $idBuffer.ToArray()
+ $headers = New-LMHeader -Auth $Script:LMAuth -Method 'POST' -ResourcePath $resourcePath -Data $payload
+ $uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $resourcePath
+
+ Resolve-LMDebugInfo -Url $uri -Headers $headers[0] -Command $MyInvocation -Payload $payload
+
+ $targetDescription = "RecycleId(s): $(($idBuffer.ToArray()) -join ', ')"
+
+ if ($PSCmdlet.ShouldProcess($targetDescription, 'Restore recently deleted resources')) {
+ $response = Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $uri -Method 'POST' -Headers $headers[0] -WebSession $headers[1] -Body $payload
+
+ if ($null -ne $response) {
+ return (Add-ObjectTypeInfo -InputObject $response -TypeName 'LogicMonitor.RecentlyDeletedRestoreResult')
+ }
+
+ $summary = [PSCustomObject]@{
+ recycleIds = $idBuffer.ToArray()
+ message = 'Restore request submitted successfully.'
+ }
+
+ return (Add-ObjectTypeInfo -InputObject $summary -TypeName 'LogicMonitor.RecentlyDeletedRestoreResult')
+ }
+ }
+}
+
From 2aa475217551de4d4ea88ba2a4697fbbcd2f5b1b Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 14:23:03 -0400
Subject: [PATCH 02/17] Fix csv export bug with Export-LMDeviceData
- expand json depth to 5 and tweak export details for CSV so datapoints are individual columns
---
Public/Export-LMDeviceData.ps1 | 43 ++++++++++++++++++++++++++++++++--
1 file changed, 41 insertions(+), 2 deletions(-)
diff --git a/Public/Export-LMDeviceData.ps1 b/Public/Export-LMDeviceData.ps1
index 12ae453..cc9d62e 100644
--- a/Public/Export-LMDeviceData.ps1
+++ b/Public/Export-LMDeviceData.ps1
@@ -135,9 +135,48 @@ function Export-LMDeviceData {
}
}
+ $csvExportList = @()
+ if ($ExportFormat -eq 'csv') {
+ foreach ($ExportItem in $DataExportList) {
+ if (-not $ExportItem.dataPoints) {
+ $row = [ordered]@{
+ deviceId = $ExportItem.deviceId
+ deviceName = $ExportItem.deviceName
+ datasourceName = $ExportItem.datasourceName
+ instanceName = $ExportItem.instanceName
+ instanceGroup = $ExportItem.instanceGroup
+ }
+ $csvExportList += [PSCustomObject]$row
+ continue
+ }
+
+ foreach ($Datapoint in @($ExportItem.dataPoints)) {
+ $row = [ordered]@{
+ deviceId = $ExportItem.deviceId
+ deviceName = $ExportItem.deviceName
+ datasourceName = $ExportItem.datasourceName
+ instanceName = $ExportItem.instanceName
+ instanceGroup = $ExportItem.instanceGroup
+ }
+
+ foreach ($Property in $Datapoint.PSObject.Properties) {
+ $row[$Property.Name] = $Property.Value
+ }
+
+ $csvExportList += [PSCustomObject]$row
+ }
+ }
+ }
+
switch ($ExportFormat) {
- "json" { $DataExportList | ConvertTo-Json -Depth 3 | Out-File -FilePath "$ExportPath\LMDeviceDataExport.json" ; return }
- "csv" { $DataExportList | Export-Csv -NoTypeInformation -Path "$ExportPath\LMDeviceDataExport.csv" ; return }
+ "json" {
+ $DataExportList | ConvertTo-Json -Depth 5 | Out-File -FilePath (Join-Path -Path $ExportPath -ChildPath 'LMDeviceDataExport.json')
+ return
+ }
+ "csv" {
+ $csvExportList | Export-Csv -NoTypeInformation -Path (Join-Path -Path $ExportPath -ChildPath 'LMDeviceDataExport.csv')
+ return
+ }
default { return $DataExportList }
}
}
From a328046b44604afc3bd4d8e9504d66035dd19ce7 Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 14:33:53 -0400
Subject: [PATCH 03/17] prepare release notes
---
README.md | 27 ++++++++++++++-------------
RELEASENOTES.md | 25 +++++++++++++++++++++++++
2 files changed, 39 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 01b3bb6..0953a3f 100644
--- a/README.md
+++ b/README.md
@@ -73,29 +73,30 @@ Connect-LMAccount -UseCachedCredential
# Change List
-## 7.6.1
+## 7.7.0
### New Cmdlets
-- **Send-LMWebhookMessage**: Send a webhook message to LM Logs.
-- **Get-LMAWSExternalId**: Generate an ExternalID for AWS onboarding.
+- **Get-LMRecentlyDeleted**: Retrieve recycle-bin entries with optional date, resource type, and deleted-by filters.
+- **Restore-LMRecentlyDeleted**: Batch restore recycle-bin items by recycle identifier.
+- **Remove-LMRecentlyDeleted**: Permanently delete recycle-bin entries in bulk.
### Updated Cmdlets
-- **Set-LMDeviceGroup**: Added *-Extra* field which takes a PSCustomObject for specifying extra cloud settings for LM Cloud resource groups.
-- **New-LMDeviceGroup**: Added *-Extra* field which takes a PSCustomObject for specifying extra cloud settings for LM Cloud resource groups.
+- **Update-LogicMonitorModule**: Hardened for non-blocking version checks; failures are logged via `Write-Verbose` and never terminate connecting cmdlets.
+- **Export-LMDeviceData**: CSV exports now expand datapoints into individual rows and JSON exports capture deeper datapoint structures.
### Examples
```powershell
-# Create a new external web uptime check
-New-LMUptimeDevice -Name "shop.example.com" -HostGroupIds '123' -Domain 'shop.example.com' -TestLocationAll
+# Retrieve all recently deleted devices for the past seven days
+Get-LMRecentlyDeleted -ResourceType device -DeletedBy "lmsupport" -Verbose
-# Update an existing uptime device by name
-Set-LMUptimeDevice -Name "shop.example.com" -Description "Updated uptime monitor" -GlobalSmAlertCond half
+# Restore a previously deleted device and confirm the operation
+Get-LMRecentlyDeleted -ResourceType device | Select-Object -First 1 -ExpandProperty id | Restore-LMRecentlyDeleted -Confirm:$false
-# Remove an uptime device
-Remove-LMUptimeDevice -Name "shop.example.com"
+# Permanently remove stale recycle-bin entries
+Get-LMRecentlyDeleted -DeletedAfter (Get-Date).AddMonths(-1) | Select-Object -ExpandProperty id | Remove-LMRecentlyDeleted -Confirm:$false
-# Migrate legacy websites to uptime and disable their alerting
-Get-LMWebsite -Type Webcheck | ConvertTo-LMUptimeDevice -TargetHostGroupIds '123' -DisableSourceAlerting
+# Export device datapoints to CSV with flattened datapoint rows
+Export-LMDeviceData -DeviceId 12345 -StartDate (Get-Date).AddHours(-6) -ExportFormat csv -ExportPath "C:\\Exports"
```
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 27709ed..56ea6f5 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,5 +1,30 @@
# Previous module release notes
+## 7.6.1
+
+### New Cmdlets
+- **Send-LMWebhookMessage**: Send a webhook message to LM Logs.
+- **Get-LMAWSExternalId**: Generate an ExternalID for AWS onboarding.
+
+### Updated Cmdlets
+- **Set-LMDeviceGroup**: Added *-Extra* field which takes a PSCustomObject for specifying extra cloud settings for LM Cloud resource groups.
+- **New-LMDeviceGroup**: Added *-Extra* field which takes a PSCustomObject for specifying extra cloud settings for LM Cloud resource groups.
+
+### Examples
+```powershell
+# Create a new external web uptime check
+New-LMUptimeDevice -Name "shop.example.com" -HostGroupIds '123' -Domain 'shop.example.com' -TestLocationAll
+
+# Update an existing uptime device by name
+Set-LMUptimeDevice -Name "shop.example.com" -Description "Updated uptime monitor" -GlobalSmAlertCond half
+
+# Remove an uptime device
+Remove-LMUptimeDevice -Name "shop.example.com"
+
+# Migrate legacy websites to uptime and disable their alerting
+Get-LMWebsite -Type Webcheck | ConvertTo-LMUptimeDevice -TargetHostGroupIds '123' -DisableSourceAlerting
+```
+
## 7.6
### New Cmdlets
From b54238433183347c2174912696e5fb952239353b Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 14:52:10 -0400
Subject: [PATCH 04/17] Update Update-LogicMonitorModule.ps1
---
Private/Update-LogicMonitorModule.ps1 | 37 +++++++++++++++++++++++++--
1 file changed, 35 insertions(+), 2 deletions(-)
diff --git a/Private/Update-LogicMonitorModule.ps1 b/Private/Update-LogicMonitorModule.ps1
index afe31ad..73a7135 100644
--- a/Private/Update-LogicMonitorModule.ps1
+++ b/Private/Update-LogicMonitorModule.ps1
@@ -41,6 +41,8 @@ function Update-LogicMonitorModule {
[Switch]$CheckOnly
)
+ $psGalleryAvailable = $null
+
foreach ($Module in $Modules) {
try {
# Read the currently installed version
@@ -63,13 +65,43 @@ function Update-LogicMonitorModule {
continue
}
+ if ($null -eq $psGalleryAvailable) {
+ try {
+ $repository = Get-PSRepository -Name 'PSGallery' -ErrorAction Stop
+ if (-not $repository) {
+ $psGalleryAvailable = $false
+ }
+ else {
+ try {
+ $probeUri = 'https://www.powershellgallery.com/api/v2/Packages?$top=1&$skip=0'
+ Invoke-RestMethod -Uri $probeUri -Method Get -TimeoutSec 5 -ErrorAction Stop | Out-Null
+ $psGalleryAvailable = $true
+ }
+ catch {
+ Write-Verbose "Unable to reach PSGallery endpoint ($probeUri). $_"
+ $psGalleryAvailable = $false
+ }
+ }
+ }
+ catch {
+ Write-Verbose "PSGallery repository is not registered on this host. $_"
+ $psGalleryAvailable = $false
+ }
+ }
+
+ if (-not $psGalleryAvailable) {
+ Write-Verbose "PSGallery repository is unavailable; skipping update check for module $Module."
+ continue
+ }
+
# Lookup the latest version online
try {
- $Online = Find-Module -Name $Module -Repository PSGallery -ErrorAction Stop
+ $Online = Find-Module -Name $Module -Repository 'PSGallery' -ErrorAction Stop
$OnlineVersion = $Online.Version
}
catch {
Write-Verbose "Unable to query online version for module $Module. $_"
+ $psGalleryAvailable = $false
continue
}
@@ -99,10 +131,11 @@ function Update-LogicMonitorModule {
Write-Information "[INFO]: Installing newer Module $Module version $OnlineVersion."
try {
- Install-Module -Name $Module -Force -AllowClobber -Verbose:$False -MinimumVersion $OnlineVersion -ErrorAction Stop
+ Install-Module -Name $Module -Repository 'PSGallery' -Force -AllowClobber -Verbose:$False -MinimumVersion $OnlineVersion -ErrorAction Stop
}
catch {
Write-Verbose "Failed to install module $Module version $OnlineVersion. $_"
+ $psGalleryAvailable = $false
continue
}
From 7d755e2fd78e681a9b5b70994d8e71fbe3724af6 Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 14:54:48 -0400
Subject: [PATCH 05/17] enable verbose
---
.github/workflows/test-win.yml | 2 +-
.github/workflows/test.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml
index 49d4274..fd9907d 100644
--- a/.github/workflows/test-win.yml
+++ b/.github/workflows/test-win.yml
@@ -46,7 +46,7 @@ jobs:
$Result = Invoke-Pester -Container $Container -Output Detailed -PassThru
#Write OpsNote to test portal indicating test status
- Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging
+ Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging -Verbose
$TimeNow = Get-Date -UFormat %m%d%Y-%H%M
$OpsNote = New-LMOpsNote -Note "Github test build submitted on $TimeNow - $($Result.Result)" -Tags @("GithubActions","TestPipeline-Win5.1","PSVersion-$Version")
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4385aa1..e3c0ca3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -46,7 +46,7 @@ jobs:
$Result = Invoke-Pester -Container $Container -Output Detailed -PassThru
#Write OpsNote to test portal indicating test status
- Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging
+ Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging -Verbose
$TimeNow = Get-Date -UFormat %m%d%Y-%H%M
$OpsNote = New-LMOpsNote -Note "Github test build submitted on $TimeNow - $($Result.Result)" -Tags @("GithubActions","TestPipeline-Core","PSVersion-$Version")
From 5b1f1661a2bab4a0790a1c30ab5292e0d88bbcf1 Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 14:56:29 -0400
Subject: [PATCH 06/17] Update README.md
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 0953a3f..7ece28f 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,7 @@ Connect-LMAccount -UseCachedCredential
#Connected to LM portal portalnamesandbox using account
```
+
# Change List
## 7.7.0
From f913d4e499db2b2c81f01a133460597e14b3992f Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 15:02:03 -0400
Subject: [PATCH 07/17] Update-LogicMonitorModule explicitly calling PSGallery
#63
---
Private/Update-LogicMonitorModule.ps1 | 37 ++-------------------------
README.md | 1 -
2 files changed, 2 insertions(+), 36 deletions(-)
diff --git a/Private/Update-LogicMonitorModule.ps1 b/Private/Update-LogicMonitorModule.ps1
index 73a7135..028263c 100644
--- a/Private/Update-LogicMonitorModule.ps1
+++ b/Private/Update-LogicMonitorModule.ps1
@@ -41,8 +41,6 @@ function Update-LogicMonitorModule {
[Switch]$CheckOnly
)
- $psGalleryAvailable = $null
-
foreach ($Module in $Modules) {
try {
# Read the currently installed version
@@ -65,43 +63,13 @@ function Update-LogicMonitorModule {
continue
}
- if ($null -eq $psGalleryAvailable) {
- try {
- $repository = Get-PSRepository -Name 'PSGallery' -ErrorAction Stop
- if (-not $repository) {
- $psGalleryAvailable = $false
- }
- else {
- try {
- $probeUri = 'https://www.powershellgallery.com/api/v2/Packages?$top=1&$skip=0'
- Invoke-RestMethod -Uri $probeUri -Method Get -TimeoutSec 5 -ErrorAction Stop | Out-Null
- $psGalleryAvailable = $true
- }
- catch {
- Write-Verbose "Unable to reach PSGallery endpoint ($probeUri). $_"
- $psGalleryAvailable = $false
- }
- }
- }
- catch {
- Write-Verbose "PSGallery repository is not registered on this host. $_"
- $psGalleryAvailable = $false
- }
- }
-
- if (-not $psGalleryAvailable) {
- Write-Verbose "PSGallery repository is unavailable; skipping update check for module $Module."
- continue
- }
-
# Lookup the latest version online
try {
- $Online = Find-Module -Name $Module -Repository 'PSGallery' -ErrorAction Stop
+ $Online = Find-Module -Name $Module -ErrorAction Stop
$OnlineVersion = $Online.Version
}
catch {
Write-Verbose "Unable to query online version for module $Module. $_"
- $psGalleryAvailable = $false
continue
}
@@ -131,11 +99,10 @@ function Update-LogicMonitorModule {
Write-Information "[INFO]: Installing newer Module $Module version $OnlineVersion."
try {
- Install-Module -Name $Module -Repository 'PSGallery' -Force -AllowClobber -Verbose:$False -MinimumVersion $OnlineVersion -ErrorAction Stop
+ Install-Module -Name $Module -Force -AllowClobber -Verbose:$False -MinimumVersion $OnlineVersion -ErrorAction Stop
}
catch {
Write-Verbose "Failed to install module $Module version $OnlineVersion. $_"
- $psGalleryAvailable = $false
continue
}
diff --git a/README.md b/README.md
index 7ece28f..0953a3f 100644
--- a/README.md
+++ b/README.md
@@ -71,7 +71,6 @@ Connect-LMAccount -UseCachedCredential
#Connected to LM portal portalnamesandbox using account
```
-
# Change List
## 7.7.0
From b4299968e97472b81d547312a7fb95476fdbaa9e Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 15:24:21 -0400
Subject: [PATCH 08/17] Update Format-LMFilter.ps1
---
Private/Format-LMFilter.ps1 | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/Private/Format-LMFilter.ps1 b/Private/Format-LMFilter.ps1
index 72a4253..2aba792 100644
--- a/Private/Format-LMFilter.ps1
+++ b/Private/Format-LMFilter.ps1
@@ -73,7 +73,16 @@ function Format-LMFilter {
}
}
else {
- $FormatedFilter += $SingleFilter.Replace("'", "`"") #replace single quotes with double quotes as reqired by LM API
+ if ($SingleFilter -match "^(\s*)'(.*)'(\s*)$") {
+ $Leading = $Matches[1]
+ $Value = $Matches[2]
+ $Trailing = $Matches[3]
+ $EscapedValue = $Value.Replace('"', '\"')
+ $FormatedFilter += $Leading + '"' + $EscapedValue + '"' + $Trailing
+ }
+ else {
+ $FormatedFilter += $SingleFilter
+ }
}
}
}
From aae20b8f2934ebc6c08cb40b97299cfa051af5a0 Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 15:33:05 -0400
Subject: [PATCH 09/17] Update Set/New-LMWebsite, add alertExpr alias and
update synopsis
When using get-lmwebsite, there is a property called alertExpr. However, that doesn't appear anywhere on the documentation page. In this set-lmwebsite command, it's referred to as -SSLAlertThresholds. If a note could be added somewhere letting us know that those are the same thing, that'd be great.
---
Public/New-LMWebsite.ps1 | 3 ++-
Public/Set-LMWebsite.ps1 | 4 ++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/Public/New-LMWebsite.ps1 b/Public/New-LMWebsite.ps1
index 53857f0..be14f08 100644
--- a/Public/New-LMWebsite.ps1
+++ b/Public/New-LMWebsite.ps1
@@ -51,7 +51,7 @@ Specifies the domain of the website to check.
Specifies the HTTP type to use for the website check. The valid values are "http" and "https". The default value is "https".
.PARAMETER SSLAlertThresholds
-Specifies the SSL alert thresholds for the website check.
+Specifies the SSL alert thresholds for the website check. This is an alias for the alertExpr parameter.
.PARAMETER PingCount
Specifies the number of pings to send for the ping check. The valid values are 5, 10, 15, 20, 30, and 60.
@@ -164,6 +164,7 @@ function New-LMWebsite {
[String]$HttpType = "https",
[Parameter(ParameterSetName = "Website")]
+ [Alias("alertExpr")]
[String[]]$SSLAlertThresholds,
[Parameter(ParameterSetName = "Ping")]
diff --git a/Public/Set-LMWebsite.ps1 b/Public/Set-LMWebsite.ps1
index 9e1509e..67144b7 100644
--- a/Public/Set-LMWebsite.ps1
+++ b/Public/Set-LMWebsite.ps1
@@ -48,6 +48,9 @@ Specifies how to handle properties. Valid values: "Add", "Replace", "Refresh".
.PARAMETER PollingInterval
Specifies the polling interval. Valid values: 1-10, 30, 60.
+.PARAMETER SSLAlertThresholds
+Specifies the SSL alert thresholds for the website check. This is an alias for the alertExpr parameter.
+
.PARAMETER TestLocationAll
Indicates whether to test from all locations. Cannot be used with TestLocationCollectorIds or TestLocationSmgIds.
@@ -122,6 +125,7 @@ function Set-LMWebsite {
[String]$HttpType,
[Parameter(ParameterSetName = "Website")]
+ [Alias("alertExpr")]
[String[]]$SSLAlertThresholds,
[Parameter(ParameterSetName = "Ping")]
From e4bc73be11bce2845b4346a76cb7c5c184d69e8e Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 17:07:00 -0400
Subject: [PATCH 10/17] Add new report cmdlets and bug fixes
- Add Invoke-LMReportExecution
- Add Get-LMReportExecutionTask
- Fix 'Cannot bind argument to parameter 'InputObject' because it is null' bug when a response is successful but null
- Fix -Debug incorrectly in certain situations indicating a GET request as DELETE
---
Private/Add-ObjectTypeInfo.ps1 | 1 +
Private/Invoke-LMRestMethod.ps1 | 4 ++
Private/New-LMHeader.ps1 | 3 +
Private/Resolve-LMDebugInfo.ps1 | 34 +++++++++--
Public/Get-LMReportExecutionTask.ps1 | 77 ++++++++++++++++++++++++
Public/Invoke-LMReportExecution.ps1 | 89 ++++++++++++++++++++++++++++
6 files changed, 202 insertions(+), 6 deletions(-)
create mode 100644 Public/Get-LMReportExecutionTask.ps1
create mode 100644 Public/Invoke-LMReportExecution.ps1
diff --git a/Private/Add-ObjectTypeInfo.ps1 b/Private/Add-ObjectTypeInfo.ps1
index 3a1c9c9..a8d0f56 100644
--- a/Private/Add-ObjectTypeInfo.ps1
+++ b/Private/Add-ObjectTypeInfo.ps1
@@ -101,6 +101,7 @@ function Add-ObjectTypeInfo {
Position = 0,
ValueFromPipeline = $true )]
+ [AllowNull()]
$InputObject,
[Parameter( Mandatory = $false,
diff --git a/Private/Invoke-LMRestMethod.ps1 b/Private/Invoke-LMRestMethod.ps1
index 8df009d..959ebbd 100644
--- a/Private/Invoke-LMRestMethod.ps1
+++ b/Private/Invoke-LMRestMethod.ps1
@@ -94,6 +94,10 @@ function Invoke-LMRestMethod {
while (-not $success -and $retryCount -le $MaxRetries) {
try {
+ if ($Headers.ContainsKey('__LMMethod')) {
+ $Headers.Remove('__LMMethod') | Out-Null
+ }
+
# Build parameters for Invoke-RestMethod
$params = @{
Uri = $Uri
diff --git a/Private/New-LMHeader.ps1 b/Private/New-LMHeader.ps1
index bf7f1c0..582ce57 100644
--- a/Private/New-LMHeader.ps1
+++ b/Private/New-LMHeader.ps1
@@ -130,5 +130,8 @@ function New-LMHeader {
$Header.Add("Content-Type", $ContentType)
$Header.Add("X-Version", $Version)
+ # Store HTTP method for diagnostics; removed prior to request dispatch
+ $Header.Add("__LMMethod", $Method)
+
return @($Header, $Session)
}
diff --git a/Private/Resolve-LMDebugInfo.ps1 b/Private/Resolve-LMDebugInfo.ps1
index f18c581..7f65ac6 100644
--- a/Private/Resolve-LMDebugInfo.ps1
+++ b/Private/Resolve-LMDebugInfo.ps1
@@ -43,13 +43,35 @@ function Resolve-LMDebugInfo {
# Add timestamp for correlation
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
- # Extract HTTP method from headers or default to GET
- $HttpMethod = if ($Headers.ContainsKey('Content-Type') -and $Payload) {
- if ($Payload -match '".*":\s*null|".*":\s*""') { "PATCH" } else { "POST" }
+ $HttpMethod = $null
+
+ if ($Headers.ContainsKey('__LMMethod')) {
+ $HttpMethod = $Headers['__LMMethod']
+ $Headers.Remove('__LMMethod') | Out-Null
+ }
+
+ if (-not $HttpMethod) {
+ $CommandName = $Command.MyCommand.Name
+ switch -Regex ($CommandName) {
+ '^(Get|Find|Search|Test|Resolve|Format|Measure|Show)-' { $HttpMethod = 'GET'; break }
+ '^(Remove|Uninstall|Disconnect|Stop|Clear|Delete)-' { $HttpMethod = 'DELETE'; break }
+ '^(Set|Update|Enable|Disable|Rename|Move|Merge|Patch|Edit)-' { $HttpMethod = 'PATCH'; break }
+ '^(New|Add|Copy|Send|Import|Invoke|Start|Publish|Submit|Approve)-' { $HttpMethod = 'POST'; break }
+ }
+ }
+
+ if (-not $HttpMethod) {
+ if ($Headers.ContainsKey('Content-Type') -and $Payload) {
+ if ($Payload -match '".*":\s*null|".*":\s*""') { $HttpMethod = 'PATCH' }
+ else { $HttpMethod = 'POST' }
+ }
+ elseif ($Payload) {
+ $HttpMethod = 'POST'
+ }
+ else {
+ $HttpMethod = 'GET'
+ }
}
- elseif ($Url -match '/\d+$' -and !$Payload) { "GET" }
- elseif (!$Payload) { "DELETE" }
- else { "POST" }
Write-Debug "============ LogicMonitor API Debug Info =============="
Write-Debug "Command: $($Command.MyCommand) | Method: $HttpMethod | Timestamp: $Timestamp"
diff --git a/Public/Get-LMReportExecutionTask.ps1 b/Public/Get-LMReportExecutionTask.ps1
new file mode 100644
index 0000000..7402a2d
--- /dev/null
+++ b/Public/Get-LMReportExecutionTask.ps1
@@ -0,0 +1,77 @@
+<#
+.SYNOPSIS
+Retrieves the status of a LogicMonitor report execution task.
+
+.DESCRIPTION
+Get-LMReportExecutionTask fetches information about a previously triggered report execution task.
+Supply the report identifier (ID or name) along with the task ID returned from
+Invoke-LMReportExecution to check completion status or retrieve the result URL.
+
+.PARAMETER ReportId
+The ID of the report whose execution task should be retrieved.
+
+.PARAMETER ReportName
+The name of the report whose execution task should be retrieved.
+
+.PARAMETER TaskId
+The execution task identifier returned when the report was triggered.
+
+.EXAMPLE
+Invoke-LMReportExecution -Id 42 | Select-Object -ExpandProperty taskId | Get-LMReportExecutionTask -ReportId 42
+
+Gets the execution status for the specified report/task combination.
+
+.EXAMPLE
+$task = Invoke-LMReportExecution -Name "Monthly Availability"
+Get-LMReportExecutionTask -ReportName "Monthly Availability" -TaskId $task.taskId
+
+Checks the task status for the report by name.
+
+.NOTES
+You must run Connect-LMAccount before running this command.
+#>
+function Get-LMReportExecutionTask {
+
+ [CmdletBinding(DefaultParameterSetName = 'ReportId')]
+ param (
+ [Parameter(Mandatory, ParameterSetName = 'ReportId')]
+ [Int]$ReportId,
+
+ [Parameter(Mandatory, ParameterSetName = 'ReportName')]
+ [String]$ReportName,
+
+ [Parameter(Mandatory)]
+ [String]$TaskId
+ )
+
+ if (-not $Script:LMAuth.Valid) {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ return
+ }
+
+ $resolvedReportId = $null
+
+ switch ($PSCmdlet.ParameterSetName) {
+ 'ReportId' {
+ $resolvedReportId = $ReportId
+ }
+ 'ReportName' {
+ $lookup = Get-LMReport -Name $ReportName
+ if (Test-LookupResult -Result $lookup.Id -LookupString $ReportName) {
+ return
+ }
+ $resolvedReportId = $lookup.Id
+ }
+ }
+
+ $resourcePath = "/report/reports/$resolvedReportId/tasks/$TaskId"
+ $headers = New-LMHeader -Auth $Script:LMAuth -Method 'GET' -ResourcePath $resourcePath
+ $uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $resourcePath
+
+ Resolve-LMDebugInfo -Url $uri -Headers $headers[0] -Command $MyInvocation
+
+ $response = Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $uri -Method 'GET' -Headers $headers[0] -WebSession $headers[1]
+
+ return (Add-ObjectTypeInfo -InputObject $response -TypeName 'LogicMonitor.ReportExecutionTask')
+}
+
diff --git a/Public/Invoke-LMReportExecution.ps1 b/Public/Invoke-LMReportExecution.ps1
new file mode 100644
index 0000000..70658f0
--- /dev/null
+++ b/Public/Invoke-LMReportExecution.ps1
@@ -0,0 +1,89 @@
+<#
+.SYNOPSIS
+Triggers the execution of a LogicMonitor report.
+
+.DESCRIPTION
+Invoke-LMReportExecution starts an on-demand run of a LogicMonitor report. The report can be
+identified by ID or name. Optional parameters allow impersonating another admin or overriding the
+email recipients for the generated output.
+
+.PARAMETER Id
+The ID of the report to execute.
+
+.PARAMETER Name
+The name of the report to execute.
+
+.PARAMETER WithAdminId
+The admin ID to impersonate when generating the report. Defaults to the current user when omitted.
+
+.PARAMETER ReceiveEmails
+One or more email addresses (comma-separated) that should receive the generated report.
+
+.EXAMPLE
+Invoke-LMReportExecution -Id 42
+
+Starts an immediate execution of the report with ID 42 using the current user's context.
+
+.EXAMPLE
+Invoke-LMReportExecution -Name "Monthly Availability" -WithAdminId 101 -ReceiveEmails "ops@example.com"
+
+Runs the "Monthly Availability" report as admin ID 101 and emails the results to ops@example.com.
+
+.NOTES
+You must run Connect-LMAccount before running this command.
+#>
+function Invoke-LMReportExecution {
+
+ [CmdletBinding(DefaultParameterSetName = 'Id')]
+ param (
+ [Parameter(Mandatory, ParameterSetName = 'Id')]
+ [Int]$Id,
+
+ [Parameter(Mandatory, ParameterSetName = 'Name')]
+ [String]$Name,
+
+ [Int]$WithAdminId = 0,
+
+ [String]$ReceiveEmails
+ )
+
+ if (-not $Script:LMAuth.Valid) {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ return
+ }
+
+ $reportId = $null
+
+ switch ($PSCmdlet.ParameterSetName) {
+ 'Id' {
+ $reportId = $Id
+ }
+ 'Name' {
+ $lookup = Get-LMReport -Name $Name
+ if (Test-LookupResult -Result $lookup.Id -LookupString $Name) {
+ return
+ }
+ $reportId = $lookup.Id
+ }
+ }
+
+ $resourcePath = "/report/reports/$reportId/executions"
+
+ $Data = @{
+ withAdminId = $WithAdminId
+ receiveEmails = $ReceiveEmails
+ }
+
+ $body = Format-LMData -Data $Data -UserSpecifiedKeys $PSBoundParameters.Keys
+
+ $headers = New-LMHeader -Auth $Script:LMAuth -Method 'POST' -ResourcePath $resourcePath -Data $body
+
+ $uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $resourcePath
+
+ Resolve-LMDebugInfo -Url $uri -Headers $headers[0] -Command $MyInvocation -Payload $body
+
+ $response = Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $uri -Method 'POST' -Headers $headers[0] -WebSession $headers[1] -Body $body
+
+ return (Add-ObjectTypeInfo -InputObject $response -TypeName 'LogicMonitor.ReportExecutionTask')
+}
+
From 29eb08b096eacfefdba8b8fd638c6eaad059de1e Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 22:40:10 -0400
Subject: [PATCH 11/17] Add Integration cmdlets
### New Cmdlets
- **Get-LMIntegration**: Retrieve integration configurations from LogicMonitor.
- **Remove-LMIntegration**: Remove integrations by ID or name.
- **Remove-LMEscalationChain**: Remove escalation chains by ID or name.
---
Logic.Monitor.Format.ps1xml | 47 ++++++++++++
Public/Get-LMIntegration.ps1 | 113 ++++++++++++++++++++++++++++
Public/Remove-LMEscalationChain.ps1 | 91 ++++++++++++++++++++++
Public/Remove-LMIntegration.ps1 | 91 ++++++++++++++++++++++
README.md | 26 +++++++
5 files changed, 368 insertions(+)
create mode 100644 Public/Get-LMIntegration.ps1
create mode 100644 Public/Remove-LMEscalationChain.ps1
create mode 100644 Public/Remove-LMIntegration.ps1
diff --git a/Logic.Monitor.Format.ps1xml b/Logic.Monitor.Format.ps1xml
index 4c95b50..404b522 100644
--- a/Logic.Monitor.Format.ps1xml
+++ b/Logic.Monitor.Format.ps1xml
@@ -1453,6 +1453,53 @@
+
+
+ LogicMonitorIntegration
+
+ LogicMonitor.Integration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
+ name
+
+
+ type
+
+
+ enabledStatus
+
+
+ description
+
+
+
+
+
+
LogicMonitorRecentlyDeleted
diff --git a/Public/Get-LMIntegration.ps1 b/Public/Get-LMIntegration.ps1
new file mode 100644
index 0000000..3ae3c07
--- /dev/null
+++ b/Public/Get-LMIntegration.ps1
@@ -0,0 +1,113 @@
+<#
+.SYNOPSIS
+Retrieves integrations from LogicMonitor.
+
+.DESCRIPTION
+The Get-LMIntegration function retrieves integration configurations from LogicMonitor. It can retrieve all integrations, a specific integration by ID or name, or filter the results.
+
+.PARAMETER Id
+The ID of the specific integration to retrieve.
+
+.PARAMETER Name
+The name of the specific integration to retrieve.
+
+.PARAMETER Filter
+A filter object to apply when retrieving integrations.
+
+.PARAMETER BatchSize
+The number of results to return per request. Must be between 1 and 1000. Defaults to 1000.
+
+.EXAMPLE
+#Retrieve all integrations
+Get-LMIntegration
+
+.EXAMPLE
+#Retrieve a specific integration by name
+Get-LMIntegration -Name "Slack-Integration"
+
+.NOTES
+You must run Connect-LMAccount before running this command.
+
+.INPUTS
+None. You cannot pipe objects to this command.
+
+.OUTPUTS
+Returns integration objects from LogicMonitor.
+#>
+
+function Get-LMIntegration {
+
+ [CmdletBinding(DefaultParameterSetName = 'All')]
+ param (
+ [Parameter(ParameterSetName = 'Id')]
+ [Int]$Id,
+
+ [Parameter(ParameterSetName = 'Name')]
+ [String]$Name,
+
+ [Parameter(ParameterSetName = 'Filter')]
+ [Object]$Filter,
+
+ [ValidateRange(1, 1000)]
+ [Int]$BatchSize = 1000
+ )
+ #Check if we are logged in and have valid api creds
+ if ($Script:LMAuth.Valid) {
+
+ #Build header and uri
+ $ResourcePath = "/setting/integrations"
+
+ #Initalize vars
+ $QueryParams = ""
+ $Count = 0
+ $Done = $false
+ $Results = @()
+
+ #Loop through requests
+ while (!$Done) {
+ #Build query params
+ switch ($PSCmdlet.ParameterSetName) {
+ "All" { $QueryParams = "?size=$BatchSize&offset=$Count&sort=+id" }
+ "Id" { $resourcePath += "/$Id" }
+ "Name" { $QueryParams = "?filter=name:`"$Name`"&size=$BatchSize&offset=$Count&sort=+id" }
+ "Filter" {
+ #List of allowed filter props
+ $PropList = @()
+ $ValidFilter = Format-LMFilter -Filter $Filter -PropList $PropList
+ $QueryParams = "?filter=$ValidFilter&size=$BatchSize&offset=$Count&sort=+id"
+ }
+ }
+
+ $Headers = New-LMHeader -Auth $Script:LMAuth -Method "GET" -ResourcePath $ResourcePath
+ $Uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $ResourcePath + $QueryParams
+
+
+
+ Resolve-LMDebugInfo -Url $Uri -Headers $Headers[0] -Command $MyInvocation
+
+ #Issue request
+ $Response = Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $Uri -Method "GET" -Headers $Headers[0] -WebSession $Headers[1]
+
+ #Stop looping if single device, no need to continue
+ if ($PSCmdlet.ParameterSetName -eq "Id") {
+ $Done = $true
+ return (Add-ObjectTypeInfo -InputObject $Response -TypeName "LogicMonitor.Integration" )
+ }
+ #Check result size and if needed loop again
+ else {
+ [Int]$Total = $Response.Total
+ [Int]$Count += ($Response.Items | Measure-Object).Count
+ $Results += $Response.Items
+ if ($Count -ge $Total) {
+ $Done = $true
+ }
+ }
+
+ }
+ return (Add-ObjectTypeInfo -InputObject $Results -TypeName "LogicMonitor.Integration" )
+ }
+ else {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ }
+}
+
diff --git a/Public/Remove-LMEscalationChain.ps1 b/Public/Remove-LMEscalationChain.ps1
new file mode 100644
index 0000000..890a68c
--- /dev/null
+++ b/Public/Remove-LMEscalationChain.ps1
@@ -0,0 +1,91 @@
+<#
+.SYNOPSIS
+Removes a LogicMonitor escalation chain.
+
+.DESCRIPTION
+The Remove-LMEscalationChain function removes a LogicMonitor escalation chain based on either its ID or name.
+
+.PARAMETER Id
+Specifies the ID of the escalation chain to be removed. This parameter is mandatory when using the 'Id' parameter set.
+
+.PARAMETER Name
+Specifies the name of the escalation chain to be removed. This parameter is mandatory when using the 'Name' parameter set.
+
+.EXAMPLE
+Remove-LMEscalationChain -Id 12345
+Removes the LogicMonitor escalation chain with ID 12345.
+
+.EXAMPLE
+Remove-LMEscalationChain -Name "Critical-Alerts"
+Removes the LogicMonitor escalation chain with the name "Critical-Alerts".
+
+.INPUTS
+You can pipe input to this function.
+
+.OUTPUTS
+Returns a PSCustomObject containing the ID of the removed escalation chain and a message indicating the success of the removal operation.
+#>
+function Remove-LMEscalationChain {
+
+ [CmdletBinding(DefaultParameterSetName = 'Id', SupportsShouldProcess, ConfirmImpact = 'High')]
+ param (
+ [Parameter(Mandatory, ParameterSetName = 'Id', ValueFromPipelineByPropertyName)]
+ [Int]$Id,
+
+ [Parameter(Mandatory, ParameterSetName = 'Name')]
+ [String]$Name
+
+ )
+ #Check if we are logged in and have valid api creds
+ begin {}
+ process {
+ if ($Script:LMAuth.Valid) {
+
+ #Lookup Id if supplying name
+ if ($Name) {
+ $LookupResult = (Get-LMEscalationChain -Name $Name).Id
+ if (Test-LookupResult -Result $LookupResult -LookupString $Name) {
+ return
+ }
+ $Id = $LookupResult
+ }
+
+ if ($PSItem) {
+ $Message = "Id: $Id | Name: $($PSItem.name)"
+ }
+ elseif ($Name) {
+ $Message = "Id: $Id | Name: $Name"
+ }
+ else {
+ $Message = "Id: $Id"
+ }
+
+ #Build header and uri
+ $ResourcePath = "/setting/alert/chains/$Id"
+
+
+ if ($PSCmdlet.ShouldProcess($Message, "Remove Escalation Chain")) {
+ $Headers = New-LMHeader -Auth $Script:LMAuth -Method "DELETE" -ResourcePath $ResourcePath
+ $Uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $ResourcePath
+
+ Resolve-LMDebugInfo -Url $Uri -Headers $Headers[0] -Command $MyInvocation
+
+ #Issue request
+ Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $Uri -Method "DELETE" -Headers $Headers[0] -WebSession $Headers[1] | Out-Null
+
+ $Result = [PSCustomObject]@{
+ Id = $Id
+ Message = "Successfully removed ($Message)"
+ }
+
+ return $Result
+ }
+
+ }
+ else {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ }
+ }
+ end {}
+}
+
diff --git a/Public/Remove-LMIntegration.ps1 b/Public/Remove-LMIntegration.ps1
new file mode 100644
index 0000000..797d2be
--- /dev/null
+++ b/Public/Remove-LMIntegration.ps1
@@ -0,0 +1,91 @@
+<#
+.SYNOPSIS
+Removes a LogicMonitor integration.
+
+.DESCRIPTION
+The Remove-LMIntegration function removes a LogicMonitor integration based on either its ID or name.
+
+.PARAMETER Id
+Specifies the ID of the integration to be removed. This parameter is mandatory when using the 'Id' parameter set.
+
+.PARAMETER Name
+Specifies the name of the integration to be removed. This parameter is mandatory when using the 'Name' parameter set.
+
+.EXAMPLE
+Remove-LMIntegration -Id 12345
+Removes the LogicMonitor integration with ID 12345.
+
+.EXAMPLE
+Remove-LMIntegration -Name "Slack-Integration"
+Removes the LogicMonitor integration with the name "Slack-Integration".
+
+.INPUTS
+You can pipe input to this function.
+
+.OUTPUTS
+Returns a PSCustomObject containing the ID of the removed integration and a message indicating the success of the removal operation.
+#>
+function Remove-LMIntegration {
+
+ [CmdletBinding(DefaultParameterSetName = 'Id', SupportsShouldProcess, ConfirmImpact = 'High')]
+ param (
+ [Parameter(Mandatory, ParameterSetName = 'Id', ValueFromPipelineByPropertyName)]
+ [Int]$Id,
+
+ [Parameter(Mandatory, ParameterSetName = 'Name')]
+ [String]$Name
+
+ )
+ #Check if we are logged in and have valid api creds
+ begin {}
+ process {
+ if ($Script:LMAuth.Valid) {
+
+ #Lookup Id if supplying name
+ if ($Name) {
+ $LookupResult = (Get-LMIntegration -Name $Name).Id
+ if (Test-LookupResult -Result $LookupResult -LookupString $Name) {
+ return
+ }
+ $Id = $LookupResult
+ }
+
+ if ($PSItem) {
+ $Message = "Id: $Id | Name: $($PSItem.name)"
+ }
+ elseif ($Name) {
+ $Message = "Id: $Id | Name: $Name"
+ }
+ else {
+ $Message = "Id: $Id"
+ }
+
+ #Build header and uri
+ $ResourcePath = "/setting/integrations/$Id"
+
+
+ if ($PSCmdlet.ShouldProcess($Message, "Remove Integration")) {
+ $Headers = New-LMHeader -Auth $Script:LMAuth -Method "DELETE" -ResourcePath $ResourcePath
+ $Uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $ResourcePath
+
+ Resolve-LMDebugInfo -Url $Uri -Headers $Headers[0] -Command $MyInvocation
+
+ #Issue request
+ Invoke-LMRestMethod -CallerPSCmdlet $PSCmdlet -Uri $Uri -Method "DELETE" -Headers $Headers[0] -WebSession $Headers[1] | Out-Null
+
+ $Result = [PSCustomObject]@{
+ Id = $Id
+ Message = "Successfully removed ($Message)"
+ }
+
+ return $Result
+ }
+
+ }
+ else {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ }
+ }
+ end {}
+}
+
diff --git a/README.md b/README.md
index 0953a3f..db8438c 100644
--- a/README.md
+++ b/README.md
@@ -79,10 +79,23 @@ Connect-LMAccount -UseCachedCredential
- **Get-LMRecentlyDeleted**: Retrieve recycle-bin entries with optional date, resource type, and deleted-by filters.
- **Restore-LMRecentlyDeleted**: Batch restore recycle-bin items by recycle identifier.
- **Remove-LMRecentlyDeleted**: Permanently delete recycle-bin entries in bulk.
+- **Get-LMIntegration**: Retrieve integration configurations from LogicMonitor.
+- **Remove-LMIntegration**: Remove integrations by ID or name.
+- **Remove-LMEscalationChain**: Remove escalation chains by ID or name.
+- **Invoke-LMReportExecution**: Trigger on-demand execution of LogicMonitor reports with optional admin impersonation and custom email recipients.
+- **Get-LMReportExecutionTask**: Check the status and retrieve results of previously triggered report executions.
### Updated Cmdlets
- **Update-LogicMonitorModule**: Hardened for non-blocking version checks; failures are logged via `Write-Verbose` and never terminate connecting cmdlets.
- **Export-LMDeviceData**: CSV exports now expand datapoints into individual rows and JSON exports capture deeper datapoint structures.
+- **Set-LMWebsite**: Added `alertExpr` alias for `SSLAlertThresholds` parameter for improved API compatibility. Updated synopsis to reflect enhanced parameter validation.
+- **New-LMWebsite**: Added `alertExpr` alias for `SSLAlertThresholds` parameter for improved API compatibility.
+- **Format-LMFilter**: Enhanced filter string escaping to properly handle special characters like parentheses, dollar signs, ampersands, and brackets in filter expressions.
+
+### Bug Fixes
+- **Add-ObjectTypeInfo**: Fixed "Cannot bind argument to parameter 'InputObject' because it is null" error by adding `[AllowNull()]` attribute to handle successful but null API responses.
+- **Resolve-LMDebugInfo**: Improved HTTP method detection logic to correctly identify request types (GET, POST, PATCH, DELETE) based on cmdlet naming conventions and headers, fixing incorrect debug output.
+- **Invoke-LMRestMethod**: Added cleanup of internal `__LMMethod` diagnostic header before dispatching requests to prevent API errors.
### Examples
```powershell
@@ -97,6 +110,19 @@ Get-LMRecentlyDeleted -DeletedAfter (Get-Date).AddMonths(-1) | Select-Object -Ex
# Export device datapoints to CSV with flattened datapoint rows
Export-LMDeviceData -DeviceId 12345 -StartDate (Get-Date).AddHours(-6) -ExportFormat csv -ExportPath "C:\\Exports"
+
+# Retrieve all integrations
+Get-LMIntegration
+
+# Remove an integration by name
+Remove-LMIntegration -Name "Slack-Integration"
+
+# Remove an escalation chain by ID
+Remove-LMEscalationChain -Id 123
+
+# Trigger a report execution and check its status
+$task = Invoke-LMReportExecution -Name "Monthly Availability" -WithAdminId 101 -ReceiveEmails "ops@example.com"
+Get-LMReportExecutionTask -ReportName "Monthly Availability" -TaskId $task.taskId
```
From de5c03fddac696ede4fe27b977df4ed0b1d6b05e Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 23:45:35 -0400
Subject: [PATCH 12/17] Add Invoke-LMAPIRequest cmdlet
---
Documentation/Invoke-LMAPIRequest.md | 361 +++++++++++++
.../Utilities/Invoke-LMAPIRequest-Guide.md | 473 ++++++++++++++++++
Public/Invoke-LMAPIRequest.ps1 | 336 +++++++++++++
README.md | 16 +
4 files changed, 1186 insertions(+)
create mode 100644 Documentation/Invoke-LMAPIRequest.md
create mode 100644 Documentation/Utilities/Invoke-LMAPIRequest-Guide.md
create mode 100644 Public/Invoke-LMAPIRequest.ps1
diff --git a/Documentation/Invoke-LMAPIRequest.md b/Documentation/Invoke-LMAPIRequest.md
new file mode 100644
index 0000000..19f84ed
--- /dev/null
+++ b/Documentation/Invoke-LMAPIRequest.md
@@ -0,0 +1,361 @@
+---
+external help file: Logic.Monitor-help.xml
+Module Name: Logic.Monitor
+online version:
+schema: 2.0.0
+---
+
+# Invoke-LMAPIRequest
+
+## SYNOPSIS
+Executes a custom LogicMonitor API request with full control over endpoint and payload.
+
+## SYNTAX
+
+### Data (Default)
+```
+Invoke-LMAPIRequest -ResourcePath -Method [-QueryParams ] [-Data ]
+ [-Version ] [-ContentType ] [-MaxRetries ] [-NoRetry] [-OutFile ]
+ [-TypeName ] [-AsHashtable] [-WhatIf] [-Confirm] []
+```
+
+### RawBody
+```
+Invoke-LMAPIRequest -ResourcePath -Method [-QueryParams ] [-RawBody ]
+ [-Version ] [-ContentType ] [-MaxRetries ] [-NoRetry] [-OutFile ]
+ [-TypeName ] [-AsHashtable] [-WhatIf] [-Confirm] []
+```
+
+## DESCRIPTION
+The Invoke-LMAPIRequest function provides advanced users with direct access to the LogicMonitor API
+while leveraging the module's authentication, retry logic, debug utilities, and error handling.
+This is useful for accessing API endpoints that don't yet have dedicated cmdlets in the module.
+
+## EXAMPLES
+
+### EXAMPLE 1
+```powershell
+Invoke-LMAPIRequest -ResourcePath "/setting/integrations" -Method GET
+```
+
+Get a custom resource not yet supported by a dedicated cmdlet.
+
+### EXAMPLE 2
+```powershell
+$data = @{
+ name = "My Integration"
+ type = "slack"
+ url = "https://hooks.slack.com/services/..."
+}
+Invoke-LMAPIRequest -ResourcePath "/setting/integrations" -Method POST -Data $data
+```
+
+Create a resource with custom payload.
+
+### EXAMPLE 3
+```powershell
+$updates = @{
+ description = "Updated description"
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices/123" -Method PATCH -Data $updates
+```
+
+Update a resource with PATCH.
+
+### EXAMPLE 4
+```powershell
+Invoke-LMAPIRequest -ResourcePath "/setting/integrations/456" -Method DELETE
+```
+
+Delete a resource.
+
+### EXAMPLE 5
+```powershell
+$queryParams = @{
+ size = 500
+ filter = 'status:"active"'
+ fields = "id,name,status"
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET -QueryParams $queryParams -Version 3
+```
+
+Get with query parameters and custom version.
+
+### EXAMPLE 6
+```powershell
+$rawJson = '{"name":"test","customField":null}'
+Invoke-LMAPIRequest -ResourcePath "/custom/endpoint" -Method POST -RawBody $rawJson
+```
+
+Use raw body for special formatting requirements.
+
+### EXAMPLE 7
+```powershell
+Invoke-LMAPIRequest -ResourcePath "/report/reports/123/download" -Method GET -OutFile "C:\Reports\report.pdf"
+```
+
+Download a report to file.
+
+### EXAMPLE 8
+```powershell
+$offset = 0
+$size = 1000
+$allResults = @()
+do {
+ $response = Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET -QueryParams @{ size = $size; offset = $offset }
+ $allResults += $response.items
+ $offset += $size
+} while ($allResults.Count -lt $response.total)
+```
+
+Get paginated results manually.
+
+## PARAMETERS
+
+### -ResourcePath
+The API resource path (e.g., "/device/devices", "/setting/integrations/123").
+Do not include the base URL or query parameters here.
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+
+Required: True
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -Method
+The HTTP method to use. Valid values: GET, POST, PATCH, PUT, DELETE.
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+Accepted values: GET, POST, PATCH, PUT, DELETE
+
+Required: True
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -QueryParams
+Optional hashtable of query parameters to append to the request URL.
+Example: @{ size = 100; offset = 0; filter = 'name:"test"' }
+
+```yaml
+Type: Hashtable
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -Data
+Optional hashtable containing the request body data. Will be automatically converted to JSON.
+Use this for POST, PATCH, and PUT requests.
+
+```yaml
+Type: Hashtable
+Parameter Sets: Data
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -RawBody
+Optional raw string body to send with the request. Use this instead of -Data when you need
+complete control over the request body format. Mutually exclusive with -Data.
+
+```yaml
+Type: String
+Parameter Sets: RawBody
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -Version
+The X-Version header value for the API request. Defaults to 3.
+Some newer API endpoints may require different version numbers.
+
+```yaml
+Type: Int32
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: 3
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -ContentType
+The Content-Type header for the request. Defaults to "application/json".
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: application/json
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -MaxRetries
+Maximum number of retry attempts for transient errors. Defaults to 3.
+Set to 0 to disable retries.
+
+```yaml
+Type: Int32
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: 3
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -NoRetry
+Switch to completely disable retry logic and fail immediately on any error.
+
+```yaml
+Type: SwitchParameter
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: False
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -OutFile
+Path to save the response content to a file. Useful for downloading reports or exports.
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -TypeName
+Optional type name to add to the returned objects (e.g., "LogicMonitor.CustomResource").
+This enables proper formatting if you have custom format definitions.
+
+```yaml
+Type: String
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -AsHashtable
+Switch to return the response as a hashtable instead of a PSCustomObject.
+
+```yaml
+Type: SwitchParameter
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: False
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -WhatIf
+Shows what would happen if the cmdlet runs. The cmdlet is not run.
+
+```yaml
+Type: SwitchParameter
+Parameter Sets: (All)
+Aliases: wi
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### -Confirm
+Prompts you for confirmation before running the cmdlet.
+
+```yaml
+Type: SwitchParameter
+Parameter Sets: (All)
+Aliases: cf
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
+### CommonParameters
+This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
+
+## INPUTS
+
+### None
+You cannot pipe objects to this command.
+
+## OUTPUTS
+
+### System.Object
+Returns the API response as a PSCustomObject by default, or as specified by -AsHashtable.
+
+## NOTES
+You must run Connect-LMAccount before running this command.
+
+This cmdlet is designed for advanced users who need to:
+- Access API endpoints not yet covered by dedicated cmdlets
+- Test new API features or beta endpoints
+- Implement custom workflows requiring direct API access
+- Prototype new functionality before requesting cmdlet additions
+
+For standard operations, use the dedicated cmdlets (Get-LMDevice, New-LMDevice, etc.) as they
+provide better parameter validation, documentation, and user experience.
+
+## RELATED LINKS
+
+[Module Documentation](https://logicmonitor.github.io/lm-powershell-module-docs/)
+
diff --git a/Documentation/Utilities/Invoke-LMAPIRequest-Guide.md b/Documentation/Utilities/Invoke-LMAPIRequest-Guide.md
new file mode 100644
index 0000000..d0b4344
--- /dev/null
+++ b/Documentation/Utilities/Invoke-LMAPIRequest-Guide.md
@@ -0,0 +1,473 @@
+# Invoke-LMAPIRequest - Advanced User Guide
+
+## Overview
+
+`Invoke-LMAPIRequest` is a universal API request cmdlet designed for advanced users who need direct access to LogicMonitor API endpoints that don't yet have dedicated cmdlets in the module. It provides full control over API requests while leveraging the module's robust infrastructure.
+
+## Why Use This Cmdlet?
+
+### Problem Statement
+With over 200+ cmdlets in the Logic.Monitor module, we still don't cover every API endpoint. Advanced users who discover new endpoints in the LogicMonitor API documentation often have to:
+- Reinvent authentication handling
+- Implement retry logic for transient failures
+- Handle rate limiting manually
+- Build debug utilities from scratch
+- Deal with error handling inconsistencies
+
+### Solution
+`Invoke-LMAPIRequest` provides a "bring your own endpoint" approach that:
+- ✅ Uses existing module authentication (API keys, Bearer tokens, Session sync)
+- ✅ Leverages built-in retry logic with exponential backoff
+- ✅ Integrates with module debug utilities (`-Debug`, `Resolve-LMDebugInfo`)
+- ✅ Handles rate limiting automatically
+- ✅ Provides consistent error handling
+- ✅ Supports all HTTP methods (GET, POST, PATCH, PUT, DELETE)
+- ✅ Allows custom API version headers
+- ✅ Includes ShouldProcess support for safety
+
+## Key Features
+
+### 1. Full CRUD Operation Support
+```powershell
+# CREATE - POST
+$data = @{ name = "New Resource"; type = "custom" }
+Invoke-LMAPIRequest -ResourcePath "/custom/endpoint" -Method POST -Data $data
+
+# READ - GET
+Invoke-LMAPIRequest -ResourcePath "/custom/endpoint/123" -Method GET
+
+# UPDATE - PATCH
+$updates = @{ description = "Updated" }
+Invoke-LMAPIRequest -ResourcePath "/custom/endpoint/123" -Method PATCH -Data $updates
+
+# DELETE
+Invoke-LMAPIRequest -ResourcePath "/custom/endpoint/123" -Method DELETE
+```
+
+**Important:** You must format data according to the API's requirements. For example, `customProperties` must be an array:
+```powershell
+# ❌ Wrong - simple hashtable
+$data = @{
+ name = "device1"
+ customProperties = @{ prop1 = "value1" }
+}
+
+# ✅ Correct - array of name/value objects
+$data = @{
+ name = "device1"
+ customProperties = @(
+ @{ name = "prop1"; value = "value1" }
+ @{ name = "prop2"; value = "value2" }
+ )
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method POST -Data $data
+```
+
+### 2. Query Parameter Support
+```powershell
+$queryParams = @{
+ size = 1000
+ offset = 0
+ filter = 'status:"active"'
+ fields = "id,name,status,created"
+ sort = "+name"
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET -QueryParams $queryParams
+```
+
+### 3. Custom API Versions
+Some newer LogicMonitor API endpoints require different version numbers:
+```powershell
+# Use API version 4 for newer endpoints
+Invoke-LMAPIRequest -ResourcePath "/new/endpoint" -Method GET -Version 4
+```
+
+### 4. Raw Body Control
+For special cases where you need exact control over JSON formatting:
+```powershell
+$rawJson = @"
+{
+ "name": "test",
+ "customField": null,
+ "preservedFormatting": true
+}
+"@
+Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method POST -RawBody $rawJson
+```
+
+### 5. File Downloads
+```powershell
+# Download reports or exports
+Invoke-LMAPIRequest -ResourcePath "/report/reports/123/download" -Method GET -OutFile "C:\Reports\monthly.pdf"
+```
+
+### 6. Manual Pagination
+```powershell
+function Get-AllDevicesManually {
+ $offset = 0
+ $size = 1000
+ $allResults = @()
+
+ do {
+ $response = Invoke-LMAPIRequest `
+ -ResourcePath "/device/devices" `
+ -Method GET `
+ -QueryParams @{ size = $size; offset = $offset; sort = "+id" }
+
+ $allResults += $response.items
+ $offset += $size
+ Write-Progress -Activity "Fetching devices" -Status "$($allResults.Count) of $($response.total)"
+ } while ($allResults.Count -lt $response.total)
+
+ return $allResults
+}
+```
+
+### 7. Type Information
+Add custom type names for proper formatting:
+```powershell
+$result = Invoke-LMAPIRequest `
+ -ResourcePath "/custom/endpoint" `
+ -Method GET `
+ -TypeName "LogicMonitor.CustomResource"
+```
+
+### 8. Hashtable Output
+For easier property manipulation:
+```powershell
+$result = Invoke-LMAPIRequest `
+ -ResourcePath "/device/devices/123" `
+ -Method GET `
+ -AsHashtable
+
+$result["customProperty"] = "newValue"
+```
+
+### 9. Formatting Output
+Control how API responses are displayed:
+```powershell
+# Default: Returns raw objects (PowerShell chooses format automatically)
+$devices = Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET
+
+# For table view, pipe to Format-Table with specific properties
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET |
+ Format-Table id, name, displayName, status, collectorId
+
+# Or use Select-Object to choose properties, then let PowerShell format
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET |
+ Select-Object id, name, displayName, status |
+ Format-Table -AutoSize
+
+# For detailed view of a single item
+Invoke-LMAPIRequest -ResourcePath "/device/devices/123" -Method GET | Format-List
+
+# Use custom type name to leverage existing format definitions
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET -TypeName "LogicMonitor.Device"
+# This will use the LogicMonitor.Device format definition from Logic.Monitor.Format.ps1xml
+
+# Pipeline remains fully functional
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET |
+ Where-Object { $_.status -eq 'normal' } |
+ Select-Object id, name, customProperties |
+ Export-Csv devices.csv # ✅ Works perfectly!
+```
+
+**Tip:** For frequently used endpoints, create a wrapper function with custom formatting:
+```powershell
+function Get-MyCustomResource {
+ Invoke-LMAPIRequest -ResourcePath "/custom/endpoint" -Method GET |
+ Format-Table id, name, status, created -AutoSize
+}
+```
+
+## Design Patterns
+
+### Pattern 1: Testing New API Features
+```powershell
+# Test a beta endpoint before requesting a dedicated cmdlet
+$betaData = @{
+ feature = "new-capability"
+ enabled = $true
+}
+Invoke-LMAPIRequest -ResourcePath "/beta/features" -Method POST -Data $betaData -Version 4
+```
+
+### Pattern 2: Bulk Operations
+```powershell
+# Bulk update multiple resources
+$deviceIds = 1..100
+foreach ($id in $deviceIds) {
+ $updates = @{ description = "Bulk updated $(Get-Date)" }
+ Invoke-LMAPIRequest -ResourcePath "/device/devices/$id" -Method PATCH -Data $updates
+ Start-Sleep -Milliseconds 100 # Rate limiting
+}
+```
+
+### Pattern 3: Custom Workflows
+```powershell
+# Complex workflow combining multiple API calls
+function Deploy-CustomConfiguration {
+ param($ConfigName, $Targets)
+
+ # Step 1: Create configuration
+ $config = @{ name = $ConfigName; type = "custom" }
+ $created = Invoke-LMAPIRequest -ResourcePath "/configs" -Method POST -Data $config
+
+ # Step 2: Apply to targets
+ foreach ($target in $Targets) {
+ $assignment = @{ configId = $created.id; targetId = $target }
+ Invoke-LMAPIRequest -ResourcePath "/configs/assignments" -Method POST -Data $assignment
+ }
+
+ # Step 3: Verify deployment
+ $status = Invoke-LMAPIRequest -ResourcePath "/configs/$($created.id)/status" -Method GET
+ return $status
+}
+```
+
+### Pattern 4: Error Handling
+```powershell
+try {
+ $result = Invoke-LMAPIRequest `
+ -ResourcePath "/risky/endpoint" `
+ -Method POST `
+ -Data $data `
+ -ErrorAction Stop
+
+ Write-Host "Success: $($result.id)"
+}
+catch {
+ Write-Error "API request failed: $($_.Exception.Message)"
+ # Fallback logic here
+}
+```
+
+## Best Practices
+
+### 1. Use Dedicated Cmdlets When Available
+```powershell
+# ❌ Don't do this
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET
+
+# ✅ Do this instead
+Get-LMDevice
+```
+
+### 2. Validate Input Before Sending
+```powershell
+function New-CustomResource {
+ param($Name, $Type)
+
+ # Validate locally first
+ if ([string]::IsNullOrWhiteSpace($Name)) {
+ throw "Name cannot be empty"
+ }
+
+ $data = @{ name = $Name; type = $Type }
+ Invoke-LMAPIRequest -ResourcePath "/custom/resources" -Method POST -Data $data
+}
+```
+
+### 3. Use -WhatIf for Testing
+```powershell
+# Test without making actual changes
+Invoke-LMAPIRequest `
+ -ResourcePath "/device/devices/123" `
+ -Method DELETE `
+ -WhatIf
+```
+
+### 4. Leverage Debug Output
+```powershell
+# Enable debug to see full request details
+Invoke-LMAPIRequest `
+ -ResourcePath "/endpoint" `
+ -Method POST `
+ -Data $data `
+ -Debug
+```
+
+### 5. Handle Pagination Properly
+```powershell
+# For large datasets, use pagination
+$size = 1000 # Max batch size
+$offset = 0
+do {
+ $batch = Invoke-LMAPIRequest `
+ -ResourcePath "/large/dataset" `
+ -Method GET `
+ -QueryParams @{ size = $size; offset = $offset }
+
+ Process-Batch $batch.items
+ $offset += $size
+} while ($batch.items.Count -eq $size)
+```
+
+## Integration with Module Features
+
+### Authentication
+Automatically uses the current session from `Connect-LMAccount`:
+```powershell
+Connect-LMAccount -AccessId $id -AccessKey $key -AccountName "company"
+Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method GET
+# No need to handle auth manually!
+```
+
+### Retry Logic
+Built-in exponential backoff for transient failures:
+```powershell
+# Automatically retries on 429, 502, 503, 504
+Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method GET -MaxRetries 5
+
+# Disable retries for time-sensitive operations
+Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method GET -NoRetry
+```
+
+### Debug Information
+Integrates with module debug utilities:
+```powershell
+# Shows full request details including headers, URL, payload
+$DebugPreference = "Continue"
+Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method POST -Data $data
+```
+
+## Common Use Cases
+
+### 1. Accessing New API Endpoints
+```powershell
+# LogicMonitor releases a new API endpoint
+# Use Invoke-LMAPIRequest until a dedicated cmdlet is available
+Invoke-LMAPIRequest -ResourcePath "/new/feature" -Method GET
+```
+
+### 2. Custom Integrations
+```powershell
+# Build custom integrations with external systems
+$webhookData = @{
+ url = "https://external-system.com/webhook"
+ events = @("alert.created", "alert.cleared")
+}
+Invoke-LMAPIRequest -ResourcePath "/integrations/webhooks" -Method POST -Data $webhookData
+```
+
+### 3. Prototyping
+```powershell
+# Prototype new functionality before requesting cmdlet additions
+# Test different approaches quickly
+$approaches = @(
+ @{ method = "approach1"; params = @{} },
+ @{ method = "approach2"; params = @{} }
+)
+
+foreach ($approach in $approaches) {
+ $result = Invoke-LMAPIRequest `
+ -ResourcePath "/test/endpoint" `
+ -Method POST `
+ -Data $approach
+
+ Measure-Performance $result
+}
+```
+
+### 4. Advanced Filtering
+```powershell
+# Complex filters not yet supported by dedicated cmdlets
+$complexFilter = 'name~"prod-*" && status:"active" && customProperties.environment:"production"'
+Invoke-LMAPIRequest `
+ -ResourcePath "/device/devices" `
+ -Method GET `
+ -QueryParams @{ filter = $complexFilter; size = 1000 }
+```
+
+## Troubleshooting
+
+### Issue: "Invalid json body" or "Cannot deserialize" Errors
+
+This usually means your data structure doesn't match the API's expected format.
+
+**Common Issue: customProperties Format**
+```powershell
+# ❌ Wrong - This will fail
+$data = @{
+ name = "device1"
+ customProperties = @{ environment = "prod" } # Simple hashtable
+}
+
+# ✅ Correct - Array of name/value objects
+$data = @{
+ name = "device1"
+ customProperties = @(
+ @{ name = "environment"; value = "prod" }
+ )
+}
+```
+
+**Solution:** Check the LogicMonitor API documentation or look at how dedicated cmdlets format the data:
+```powershell
+# Compare with working cmdlet
+New-LMDevice -Name "test" -DisplayName "test" -PreferredCollectorId 1 -Properties @{test="value"} -Debug
+
+# Look at the "Request Payload" in debug output to see the correct format
+```
+
+### Issue: Authentication Errors
+```powershell
+# Verify you're connected
+if (-not $Script:LMAuth.Valid) {
+ Connect-LMAccount -AccessId $id -AccessKey $key -AccountName "company"
+}
+```
+
+### Issue: Rate Limiting
+```powershell
+# Increase retry attempts or add delays
+Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method GET -MaxRetries 10
+
+# Or add manual delays in loops
+foreach ($item in $items) {
+ Invoke-LMAPIRequest -ResourcePath "/endpoint/$item" -Method GET
+ Start-Sleep -Milliseconds 500
+}
+```
+
+### Issue: Unexpected Response Format
+```powershell
+# Use -Debug to see raw response
+$result = Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method GET -Debug
+
+# Or capture as hashtable for inspection
+$result = Invoke-LMAPIRequest -ResourcePath "/endpoint" -Method GET -AsHashtable
+$result.Keys | ForEach-Object { Write-Host "$_: $($result[$_])" }
+```
+
+## Contributing
+
+If you find yourself frequently using `Invoke-LMAPIRequest` for a specific endpoint, consider:
+1. Opening a GitHub issue requesting a dedicated cmdlet
+2. Contributing a PR with the new cmdlet implementation
+3. Sharing your use case to help prioritize development
+
+## Related Cmdlets
+
+- `Connect-LMAccount` - Establish authentication
+- `Get-LMDevice`, `New-LMDevice`, etc. - Dedicated resource cmdlets
+- `Build-LMFilter` - Interactive filter builder
+- `Invoke-LMRestMethod` - Internal REST method wrapper (not for direct use)
+
+## Summary
+
+`Invoke-LMAPIRequest` bridges the gap between the module's current capabilities and the full LogicMonitor API surface area. It empowers advanced users to:
+- Access any API endpoint immediately
+- Prototype new functionality
+- Build custom integrations
+- Test beta features
+
+While maintaining the benefits of:
+- Centralized authentication
+- Robust error handling
+- Automatic retries
+- Consistent debugging
+- Module best practices
+
+Use it when you need flexibility, but prefer dedicated cmdlets for routine operations.
+
diff --git a/Public/Invoke-LMAPIRequest.ps1 b/Public/Invoke-LMAPIRequest.ps1
new file mode 100644
index 0000000..997835f
--- /dev/null
+++ b/Public/Invoke-LMAPIRequest.ps1
@@ -0,0 +1,336 @@
+<#
+.SYNOPSIS
+Executes a custom LogicMonitor API request with full control over endpoint and payload.
+
+.DESCRIPTION
+The Invoke-LMAPIRequest function provides advanced users with direct access to the LogicMonitor API
+while leveraging the module's authentication, retry logic, debug utilities, and error handling.
+This is useful for accessing API endpoints that don't yet have dedicated cmdlets in the module.
+
+.PARAMETER ResourcePath
+The API resource path (e.g., "/device/devices", "/setting/integrations/123").
+Do not include the base URL or query parameters here.
+
+.PARAMETER Method
+The HTTP method to use. Valid values: GET, POST, PATCH, PUT, DELETE.
+
+.PARAMETER QueryParams
+Optional hashtable of query parameters to append to the request URL.
+Example: @{ size = 100; offset = 0; filter = 'name:"test"' }
+
+.PARAMETER Data
+Optional hashtable containing the request body data. Will be automatically converted to JSON.
+Use this for POST, PATCH, and PUT requests.
+
+.PARAMETER RawBody
+Optional raw string body to send with the request. Use this instead of -Data when you need
+complete control over the request body format. Mutually exclusive with -Data.
+
+.PARAMETER Version
+The X-Version header value for the API request. Defaults to 3.
+Some newer API endpoints may require different version numbers.
+
+.PARAMETER ContentType
+The Content-Type header for the request. Defaults to "application/json".
+
+.PARAMETER MaxRetries
+Maximum number of retry attempts for transient errors. Defaults to 3.
+Set to 0 to disable retries.
+
+.PARAMETER NoRetry
+Switch to completely disable retry logic and fail immediately on any error.
+
+.PARAMETER OutFile
+Path to save the response content to a file. Useful for downloading reports or exports.
+
+.PARAMETER TypeName
+Optional type name to add to the returned objects (e.g., "LogicMonitor.CustomResource").
+This enables proper formatting if you have custom format definitions.
+
+.PARAMETER AsHashtable
+Switch to return the response as a hashtable instead of a PSCustomObject.
+
+.EXAMPLE
+# Get a custom resource not yet supported by a dedicated cmdlet
+Invoke-LMAPIRequest -ResourcePath "/setting/integrations" -Method GET
+
+.EXAMPLE
+# Create a resource with custom payload
+$data = @{
+ name = "My Integration"
+ type = "slack"
+ url = "https://hooks.slack.com/services/..."
+}
+Invoke-LMAPIRequest -ResourcePath "/setting/integrations" -Method POST -Data $data
+
+.EXAMPLE
+# Create a device with custom properties (note the array format)
+$data = @{
+ name = "server1"
+ displayName = "Production Server"
+ preferredCollectorId = 5
+ customProperties = @(
+ @{ name = "environment"; value = "production" }
+ @{ name = "owner"; value = "ops-team" }
+ )
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method POST -Data $data
+
+.EXAMPLE
+# Update a resource with PATCH
+$updates = @{
+ description = "Updated description"
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices/123" -Method PATCH -Data $updates
+
+.EXAMPLE
+# Delete a resource
+Invoke-LMAPIRequest -ResourcePath "/setting/integrations/456" -Method DELETE
+
+.EXAMPLE
+# Get with query parameters and custom version
+$queryParams = @{
+ size = 500
+ filter = 'status:"active"'
+ fields = "id,name,status"
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET -QueryParams $queryParams -Version 3
+
+.EXAMPLE
+# Use raw body for special formatting requirements
+$rawJson = '{"name":"test","customField":null}'
+Invoke-LMAPIRequest -ResourcePath "/custom/endpoint" -Method POST -RawBody $rawJson
+
+.EXAMPLE
+# Download a report to file
+Invoke-LMAPIRequest -ResourcePath "/report/reports/123/download" -Method GET -OutFile "C:\Reports\report.pdf"
+
+.EXAMPLE
+# Format output as table
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET | Format-Table id, name, displayName, status
+
+# Use existing format definition
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET -TypeName "LogicMonitor.Device"
+
+.EXAMPLE
+# Get paginated results manually
+$offset = 0
+$size = 1000
+$allResults = @()
+do {
+ $response = Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method GET -QueryParams @{ size = $size; offset = $offset }
+ $allResults += $response.items
+ $offset += $size
+} while ($allResults.Count -lt $response.total)
+
+.NOTES
+You must run Connect-LMAccount before running this command.
+
+This cmdlet is designed for advanced users who need to:
+- Access API endpoints not yet covered by dedicated cmdlets
+- Test new API features or beta endpoints
+- Implement custom workflows requiring direct API access
+- Prototype new functionality before requesting cmdlet additions
+
+For standard operations, use the dedicated cmdlets (Get-LMDevice, New-LMDevice, etc.) as they
+provide better parameter validation, documentation, and user experience.
+
+.INPUTS
+None. You cannot pipe objects to this command.
+
+.OUTPUTS
+Returns the API response as a PSCustomObject by default, or as specified by -AsHashtable.
+#>
+function Invoke-LMAPIRequest {
+
+ [CmdletBinding(DefaultParameterSetName = 'Data', SupportsShouldProcess, ConfirmImpact = 'Medium')]
+ param (
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [String]$ResourcePath,
+
+ [Parameter(Mandatory)]
+ [ValidateSet("GET", "POST", "PATCH", "PUT", "DELETE")]
+ [String]$Method,
+
+ [Hashtable]$QueryParams,
+
+ [Parameter(ParameterSetName = 'Data')]
+ [Hashtable]$Data,
+
+ [Parameter(ParameterSetName = 'RawBody')]
+ [String]$RawBody,
+
+ [ValidateRange(1, 10)]
+ [Int]$Version = 3,
+
+ [String]$ContentType = "application/json",
+
+ [ValidateRange(0, 10)]
+ [Int]$MaxRetries = 3,
+
+ [Switch]$NoRetry,
+
+ [String]$OutFile,
+
+ [String]$TypeName,
+
+ [Switch]$AsHashtable
+ )
+
+ #Check if we are logged in and have valid api creds
+ if (-not $Script:LMAuth.Valid) {
+ Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
+ return
+ }
+
+ # Ensure ResourcePath starts with /
+ if (-not $ResourcePath.StartsWith('/')) {
+ $ResourcePath = '/' + $ResourcePath
+ }
+
+ # Build the request body
+ $Body = $null
+ if ($PSCmdlet.ParameterSetName -eq 'Data' -and $Data) {
+ # Convert hashtable to JSON
+ $Body = $Data | ConvertTo-Json -Depth 10 -Compress
+ }
+ elseif ($PSCmdlet.ParameterSetName -eq 'RawBody' -and $RawBody) {
+ $Body = $RawBody
+ }
+
+ # Build query string from QueryParams hashtable
+ $QueryString = ""
+ if ($QueryParams -and $QueryParams.Count -gt 0) {
+ $queryParts = @()
+ foreach ($key in $QueryParams.Keys) {
+ $value = $QueryParams[$key]
+ if ($null -ne $value) {
+ # URL encode the value
+ $encodedValue = [System.Web.HttpUtility]::UrlEncode($value.ToString())
+ $queryParts += "$key=$encodedValue"
+ }
+ }
+ if ($queryParts.Count -gt 0) {
+ $QueryString = "?" + ($queryParts -join "&")
+ }
+ }
+
+ # Build the full URI
+ $Uri = "https://$($Script:LMAuth.Portal).$(Get-LMPortalURI)" + $ResourcePath + $QueryString
+
+ # Create a message for ShouldProcess
+ $operationDescription = switch ($Method) {
+ "GET" { "Retrieve from" }
+ "POST" { "Create resource at" }
+ "PATCH" { "Update resource at" }
+ "PUT" { "Replace resource at" }
+ "DELETE" { "Delete resource at" }
+ }
+
+ # Adjust ConfirmImpact based on method
+ $shouldProcessTarget = $ResourcePath
+ $shouldProcessAction = $operationDescription
+
+ # Determine if we should prompt based on method
+ # GET requests should never prompt unless explicitly requested
+ # DELETE requests should always prompt unless explicitly suppressed
+ $shouldPrompt = $true
+ if ($Method -eq "GET") {
+ # For GET, only process if -Confirm was explicitly passed
+ if ($PSBoundParameters.ContainsKey('Confirm')) {
+ $shouldPrompt = $PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessAction)
+ }
+ else {
+ $shouldPrompt = $true # Skip ShouldProcess for GET
+ }
+ }
+ elseif ($Method -eq "DELETE") {
+ # For DELETE, always use ShouldProcess with High impact
+ $shouldPrompt = $PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessAction, "Are you sure you want to delete this resource?")
+ }
+ else {
+ # For POST, PATCH, PUT use normal ShouldProcess
+ $shouldPrompt = $PSCmdlet.ShouldProcess($shouldProcessTarget, $shouldProcessAction)
+ }
+
+ if ($shouldPrompt) {
+
+ # Generate headers with custom version
+ $Headers = New-LMHeader -Auth $Script:LMAuth -Method $Method -ResourcePath $ResourcePath -Data $Body -Version $Version -ContentType $ContentType
+
+ # Output debug information
+ Resolve-LMDebugInfo -Url $Uri -Headers $Headers[0] -Command $MyInvocation -Payload $Body
+
+ # Build parameters for Invoke-LMRestMethod
+ $restParams = @{
+ Uri = $Uri
+ Method = $Method
+ Headers = $Headers[0]
+ WebSession = $Headers[1]
+ CallerPSCmdlet = $PSCmdlet
+ }
+
+ if ($Body) {
+ $restParams.Body = $Body
+ }
+
+ if ($OutFile) {
+ $restParams.OutFile = $OutFile
+ }
+
+ if ($MaxRetries -eq 0 -or $NoRetry) {
+ $restParams.NoRetry = $true
+ }
+ else {
+ $restParams.MaxRetries = $MaxRetries
+ }
+
+ # Issue request
+ $Response = Invoke-LMRestMethod @restParams
+
+ # Handle the response
+ if ($null -eq $Response) {
+ return $null
+ }
+
+ # If OutFile was specified, the response is already saved
+ if ($OutFile) {
+ Write-Verbose "Response saved to: $OutFile"
+ return [PSCustomObject]@{
+ Success = $true
+ FilePath = $OutFile
+ Message = "Response saved successfully"
+ }
+ }
+
+ # Return the items array if it exists, otherwise return the response object
+ if ($Response.items) {
+ $Response = $Response.items
+ }
+
+ # Add type information if specified
+ if ($TypeName) {
+ $Response = Add-ObjectTypeInfo -InputObject $Response -TypeName $TypeName
+ }
+
+ # Convert to hashtable if requested
+ if ($AsHashtable -and $Response) {
+ if ($Response -is [Array]) {
+ return $Response | ForEach-Object {
+ $hashtable = @{}
+ $_.PSObject.Properties | ForEach-Object { $hashtable[$_.Name] = $_.Value }
+ $hashtable
+ }
+ }
+ else {
+ $hashtable = @{}
+ $Response.PSObject.Properties | ForEach-Object { $hashtable[$_.Name] = $_.Value }
+ return $hashtable
+ }
+ }
+
+ return $Response
+ }
+}
+
diff --git a/README.md b/README.md
index db8438c..34910c0 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,7 @@ Connect-LMAccount -UseCachedCredential
- **Remove-LMEscalationChain**: Remove escalation chains by ID or name.
- **Invoke-LMReportExecution**: Trigger on-demand execution of LogicMonitor reports with optional admin impersonation and custom email recipients.
- **Get-LMReportExecutionTask**: Check the status and retrieve results of previously triggered report executions.
+- **Invoke-LMAPIRequest**: Universal API request cmdlet for advanced users to access any LogicMonitor API endpoint with custom payloads while leveraging module authentication, retry logic, and debug utilities.
### Updated Cmdlets
- **Update-LogicMonitorModule**: Hardened for non-blocking version checks; failures are logged via `Write-Verbose` and never terminate connecting cmdlets.
@@ -123,6 +124,21 @@ Remove-LMEscalationChain -Id 123
# Trigger a report execution and check its status
$task = Invoke-LMReportExecution -Name "Monthly Availability" -WithAdminId 101 -ReceiveEmails "ops@example.com"
Get-LMReportExecutionTask -ReportName "Monthly Availability" -TaskId $task.taskId
+
+# Use the universal API request cmdlet for endpoints
+Invoke-LMAPIRequest -ResourcePath "/setting/integrations" -Method GET -QueryParams @{ size = 500 }
+
+# Create a device with full control
+$customData = @{
+ name = "1.1.1.1"
+ displayName = "Custom Device"
+ preferredCollectorId = 76
+ deviceType = 0
+ customProperties = @(
+ @{name="propname";value="value"}
+ )
+}
+Invoke-LMAPIRequest -ResourcePath "/device/devices" -Method POST -Data $customData -Version 3
```
From 7160bbcbc432332953c527d90bb4558bd1001672 Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 23:53:56 -0400
Subject: [PATCH 13/17] fix verbose in build fix test case for devices
---
.github/workflows/test-win.yml | 2 +-
.github/workflows/test.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml
index fd9907d..49d4274 100644
--- a/.github/workflows/test-win.yml
+++ b/.github/workflows/test-win.yml
@@ -46,7 +46,7 @@ jobs:
$Result = Invoke-Pester -Container $Container -Output Detailed -PassThru
#Write OpsNote to test portal indicating test status
- Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging -Verbose
+ Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging
$TimeNow = Get-Date -UFormat %m%d%Y-%H%M
$OpsNote = New-LMOpsNote -Note "Github test build submitted on $TimeNow - $($Result.Result)" -Tags @("GithubActions","TestPipeline-Win5.1","PSVersion-$Version")
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e3c0ca3..4385aa1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -46,7 +46,7 @@ jobs:
$Result = Invoke-Pester -Container $Container -Output Detailed -PassThru
#Write OpsNote to test portal indicating test status
- Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging -Verbose
+ Connect-LMAccount -AccessId $env:LM_ACCESS_ID -AccessKey $env:LM_ACCESS_KEY -AccountName $env:LM_PORTAL -DisableConsoleLogging
$TimeNow = Get-Date -UFormat %m%d%Y-%H%M
$OpsNote = New-LMOpsNote -Note "Github test build submitted on $TimeNow - $($Result.Result)" -Tags @("GithubActions","TestPipeline-Core","PSVersion-$Version")
From 632d39ac1bf9c12a87480e317addcf4cc778597a Mon Sep 17 00:00:00 2001
From: Steve Villardi <42367049+stevevillardi@users.noreply.github.com>
Date: Wed, 29 Oct 2025 23:57:36 -0400
Subject: [PATCH 14/17] update support docs for v7.7.0
---
Documentation/Get-LMIntegration.md | 146 ++++
Documentation/Get-LMRecentlyDeleted.md | 166 +++++
Documentation/Get-LMReportExecutionTask.md | 121 ++++
Documentation/Invoke-LMAPIRequest.md | 738 +++++++++++----------
Documentation/Invoke-LMReportExecution.md | 138 ++++
Documentation/New-LMWebsite.md | 2 +-
Documentation/Remove-LMEscalationChain.md | 135 ++++
Documentation/Remove-LMIntegration.md | 134 ++++
Documentation/Remove-LMRecentlyDeleted.md | 108 +++
Documentation/Restore-LMRecentlyDeleted.md | 108 +++
Documentation/Set-LMWebsite.md | 2 +-
11 files changed, 1435 insertions(+), 363 deletions(-)
create mode 100644 Documentation/Get-LMIntegration.md
create mode 100644 Documentation/Get-LMRecentlyDeleted.md
create mode 100644 Documentation/Get-LMReportExecutionTask.md
create mode 100644 Documentation/Invoke-LMReportExecution.md
create mode 100644 Documentation/Remove-LMEscalationChain.md
create mode 100644 Documentation/Remove-LMIntegration.md
create mode 100644 Documentation/Remove-LMRecentlyDeleted.md
create mode 100644 Documentation/Restore-LMRecentlyDeleted.md
diff --git a/Documentation/Get-LMIntegration.md b/Documentation/Get-LMIntegration.md
new file mode 100644
index 0000000..d841028
--- /dev/null
+++ b/Documentation/Get-LMIntegration.md
@@ -0,0 +1,146 @@
+---
+external help file: Logic.Monitor-help.xml
+Module Name: Logic.Monitor
+online version:
+schema: 2.0.0
+---
+
+# Get-LMIntegration
+
+## SYNOPSIS
+Retrieves integrations from LogicMonitor.
+
+## SYNTAX
+
+### All (Default)
+```
+Get-LMIntegration [-BatchSize ] [-ProgressAction ] []
+```
+
+### Id
+```
+Get-LMIntegration [-Id ] [-BatchSize ] [-ProgressAction ] []
+```
+
+### Name
+```
+Get-LMIntegration [-Name ] [-BatchSize ] [-ProgressAction ]
+ []
+```
+
+### Filter
+```
+Get-LMIntegration [-Filter