Public/Add-TCMMonitorType.ps1
|
function Add-TCMMonitorType { <# .SYNOPSIS Add resource types from a template to an existing monitor without losing drift. .DESCRIPTION When you want to expand monitoring coverage (e.g., add CISA SCuBA types to an existing SecurityCritical monitor), this cmdlet: 1. Reads the current monitor baseline 2. Determines which new resource types are needed 3. Takes a targeted snapshot of ONLY those new types 4. Merges the new resources into the existing baseline 5. Updates the monitor with the merged baseline IMPORTANT: Updating a monitor baseline DOES delete existing drift records. However, this cmdlet minimizes impact — your existing baseline values stay identical, so TCM will immediately re-detect the same drifts on the next monitoring cycle (within 6 hours). Only the drift *history* is lost, not the detection ability. .PARAMETER MonitorId The monitor to expand. If omitted, uses the first active monitor. .PARAMETER MonitorName Resolve monitor by display name instead of ID. .PARAMETER Template Name(s) of built-in templates (from templates/ folder). Available: EasyTCM-SecurityCritical, EasyTCM-Recommended, CISA-SCuBA-Entra, CISA-SCuBA-Exchange, CISA-SCuBA-Teams .PARAMETER TemplatePath Path(s) to custom template JSON files. .PARAMETER Force Skip the confirmation prompt. .EXAMPLE # Add CISA Exchange + Teams types to existing monitor Add-TCMMonitorTypes -Template CISA-SCuBA-Exchange, CISA-SCuBA-Teams .EXAMPLE # Expand to full Recommended profile Add-TCMMonitorTypes -Template EasyTCM-Recommended .EXAMPLE # Non-interactive Add-TCMMonitorTypes -Template CISA-SCuBA-Entra -Force #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [string]$MonitorId, [string]$MonitorName, [string[]]$Template, [string[]]$TemplatePath, [switch]$Force ) if (-not $Template -and -not $TemplatePath) { throw 'Specify at least one -Template or -TemplatePath.' } Write-Host '' Write-Host '➕ Add-TCMMonitorType — Expand monitor coverage without rebaselining' -ForegroundColor Cyan Write-Host '' # ── Resolve monitor ───────────────────────────────────────────── Write-Host 'Retrieving current monitor...' -ForegroundColor White $monitor = if ($MonitorId) { Get-TCMMonitor -Id $MonitorId -IncludeBaseline } elseif ($MonitorName) { $all = Get-TCMMonitor $match = @($all) | Where-Object { $dn = if ($_ -is [System.Collections.IDictionary]) { $_['displayName'] } else { $_.displayName } $dn -eq $MonitorName } if (-not $match) { Write-Error "No monitor found with name '$MonitorName'." return } $matchId = if ($match -is [System.Collections.IDictionary]) { $match['id'] } else { $match.id } Get-TCMMonitor -Id $matchId -IncludeBaseline } else { $all = Get-TCMMonitor if (-not $all) { Write-Error 'No monitor found. Create one first with Start-TCMMonitoring.' return } $first = if ($all -is [array]) { $all[0] } else { $all } $firstId = if ($first -is [System.Collections.IDictionary]) { $first['id'] } else { $first.id } Get-TCMMonitor -Id $firstId -IncludeBaseline } if (-not $monitor) { Write-Error 'Monitor not found.' return } $mId = if ($monitor -is [System.Collections.IDictionary]) { $monitor['id'] } else { $monitor.id } $mName = if ($monitor -is [System.Collections.IDictionary]) { $monitor['displayName'] } else { $monitor.displayName } $existingBaseline = if ($monitor -is [System.Collections.IDictionary]) { $monitor['Baseline'] } else { $monitor.Baseline } # Get existing resource types $existingResources = @() if ($existingBaseline) { $bResources = if ($existingBaseline -is [System.Collections.IDictionary]) { $existingBaseline['resources'] } else { $existingBaseline.resources } if ($bResources) { $existingResources = @($bResources) } } $existingTypes = @($existingResources | ForEach-Object { if ($_ -is [System.Collections.IDictionary]) { $_['resourceType'] } else { $_.resourceType } } | Sort-Object -Unique) Write-Host " Monitor: $mName" -ForegroundColor DarkGray Write-Host " Current: $($existingResources.Count) resources across $($existingTypes.Count) types" -ForegroundColor DarkGray # ── Resolve template types ────────────────────────────────────── $templateTypes = [System.Collections.Generic.List[string]]::new() $templateNames = [System.Collections.Generic.List[string]]::new() $templatesDir = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'templates' foreach ($name in @($Template)) { if (-not $name) { continue } $fileName = $name.ToLower() + '.json' $path = Join-Path $templatesDir $fileName if (-not (Test-Path $path)) { throw "Template '$name' not found at: $path" } $tmpl = Get-Content $path -Raw | ConvertFrom-Json $templateNames.Add($tmpl.metadata.displayName) foreach ($rt in $tmpl.resourceTypes) { if ($rt -notin $templateTypes) { $templateTypes.Add($rt) } } } foreach ($path in @($TemplatePath)) { if (-not $path) { continue } if (-not (Test-Path $path)) { throw "Template file not found: $path" } $tmpl = Get-Content $path -Raw | ConvertFrom-Json $templateNames.Add($tmpl.metadata.displayName) foreach ($rt in $tmpl.resourceTypes) { if ($rt -notin $templateTypes) { $templateTypes.Add($rt) } } } # ── Determine new types ───────────────────────────────────────── $newTypes = @($templateTypes | Where-Object { $_ -notin $existingTypes }) Write-Host '' Write-Host " Template(s): $($templateNames -join ' + ')" -ForegroundColor Cyan Write-Host " Template types: $($templateTypes.Count) total" -ForegroundColor DarkGray Write-Host " Already monitored: $($templateTypes.Count - $newTypes.Count)" -ForegroundColor DarkGray Write-Host " New types to add: $($newTypes.Count)" -ForegroundColor $(if ($newTypes.Count -gt 0) { 'Yellow' } else { 'Green' }) if ($newTypes.Count -eq 0) { Write-Host '' Write-Host '✅ Monitor already covers all resource types in the specified template(s).' -ForegroundColor Green Write-Host '' return } foreach ($nt in $newTypes) { $shortName = ($nt -split '\.')[-1] Write-Host " + $shortName ($nt)" -ForegroundColor Yellow } Write-Host '' Write-Host " ⚠ Updating the baseline will clear existing drift records." -ForegroundColor Yellow Write-Host " Existing drifts will be re-detected within 6 hours (baseline values preserved)." -ForegroundColor DarkGray # ── Confirmation ──────────────────────────────────────────────── if (-not $Force) { if (-not $PSCmdlet.ShouldProcess("Monitor '$mName'", "Add $($newTypes.Count) resource types (clears drift history)")) { return } } # ── Snapshot only the new types ───────────────────────────────── Write-Host '' Write-Host "Taking targeted snapshot of $($newTypes.Count) new resource types..." -ForegroundColor White $snapshotName = "AddTypes $(Get-Date -Format 'yyyyMMdd HHmmss')" $snapshot = New-TCMSnapshot -DisplayName $snapshotName -Resources $newTypes -Wait $snapshotStatus = if ($snapshot -is [System.Collections.IDictionary]) { $snapshot['status'] } else { $snapshot.status } $snapshotId = if ($snapshot -is [System.Collections.IDictionary]) { $snapshot['id'] } else { $snapshot.id } if ($snapshotStatus -notin @('succeeded', 'succeededWithWarnings', 'partiallySuccessful')) { Write-Error "Snapshot failed with status: $snapshotStatus" return } # ── Convert new types to baseline resources ───────────────────── Write-Host 'Converting new resources...' -ForegroundColor White $snapshotContent = Get-TCMSnapshot -Id $snapshotId -IncludeContent $newBaseline = ConvertTo-TCMBaseline -SnapshotContent $snapshotContent -Profile Full -DisplayName 'temp' $newResources = @() if ($newBaseline -and $newBaseline.Resources) { $newResources = @($newBaseline.Resources) } Write-Host " Found $($newResources.Count) resource instances in new types" -ForegroundColor DarkGray # ── Merge baselines ───────────────────────────────────────────── Write-Host 'Merging into existing baseline...' -ForegroundColor White $mergedResources = [System.Collections.Generic.List[object]]::new() # Keep all existing resources unchanged foreach ($r in $existingResources) { $mergedResources.Add($r) } # Add new resources foreach ($r in $newResources) { $mergedResources.Add($r) } $mergedBaseline = @{ DisplayName = $mName Resources = $mergedResources } # Quota check $dailyCost = $mergedResources.Count * 4 $quotaPercent = [math]::Round(($dailyCost / 800) * 100, 1) $color = if ($quotaPercent -gt 80) { 'Red' } elseif ($quotaPercent -gt 50) { 'Yellow' } else { 'Green' } Write-Host " Merged: $($mergedResources.Count) total resources (was $($existingResources.Count))" -ForegroundColor Cyan Write-Host " Quota impact: $dailyCost / 800 per day ($quotaPercent%)" -ForegroundColor $color if ($quotaPercent -gt 100) { Write-Warning "Merged baseline exceeds daily quota! Consider removing low-value types with -ExcludeResources on ConvertTo-TCMBaseline." } # ── Update the monitor ────────────────────────────────────────── Write-Host 'Updating monitor...' -ForegroundColor White Update-TCMMonitor -Id $mId -Baseline $mergedBaseline -Confirm:$false # ── Clean up snapshot ─────────────────────────────────────────── Remove-TCMSnapshot -Id $snapshotId -Confirm:$false 2>$null # ── Done ──────────────────────────────────────────────────────── Write-Host '' Write-Host '✅ Monitor expanded successfully!' -ForegroundColor Green Write-Host " $($existingResources.Count) existing + $($newResources.Count) new = $($mergedResources.Count) total resources" -ForegroundColor White Write-Host " $($newTypes.Count) resource types added from: $($templateNames -join ', ')" -ForegroundColor White Write-Host '' Write-Host ' Existing drift records were cleared but will be re-detected within 6 hours.' -ForegroundColor DarkGray Write-Host ' Next: Get-TCMQuota to verify quota | Show-TCMDrift after 6h' -ForegroundColor DarkGray Write-Host '' } |