# List-ComputerGroups.ps1 — robust fallback + HTTP 400 handling + introspection param( [string]$Url, [string]$Token, [switch]$SkipCertCheck, [int]$PageSize = 200, [string]$NameLike, [switch]$Raw, [switch]$Grid, [string]$ExportCsv, [switch]$Introspect ) $ErrorActionPreference = 'Stop' try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} function Get-GatewayUri { param([Parameter(Mandatory)][string]$HostLike) $h = $HostLike.Trim() if ($h -match '^https?://') { $h = $h -replace '^https?://','' } $h = $h.TrimEnd('/') if ([string]::IsNullOrWhiteSpace($h)) { throw 'TaniumUrl empty after normalization.' } "https://$h/plugin/products/gateway/graphql" } # TLS bypass for Windows PowerShell 5.1 $script:__oldCb = $null function Enter-InsecureTls { param([switch]$Enable) if (-not $Enable) { return } $script:__oldCb = [System.Net.ServicePointManager]::ServerCertificateValidationCallback $cb = [System.Net.Security.RemoteCertificateValidationCallback]{ param($s,$c,$ch,$e) $true } [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $cb } function Exit-InsecureTls { if ($script:__oldCb -ne $null) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $script:__oldCb $script:__oldCb = $null } } # Load config if needed $BaseDir = if ($PSScriptRoot) { $PSScriptRoot } else { $pwd.Path } $configPath = Join-Path $BaseDir 'config.json' if (-not $Url -or -not $Token) { if (-not (Test-Path $configPath)) { throw "Missing -Url/-Token and config.json not found: $configPath" } $cfg = Get-Content -Path $configPath -Raw | ConvertFrom-Json if (-not $Url) { $Url = [string]$cfg.TaniumUrl } if (-not $Token) { $Token = [string]$cfg.TaniumApiToken } if (-not $PSBoundParameters.ContainsKey('SkipCertCheck')) { $SkipCertCheck = [bool]$cfg.SkipCertificateCheck } } $gateway = Get-GatewayUri -HostLike $Url $headers = @{ 'Content-Type' = 'application/json'; session = $Token } # ---- Introspection (optional) if ($Introspect) { $IntrospectQuery = @' query { __type(name: "ComputerGroup") { name fields { name type { kind name ofType { kind name } } } } } '@ $body = @{ query = $IntrospectQuery } | ConvertTo-Json -Depth 6 $resp = $null $ps7 = ($PSVersionTable.PSVersion.Major -ge 7) if ($ps7 -and $SkipCertCheck) { $resp = Invoke-RestMethod -SkipCertificateCheck -Method Post -Uri $gateway -Headers $headers -Body $body } else { try { Enter-InsecureTls -Enable:$SkipCertCheck; $resp = Invoke-RestMethod -Method Post -Uri $gateway -Headers $headers -Body $body } finally { Exit-InsecureTls } } if ($resp.errors) { Write-Error (($resp.errors | ForEach-Object message) -join '; '); return } $resp.data.__type.fields | Select-Object name,@{n='type';e={$_.type.name ?? $_.type.ofType.name}} | Format-Table -AutoSize return } # ---- Queries (order = safest → richer) $Q_Basic = @' query listComputerGroups($first: Int, $after: Cursor) { computerGroups(first: $first, after: $after) { edges { node { id name managementRightsEnabled } } pageInfo { hasNextPage endCursor } } } '@ $Q_ContentSet = @' query listComputerGroups($first: Int, $after: Cursor) { computerGroups(first: $first, after: $after) { edges { node { id name managementRightsEnabled contentSet { name } } } pageInfo { hasNextPage endCursor } } } '@ $Q_ContentSetV2 = @' query listComputerGroups($first: Int, $after: Cursor) { computerGroups(first: $first, after: $after) { edges { node { id name managementRightsEnabled contentSetV2 { name } } } pageInfo { hasNextPage endCursor } } } '@ $Q_Filter = @' query listComputerGroups($first: Int, $after: Cursor) { computerGroups(first: $first, after: $after) { edges { node { id name managementRightsEnabled filter { memberOf { name } sensor { name } op value } } } pageInfo { hasNextPage endCursor } } } '@ $queries = @($Q_Basic, $Q_ContentSet, $Q_ContentSetV2, $Q_Filter) function Invoke-Gql { param([string]$Query, [hashtable]$Vars, [string]$OpName) $body = @{ query = $Query; variables = $Vars; operationName = $OpName } | ConvertTo-Json -Depth 10 $ps7 = ($PSVersionTable.PSVersion.Major -ge 7) try { if ($ps7 -and $SkipCertCheck) { return Invoke-RestMethod -SkipCertificateCheck -Method Post -Uri $gateway -Headers $headers -ContentType 'application/json' -Body $body } else { Enter-InsecureTls -Enable:$SkipCertCheck return Invoke-RestMethod -Method Post -Uri $gateway -Headers $headers -ContentType 'application/json' -Body $body } } catch { # If server returned HTTP 400 with a JSON GraphQL error, parse it and return so we can fallback. $we = $_.Exception if ($we.Response) { try { $sr = New-Object IO.StreamReader($we.Response.GetResponseStream()) $txt = $sr.ReadToEnd() if ($txt) { return ($txt | ConvertFrom-Json) } } catch { } } throw # non-GraphQL error -> bubble up } finally { Exit-InsecureTls } } # ---- Fetch with fallback chain $allEdges = New-Object System.Collections.Generic.List[object] $cursor = $null $qIndex = 0 $opName = 'listComputerGroups' while ($true) { $vars = @{ first = $PageSize; after = $cursor } $resp = Invoke-Gql -Query $queries[$qIndex] -Vars $vars -OpName $opName if ($resp.errors) { if ($qIndex -lt ($queries.Count - 1)) { # restart from page 1 with next query $qIndex++; $cursor = $null; $allEdges.Clear() continue } else { throw ("GraphQL errors: " + (($resp.errors | ForEach-Object { $_.message }) -join '; ')) } } $edges = $resp.data.computerGroups.edges if ($edges) { $allEdges.AddRange($edges) } $pi = $resp.data.computerGroups.pageInfo if (-not $pi -or -not $pi.hasNextPage) { break } $cursor = $pi.endCursor } if ($Raw) { [pscustomobject]@{ total = $allEdges.Count; edges = $allEdges } | ConvertTo-Json -Depth 14; return } if ($allEdges.Count -eq 0) { Write-Output 'No computer groups returned.'; return } function Get-ContentSetName($n) { if ($n.PSObject.Properties.Match('contentSet').Count -gt 0 -and $n.contentSet) { return $n.contentSet.name } if ($n.PSObject.Properties.Match('contentSetV2').Count -gt 0 -and $n.contentSetV2) { return $n.contentSetV2.name } return '' } function Get-FilterSummary($n) { if (-not $n.PSObject.Properties.Match('filter')) { return '' } $f = $n.filter; if ($null -eq $f) { return '' } if ($f.memberOf -and $f.memberOf.name) { return ("memberOf: " + $f.memberOf.name) } if ($f.sensor -and $f.sensor.name) { return ("sensor '" + $f.sensor.name + "' " + $f.op + " '" + $f.value + "'") } return '' } $rows = foreach ($edge in $allEdges) { $n = $edge.node if ($NameLike -and -not ($n.name -match $NameLike)) { continue } [pscustomobject]@{ Id = $n.id Name = $n.name ManagementRights = $n.managementRightsEnabled ContentSet = (Get-ContentSetName $n) Filter = (Get-FilterSummary $n) } } if (-not $rows) { Write-Output 'No matching groups.'; return } $rowsSorted = $rows | Sort-Object Name if ($ExportCsv) { try { $rowsSorted | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $ExportCsv; Write-Host "Exported to: $ExportCsv" -ForegroundColor Green } catch { Write-Warning "CSV export failed: $($_.Exception.Message)" } } if ($Grid) { $title = "Computer Groups — {0} item(s)" -f ($rowsSorted.Count) if (Get-Command Out-GridView -ErrorAction SilentlyContinue) { $rowsSorted | Out-GridView -Title $title; return } else { Write-Warning "Out-GridView not available. On PS7+: Install-Module Microsoft.PowerShell.GraphicalTools" } } $rowsSorted | Format-Table -AutoSize