From 851c85ec3d0e6045920b0d60b909f8cb67f6d3ba Mon Sep 17 00:00:00 2001 From: David Wuibaille Date: Fri, 31 Oct 2025 08:59:02 +0100 Subject: [PATCH] Auto-commit: 2025-10-31 08:59:02 --- .gitignore | 6 - API/ComputerGroup - Get.ps1 | 106 +++++++ API/ComputerGroup - New.ps1 | 115 ++++++++ API/Deploy - Create.ps1 | 196 +++++++++++++ API/Deploy - Get.ps1 | 59 ++++ API/Interact - GetQuestion.ps1 | 93 ++++++ API/Package - Create.ps1 | 208 ++++++++++++++ API/Package - Get.ps1 | 139 +++++++++ API/RBAC_CreateComputerGroup_graphql.ps1 | 218 +++++++++++++++ API/RBAC_ExportRole_Redden-TanREST.ps1 | 89 ++++++ API/RBAC_ImportRole_Redden-TanREST KO.ps1 | 264 ++++++++++++++++++ API/RBAC_ListComputerGroup_GraphQL.ps1 | 219 +++++++++++++++ API/RBAC_ListContentset_Rest.ps1 | 120 ++++++++ API/RBAC_ListRole_Rest.ps1 | 106 +++++++ ...RBAC_ListUserAndGroup_ComputerGroup KO.ps1 | 204 ++++++++++++++ ...C_ListUserAndGroup_Role_Redden-TanREST.ps1 | 220 +++++++++++++++ ...istUserAndGroup_details_Redden-TanREST.ps1 | 82 ++++++ API/Sensor_List_Rest.ps1 | 165 +++++++++++ API/graphql - CheckBiosVersion.ps1 | 237 ++++++++++++++++ API/graphql - EnvironmentVariable.ps1 | 119 ++++++++ API/graphql - ListComputerGroup.ps1 | 219 +++++++++++++++ API/rest - list sensor detail.ps1 | 238 ++++++++++++++++ API/test.ps1 | 89 ++++++ .../readme.md | 41 +++ Packages/RegistryOnHKLM/DisableWPAD.ps1 | 22 ++ Packages/RegistryOnHKLM/readme.md | 10 + Packages/shutdown.ps1 | 37 +++ Provision/unattend.xml/readme.md | 5 + Provision/unattend.xml/unattend.xml | 114 ++++++++ README.md | Bin 0 -> 22 bytes 30 files changed, 3734 insertions(+), 6 deletions(-) create mode 100644 API/ComputerGroup - Get.ps1 create mode 100644 API/ComputerGroup - New.ps1 create mode 100644 API/Deploy - Create.ps1 create mode 100644 API/Deploy - Get.ps1 create mode 100644 API/Interact - GetQuestion.ps1 create mode 100644 API/Package - Create.ps1 create mode 100644 API/Package - Get.ps1 create mode 100644 API/RBAC_CreateComputerGroup_graphql.ps1 create mode 100644 API/RBAC_ExportRole_Redden-TanREST.ps1 create mode 100644 API/RBAC_ImportRole_Redden-TanREST KO.ps1 create mode 100644 API/RBAC_ListComputerGroup_GraphQL.ps1 create mode 100644 API/RBAC_ListContentset_Rest.ps1 create mode 100644 API/RBAC_ListRole_Rest.ps1 create mode 100644 API/RBAC_ListUserAndGroup_ComputerGroup KO.ps1 create mode 100644 API/RBAC_ListUserAndGroup_Role_Redden-TanREST.ps1 create mode 100644 API/RBAC_ListUserAndGroup_details_Redden-TanREST.ps1 create mode 100644 API/Sensor_List_Rest.ps1 create mode 100644 API/graphql - CheckBiosVersion.ps1 create mode 100644 API/graphql - EnvironmentVariable.ps1 create mode 100644 API/graphql - ListComputerGroup.ps1 create mode 100644 API/rest - list sensor detail.ps1 create mode 100644 API/test.ps1 create mode 100644 Interact/Reg-Filter-Even-Numbered-hostnames/readme.md create mode 100644 Packages/RegistryOnHKLM/DisableWPAD.ps1 create mode 100644 Packages/RegistryOnHKLM/readme.md create mode 100644 Packages/shutdown.ps1 create mode 100644 Provision/unattend.xml/readme.md create mode 100644 Provision/unattend.xml/unattend.xml diff --git a/.gitignore b/.gitignore index 6eba7e8..d0aa614 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,3 @@ -config*.json -*.secret.json -.env -.env.* -*.key -*.pem # Ignore local config.json config.json diff --git a/API/ComputerGroup - Get.ps1 b/API/ComputerGroup - Get.ps1 new file mode 100644 index 0000000..d956101 --- /dev/null +++ b/API/ComputerGroup - Get.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS +Initialize Tanium session from config.json (no env vars). Prefer -CredentialObject (hashtable). +Falls back to -BaseURI/-Token or -BaseURI/-ApiToken, then ephemeral CLIXML if needed. +#> + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# --- Load config.json --- +$configPath = Join-Path $PSScriptRoot 'config.json' +if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } + +Write-Host "Reading configuration from: $configPath" +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + +$TaniumUrl = $config.TaniumUrl +$TaniumApiToken = $config.TaniumApiToken +if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumApiToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided in config.json." +} + +# Normalize to bare host (strip scheme and trailing slash) +$BaseUriHost = (($TaniumUrl -replace '^https?://','') -replace '/+$','') +$SecureToken = $TaniumApiToken | ConvertTo-SecureString -AsPlainText -Force + +# --- Initialize session (feature-detected) --- +Write-Host "Initializing Tanium session..." +$cmd = Get-Command Initialize-TaniumSession -ErrorAction Stop +$paramNames = $cmd.Parameters.Keys +$initialized = $false +$lastError = $null + +# 1) Preferred: -CredentialObject (expects a hashtable with ContainsKey) +if (-not $initialized -and ($paramNames -contains 'CredentialObject')) { + try { + $credHash = @{ + baseURI = $BaseUriHost + token = $SecureToken + } + Initialize-TaniumSession -CredentialObject $credHash + $initialized = $true + Write-Host "Session initialized via -CredentialObject (hashtable)." + } catch { $lastError = $_ } +} + +# 2) Fallback: -BaseURI/-Token (token may be SecureString or string depending on module) +if (-not $initialized -and ($paramNames -contains 'BaseURI') -and ($paramNames -contains 'Token')) { + try { + Initialize-TaniumSession -BaseURI $BaseUriHost -Token $SecureToken + $initialized = $true + Write-Host "Session initialized via -BaseURI/-Token (SecureString)." + } catch { + $lastError = $_ + try { + Initialize-TaniumSession -BaseURI $BaseUriHost -Token $TaniumApiToken + $initialized = $true + Write-Host "Session initialized via -BaseURI/-Token (plain string)." + } catch { $lastError = $_ } + } +} + +# 3) Fallback: -BaseURI/-ApiToken (some versions use ApiToken) +if (-not $initialized -and ($paramNames -contains 'BaseURI') -and ($paramNames -contains 'ApiToken')) { + try { + Initialize-TaniumSession -BaseURI $BaseUriHost -ApiToken $SecureToken + $initialized = $true + Write-Host "Session initialized via -BaseURI/-ApiToken (SecureString)." + } catch { + $lastError = $_ + try { + Initialize-TaniumSession -BaseURI $BaseUriHost -ApiToken $TaniumApiToken + $initialized = $true + Write-Host "Session initialized via -BaseURI/-ApiToken (plain string)." + } catch { $lastError = $_ } + } +} + +# 4) Last resort: ephemeral CLIXML (-PathToXML), then cleanup +if (-not $initialized -and ($paramNames -contains 'PathToXML')) { + try { + $TempXml = Join-Path $env:TEMP ('tanium-session-{0}.apicred' -f ([guid]::NewGuid())) + @{ baseURI = $BaseUriHost; token = $SecureToken } | Export-Clixml -Path $TempXml + Initialize-TaniumSession -PathToXML $TempXml + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + $initialized = $true + Write-Host "Session initialized via -PathToXML (ephemeral file removed)." + } catch { $lastError = $_ } +} + +if (-not $initialized) { + Write-Error "Failed to initialize Tanium session. Last error: $($lastError.Exception.Message)" + throw +} + +# --- Retrieve & display groups --- +Write-Host "Retrieving all Computer Groups..." +$groups = Get-ComputerGroup -All + +if (Get-Command Out-GridView -ErrorAction SilentlyContinue) { + $groups | Out-GridView -Title 'Tanium Computer Groups' +} else { + Write-Warning "Out-GridView not available; showing a console table instead." + $groups | Format-Table -Auto +} diff --git a/API/ComputerGroup - New.ps1 b/API/ComputerGroup - New.ps1 new file mode 100644 index 0000000..4d37c4a --- /dev/null +++ b/API/ComputerGroup - New.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS +Initialize Tanium session from config.json, then create Computer Groups only if they do not already exist. +#> + +# ========================= +# Block 1 - Prerequisites +# ========================= +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# ========================= +# Block 2 - Load config.json & init session +# ========================= +$configPath = Join-Path $PSScriptRoot 'config.json' +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' + +try { + if (-not (Test-Path $configPath)) { + throw "Configuration file not found: $configPath" + } + + Write-Host "Reading configuration from: $configPath" + $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + + # Values (fallback to environment variables) + $TaniumUrl = $config.TaniumUrl + $TaniumApiToken = $config.TaniumApiToken + if ([string]::IsNullOrWhiteSpace($TaniumUrl)) { $TaniumUrl = $env:TANIUM_URL } + if ([string]::IsNullOrWhiteSpace($TaniumApiToken)) { $TaniumApiToken = $env:TANIUM_TOKEN } + if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumApiToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided (config.json or environment variables)." + } + + # Normalize: bare host (no scheme / trailing slash) + if ($TaniumUrl -match '^https?://') { + $TaniumUrl = $TaniumUrl -replace '^https?://', '' -replace '/+$', '' + Write-Host "Normalized TaniumUrl to host: $TaniumUrl" + } + + # Build temporary CLIXML for Initialize-TaniumSession + $ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumApiToken | ConvertTo-SecureString -AsPlainText -Force) + } + Write-Host "Writing temporary CLIXML to: $TempXml" + $ExportObject | Export-Clixml -Path $TempXml + + Write-Host "Initializing Tanium session..." + Initialize-TaniumSession -PathToXML $TempXml + Write-Host "Tanium session initialized successfully." +} +catch { + Write-Error "Failed to initialize Tanium session. Details: $($_.Exception.Message)" + throw +} + +try { + # ========================= + # Block 3 - Ensure Computer Groups exist + # ========================= + + # Hashtable of groups to ensure: Name => Filter text + $ComputerGroups = @{ + "David_LTSC2019" = "(Computer Name contains 2019)" + "David_LTSC2021" = "(Computer Name contains 2021)" + "David_LTSC2024" = "(Computer Name contains 2024)" + } + + # Default Content Set (id = 0) – change if needed + $contentSetDefault = [pscustomobject]@{ id = 0 } + + Write-Host "Ensuring Computer Groups exist (create if missing)..." + foreach ($kv in $ComputerGroups.GetEnumerator()) { + $name = $kv.Key + $filterText = $kv.Value + + try { + $existing = Get-ComputerGroup -Name $name -ErrorAction SilentlyContinue + if ($existing) { + Write-Host "Group '$name' already exists (Id: $($existing.id)). Skipping." + continue + } + + Write-Host "Creating group: '$name' with filter: $filterText" + New-ComputerGroup ` + -Name $name ` + -Type 0 ` + -Text $filterText ` + -Content_Set $contentSetDefault ` + -Filter_Flag $true ` + -Management_Rights_Flag $true + + Write-Host "Created group: '$name'." + } + catch { + Write-Error "Failed processing group '$name'. Details: $($_.Exception.Message)" + } + } + +} +finally { + # ========================= + # Block 4 - Cleanup + # ========================= + try { + if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" + } + } catch { + Write-Warning "Could not remove temporary CLIXML ($TempXml): $($_.Exception.Message)" + } +} diff --git a/API/Deploy - Create.ps1 b/API/Deploy - Create.ps1 new file mode 100644 index 0000000..414b3d1 --- /dev/null +++ b/API/Deploy - Create.ps1 @@ -0,0 +1,196 @@ +<# +.SYNOPSIS +Create a Tanium Deploy software package using the Deploy module cmdlets. +Strategy: clone an existing package to get the exact schema, then modify a few fields. + +REQUIRES: Redden-TanREST (for the Deploy cmdlets your screenshot shows) +#> + +# =============== Block 0 - Parameters to edit =============== +# New package name and install command +$NewPackageName = 'Demo - My Package (API)' +$InstallCommandLine = 'cmd /c echo Hello from Deploy > %TEMP%\hello.txt' + +# Choose ONE mode: 'CommandOnly' | 'LocalFile' | 'RemoteURL' +$Mode = 'CommandOnly' + +# If Mode = LocalFile +$LocalFilePath = 'C:\Temp\payload.msi' + +# If Mode = RemoteURL +$RemoteFileUrl = 'https://example.com/payload.msi' + +# An existing simple package to use as a schema template (must exist in your Deploy) +$TemplatePackageName = 'Notepad++ (Gallery)' # <-- mets ici un paquet simple qui existe chez toi + +# Optional: content set ID (leave as-is to keep the template's) +$OverrideContentSetId = $null # ex: 0 (ou $null pour garder l’ID du template) + +# =============== Block 1 - Import & Session =============== +$ErrorActionPreference = 'Stop' +Import-Module Redden-TanREST -Force + +# Load config.json next to this script +$configPath = Join-Path $PSScriptRoot 'config.json' +if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } + +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json +$TaniumUrl = $config.TaniumUrl +$TaniumToken = $config.TaniumApiToken +if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided in config.json." +} +if ($TaniumUrl -match '^https?://') { $TaniumUrl = $TaniumUrl -replace '^https?://','' -replace '/+$','' } + +# Create a short-lived CLIXML for Initialize-TaniumSession (same pattern as your other scripts) +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' +$ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumToken | ConvertTo-SecureString -AsPlainText -Force) +} +$ExportObject | Export-Clixml -Path $TempXml +Initialize-TaniumSession -PathToXML $TempXml +Write-Host "Tanium session initialized." + +# =============== Block 2 - Helpers =============== +function DeepClone($obj) { + # round-trip through JSON for a deep copy + return ($obj | ConvertTo-Json -Depth 100 | ConvertFrom-Json) +} + +function Remove-ReadOnlyProps { + param([Parameter(Mandatory)]$o) + # remove common read-only or server-managed fields wherever they appear + $remove = @('id','createdAt','createdBy','updatedAt','updatedBy','lastModified','created','modified','revision','status') + if ($o -is [System.Collections.IDictionary]) { + foreach ($k in @($o.Keys)) { + if ($remove -contains [string]$k) { $o.Remove($k) | Out-Null; continue } + $o[$k] = Remove-ReadOnlyProps $o[$k] + } + return $o + } elseif ($o -is [System.Collections.IEnumerable] -and -not ($o -is [string])) { + $new = @() + foreach ($item in $o) { $new += ,(Remove-ReadOnlyProps $item) } + return $new + } else { + return $o + } +} + +function Replace-AllTempFileIds { + param( + [Parameter(Mandatory)]$o, + [Parameter(Mandatory)][string]$NewTempFileId + ) + # replace any property named 'tempFileId' or 'fileId' with the new tempfile id (common API shapes) + $matchKeys = @('tempFileId','fileId') + if ($o -is [System.Collections.IDictionary]) { + foreach ($k in @($o.Keys)) { + if ($matchKeys -contains [string]$k) { + $o[$k] = $NewTempFileId + } else { + $o[$k] = Replace-AllTempFileIds -o $o[$k] -NewTempFileId $NewTempFileId + } + } + return $o + } elseif ($o -is [System.Collections.IEnumerable] -and -not ($o -is [string])) { + $new = @() + foreach ($item in $o) { $new += ,(Replace-AllTempFileIds -o $item -NewTempFileId $NewTempFileId) } + return $new + } else { + return $o + } +} + +function Update-InstallCommand { + param( + [Parameter(Mandatory)]$o, + [Parameter(Mandatory)][string]$CommandLine + ) + # best-effort: update common command fields if found + $cmdKeys = @('command','commandLine','installCommand','install_command','command_line') + if ($o -is [System.Collections.IDictionary]) { + foreach ($k in @($o.Keys)) { + if ($cmdKeys -contains [string]$k -and ($o[$k] -is [string])) { + $o[$k] = $CommandLine + } else { + $o[$k] = Update-InstallCommand -o $o[$k] -CommandLine $CommandLine + } + } + return $o + } elseif ($o -is [System.Collections.IEnumerable] -and -not ($o -is [string])) { + $new = @() + foreach ($item in $o) { $new += ,(Update-InstallCommand -o $item -CommandLine $CommandLine) } + return $new + } else { + return $o + } +} + +# =============== Block 3 - Fetch template & prepare body =============== +Write-Host "Fetching template package: $TemplatePackageName" +$template = Get-DeploySoftwarePackage -Name $TemplatePackageName -IncludeHidden +if (-not $template) { throw "Template package '$TemplatePackageName' not found." } +# If multiple, take the first +if ($template -is [System.Array]) { $template = $template[0] } + +# Deep clone and strip read-only fields +$bodyObj = DeepClone $template +$bodyObj = Remove-ReadOnlyProps $bodyObj + +# Apply basic changes +$bodyObj.name = $NewPackageName +if ($OverrideContentSetId -ne $null) { + if ($bodyObj.contentSet -and $bodyObj.contentSet.id -ne $null) { + $bodyObj.contentSet.id = [int]$OverrideContentSetId + } +} + +# Update command line everywhere it appears (best-effort) +$bodyObj = Update-InstallCommand -o $bodyObj -CommandLine $InstallCommandLine + +# =============== Block 4 - Attach file if requested =============== +$tempFile = $null +switch ($Mode) { + 'LocalFile' { + Write-Host "Uploading local file: $LocalFilePath" + if (-not (Test-Path $LocalFilePath)) { throw "Local file not found: $LocalFilePath" } + $tempFile = New-DeployTempFile -FilePath $LocalFilePath + if (-not $tempFile) { throw "Failed to upload local file." } + $bodyObj = Replace-AllTempFileIds -o $bodyObj -NewTempFileId ([string]$tempFile.id) + } + 'RemoteURL' { + Write-Host "Registering remote file URL: $RemoteFileUrl" + $tempFile = New-DeployTempFile -RemoteURL $RemoteFileUrl + if (-not $tempFile) { throw "Failed to register remote URL." } + $bodyObj = Replace-AllTempFileIds -o $bodyObj -NewTempFileId ([string]$tempFile.id) + } + 'CommandOnly' { + Write-Host "No file attachment (command-only mode)." + } + default { throw "Unknown Mode '$Mode' (use CommandOnly | LocalFile | RemoteURL)" } +} + +# =============== Block 5 - Create software package =============== +# Convert to JSON for -Body +$bodyJson = $bodyObj | ConvertTo-Json -Depth 100 +Write-Host "Creating Deploy software package: $NewPackageName" +$newPkg = New-DeploySoftwarePackage -Body ($bodyJson | ConvertFrom-Json) +# Certains modules attendent un objet PS, d’autres une chaîne JSON ; si erreur, essaie directement -Body $bodyJson + +if (-not $newPkg) { + Write-Warning "New-DeploySoftwarePackage returned no object. Verify in console if the package exists." +} else { + Write-Host "Created: ID=$($newPkg.id) Name=$($newPkg.name)" +} + +# Optional: verify by name +$check = Get-DeploySoftwarePackage -Name $NewPackageName -IncludeHidden +if ($check) { + Write-Host "Verified package exists. Done." +} else { + Write-Warning "Package not found via Get-DeploySoftwarePackage; please check the console or API logs." +} + +# Cleanup temp clixml (optional) +Remove-Item -Path $TempXml -Force -ErrorAction SilentlyContinue diff --git a/API/Deploy - Get.ps1 b/API/Deploy - Get.ps1 new file mode 100644 index 0000000..995f698 --- /dev/null +++ b/API/Deploy - Get.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS +List Tanium Deploy Software Packages using Get-DeploySoftware. +Reads URL/token from config.json, initializes the session, then queries by: + - All | ByName | ByID | ByVendor | ByCommand | NameRegex +and displays results in Out-GridView (fallback to console table). +#> + +# ========================= +# Block 1 - Prerequisites +# ========================= +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# ========================= +# Block 2 - Load config & init session +# ========================= +$configPath = Join-Path $PSScriptRoot 'config.json' +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' + +if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } + +Write-Host "Reading configuration from: $configPath" +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json +$TaniumUrl = $config.TaniumUrl +$TaniumToken = $config.TaniumApiToken + +if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided (config.json or environment variables)." +} +if ($TaniumUrl -match '^https?://') { + $TaniumUrl = $TaniumUrl -replace '^https?://','' -replace '/+$','' + Write-Host "Normalized TaniumUrl to host: $TaniumUrl" +} + +$ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumToken | ConvertTo-SecureString -AsPlainText -Force) +} +$ExportObject | Export-Clixml -Path $TempXml + +Write-Host "Initializing Tanium session..." +Initialize-TaniumSession -PathToXML $TempXml +Write-Host "Tanium session initialized." + +# ========================= +# Block 3 - Choose query mode +# ========================= +Get-DeploySoftware -All | Out-GridView + +# ========================= +# Block 5 - Cleanup +# ========================= +if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" +} + diff --git a/API/Interact - GetQuestion.ps1 b/API/Interact - GetQuestion.ps1 new file mode 100644 index 0000000..66dded3 --- /dev/null +++ b/API/Interact - GetQuestion.ps1 @@ -0,0 +1,93 @@ +<# +.SYNOPSIS +Initialize Tanium session from config.json, ensure computer groups exist (create if missing), +then ask an Interact question and display results in Out-GridView. + +.NOTES +- Keep config.json out of version control. +#> + +# ========================= +# Block 1 - Prerequisites +# ========================= +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + + +# ========================= +# Block 2 - Load config.json & init session +# ========================= +$configPath = Join-Path $PSScriptRoot 'config.json' + +try { + if (-not (Test-Path $configPath)) { + throw "Configuration file not found: $configPath" + } + + Write-Host "Reading configuration from: $configPath" + $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + + # Values (fallback to environment variables) + $TaniumUrl = $config.TaniumUrl + $TaniumApiToken = $config.TaniumApiToken + if ([string]::IsNullOrWhiteSpace($TaniumUrl)) { $TaniumUrl = $env:TANIUM_URL } + if ([string]::IsNullOrWhiteSpace($TaniumApiToken)) { $TaniumApiToken = $env:TANIUM_TOKEN } + if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumApiToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided (config.json or environment variables)." + } + + # Normalize: bare host (no scheme / trailing slash) + if ($TaniumUrl -match '^https?://') { + $TaniumUrl = $TaniumUrl -replace '^https?://', '' -replace '/+$', '' + Write-Host "Normalized TaniumUrl to host: $TaniumUrl" + } + + # Build temporary CLIXML for Initialize-TaniumSession + $TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' + $ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumApiToken | ConvertTo-SecureString -AsPlainText -Force) + } + Write-Host "Writing temporary CLIXML to: $TempXml" + $ExportObject | Export-Clixml -Path $TempXml + + Write-Host "Initializing Tanium session..." + Initialize-TaniumSession -PathToXML $TempXml + Write-Host "Tanium session initialized successfully." +} +catch { + Write-Error "Failed to initialize Tanium session. Details: $($_.Exception.Message)" + throw +} + +# ========================= +# Block 3 - Interact question + Out-GridView (no flattener) +# ========================= + +$canonicalText = 'Get Computer Name and IP Address from all machines with Operating System contains Windows' +$minResponsePct = 95 +$expireSeconds = 1200 + +Write-Host "Asking Interact question..." +$result = Get-InteractQuestionResult ` + -CanonicalText $canonicalText ` + -MinResponsePercent $minResponsePct ` + -ExpireSeconds $expireSeconds ` + -Verbose + +$result | Out-GridView -Title 'Windows endpoints' + + + +# ========================= +# Block 5 - Cleanup +# ========================= +try { + if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" + } +} catch { + Write-Warning "Could not remove temporary CLIXML ($TempXml): $($_.Exception.Message)" +} diff --git a/API/Package - Create.ps1 b/API/Package - Create.ps1 new file mode 100644 index 0000000..8ec703e --- /dev/null +++ b/API/Package - Create.ps1 @@ -0,0 +1,208 @@ +<# +.SYNOPSIS +Create Tanium action packages from a definition array supporting three modes: + - CommandOnly : only a command (no files) + - Upload : upload local files into the package, then execute from .\__Download\ + - Url : reference a remote file by URL with SHA-256 and "check for update" TTL + +The script: +1) Initializes a Tanium session from config.json (same folder as the script), +2) Iterates over $packages and creates each package accordingly, +3) Optionally uploads payloads (Upload mode), or references URL files (Url mode), +4) Cleans up the temporary CLIXML. + +NOTES +- Keep config.json out of version control. +- When executing an uploaded file, always reference it via .\__Download\ in -Command. +#> + +# ========================= +# Block 1 - Prerequisites +# ========================= +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# ========================= +# Block 2 - Load config.json & init session +# ========================= +$configPath = Join-Path $PSScriptRoot 'config.json' +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' + +try { + if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } + + Write-Host "Reading configuration from: $configPath" + $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + + $TaniumUrl = $config.TaniumUrl + $TaniumApiToken = $config.TaniumApiToken + if ([string]::IsNullOrWhiteSpace($TaniumUrl)) { $TaniumUrl = $env:TANIUM_URL } + if ([string]::IsNullOrWhiteSpace($TaniumApiToken)) { $TaniumApiToken = $env:TANIUM_TOKEN } + if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumApiToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided (config.json or environment variables)." + } + + # Normalize: bare host (no scheme / trailing slash) + if ($TaniumUrl -match '^https?://') { + $TaniumUrl = $TaniumUrl -replace '^https?://', '' -replace '/+$', '' + Write-Host "Normalized TaniumUrl to host: $TaniumUrl" + } + + # Build temporary CLIXML for Initialize-TaniumSession + $ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumApiToken | ConvertTo-SecureString -AsPlainText -Force) + } + Write-Host "Writing temporary CLIXML to: $TempXml" + $ExportObject | Export-Clixml -Path $TempXml + + Write-Host "Initializing Tanium session..." + Initialize-TaniumSession -PathToXML $TempXml + Write-Host "Tanium session initialized successfully." +} +catch { + Write-Error "Failed to initialize Tanium session. Details: $($_.Exception.Message)" + throw +} + +# ========================= +# Block 3 - Global settings +# ========================= +$commandTimeout = 600 # seconds for New-TaniumPackage -Command_Timeout (default is 60) +$packageTTL = 3600 # seconds for New-TaniumPackage -Expire_Seconds (default is 660) + +# Helper: optional SHA-256 calculator for a remote URL (only used if you don't supply it) +function Get-RemoteFileSha256 { + param([Parameter(Mandatory)][string]$Url) + $tmp = Join-Path $env:TEMP ([IO.Path]::GetRandomFileName()) + try { + Invoke-WebRequest -Uri $Url -OutFile $tmp + (Get-FileHash -Algorithm SHA256 -Path $tmp).Hash.ToLowerInvariant() + } finally { + Remove-Item $tmp -Force -ErrorAction SilentlyContinue + } +} + +# ========================= +# Block 4 - Packages to create (3 cases) +# ========================= +$packages = @( + # 1) Command only (no files) + @{ + Name = 'Case1 - Command only' + Command = 'cmd /c ipconfig /all > %TEMP%\net.txt' + Mode = 'CommandOnly' + ContentSetID = 0 + }, + + # 2) Upload local files (put files in UploadFolder; reference via .\__Download\ in Command) + @{ + Name = 'Case2 - Upload a file' + Command = 'cmd /c cscript.exe .\__Download\cleanup.vbs' + Mode = 'Upload' + UploadFolder = 'C:\temp\pkg-cleanup' # must contain cleanup.vbs (and any other needed files) + ContentSetID = 2 + }, + + # 3) URL file with "check for update" TTL (+ required SHA-256) + @{ + Name = 'Case3 - URL file with update check' + Command = 'cmd /c cscript.exe .\__Download\remove-sample-files.vbs' + Mode = 'Url' + UrlFile = @{ + Url = 'https://example.com/remove-sample-files.vbs' + Name = 'remove-sample-files.vbs' + Sha256 = '' # if empty, the script will compute it + CheckForUpdateSeconds = 86400 # e.g. 1 day; 0 = Never + } + ContentSetID = 2 + } +) + +# ========================= +# Block 5 - Creation loop +# ========================= +foreach ($pkg in $packages) { + Write-Host "`n=== Package: $($pkg.Name) ===" + + try { + switch ($pkg.Mode) { + + 'CommandOnly' { + $newPkg = New-TaniumPackage ` + -Name $pkg.Name ` + -Command $pkg.Command ` + -Expire_Seconds $packageTTL ` + -Command_Timeout $commandTimeout ` + -ContentSetID $pkg.ContentSetID + + Write-Host "Created (ID=$($newPkg.id))" + } + + 'Upload' { + if (-not (Test-Path $pkg.UploadFolder)) { + throw "Upload folder not found: $($pkg.UploadFolder)" + } + + $newPkg = New-TaniumPackage ` + -Name $pkg.Name ` + -Command $pkg.Command ` + -Expire_Seconds $packageTTL ` + -Command_Timeout $commandTimeout ` + -ContentSetID $pkg.ContentSetID + + Write-Host "Created (ID=$($newPkg.id)); uploading payload..." + Update-ActionPackageFile -PackageID $newPkg.id -UploadFolder $pkg.UploadFolder + Write-Host "Payload uploaded." + } + + 'Url' { + # Build Files[] entry expected by the API + $sha = $pkg.UrlFile.Sha256 + if ([string]::IsNullOrWhiteSpace($sha)) { + Write-Host "Computing SHA-256 for URL: $($pkg.UrlFile.Url)" + $sha = Get-RemoteFileSha256 -Url $pkg.UrlFile.Url + } + + $fileObj = [pscustomobject]@{ + name = $pkg.UrlFile.Name + url = $pkg.UrlFile.Url + hash = $sha + # The property below controls "Check for update" TTL in seconds. + # If your Tanium uses a different name (e.g. cache_ttl_seconds/refresh_seconds), adjust here: + check_for_update_seconds = $pkg.UrlFile.CheckForUpdateSeconds + } + + $newPkg = New-TaniumPackage ` + -Name $pkg.Name ` + -Command $pkg.Command ` + -Expire_Seconds $packageTTL ` + -Command_Timeout $commandTimeout ` + -ContentSetID $pkg.ContentSetID ` + -Files @($fileObj) + + Write-Host "Created (ID=$($newPkg.id)) with remote file URL." + } + + default { + throw "Unknown Mode for package '$($pkg.Name)'. Use 'CommandOnly' | 'Upload' | 'Url'." + } + } + } + catch { + Write-Error "Failed to process package '$($pkg.Name)': $($_.Exception.Message)" + } +} + +# ========================= +# Block 6 - Cleanup +# ========================= +try { + if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" + } +} catch { + Write-Warning "Could not remove temporary CLIXML ($TempXml): $($_.Exception.Message)" +} diff --git a/API/Package - Get.ps1 b/API/Package - Get.ps1 new file mode 100644 index 0000000..4ddbfca --- /dev/null +++ b/API/Package - Get.ps1 @@ -0,0 +1,139 @@ +<# +.SYNOPSIS +Initialize Tanium session from config.json, then list Tanium Packages +(using Get-TaniumPackage in several modes) and show them in Out-GridView. +#> + +# ========================= +# Block 1 - Prerequisites +# ========================= +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# ========================= +# Block 2 - Load config.json & init session +# ========================= +$configPath = Join-Path $PSScriptRoot 'config.json' +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' + +try { + if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } + + Write-Host "Reading configuration from: $configPath" + $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + + $TaniumUrl = $config.TaniumUrl + $TaniumApiToken = $config.TaniumApiToken + if ([string]::IsNullOrWhiteSpace($TaniumUrl)) { $TaniumUrl = $env:TANIUM_URL } + if ([string]::IsNullOrWhiteSpace($TaniumApiToken)) { $TaniumApiToken = $env:TANIUM_TOKEN } + if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumApiToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided (config.json or environment variables)." + } + + # Normalize: bare host (no scheme / trailing slash) + if ($TaniumUrl -match '^https?://') { + $TaniumUrl = $TaniumUrl -replace '^https?://', '' -replace '/+$', '' + Write-Host "Normalized TaniumUrl to host: $TaniumUrl" + } + + # Temporary CLIXML for Initialize-TaniumSession + $ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumApiToken | ConvertTo-SecureString -AsPlainText -Force) + } + Write-Host "Writing temporary CLIXML to: $TempXml" + $ExportObject | Export-Clixml -Path $TempXml + + Write-Host "Initializing Tanium session..." + Initialize-TaniumSession -PathToXML $TempXml + Write-Host "Tanium session initialized successfully." +} +catch { + Write-Error "Failed to initialize Tanium session. Details: $($_.Exception.Message)" + throw +} + +# ========================= +# Block 3 - Pick your query mode +# ========================= +# Choose ONE mode: 'All' | 'ByName' | 'ByID' | 'NameRegex' | 'ByParam' +$QueryMode = 'All' +$IncludeHidden = $false + +# Fill only the variables needed by the chosen mode: +$ByName_Name = 'My Awesome Package' +$ByID_Id = 123 +$NameRegex_Pattern= '.*Awesome.*' +$ByParam_Field = 'command' # e.g. 'command' +$ByParam_Operator = 'RegexMatch' # e.g. 'Equal' | 'RegexMatch' +$ByParam_Value = '.*cleanup\.vbs' # regex string if using RegexMatch +$ByParam_Type = 'String' # 'String' | 'Version' | 'Numeric' | 'IPAddress' | 'Date' | 'DataSize' | 'NumericInteger' + +# ========================= +# Block 4 - Retrieve packages +# ========================= +try { + switch ($QueryMode) { + 'All' { + Write-Host "Retrieving ALL packages..." + $packages = Get-TaniumPackage -All -IncludeHidden:$IncludeHidden + } + 'ByName' { + Write-Host "Retrieving package by name: $ByName_Name" + $packages = Get-TaniumPackage -Name $ByName_Name -IncludeHidden:$IncludeHidden + } + 'ByID' { + Write-Host "Retrieving package by ID: $ByID_Id" + $packages = Get-TaniumPackage -ID $ByID_Id -IncludeHidden:$IncludeHidden + } + 'NameRegex' { + Write-Host "Retrieving packages by NameRegex: $NameRegex_Pattern" + $packages = Get-TaniumPackage -NameRegex $NameRegex_Pattern -IncludeHidden:$IncludeHidden + } + 'ByParam' { + Write-Host "Retrieving packages by field filter: $ByParam_Field $ByParam_Operator $ByParam_Value ($ByParam_Type)" + $packages = Get-TaniumPackage -Field $ByParam_Field -Operator $ByParam_Operator -Value $ByParam_Value -Type $ByParam_Type -IncludeHidden:$IncludeHidden + } + default { + throw "Unknown QueryMode '$QueryMode'. Use: All | ByName | ByID | NameRegex | ByParam." + } + } + + if (-not $packages) { + Write-Warning "No packages found." + } + + # Try to present common fields; unknown props will appear blank (that's fine) + $view = $packages | Select-Object ` + @{n='id';e={$_.id}}, + @{n='name';e={$_.name}}, + @{n='display_name';e={$_.display_name}}, + @{n='command';e={$_.command}}, + @{n='expire_seconds';e={$_.expire_seconds}}, + @{n='command_timeout';e={$_.command_timeout}}, + @{n='content_set_id';e={$_.content_set.id}}, + @{n='hidden';e={$_.hidden}} + + if (Get-Command Out-GridView -ErrorAction SilentlyContinue) { + $view | Out-GridView -Title "Tanium Packages ($QueryMode)" + } else { + Write-Warning "Out-GridView not available; showing a console table instead." + $view | Format-Table -Auto + } +} +catch { + Write-Error "Failed to retrieve/display packages. Details: $($_.Exception.Message)" +} + +# ========================= +# Block 5 - Cleanup +# ========================= +try { + if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" + } +} catch { + Write-Warning "Could not remove temporary CLIXML ($TempXml): $($_.Exception.Message)" +} diff --git a/API/RBAC_CreateComputerGroup_graphql.ps1 b/API/RBAC_CreateComputerGroup_graphql.ps1 new file mode 100644 index 0000000..a717756 --- /dev/null +++ b/API/RBAC_CreateComputerGroup_graphql.ps1 @@ -0,0 +1,218 @@ +<# +.SYNOPSIS +Create or delete a Tanium Computer Group via Gateway GraphQL. + +.DESCRIPTION +- CREATE: filter "Computer Name STARTS_WITH " (usable as Computer Management Group; optional Content Set). +- DELETE: delete a Computer Group by its Tanium ID. +- Reads URL/token from config.json (same folder) unless -Url/-Token are provided. +- Optional TLS bypass for lab with -SkipCertCheck. + +.USAGE (examples) +# Create a group (no defaults; you MUST pass -GroupName and -Prefix) +.\Create-ComputerGroup.ps1 -GroupName "LAB - starts with LAB" -Prefix "LAB" -MrEnabled:$true +.\Create-ComputerGroup.ps1 -GroupName "LAB - starts with LAB" -Prefix "LAB" -ContentSetName "My Content Set" + +# Delete by ID +.\Create-ComputerGroup.ps1 -Delete -Id "12345" + +# Override URL/token +.\Create-ComputerGroup.ps1 -Url tanium.pp.dktinfra.io -Token 'token-xxxx' -GroupName "LAB ..." -Prefix "LAB" + +# Raw JSON (debug) +.\Create-ComputerGroup.ps1 -GroupName "LAB ..." -Prefix "LAB" -Raw +#> + +param( + [string]$Url, + [string]$Token, + [switch]$SkipCertCheck, + + # CREATE params (no defaults; both required for creation) + [string]$GroupName, + [string]$Prefix, + [bool] $MrEnabled = $true, + [string]$ContentSetName, + + # DELETE params + [switch]$Delete, + [string]$Id, + + [switch]$Raw +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + +function Show-ShortHelp { + Write-Host "" + Write-Host "Create mode (requires -GroupName and -Prefix):" -ForegroundColor Cyan + Write-Host " .\Create-ComputerGroup.ps1 -GroupName 'LAB - starts with LAB' -Prefix 'LAB' [-MrEnabled:`$true] [-ContentSetName 'My Content Set']" -ForegroundColor Gray + Write-Host "" + Write-Host "Delete mode (requires -Delete and -Id):" -ForegroundColor Cyan + Write-Host " .\Create-ComputerGroup.ps1 -Delete -Id '12345'" -ForegroundColor Gray + Write-Host "" + Write-Host "Common options: -Url -Token -SkipCertCheck -Raw" -ForegroundColor DarkGray +} + +# ---------- Helpers ---------- +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 (temporary, restored after call) +$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 } +} + +# ---------- Endpoint & headers ---------- +$gateway = Get-GatewayUri -HostLike $Url +$headers = @{ 'Content-Type' = 'application/json'; session = $Token } + +# ---------- GraphQL mutations ---------- +$Create_WithContentSet = @' +mutation createGroup($name: String!, $mrEnabled: Boolean!, $prefix: String!, $contentSetName: String!) { + computerGroupCreate( + input: { + name: $name + managementRightsEnabled: $mrEnabled + contentSetRef: { name: $contentSetName } + filter: { + sensor: { name: "Computer Name" } + op: STARTS_WITH + value: $prefix + } + } + ) { + group { id name } + error { message } + } +} +'@ + +$Create_NoContentSet = @' +mutation createGroup($name: String!, $mrEnabled: Boolean!, $prefix: String!) { + computerGroupCreate( + input: { + name: $name + managementRightsEnabled: $mrEnabled + filter: { + sensor: { name: "Computer Name" } + op: STARTS_WITH + value: $prefix + } + } + ) { + group { id name } + error { message } + } +} +'@ + +$Delete_ById = @' +mutation deleteComputerGroup($id: ID!) { + computerGroupDelete(ref: { id: $id }) { + id + error { message } + } +} +'@ + +# ---------- Mode selection & validation ---------- +if ($Delete) { + if ([string]::IsNullOrWhiteSpace($Id)) { + Write-Warning "Missing -Id for deletion." + Show-ShortHelp + return + } + $Query = $Delete_ById + $Variables = @{ id = $Id } + $OpName = 'deleteComputerGroup' +} +else { + if ([string]::IsNullOrWhiteSpace($GroupName) -or [string]::IsNullOrWhiteSpace($Prefix)) { + Write-Warning "Missing -GroupName and/or -Prefix for creation." + Show-ShortHelp + return + } + $useCS = -not [string]::IsNullOrWhiteSpace($ContentSetName) + $Query = if ($useCS) { $Create_WithContentSet } else { $Create_NoContentSet } + $Variables = @{ name = $GroupName; mrEnabled = $MrEnabled; prefix = $Prefix } + if ($useCS) { $Variables.contentSetName = $ContentSetName } + $OpName = 'createGroup' +} + +# ---------- Execute ---------- +$bodyObj = @{ query = $Query; variables = $Variables; operationName = $OpName } +$bodyJson = $bodyObj | ConvertTo-Json -Depth 10 + +$resp = $null +$ps7 = ($PSVersionTable.PSVersion.Major -ge 7) +if ($ps7 -and $SkipCertCheck) { + $resp = Invoke-RestMethod -SkipCertificateCheck -Method Post -Uri $gateway -Headers $headers -ContentType 'application/json' -Body $bodyJson +} +else { + try { + Enter-InsecureTls -Enable:$SkipCertCheck + $resp = Invoke-RestMethod -Method Post -Uri $gateway -Headers $headers -ContentType 'application/json' -Body $bodyJson + } + finally { Exit-InsecureTls } +} + +# ---------- Output ---------- +if ($Raw) { $resp | ConvertTo-Json -Depth 12; return } + +if ($resp.errors) { + Write-Error ("GraphQL errors: " + (($resp.errors | ForEach-Object { $_.message }) -join '; ')) + return +} + +if ($Delete) { + $del = $resp.data.computerGroupDelete + if ($del.error -and $del.error.message) { + Write-Error ("Delete failed: {0}" -f $del.error.message) + } elseif ($del.id) { + Write-Host ("Deleted group ID: {0}" -f $del.id) -ForegroundColor Green + } else { + Write-Warning "No confirmation returned. Use -Raw to inspect the full response." + } +} +else { + $crt = $resp.data.computerGroupCreate + if ($crt.error -and $crt.error.message) { + Write-Error ("Create failed: {0}" -f $crt.error.message) + } elseif ($crt.group) { + Write-Host ("Created group: {0} (ID: {1})" -f $crt.group.name, $crt.group.id) -ForegroundColor Green + } else { + Write-Warning "No group returned. Use -Raw to inspect the full response." + } +} diff --git a/API/RBAC_ExportRole_Redden-TanREST.ps1 b/API/RBAC_ExportRole_Redden-TanREST.ps1 new file mode 100644 index 0000000..c7020ba --- /dev/null +++ b/API/RBAC_ExportRole_Redden-TanREST.ps1 @@ -0,0 +1,89 @@ +#requires -Version 7.0 +<# +.SYNOPSIS +Initialize Tanium (Redden-TanREST) from config.json, then +export all roles whose name starts with a given prefix (default: CASH). + +.PARAMETER Prefix +Role name prefix to match (prefix match, case-insensitive). Default: CASH. + +.PARAMETER OutputFolder +Destination folder for JSON exports. Default: %TEMP%\RBAC +#> + +param( + [string]$Prefix = 'CASH', + [string]$OutputFolder = "$env:TEMP\RBAC" +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# --- Load config +$ConfigPath = Join-Path $PSScriptRoot 'config.json' +if (-not (Test-Path $ConfigPath)) { throw "Configuration file not found: $ConfigPath" } +$config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json +$TaniumUrl = ($config.TaniumUrl -replace '^https?://','').TrimEnd('/') +$TaniumTok = $config.TaniumApiToken +if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumTok)) { + throw "Both TaniumUrl and TaniumApiToken must be provided in $ConfigPath." +} + +# --- Prepare output +if (-not (Test-Path $OutputFolder)) { New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null } + +# --- Temporary CLIXML for Initialize-TaniumSession +$TempXml = Join-Path $env:TEMP ("tanium-session-{0}.apicred" -f ([guid]::NewGuid().ToString('N'))) +@{ baseURI = $TaniumUrl; token = ($TaniumTok | ConvertTo-SecureString -AsPlainText -Force) } | + Export-Clixml -Path $TempXml -Force + +Write-Host "Initializing Tanium session..." +try { + Initialize-TaniumSession -PathToXML $TempXml | Out-Null + Write-Host "Session OK." + + # ---------- GET roles starting with prefix ---------- + # Prefer server-side regex; fall back to client-side if module/endpoint refuses inline (?i) + $regex = "(?i)^$([regex]::Escape($Prefix))" + $roles = $null + try { $roles = Get-Role -NameRegex $regex } catch { $roles = $null } + if (-not $roles) { + # fallback: pull all and filter locally + $roles = Get-Role -All | Where-Object { $_.name -match $regex -or $_.Name -match $regex } + } + + if (-not $roles) { + Write-Warning "No roles found starting with '$Prefix'." + return + } + + # ---------- Export each role ---------- + $exported = @() + foreach ($r in @($roles)) { + $id = if ($r.PSObject.Properties.Name -contains 'id') { $r.id } elseif ($r.PSObject.Properties.Name -contains 'ID') { $r.ID } else { $null } + $name = if ($r.PSObject.Properties.Name -contains 'name') { $r.name } elseif ($r.PSObject.Properties.Name -contains 'Name') { $r.Name } else { $null } + if (-not $name) { continue } + + try { + Export-RoleToJSON -RoleName $name -OutputFolder $OutputFolder -SkipReInitialize:$true -ErrorAction Stop + Write-Host ("✓ Exported: {0} (ID: {1})" -f $name, $id) + $exported += [pscustomobject]@{ Id=$id; Name=$name } + } + catch { + Write-Warning ("Export failed for role '{0}' (ID: {1}) — {2}" -f $name, $id, $_.Exception.Message) + } + } + + if ($exported) { + Write-Host "`nSummary:" + $exported | Sort-Object Name | Format-Table Id,Name -AutoSize + Write-Host "`nJSON files in: $OutputFolder" + } +} +finally { + if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" + } +} diff --git a/API/RBAC_ImportRole_Redden-TanREST KO.ps1 b/API/RBAC_ImportRole_Redden-TanREST KO.ps1 new file mode 100644 index 0000000..7569b66 --- /dev/null +++ b/API/RBAC_ImportRole_Redden-TanREST KO.ps1 @@ -0,0 +1,264 @@ +#requires -Version 7.0 +<# +.SYNOPSIS +Ensure a Tanium role by NAME: update if it exists, else create, then update from JSON. + +.DESCRIPTION +- Accepts two JSON shapes: + (A) Custom: { "Description": "...", "Permissions": { "": { "privileges": { "#x":[ "...", ... ] } } }, "Display Name": "..." } + (B) Tanium export: { "object_list": { "content_set_roles": [ { name, description, content_set_role_privileges: [...] } ] } } +- Forces role name to -RoleName. +- Creates role if missing, then calls Update-TaniumRole. + +.PARAMETER RoleName +Target role name to ensure (forced into JSON before update). + +.PARAMETER PathToRoleJSON +Path to the role JSON file (custom or Tanium export). + +.PARAMETER ContentSetOverride +Optional content set ID to force in Update-TaniumRole. + +.PARAMETER ConfigPath +Path to config.json containing TaniumUrl and TaniumApiToken (default: alongside script). + +.EXAMPLE +.\Ensure-TaniumRole.ps1 -RoleName "test - T2_TANIUM - N2 - Patch Operator" -PathToRoleJSON .\test.json +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$RoleName, + + [Parameter(Mandatory = $true, Position = 1)] + [ValidateScript({ + if (-not (Test-Path $_ -PathType Leaf)) { throw "File not found: $_" } + if ([IO.Path]::GetExtension($_) -notin '.json','.JSON') { throw "Not a .json file: $_" } + $true + })] + [string]$PathToRoleJSON, + + [Parameter(Position = 2)] + [int]$ContentSetOverride = 0, + + [string]$ConfigPath = (Join-Path $PSScriptRoot 'config.json') +) + +begin { + $ErrorActionPreference = 'Stop' + try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + + if (-not (Get-Command -Name Update-TaniumRole -ErrorAction SilentlyContinue)) { + throw "Update-TaniumRole not found. Import the module that defines it before running this script." + } + + # Load input JSON (any shape) + try { + $inObj = Get-Content -Path $PathToRoleJSON -Raw | ConvertFrom-Json -ErrorAction Stop + } catch { + throw "Invalid JSON in '$PathToRoleJSON': $($_.Exception.Message)" + } + + function Convert-CustomRoleJsonToTaniumExport { + <# + Input (custom): + { + "Description": "...", + "Permissions": { + "ContentSetA": { "privileges": { "#patch": [ "patch X", "patch Y" ], "#admin":[ "read sensor" ] } }, + "ContentSetB": { ... } + }, + "Display Name": "..." + } + Output (export): + { + "comment":"Generated for Update-TaniumRole", + "version":2, + "object_list":{ + "content_set_roles":[ + { + "name":"", + "description":"", + "deny_flag":0, + "all_content_sets_flag":0, + "reserved_name":"", + "category":"custom", + "content_set_role_privileges":[ + { "content_set":{"name":"ContentSetA"}, "content_set_privilege":{"name":"patch X"}, "json_metadata":"" }, + ... + ], + "metadata":[] + } + ] + } + } + #> + param( + [Parameter(Mandatory)][object]$InputObject, + [Parameter(Mandatory)][string]$NameToForce + ) + + $desc = '' + if ($InputObject.'Description') { $desc = [string]$InputObject.'Description' } + elseif ($InputObject.description) { $desc = [string]$InputObject.description } + + $privRows = New-Object System.Collections.Generic.List[object] + if ($InputObject.Permissions -is [object]) { + foreach ($csName in $InputObject.Permissions.PSObject.Properties.Name) { + $csBlock = $InputObject.Permissions.$csName + if ($csBlock -and $csBlock.privileges) { + foreach ($cat in $csBlock.privileges.PSObject.Properties.Name) { + $vals = $csBlock.privileges.$cat + if ($vals -is [System.Collections.IEnumerable]) { + foreach ($p in $vals) { + if ([string]::IsNullOrWhiteSpace($p)) { continue } + $privRows.Add([pscustomobject]@{ + content_set = @{ name = $csName } + content_set_privilege = @{ name = [string]$p } + json_metadata = "" + }) + } + } + } + } + } + } + + # De-duplicate + $unique = $privRows | + Group-Object { "$($_.content_set.name)|$($_.content_set_privilege.name)" } | + ForEach-Object { $_.Group[0] } + + return [pscustomobject]@{ + comment = "Generated for Update-TaniumRole" + version = 2 + object_list = @{ + content_set_roles = @( + [pscustomobject]@{ + name = $NameToForce + description = $desc + deny_flag = 0 + all_content_sets_flag = 0 + reserved_name = "" + category = "custom" + content_set_role_privileges = $unique + metadata = @() + } + ) + } + } + } + + # Normalize to Tanium export shape + $exportObj = $null + if ($inObj.object_list -and $inObj.object_list.content_set_roles) { + # Already export shape + $exportObj = $inObj + # enforce RoleName on the single role (or first) + $rolesArr = @($exportObj.object_list.content_set_roles) + if ($rolesArr.Count -eq 0) { throw "Export JSON contains no roles." } + if ($rolesArr.Count -gt 1) { + # Force only the first for safety; or throw if you want strict single-role files + throw "JSON contains multiple roles. Provide a single-role file or split it." + } + $exportObj.object_list.content_set_roles[0].name = $RoleName + } + else { + # Custom shape -> convert + if (-not $inObj.Permissions) { + throw "No content_set_roles[] found AND no Permissions{} block found in JSON." + } + $exportObj = Convert-CustomRoleJsonToTaniumExport -InputObject $inObj -NameToForce $RoleName + } + + # Write temp JSON (don't touch original) + $ts = Get-Date -Format 'yyyyMMdd-HHmmss' + $tempJson = Join-Path $env:TEMP ("tanium-role-{0}-{1}.json" -f ($RoleName -replace '[^\w\-\. ]','_'), $ts) + $exportObj | ConvertTo-Json -Depth 100 | Set-Content -Path $tempJson -Encoding utf8 + + # Tanium API bootstrap + if (-not (Test-Path $ConfigPath -PathType Leaf)) { + throw "Config file not found: $ConfigPath" + } + $cfg = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json + $baseUrl = ($cfg.TaniumUrl -replace '/+$','') + if ($baseUrl -notmatch '^https?://') { $baseUrl = "https://$baseUrl" } + $headers = @{ + 'Accept' = 'application/json' + 'Content-Type' = 'application/json' + } + if ($cfg.TaniumApiToken) { + $headers['X-Api-Token'] = $cfg.TaniumApiToken + $headers['session'] = $cfg.TaniumApiToken + } + + function Invoke-TaniumApi { + param( + [Parameter(Mandatory)][ValidateSet('GET','POST','PATCH','PUT','DELETE')] [string]$Method, + [Parameter(Mandatory)][string]$Path, + [object]$Body = $null + ) + $uri = '{0}{1}' -f $baseUrl, $Path + if ($Body -ne $null) { + Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -Body ($Body | ConvertTo-Json -Depth 20) -ErrorAction Stop + } else { + Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -ErrorAction Stop + } + } + + function Get-TaniumUserRolesAll { + $resp = Invoke-TaniumApi -Method GET -Path '/api/v2/user_roles' + if ($resp.items) { return @($resp.items) } + if ($resp.data) { return @($resp.data) } + if ($resp.user_roles) { return @($resp.user_roles) } + if ($resp -is [System.Collections.IEnumerable]) { return @($resp) } + return @() + } + + function Ensure-TaniumRoleIdByName { + param([Parameter(Mandatory)][string]$Name,[string]$Description = '') + $all = Get-TaniumUserRolesAll + $existing = $all | Where-Object { $_.name -eq $Name } | Select-Object -First 1 + if ($existing -and $existing.id) { return [int]$existing.id } + # create + $body = @{ + name = $Name + description = $Description + category = 'custom' + } + $resp = Invoke-TaniumApi -Method POST -Path '/api/v2/user_roles' -Body $body + if ($resp.id) { return [int]$resp.id } + if ($resp.item -and $resp.item.id) { return [int]$resp.item.id } + if ($resp.data -and $resp.data.id) { return [int]$resp.data.id } + $all2 = Get-TaniumUserRolesAll + $match = $all2 | Where-Object { $_.name -eq $Name } | Select-Object -First 1 + if ($match -and $match.id) { return [int]$match.id } + throw "Role '$Name' created but ID could not be determined." + } + + # Description from export for creation metadata + $desc = '' + try { $desc = [string]$exportObj.object_list.content_set_roles[0].description } catch {} + $script:TargetRoleId = Ensure-TaniumRoleIdByName -Name $RoleName -Description $desc +} + +process { + try { + $params = @{ + RoleIDs = @($script:TargetRoleId) + PathToRoleJSON = $tempJson + } + if ($ContentSetOverride -gt 0) { $params['ContentSetOverride'] = $ContentSetOverride } + + Write-Host "Ensured role '$RoleName' (ID: $($script:TargetRoleId)). Applying JSON..." -ForegroundColor Cyan + $result = Update-TaniumRole @params + $result + } + finally { + if (Test-Path $tempJson) { + Remove-Item -Path $tempJson -Force -ErrorAction SilentlyContinue + } + } +} diff --git a/API/RBAC_ListComputerGroup_GraphQL.ps1 b/API/RBAC_ListComputerGroup_GraphQL.ps1 new file mode 100644 index 0000000..56d8011 --- /dev/null +++ b/API/RBAC_ListComputerGroup_GraphQL.ps1 @@ -0,0 +1,219 @@ +# 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 diff --git a/API/RBAC_ListContentset_Rest.ps1 b/API/RBAC_ListContentset_Rest.ps1 new file mode 100644 index 0000000..fe95683 --- /dev/null +++ b/API/RBAC_ListContentset_Rest.ps1 @@ -0,0 +1,120 @@ +<# +.SYNOPSIS +List Tanium **Content Sets** via the REST API, using Url/Token from `config.json` by default. +Supports optional filter by name (regex), Out‑GridView, and CSV export. + +.USAGE +# Basic (reads config.json in the same folder) +./List-ContentSets.ps1 + +# Filter by name (regex), show grid and export CSV +./List-ContentSets.ps1 -NameLike '^Deploy' -Grid -ExportCsv C:\Temp\content_sets.csv + +# Raw JSON +./List-ContentSets.ps1 -Raw + +# Override Url/Token explicitly +./List-ContentSets.ps1 -Url tanium.pp.dktinfra.io -Token 'token-xxxx' +#> + +param( + [string]$Url, + [string]$Token, + [switch]$SkipCertCheck, + + [string]$NameLike, + [switch]$Raw, + [switch]$Grid, + [string]$ExportCsv +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + +# ---------- Helpers ---------- +function New-BaseUri([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" +} + +# 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 } +} + +$Base = New-BaseUri $Url +$Headers = @{ session = $Token; 'Content-Type' = 'application/json' } + +function Invoke-Tanium([string]$Method,[string]$Path,[object]$BodyObj) { + $uri = if ($Path -match '^https?://') { $Path } else { "$Base$Path" } + $body = if ($null -ne $BodyObj) { $BodyObj | ConvertTo-Json -Depth 10 } else { $null } + $ps7 = ($PSVersionTable.PSVersion.Major -ge 7) + if ($ps7 -and $SkipCertCheck) { + return Invoke-RestMethod -SkipCertificateCheck -Method $Method -Uri $uri -Headers $Headers -Body $body + } else { + try { Enter-InsecureTls -Enable:$SkipCertCheck; return Invoke-RestMethod -Method $Method -Uri $uri -Headers $Headers -Body $body } + finally { Exit-InsecureTls } + } +} + +# ---------- Fetch Content Sets ---------- +# Common endpoint for content sets +$resp = Invoke-Tanium GET "/api/v2/content_sets" $null + +# Tanium REST often returns either .data or .items +$contentSets = if ($resp.data) { $resp.data } elseif ($resp.items) { $resp.items } else { $resp } + +if ($Raw) { $contentSets | ConvertTo-Json -Depth 12; return } +if (-not $contentSets) { Write-Output 'No content sets returned.'; return } + +# ---------- Shape output ---------- +$rows = foreach ($cs in $contentSets) { + if ($NameLike -and -not ($cs.name -match $NameLike)) { continue } + [pscustomobject]@{ + Id = $cs.id + Name = $cs.name + Description = $cs.description + } +} + +if (-not $rows) { Write-Output 'No matching content sets.'; 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 = "Content Sets — {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 \ No newline at end of file diff --git a/API/RBAC_ListRole_Rest.ps1 b/API/RBAC_ListRole_Rest.ps1 new file mode 100644 index 0000000..f75718a --- /dev/null +++ b/API/RBAC_ListRole_Rest.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS +List Tanium Roles via REST (uses /api/v2/content_set_roles), reading Url/Token from config.json. + +.USAGE +.\List-Roles.ps1 +.\List-Roles.ps1 -NameLike '^Ops' -Grid -ExportCsv C:\Temp\roles.csv +.\List-Roles.ps1 -Raw +#> + +param( + [string]$Url, + [string]$Token, + [switch]$SkipCertCheck, + + [string]$NameLike, + [switch]$Raw, + [switch]$Grid, + [string]$ExportCsv +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + +function New-BaseUri([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" +} + +# 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 } +} + +$Base = New-BaseUri $Url +$Headers = @{ session = $Token; 'Content-Type' = 'application/json' } + +function Invoke-Tanium([string]$Method,[string]$Path,[object]$BodyObj) { + $uri = if ($Path -match '^https?://') { $Path } else { "$Base$Path" } + $body = if ($null -ne $BodyObj) { $BodyObj | ConvertTo-Json -Depth 10 } else { $null } + $ps7 = ($PSVersionTable.PSVersion.Major -ge 7) + if ($ps7 -and $SkipCertCheck) { + return Invoke-RestMethod -SkipCertificateCheck -Method $Method -Uri $uri -Headers $Headers -Body $body + } else { + try { Enter-InsecureTls -Enable:$SkipCertCheck; return Invoke-RestMethod -Method $Method -Uri $uri -Headers $Headers -Body $body } + finally { Exit-InsecureTls } + } +} + +# ---- Fetch roles (correct endpoint) ---- +$resp = Invoke-Tanium GET "/api/v2/content_set_roles" $null +# Most Tanium APIs return .data here +$roles = if ($resp.data) { $resp.data } elseif ($resp.items) { $resp.items } else { $resp } + +if ($Raw) { $roles | ConvertTo-Json -Depth 12; return } +if (-not $roles) { Write-Output "No roles returned."; return } + +# ---- Shape output ---- +$rows = foreach ($r in $roles) { + if ($NameLike -and -not ($r.name -match $NameLike)) { continue } + [pscustomobject]@{ + Id = $r.id + Name = $r.name + Description = $r.description + } +} + +if (-not $rows) { Write-Output "No matching roles."; 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 = "Roles — {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 diff --git a/API/RBAC_ListUserAndGroup_ComputerGroup KO.ps1 b/API/RBAC_ListUserAndGroup_ComputerGroup KO.ps1 new file mode 100644 index 0000000..5fb5cdb --- /dev/null +++ b/API/RBAC_ListUserAndGroup_ComputerGroup KO.ps1 @@ -0,0 +1,204 @@ +#requires -Version 7.0 +<# +.SYNOPSIS +Liste les Computer Groups (Management Rights) effectifs par UTILISATEUR et par USER GROUP, +en affichant les NOMS (pas les mrgroup_*). +- Auth depuis config.json (TaniumUrl, TaniumApiToken) via CLIXML -> Initialize-TaniumSession +- Déplie .group.id avec Get-MRGroupsFromGroup et mappe ID->Nom via Get-ManagementRightsGroup (fallback REST) +#> + +# ========================= +# Block 1 - Prérequis +# ========================= +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# ========================= +# Block 2 - Chargement config & init session +# ========================= +$configPath = Join-Path $PSScriptRoot 'config.json' +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' + +if (-not (Test-Path $configPath -PathType Leaf)) { + throw "Configuration file not found: $configPath (expected: TaniumUrl, TaniumApiToken)" +} + +Write-Host "Reading configuration from: $configPath" +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json +$TaniumUrl = $config.TaniumUrl +$TaniumToken = $config.TaniumApiToken + +if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided." +} +if ($TaniumUrl -match '^https?://') { + $TaniumUrl = $TaniumUrl -replace '^https?://','' -replace '/+$','' + Write-Host "Normalized TaniumUrl to host: $TaniumUrl" +} + +$ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumToken | ConvertTo-SecureString -AsPlainText -Force) +} +$ExportObject | Export-Clixml -Path $TempXml + +Write-Host "Initializing Tanium session..." +Initialize-TaniumSession -PathToXML $TempXml +Write-Host "Tanium session initialized." + +# ========================= +# Block 3 - Index MR id -> nom (module puis REST fallback) +# ========================= +$Script:MRIndex = @{} +try { + $allMR = Get-ManagementRightsGroup -All +} catch { $allMR = @() } + +if (-not $allMR -or $allMR.Count -eq 0) { + # fallback REST + $baseUri = $TaniumUrl; if ($baseUri -notmatch '^https?://') { $baseUri = "https://$baseUri" } + $headers = @{ + 'Accept' = 'application/json' + 'Content-Type' = 'application/json' + 'X-Api-Token' = $TaniumToken + 'session' = $TaniumToken + } + try { + $resp = Invoke-RestMethod -Method GET -Uri "$baseUri/api/v2/management_rights_groups?limit=5000" -Headers $headers + $allMR = @($resp.items ?? $resp.data ?? $resp) + } catch { + Write-Warning "Could not fetch Management Rights groups via REST: $($_.Exception.Message)" + $allMR = @() + } +} +foreach ($g in $allMR) { + if ($null -ne $g.id) { $Script:MRIndex[[int]$g.id] = [string]$g.name } +} + +# ========================= +# Block 4 - Helpers +# ========================= +function Expand-NamedMRGroups { + <# + .SYNOPSIS + A partir d'un ID de groupe fusionné (.group.id), retourne les Computer Groups *nommés* (id/name). + Utilise Get-MRGroupsFromGroup puis mappe ID->Nom via $Script:MRIndex et filtre mrgroup_*. + #> + param([Parameter(Mandatory)][int]$MergedGroupID) + + if ($MergedGroupID -lt 0) { return @() } + + $items = @() + try { $items = @( Get-MRGroupsFromGroup -ID $MergedGroupID ) } catch { $items = @() } + + $mapped = @() + foreach ($i in $items) { + $id = [int]$i.id + $name = [string]$i.name + if (-not $name -or $name -like 'mrgroup_*') { $name = $Script:MRIndex[$id] } + if ($name) { + $mapped += [pscustomobject]@{ id = $id; name = $name } + } + } + if ($mapped.Count -gt 0) { + $mapped = $mapped | Sort-Object id -Unique + } + return $mapped +} + +function Get-UsersWithComputerGroups { + <# + .SYNOPSIS Récupère Users + leurs Computer Groups effectifs (nommés). + #> + $users = @() + try { $users = @(Get-User -All) } + catch { $users = @(Get-User -Summary) } + + $rows = @() + foreach ($u in $users) { + $uid = [int]($u.id) + $name = [string]($u.display_name ?? $u.displayName ?? $u.name ?? $u.username) + $login = [string]($u.username) + + $mergedId = 0 + try { if ($u.group -and $u.group.id -ne $null) { $mergedId = [int]$u.group.id } } catch {} + + $cgNamed = if ($mergedId -ge 0) { Expand-NamedMRGroups -MergedGroupID $mergedId } else { @() } + $cgNames = $cgNamed.name | Sort-Object -Unique + $cgIds = $cgNamed.id | Sort-Object -Unique + + $rows += [pscustomobject]@{ + id = $uid + name = $name + username = $login + mergedGroup = $mergedId + groupsCount = @($cgNames).Count + groupNames = ($cgNames -join ', ') + groupIDs = ($cgIds -join ', ') + } + } + if ($rows.Count -gt 0) { $rows = $rows | Sort-Object name } + return $rows +} + +function Get-UserGroupsWithComputerGroups { + <# + .SYNOPSIS Récupère User Groups + leurs Computer Groups effectifs (nommés). + #> + $ugs = @(Get-UserGroup -All) + + $rows = @() + foreach ($g in $ugs) { + $gid = [int]($g.id) + $gname = [string]($g.name) + + $mergedId = 0 + try { if ($g.group -and $g.group.id -ne $null) { $mergedId = [int]$g.group.id } } catch {} + + $cgNamed = if ($mergedId -ge 0) { Expand-NamedMRGroups -MergedGroupID $mergedId } else { @() } + $cgNames = $cgNamed.name | Sort-Object -Unique + $cgIds = $cgNamed.id | Sort-Object -Unique + + $rows += [pscustomobject]@{ + id = $gid + name = $gname + mergedGroup = $mergedId + groupsCount = @($cgNames).Count + groupNames = ($cgNames -join ', ') + groupIDs = ($cgIds -join ', ') + } + } + if ($rows.Count -gt 0) { $rows = $rows | Sort-Object name } + return $rows +} + +# ========================= +# Block 5 - Exécution + affichage +# ========================= +Write-Host ">> Gathering Users + Effective Computer Groups..." -ForegroundColor Cyan +$UsersWithCG = Get-UsersWithComputerGroups + +Write-Host ">> Gathering User Groups + Effective Computer Groups..." -ForegroundColor Cyan +$UGsWithCG = Get-UserGroupsWithComputerGroups + +$hasOGV = $null -ne (Get-Command Out-GridView -ErrorAction SilentlyContinue) +if ($hasOGV) { + $UsersWithCG | Out-GridView -Title 'Tanium Users – Effective Computer Groups (by name)' -Wait + $UGsWithCG | Out-GridView -Title 'Tanium User Groups – Effective Computer Groups (by name)' -Wait +} else { + Write-Host "`n=== USERS – EFFECTIVE COMPUTER GROUPS ===" -ForegroundColor Green + $UsersWithCG | Format-Table id,name,username,groupsCount,groupNames -AutoSize + + Write-Host "`n=== USER GROUPS – EFFECTIVE COMPUTER GROUPS ===" -ForegroundColor Green + $UGsWithCG | Format-Table id,name,groupsCount,groupNames -AutoSize + Write-Host "`n(Out-GridView not available; showing tables instead)" -ForegroundColor Yellow +} + +# ========================= +# Block 6 - Cleanup +# ========================= +if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" +} diff --git a/API/RBAC_ListUserAndGroup_Role_Redden-TanREST.ps1 b/API/RBAC_ListUserAndGroup_Role_Redden-TanREST.ps1 new file mode 100644 index 0000000..204548a --- /dev/null +++ b/API/RBAC_ListUserAndGroup_Role_Redden-TanREST.ps1 @@ -0,0 +1,220 @@ +#requires -Version 7.0 +<# +.SYNOPSIS +Affiche les rôles par UTILISATEUR (hors "Tanium Internal*") et/ou par GROUPE, +avec filtres -User et -Group. Sections affichées selon les filtres. +#> + +[CmdletBinding()] +param( + [string[]]$User, # filtre sur display name / username (substring ou wildcard) + [string[]]$Group # filtre sur nom du groupe (substring ou wildcard) +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + +# --- Auth depuis config.json --- +$ConfigPath = Join-Path $PSScriptRoot 'config.json' +if (-not (Test-Path $ConfigPath -PathType Leaf)) { + throw "Config introuvable: $ConfigPath (attendu: { `"TaniumUrl`", `"TaniumApiToken`" })" +} +$cfg = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json +$baseUrl = ($cfg.TaniumUrl -replace '/+$',''); if ($baseUrl -notmatch '^https?://') { $baseUrl = "https://$baseUrl" } + +$headers = @{ + 'Accept' = 'application/json' + 'Content-Type' = 'application/json' +} +if ($cfg.TaniumApiToken) { + $headers['X-Api-Token'] = $cfg.TaniumApiToken + $headers['session'] = $cfg.TaniumApiToken +} + +function Invoke-TaniumApi { + param( + [Parameter(Mandatory)][ValidateSet('GET','POST','PATCH','PUT','DELETE')] [string]$Method, + [Parameter(Mandatory)][string]$Path, + [hashtable]$Query, + [object]$Body + ) + $b = [System.UriBuilder]("$baseUrl$Path") + if ($Query) { + $nv = [System.Web.HttpUtility]::ParseQueryString([string]::Empty) + foreach ($k in $Query.Keys) { $nv[$k] = [string]$Query[$k] } + $b.Query = $nv.ToString() + } + $uri = $b.Uri.AbsoluteUri + if ($PSBoundParameters.ContainsKey('Body')) { Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -Body ($Body | ConvertTo-Json -Depth 20) } + else { Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers } +} + +function Get-TaniumItems { + param([Parameter(Mandatory)][object]$Response) + if ($Response.items) { ,@($Response.items) } + elseif ($Response.data) { ,@($Response.data) } + elseif ($Response.users) { ,@($Response.users) } + elseif ($Response.user_groups) { ,@($Response.user_groups) } + else { ,@($Response) } +} + +# helper: match substring/wildcard, case-insensitive +function Test-Match { + param([string]$Text, [string[]]$Patterns) + if (-not $Patterns -or $Patterns.Count -eq 0) { return $true } + foreach ($p in $Patterns) { + if ([string]::IsNullOrWhiteSpace($p)) { continue } + $pat = $p + if ($pat -notmatch '[\*\?\[\]]') { $pat = "*$pat*" } # ajoute wildcards si absent + if ($Text -like $pat -or $Text -like $pat.ToLower() -or $Text -like $pat.ToUpper()) { return $true } + } + return $false +} + +# Déterminer quelles sections afficher +$hasUserFilter = $PSBoundParameters.ContainsKey('User') -and $User -and $User.Count -gt 0 +$hasGroupFilter = $PSBoundParameters.ContainsKey('Group') -and $Group -and $Group.Count -gt 0 +if ($hasUserFilter -and -not $hasGroupFilter) { $showUsers = $true; $showGroups = $false } +elseif ($hasGroupFilter -and -not $hasUserFilter) { $showUsers = $false; $showGroups = $true } +else { $showUsers = $true; $showGroups = $true } # aucun filtre OU les deux => montrer les deux + +# --- Rôles (nécessaire pour libellés) --- +Write-Host ">> Récupère la liste des rôles..." -ForegroundColor Cyan +$rolesAll = Get-TaniumItems (Invoke-TaniumApi -Method GET -Path '/api/v2/content_set_roles' -Query @{ limit = 5000 }) +$roleIndex = @{}; foreach ($r in $rolesAll) { if ($null -ne $r.id) { $roleIndex[[int]$r.id] = [string]$r.name } } + +function Format-RoleLabel { + param([int]$Id, [string]$Name) + if ([string]::IsNullOrWhiteSpace($Name)) { $Name = $roleIndex[$Id] } + if ([string]::IsNullOrWhiteSpace($Name)) { "RoleID:$Id" } else { "RoleID:$Id ($Name)" } +} + +# --- Utilisateurs (+ memberships) -------------------------------------------- +$usersWithRoles = @() +if ($showUsers) { + Write-Host ">> Récupère les utilisateurs..." -ForegroundColor Cyan + $usersAll = Get-TaniumItems (Invoke-TaniumApi -Method GET -Path '/api/v2/users' -Query @{ limit = 5000 }) + + $users = $usersAll | Where-Object { + $disp = $_.display_name ?? $_.displayName ?? $_.name ?? $_.username + -not ($disp -like 'Tanium Internal*') -and ( + (Test-Match -Text $disp -Patterns $User) -or (Test-Match -Text $_.username -Patterns $User) + ) + } + + Write-Host ">> Récupère les memberships (User<->Role)..." -ForegroundColor Cyan + $userRoleMships = Get-TaniumItems (Invoke-TaniumApi -Method GET -Path '/api/v2/content_set_role_memberships' -Query @{ limit = 50000 }) + + # Index users + $userIndex = @{} + foreach ($u in $users) { + $userIndex[[int]$u.id] = [pscustomobject]@{ + ID = [int]$u.id + Name = $u.display_name ?? $u.displayName ?? $u.name ?? $u.username + Username = $u.username + Email = $u.email + } + } + + # Rôles par user + $rolesByUser = @{} + foreach ($m in $userRoleMships) { + if ($null -eq $m.user -or $null -eq $m.content_set_role) { continue } + $uid = [int]$m.user.id + if (-not $userIndex.ContainsKey($uid)) { continue } # filtré/exclu + $rid = [int]$m.content_set_role.id + $rname = [string]$m.content_set_role.name + $label = Format-RoleLabel -Id $rid -Name $rname + if (-not $rolesByUser.ContainsKey($uid)) { $rolesByUser[$uid] = @() } + $rolesByUser[$uid] += $label + } + + $usersWithRoles = $userIndex.GetEnumerator() | + ForEach-Object { + $uid = $_.Key + $info = $_.Value + $roles = @() + if ($rolesByUser.ContainsKey($uid)) { + $roles = $rolesByUser[$uid] | Where-Object { $_ } | Sort-Object -Unique + } + [pscustomobject]@{ + ID = $info.ID + Nom = $info.Name + Username = $info.Username + RolesCount = $roles.Count + Roles = ($roles -join ', ') + } + } | + Sort-Object -Property Nom +} + +# --- Groupes (+ memberships) -------------------------------------------------- +$groupsWithRoles = @() +if ($showGroups) { + Write-Host ">> Récupère les groupes..." -ForegroundColor Cyan + $groupsAll = Get-TaniumItems (Invoke-TaniumApi -Method GET -Path '/api/v2/user_groups' -Query @{ limit = 5000 }) + $groups = $groupsAll | Where-Object { + $gname = $_.name ?? $_.display_name ?? $_.displayName + Test-Match -Text $gname -Patterns $Group + } + + Write-Host ">> Récupère les memberships (Group<->Role)..." -ForegroundColor Cyan + $groupRoleMships = Get-TaniumItems (Invoke-TaniumApi -Method GET -Path '/api/v2/content_set_user_group_role_memberships' -Query @{ limit = 50000 }) + + # Index groupes + $groupIndex = @{} + foreach ($g in $groups) { + $groupIndex[[int]$g.id] = [pscustomobject]@{ + ID = [int]$g.id + Name = $g.name ?? $g.display_name ?? $g.displayName + } + } + + # Rôles par groupe + $rolesByGroup = @{} + foreach ($m in $groupRoleMships) { + if ($null -eq $m.user_group -or $null -eq $m.content_set_role) { continue } + $gid = [int]$m.user_group.id + if (-not $groupIndex.ContainsKey($gid)) { continue } # filtré/exclu + $rid = [int]$m.content_set_role.id + $rname = [string]$m.content_set_role.name + $label = Format-RoleLabel -Id $rid -Name $rname + if (-not $rolesByGroup.ContainsKey($gid)) { $rolesByGroup[$gid] = @() } + $rolesByGroup[$gid] += $label + } + + $groupsWithRoles = $groupIndex.GetEnumerator() | + ForEach-Object { + $gid = $_.Key + $g = $_.Value + $roles = @() + if ($rolesByGroup.ContainsKey($gid)) { + $roles = $rolesByGroup[$gid] | Where-Object { $_ } | Sort-Object -Unique + } + [pscustomobject]@{ + ID = $g.ID + Groupe = $g.Name + RolesCount = $roles.Count + Roles = ($roles -join ', ') + } + } | + Sort-Object -Property Groupe +} + +# --- Affichage conditionnel --- +if ($showUsers) { + Write-Host "" + Write-Host "=== ROLES PAR UTILISATEUR (hors 'Tanium Internal*') ===" -ForegroundColor Green + $usersWithRoles | Format-Table -Property ID,Nom,Username,RolesCount,Roles -AutoSize +} +if ($showGroups) { + Write-Host "" + Write-Host "=== ROLES PAR GROUPE ===" -ForegroundColor Green + $groupsWithRoles | Format-Table -Property ID,Groupe,RolesCount,Roles -AutoSize +} + +# --- Sortie exploitable (ne s’affiche que si tu ne l’assignes pas) --- +[pscustomobject]@{ + UsersWithRoles = $usersWithRoles + GroupsWithRoles = $groupsWithRoles +} diff --git a/API/RBAC_ListUserAndGroup_details_Redden-TanREST.ps1 b/API/RBAC_ListUserAndGroup_details_Redden-TanREST.ps1 new file mode 100644 index 0000000..a438ffa --- /dev/null +++ b/API/RBAC_ListUserAndGroup_details_Redden-TanREST.ps1 @@ -0,0 +1,82 @@ +#requires -Version 7.0 +<# +.SYNOPSIS +List Tanium Deploy Software Packages using Get-DeploySoftware. +Reads URL/token from config.json, initializes the session, then queries by: + - All | ByName | ByID | ByVendor | ByCommand | NameRegex +and displays results in Out-GridView (fallback to console table). +#> + +# ========================= +# Block 1 - Prerequisites +# ========================= +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# ========================= +# Block 2 - Load config & init session +# ========================= +$configPath = Join-Path $PSScriptRoot 'config.json' +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' + +if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } + +Write-Host "Reading configuration from: $configPath" +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json +$TaniumUrl = $config.TaniumUrl +$TaniumToken = $config.TaniumApiToken + +if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided (config.json or environment variables)." +} +if ($TaniumUrl -match '^https?://') { + $TaniumUrl = $TaniumUrl -replace '^https?://','' -replace '/+$','' + Write-Host "Normalized TaniumUrl to host: $TaniumUrl" +} + +$ExportObject = @{ + baseURI = $TaniumUrl + token = ($TaniumToken | ConvertTo-SecureString -AsPlainText -Force) +} +$ExportObject | Export-Clixml -Path $TempXml + +Write-Host "Initializing Tanium session..." +Initialize-TaniumSession -PathToXML $TempXml +Write-Host "Tanium session initialized." + +# ========================= +# Block 3 - Queries +# ========================= +#Get-DeploySoftware -All | Out-GridView +#Get-Role -All | Out-GridView +#Get-UserAndGroupDetail + +# -- Out-GridView available? (PS7 tip: Install-Module Microsoft.PowerShell.GraphicalTools) +$hasOGV = [bool](Get-Command Out-GridView -ErrorAction SilentlyContinue) + +Write-Host "Fetching Users..." -ForegroundColor Cyan +$users = Get-User -All | Sort-Object displayName, username +if ($hasOGV) { + $users | Out-GridView -Title "Tanium Users (All)" -Wait +} else { + $users | Format-Table id, displayName, username, email -AutoSize + Write-Host "(Out-GridView not available; showing table output)" -ForegroundColor Yellow +} + +Write-Host "Fetching User Groups..." -ForegroundColor Cyan +$groups = Get-UserGroup -All | Sort-Object name +if ($hasOGV) { + $groups | Out-GridView -Title "Tanium User Groups (All)" -Wait +} else { + $groups | Format-Table id, name, description -AutoSize + Write-Host "(Out-GridView not available; showing table output)" -ForegroundColor Yellow +} + +# ========================= +# Block 5 - Cleanup +# ========================= +if (Test-Path $TempXml) { + Remove-Item $TempXml -Force -ErrorAction SilentlyContinue + Write-Host "Temporary CLIXML removed: $TempXml" +} diff --git a/API/Sensor_List_Rest.ps1 b/API/Sensor_List_Rest.ps1 new file mode 100644 index 0000000..101ce5d --- /dev/null +++ b/API/Sensor_List_Rest.ps1 @@ -0,0 +1,165 @@ +<# +.SYNOPSIS +List Tanium **Sensors** via the REST API, using Url/Token from `config.json` by default. +Shows: Id, Name, Content Set, Category, Parameters?, CacheSeconds (if exposed). +Supports filters by name/content set/category, Out‑GridView, and CSV export. + +.USAGE +# Basic (reads config.json in the same folder) +./List-Sensors.ps1 + +# Filter by name/content set/category (regex), show grid and export CSV +./List-Sensors.ps1 -NameLike '^BIOS' -ContentSetLike 'Deploy' -CategoryLike 'Hardware' -Grid -ExportCsv C:\Temp\sensors.csv + +# Raw JSON +./List-Sensors.ps1 -Raw + +# Override Url/Token explicitly +./List-Sensors.ps1 -Url tanium.pp.dktinfra.io -Token 'token-xxxx' +#> + +param( + [string]$Url, + [string]$Token, + [switch]$SkipCertCheck, + + [string]$NameLike, + [string]$ContentSetLike, + [string]$CategoryLike, + + [switch]$Raw, + [switch]$Grid, + [string]$ExportCsv +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + +# ---------- Helpers ---------- +function New-BaseUri([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" +} + +# 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 } +} + +$Base = New-BaseUri $Url +$Headers = @{ session = $Token; 'Content-Type' = 'application/json' } + +function Invoke-Tanium([string]$Method,[string]$Path,[object]$BodyObj) { + $uri = if ($Path -match '^https?://') { $Path } else { "$Base$Path" } + $body = if ($null -ne $BodyObj) { $BodyObj | ConvertTo-Json -Depth 10 } else { $null } + $ps7 = ($PSVersionTable.PSVersion.Major -ge 7) + if ($ps7 -and $SkipCertCheck) { + return Invoke-RestMethod -SkipCertificateCheck -Method $Method -Uri $uri -Headers $Headers -Body $body + } else { + try { Enter-InsecureTls -Enable:$SkipCertCheck; return Invoke-RestMethod -Method $Method -Uri $uri -Headers $Headers -Body $body } + finally { Exit-InsecureTls } + } +} + +# ---------- Fetch Sensors + Content Sets (for names) ---------- +$resp = Invoke-Tanium GET "/api/v2/sensors" $null +$sensors = if ($resp.data) { $resp.data } elseif ($resp.items) { $resp.items } else { $resp } + +$csResp = Invoke-Tanium GET "/api/v2/content_sets" $null +$contentSets = if ($csResp.data) { $csResp.data } elseif ($csResp.items) { $csResp.items } else { $csResp } +$csById = @{} +foreach ($cs in $contentSets) { if ($cs.id -ne $null) { $csById[$cs.id] = $cs } } + +if ($Raw) { + [pscustomobject]@{ sensors = $sensors; contentSets = $contentSets } | ConvertTo-Json -Depth 14 + return +} + +if (-not $sensors) { Write-Output 'No sensors returned.'; return } + +# ---------- Helpers to read maybe-present fields ---------- +function TryField($obj, $path) { + try { + $val = $obj + foreach ($p in ($path -split '\.')) { + if ($null -eq $val) { return $null } + $pp = $val.PSObject.Properties[$p] + if (-not $pp) { return $null } + $val = $pp.Value + } + return $val + } catch { return $null } +} + +# ---------- Shape output ---------- +$rows = foreach ($s in $sensors) { + $name = TryField $s 'name' + if ($NameLike -and $name -and -not ($name -match $NameLike)) { continue } + + # content set: could be nested or just id + $csName = '' + $csId = TryField $s 'content_set.id' + if ($null -eq $csId) { $csId = TryField $s 'content_set_id' } + if ($null -ne $csId -and $csById.ContainsKey($csId)) { $csName = $csById[$csId].name } + if ($ContentSetLike -and $csName -and -not ($csName -match $ContentSetLike)) { continue } + + $category = TryField $s 'category' + if ($CategoryLike -and $category -and -not ($category -match $CategoryLike)) { continue } + + $hasParams = $false + if (TryField $s 'parameters') { $hasParams = $true } + elseif (TryField $s 'parameter_definitions') { $hasParams = $true } + elseif (TryField $s 'parameterDefinitions') { $hasParams = $true } + + $cacheSec = TryField $s 'expiration_seconds' + if ($null -eq $cacheSec) { $cacheSec = TryField $s 'cache_seconds' } + + [pscustomobject]@{ + Id = TryField $s 'id' + Name = $name + ContentSet = $csName + Category = $category + Parameters = $hasParams + CacheSeconds = $cacheSec + } +} + +if (-not $rows) { Write-Output 'No matching sensors.'; 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 = "Sensors — {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 diff --git a/API/graphql - CheckBiosVersion.ps1 b/API/graphql - CheckBiosVersion.ps1 new file mode 100644 index 0000000..16ee531 --- /dev/null +++ b/API/graphql - CheckBiosVersion.ps1 @@ -0,0 +1,237 @@ +<# +.SYNOPSIS +Query Tanium Gateway (GraphQL) for endpoints and return Computer Name (NetBIOS), Model, and BIOS Version. +Supports pagination, optional filtering from a simple list file (one name per line), Out-GridView, and CSV export. + +.DESCRIPTION +- Uses GraphQL operation exampleGetEndpoints with sensors "Model" and "BIOS Version". +- Paginates using pageInfo.hasNextPage / endCursor until all endpoints are collected. +- Optional filtering: provide -ListFile with one computer name per line (NetBIOS or FQDN). +- Normalizes names by taking the part before the first dot and uppercasing (so NetBIOS and FQDN match). +- Reads URL/token from a local config.json if not passed as parameters. +- Optional TLS bypass for lab (-SkipCertCheck or via config.json flag). + +.USAGE +# With config.json (same folder): { "TaniumUrl": "tanium.pp.dktinfra.io", "TaniumApiToken": "token-xxxx", "SkipCertificateCheck": true } +./APICheckBiosVersion.ps1 -Count 200 -Time 30 -MaxAge 900 -PageSize 200 + +# Filter from a list (one name per line, no header) +./APICheckBiosVersion.ps1 -ListFile .\machines.txt + +# Grid view and CSV export +./APICheckBiosVersion.ps1 -ListFile .\machines.txt -Grid -ExportCsv C:\Temp\bios.csv + +# Raw JSON of aggregated edges +./APICheckBiosVersion.ps1 -Raw + +.PARAMETERS +-Url, -Token Override Tanium URL and API token (otherwise read from config.json). +-Count, -Time, -MaxAge GraphQL collection knobs (expectedCount, stableWaitTime, maxAge seconds). +-PageSize Page size for pagination (GraphQL 'first'). +-ListFile Optional path to a simple list (one hostname per line) to filter results. +-Raw Output aggregated JSON instead of table/grid. +-Grid Show results in Out-GridView (if available). +-Pick With -Grid, allow selection and output only the selection. +-ExportCsv Path to write CSV export. +-SkipCertCheck Ignore TLS cert validation (lab/dev only). + +.NOTES +Requires: PowerShell 5.1+ (Out-GridView on PS7 via Microsoft.PowerShell.GraphicalTools). +#> + +param( + [string]$Url, + [string]$Token, + [switch]$SkipCertCheck, + + [int]$Count = 200, + [int]$Time = 30, + [int]$MaxAge = 900, + [int]$PageSize = 200, + + [string]$ListFile, + [switch]$Raw, + [switch]$Grid, + [switch]$Pick, + [string]$ExportCsv +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + +# ---------- Helpers ---------- +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 (temporary, restored after call) +$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 + } +} + +function Get-HostKey { + param([string]$Name) + if ([string]::IsNullOrWhiteSpace($Name)) { return '' } + $n = $Name.Trim() + $nb = ($n -split '\.', 2)[0] # take part before first dot if FQDN + return $nb.ToUpperInvariant() +} + +function Read-TargetsFromFile { + param([Parameter(Mandatory)][string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { throw "List file not found: $Path" } + $set = New-Object System.Collections.Generic.HashSet[string] + Get-Content -LiteralPath $Path -Encoding UTF8 | ForEach-Object { + $line = $_.Trim() + if (-not [string]::IsNullOrWhiteSpace($line)) { [void]$set.Add((Get-HostKey $line)) } + } + if ($set.Count -eq 0) { throw "List file is empty: $Path" } + return $set +} + +function Get-ValuesForSensor { + param([array]$Columns, [string]$SensorName) + $match = $Columns | Where-Object { $_.sensor.name -eq $SensorName } + if (-not $match) { return '' } + $vals = @() + foreach ($c in $match) { + foreach ($v in $c.values) { + if ($null -ne $v -and ("$v").Trim()) { $vals += ("$v") } + } + } + # de-dup while preserving order + $uniq = @() + foreach ($v in $vals) { if (-not $uniq.Contains($v)) { $uniq += $v } } + return ($uniq -join ' | ') +} + +# ---------- 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 } +} + +# ---------- Prepare ---------- +$gateway = Get-GatewayUri -HostLike $Url +$headers = @{ 'Content-Type' = 'application/json'; session = $Token } +$targetSet = $null +if ($ListFile) { $targetSet = Read-TargetsFromFile -Path $ListFile } + +# ---------- GraphQL (paginated) ---------- +$Query = @' +query exampleGetEndpoints($count: Int, $time: Int, $maxAge: Int, $first: Int, $after: Cursor) { + endpoints(source: { ts: { expectedCount: $count, stableWaitTime: $time, maxAge: $maxAge } }, first: $first, after: $after) { + edges { node { + computerID + name + serialNumber + ipAddress + sensorReadings(sensors: [{name: "Model"}, {name: "BIOS Version"}]) { + columns { name values sensor { name } } + } + }} + pageInfo { hasNextPage endCursor } + } +} +'@ + +$allEdges = New-Object System.Collections.Generic.List[object] +$cursor = $null + +while ($true) { + $variables = @{ count = $Count; time = $Time; maxAge = $MaxAge; first = $PageSize; after = $cursor } + $bodyObj = @{ query = $Query; variables = $variables; operationName = 'exampleGetEndpoints' } + $bodyJson = $bodyObj | ConvertTo-Json -Depth 12 + + $resp = $null + $ps7 = ($PSVersionTable.PSVersion.Major -ge 7) + if ($ps7 -and $SkipCertCheck) { + $resp = Invoke-RestMethod -SkipCertificateCheck -Method Post -Uri $gateway -Headers $headers -ContentType 'application/json' -Body $bodyJson + } + else { + try { Enter-InsecureTls -Enable:$SkipCertCheck + $resp = Invoke-RestMethod -Method Post -Uri $gateway -Headers $headers -ContentType 'application/json' -Body $bodyJson + } + finally { Exit-InsecureTls } + } + + if ($resp.errors) { throw ("GraphQL errors: " + (($resp.errors | ForEach-Object { $_.message }) -join '; ')) } + + $edges = $resp.data.endpoints.edges + if ($edges) { $allEdges.AddRange($edges) } + + $pi = $resp.data.endpoints.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 'Aucun endpoint retourné.'; return } + +# ---------- Filter (optional) + Output ---------- +$rows = foreach ($edge in $allEdges) { + $n = $edge.node + $nb = Get-HostKey $n.name + if ($targetSet -and -not $targetSet.Contains($nb)) { continue } + $cols = $n.sensorReadings.columns + [pscustomobject]@{ + Nom = $nb + Modèle = (Get-ValuesForSensor -Columns $cols -SensorName 'Model') + VersionBIOS = (Get-ValuesForSensor -Columns $cols -SensorName 'BIOS Version') + } +} + +if (-not $rows) { Write-Output 'Aucun poste correspondant.'; return } +$rowsSorted = $rows | Sort-Object Nom + +# Optional CSV export +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)" } +} + +# Grid mode +if ($Grid) { + $title = "BIOS Versions — {0} poste(s)" -f ($rowsSorted.Count) + if (Get-Command Out-GridView -ErrorAction SilentlyContinue) { + if ($Pick) { + $sel = $rowsSorted | Out-GridView -Title $title -PassThru + if ($sel) { $sel | Format-Table -AutoSize } + return + } else { + $rowsSorted | Out-GridView -Title $title + return + } + } else { + Write-Warning "Out-GridView n'est pas disponible. Sous PowerShell 7+, installe: Install-Module Microsoft.PowerShell.GraphicalTools" + } +} + +# Default console table +$rowsSorted | Format-Table -AutoSize diff --git a/API/graphql - EnvironmentVariable.ps1 b/API/graphql - EnvironmentVariable.ps1 new file mode 100644 index 0000000..953cca2 --- /dev/null +++ b/API/graphql - EnvironmentVariable.ps1 @@ -0,0 +1,119 @@ +<# +.SYNOPSIS +Read URL/token from config.json, authenticate to Tanium Gateway (GraphQL), +then query "System Environment Variables" and print selected variables. +#> + +# ========================= +# Block 1 - Load config & build auth +# ========================= +$ErrorActionPreference = 'Stop' + +# config.json is next to this script +$configPath = Join-Path $PSScriptRoot 'config.json' +if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } + +Write-Host "Reading configuration from: $configPath" +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json +$TaniumUrl = $config.TaniumUrl +$TaniumToken = $config.TaniumApiToken + +if ([string]::IsNullOrWhiteSpace($TaniumUrl) -or [string]::IsNullOrWhiteSpace($TaniumToken)) { + throw "Both TaniumUrl and TaniumApiToken must be provided (config.json or environment variables)." +} + +# Normalize host (no scheme/trailing slash) +if ($TaniumUrl -match '^https?://') { $TaniumUrl = $TaniumUrl -replace '^https?://','' -replace '/+$','' } + +# Gateway GraphQL endpoint +$uri = "https://$TaniumUrl/plugin/products/gateway/graphql" + +# HTTP headers with session token +$headers = @{ + "Content-Type" = "application/json" + "session" = $TaniumToken +} + +# ========================= +# Block 2 - Quick auth check (GraphQL ping) +# ========================= +try { + $pingBody = @{ query = 'query { __typename }' } | ConvertTo-Json + $pingResp = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $pingBody + if ($pingResp.errors) { + $msg = ($pingResp.errors | ForEach-Object { $_.message }) -join '; ' + throw "GraphQL ping returned errors: $msg" + } + Write-Host "Authentication OK (Gateway reachable)." +} +catch { + throw "Authentication or connectivity failed: $($_.Exception.Message)" +} + +# ========================= +# Block 3 - Actual query +# ========================= +# GraphQL variables +$variables = @{ + count = 1 # adjust as needed + time = 10 + maxAge = 600 +} + +# GraphQL query (columns/values only — no 'rows') +$query = @' +query getEndpoints($count: Int, $time: Int, $maxAge: Int) { + endpoints(source: { ts: { expectedCount: $count, stableWaitTime: $time, maxAge: $maxAge }}) { + edges { + node { + computerID + name + serialNumber + ipAddress + sensorReadings(sensors: [{ name: "System Environment Variables" }]) { + columns { + name + values + sensor { name } + } + } + } + } + } +} +'@ + +# Build request body +$body = @{ query = $query; variables = $variables } | ConvertTo-Json -Depth 20 + +# Call Gateway +$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body +if ($response.errors) { + $msg = ($response.errors | ForEach-Object { $_.message }) -join '; ' + throw "GraphQL returned errors: $msg" +} + +# ========================= +# Block 4 - Parse & print +# ========================= +$endpoints = $response.data.endpoints.edges | ForEach-Object { $_.node } + +# Env vars to keep (case-sensitive per your example) +$varsToKeep = @("PROCESSOR_LEVEL", "windir", "PROCESSOR_REVISION") + +foreach ($endpoint in $endpoints) { + Write-Host "`n$($endpoint.name) [$($endpoint.ipAddress)] $($endpoint.serialNumber) :" + foreach ($reading in $endpoint.sensorReadings) { + foreach ($col in $reading.columns) { + foreach ($env in ($col.values | Where-Object { $_ })) { + if ($env -match "^(.*?)=(.*)$") { + $name = $matches[1] + $value = $matches[2] + if ($varsToKeep -contains $name) { + Write-Host " $name = $value" + } + } + } + } + } +} diff --git a/API/graphql - ListComputerGroup.ps1 b/API/graphql - ListComputerGroup.ps1 new file mode 100644 index 0000000..56d8011 --- /dev/null +++ b/API/graphql - ListComputerGroup.ps1 @@ -0,0 +1,219 @@ +# 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 diff --git a/API/rest - list sensor detail.ps1 b/API/rest - list sensor detail.ps1 new file mode 100644 index 0000000..35dfe46 --- /dev/null +++ b/API/rest - list sensor detail.ps1 @@ -0,0 +1,238 @@ +<# +.SYNOPSIS +Export or print the scripts used by Sensors per OS (Windows/Mac/Linux/AIX/Solaris…), via Tanium REST. +Reads Url/Token from config.json. Can dump files (-DumpPath) and/or print scripts to console (-Print). + +.USAGE +# Index only (table) +.\Export-SensorScriptsByOS.ps1 + +# Dump all scripts to files + show a grid +.\Export-SensorScriptsByOS.ps1 -DumpPath .\out -Grid + +# Only Windows scripts of sensors matching a name regex, and print to console +.\Export-SensorScriptsByOS.ps1 -NameLike '^WSUS|BIOS' -Os Windows -Print + +# One sensor by id, dump to folder and print +.\Export-SensorScriptsByOS.ps1 -Id 665 -DumpPath C:\Temp\sensor_src -Print + +# Export index to CSV +.\Export-SensorScriptsByOS.ps1 -DumpPath .\out -ExportCsv .\index.csv +#> + +param( + [string]$Url, + [string]$Token, + [switch]$SkipCertCheck, + + [string]$NameLike, + [int]$Id, + + [string[]]$Os, # ex: -Os Windows,Mac + [switch]$Print, # print script text in console + [string]$DumpPath, # write each script to a file + [string]$ExportCsv, # write index CSV + + [switch]$Grid, + [switch]$Raw +) + +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + +# ---------- Helpers ---------- +function New-BaseUri([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" +} + +# 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 ---------- +$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 } +} + +$Base = New-BaseUri $Url +$Headers = @{ session = $Token; 'Content-Type' = 'application/json' } + +function Invoke-Tanium([string]$Method,[string]$Path,[object]$BodyObj,[switch]$AllowError) { + $uri = if ($Path -match '^https?://') { $Path } else { "$Base$Path" } + $body = if ($null -ne $BodyObj) { $BodyObj | ConvertTo-Json -Depth 12 } else { $null } + $ps7 = ($PSVersionTable.PSVersion.Major -ge 7) + try { + if ($ps7 -and $SkipCertCheck) { + return Invoke-RestMethod -SkipCertificateCheck -Method $Method -Uri $uri -Headers $Headers -Body $body + } else { + Enter-InsecureTls -Enable:$SkipCertCheck + return Invoke-RestMethod -Method $Method -Uri $uri -Headers $Headers -Body $body + } + } + catch { if ($AllowError) { return $null } else { throw } } + finally { Exit-InsecureTls } +} + +function TryGet($obj, [string[]]$names) { + foreach ($n in $names) { + $p = $obj.PSObject.Properties[$n] + if ($p -and $p.Value) { return $p.Value } + } + return $null +} +function CleanName([string]$s) { if ($s) { ($s -replace '[\\/:*?"<>|]','_').Trim() } } +function Detect-OS([string]$raw) { + if (-not $raw) { return $null } + $r = $raw.ToLowerInvariant() + if ($r -match 'win') { return 'Windows' } + if ($r -match 'mac|darwin|osx'){ return 'Mac' } + if ($r -match 'lin|posix|unix'){ return 'Linux' } + # laissons tels quels pour AIX/Solaris/HP-UX/etc. + return ($raw -replace '\s+',' ').Trim() +} + +# Per-OS extraction from a detailed sensor payload +function Get-SourcesByOS([object]$sensorDetail) { + $map = @{} + # 1) flat fields + $ws = TryGet $sensorDetail @('windows_source','windowsQuery','windows_query') + $ls = TryGet $sensorDetail @('linux_source','posix_source','linuxQuery','linux_query','posixQuery','posix_query','solaris_source','aix_source') + $ms = TryGet $sensorDetail @('mac_source','darwin_source','macQuery','mac_query') + $generic = TryGet $sensorDetail @('source','query_text','script','definition','definition_text','content') + if ($ws) { $map['Windows'] = [string]$ws } + if ($ms) { $map['Mac'] = [string]$ms } + if ($ls) { $map['Linux'] = [string]$ls } # certaines versions renvoient aussi AIX/Solaris en champs dédiés : traités ci-dessous + + # 2) nested arrays, avec platform/operating_system + source/text/... + foreach ($arrName in @('queries','sources','os_queries','platforms')) { + $arr = $sensorDetail.PSObject.Properties[$arrName].Value + if ($arr -and ($arr -is [System.Collections.IEnumerable])) { + foreach ($q in $arr) { + $plat = TryGet $q @('platform','operating_system','os','name','type') + $code = TryGet $q @('source','text','query_text','script','definition','content','body') + if ($code) { + $os = Detect-OS ([string]$plat) + if (-not $os) { $os = 'All' } + # Ne pas écraser si déjà présent + if (-not $map.ContainsKey($os)) { $map[$os] = [string]$code } + } + } + } + } + if ($map.Count -eq 0 -and $generic) { $map['All'] = [string]$generic } + return $map +} + +# ---------- Get sensor list (or just one) ---------- +$items = $null +if ($Id) { + $d = Invoke-Tanium GET "/api/v2/sensors/$Id" $null -AllowError + if ($Raw) { $d | ConvertTo-Json -Depth 14; return } + $list = Invoke-Tanium GET "/api/v2/sensors" $null + $items = if ($list.data) { $list.data } elseif ($list.items) { $list.items } else { $list } + $items = $items | Where-Object { $_.id -eq $Id } +} else { + $list = Invoke-Tanium GET "/api/v2/sensors" $null + $items = if ($list.data) { $list.data } elseif ($list.items) { $list.items } else { $list } +} + +if (-not $items) { Write-Output 'No sensors visible via API.'; return } +if ($NameLike) { $items = $items | Where-Object { $_.name -match $NameLike } } +if (-not $items) { Write-Output 'No matching sensors.'; return } + +# Prepare dump folder if requested +if ($DumpPath) { + if (-not (Test-Path -LiteralPath $DumpPath)) { New-Item -ItemType Directory -Path $DumpPath | Out-Null } +} + +# Normalize OS filter to case-insensitive hashset +$osFilter = $null +if ($Os -and $Os.Count -gt 0) { + $osFilter = New-Object System.Collections.Generic.HashSet[string] ([StringComparer]::OrdinalIgnoreCase) + foreach ($o in $Os) { if ($o) { [void]$osFilter.Add($o) } } +} + +$rows = New-Object System.Collections.Generic.List[object] + +foreach ($s in $items) { + $sid = $s.id; $sname = [string]$s.name + $detail = Invoke-Tanium GET "/api/v2/sensors/$sid" $null -AllowError + # unwrap common envelopes + $sensorDetail = $null + foreach ($k in 'data','sensor','item') { if ($null -eq $sensorDetail -and $detail.PSObject.Properties[$k]) { $sensorDetail = $detail.$k } } + if (-not $sensorDetail) { if ($detail.items -and $detail.items.Count -gt 0) { $sensorDetail = $detail.items[0] } else { $sensorDetail = $detail } } + + $sources = Get-SourcesByOS $sensorDetail + if ($sources.Keys.Count -eq 0) { + $rows.Add([pscustomobject]@{ Id=$sid; Name=$sname; OS='—'; HasScript=$false; Length=0; Path=''; Text=$null }) + continue + } + + # apply OS filter if any + $osKeys = $sources.Keys + if ($osFilter) { $osKeys = $osKeys | Where-Object { $osFilter.Contains($_) } } + + foreach ($os in $osKeys) { + $txt = [string]$sources[$os] + $len = ($txt | Measure-Object -Character).Characters + $outPath = '' + if ($DumpPath) { + $fn = (CleanName($sname) + "__" + ($os -replace '\s+','_') + ".txt") + $outPath = Join-Path $DumpPath $fn + Set-Content -LiteralPath $outPath -Value $txt -Encoding UTF8 + } + $rows.Add([pscustomobject]@{ Id=$sid; Name=$sname; OS=$os; HasScript=$true; Length=$len; Path=$outPath; Text=$txt }) + } +} + +$rows = $rows | Sort-Object Name, OS + +# Optional CSV of the index (no full text) +if ($ExportCsv) { + try { + $rows | Select-Object Id,Name,OS,HasScript,Length,Path | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $ExportCsv + Write-Host "Index exported to: $ExportCsv" -ForegroundColor Green + } catch { Write-Warning "CSV export failed: $($_.Exception.Message)" } +} + +# Grid +if ($Grid) { + if (Get-Command Out-GridView -ErrorAction SilentlyContinue) { + $rows | Select-Object Id,Name,OS,HasScript,Length,Path | Out-GridView -Title ("Sensor Scripts by OS — {0} rows" -f $rows.Count) + } else { + Write-Warning "Out-GridView not available. On PS7+: Install-Module Microsoft.PowerShell.GraphicalTools" + } +} + +# Table +$rows | Select-Object Id,Name,OS,HasScript,Length,Path | Format-Table -AutoSize + +# Print scripts to console +if ($Print) { + foreach ($r in $rows) { + if (-not $r.HasScript -or -not $r.Text) { continue } + Write-Host "`n===== SENSOR: $($r.Name) | OS: $($r.OS) | Id: $($r.Id) =====" -ForegroundColor Cyan + $r.Text + } +} diff --git a/API/test.ps1 b/API/test.ps1 new file mode 100644 index 0000000..5ee6b85 --- /dev/null +++ b/API/test.ps1 @@ -0,0 +1,89 @@ +<# +But : lire les infos endpoints à partir du cache (TDS) via GraphQL (Tanium Gateway) +#> + +# --- Pré-requis : même init que ton script --- +$ErrorActionPreference = 'Stop' +try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} +Import-Module Redden-TanREST -Force + +# Charger config.json + Initialiser la session (identique à ton script) +$configPath = Join-Path $PSScriptRoot 'config.json' +if (-not (Test-Path $configPath)) { throw "Configuration file not found: $configPath" } +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + +$TaniumUrl = if ($config.TaniumUrl) { $config.TaniumUrl } else { $env:TANIUM_URL } +$TaniumApiToken = if ($config.TaniumApiToken) { $config.TaniumApiToken } else { $env:TANIUM_TOKEN } +if ($TaniumUrl -match '^https?://') { $TaniumUrl = $TaniumUrl -replace '^https?://','' -replace '/+$','' } + +$TempXml = Join-Path $env:TEMP 'tanium-session-tmp.apicred' +@{ + baseURI = $TaniumUrl + token = ($TaniumApiToken | ConvertTo-SecureString -AsPlainText -Force) +} | Export-Clixml -Path $TempXml + +Initialize-TaniumSession -PathToXML $TempXml +# (Le point d’accès GraphQL est /plugin/products/gateway/graphql côté Tanium; même auth que REST. :contentReference[oaicite:1]{index=1}) + +# --- Requête GraphQL (TDS/cached) --- +# NB : ici on filtre sur l’OS qui "contient Windows". +# Si le champ diffère dans ton schéma (ex: operatingSystemName), adapte 'path' et/ou les champs retournés. +$query = @' +query ($first:Int, $after:Cursor, $os:String!) { + endpoints( + first: $first + after: $after + # Filtre simple : champ "operatingSystem" qui contient la valeur $os + filter: { path: "operatingSystem", value: $os, op: CONTAINS } + ) { + totalRecords + edges { + node { + id + name + ipAddress + serialNumber + operatingSystem + eidLastSeen + } + } + pageInfo { hasNextPage endCursor } + } +} +'@ + +# Variables initiales (page de 500 éléments) +$variables = @{ + first = 500 + after = $null + os = 'Windows' +} + +# --- Exécution + pagination --- +$all = New-Object System.Collections.Generic.List[object] + +do { + # Invoke-TaniumGateway exécute la requête/variables avec ta session Redden-TanREST. :contentReference[oaicite:2]{index=2} + $out = Invoke-TaniumGateway -Query $query -Variables $variables + + $page = $out.data.endpoints + foreach ($edge in $page.edges) { + $n = $edge.node + $all.Add([pscustomobject]@{ + Id = $n.id + Name = $n.name + IP = $n.ipAddress + Serial = $n.serialNumber + OS = $n.operatingSystem + LastSeen = $n.eidLastSeen + }) + } + + $variables.after = $page.pageInfo.endCursor +} while ($page.pageInfo.hasNextPage) + +# Affichage (Grid) +$all | Out-GridView -Title 'Windows endpoints (TDS cached via GraphQL)' + +# --- Nettoyage --- +Remove-Item $TempXml -Force -ErrorAction SilentlyContinue diff --git a/Interact/Reg-Filter-Even-Numbered-hostnames/readme.md b/Interact/Reg-Filter-Even-Numbered-hostnames/readme.md new file mode 100644 index 0000000..4c77929 --- /dev/null +++ b/Interact/Reg-Filter-Even-Numbered-hostnames/readme.md @@ -0,0 +1,41 @@ +# 🔍 Regex Filter for Even-Numbered Hostnames + +This regex is used in **Tanium filters** to match hostnames ending with an **even digit** in their first label. + +--- + +## ✅ Even-Numbered Hostnames (FQDN) + +```regex +^[^.]*[02468]\..+ +``` +^[^.]* → matches the first label (hostname part before the first dot). +[02468] → requires it to end with an even digit. +\..+ → ensures there is a domain suffix (must be a FQDN). + +Examples +- pc100.dkcorp.net ✅ +- pc8.lab.example.org ✅ +- pc101.sud.dkcorp.net ❌ (ends with odd digit 1) +- pc8 ❌ (not a FQDN, no dot) + +## ✅ Even-Numbered Hostnames (Optional FQDN) + +```regex +^[^.]*[02468](?:\..+)?$ +``` +^[^.]* → matches the first label (hostname part before the first dot). +[02468] → requires it to end with an even digit. +(?:\..+)? → optional dot + domain (accepts both hostname and FQDN) + +✅ Examples +Matches: +- pc8 +- pc100 +- pc8.lab +- pc100.dkcorp.net + +❌ Does not match: +- pc101 (ends with odd digit) +- pc8. (nothing after the dot) +- pc.8 (even digit not at the end of first label) diff --git a/Packages/RegistryOnHKLM/DisableWPAD.ps1 b/Packages/RegistryOnHKLM/DisableWPAD.ps1 new file mode 100644 index 0000000..bf27c9f --- /dev/null +++ b/Packages/RegistryOnHKLM/DisableWPAD.ps1 @@ -0,0 +1,22 @@ +$ErrorActionPreference = 'Stop' + +$isOS64 = [Environment]::Is64BitOperatingSystem +$isProc64 = [Environment]::Is64BitProcess + +$KeyPS = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp' +$KeyRel = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp' +$Name = 'DisableWpad' + +if ($isOS64 -and -not $isProc64) { + # 32-bit PowerShell on 64-bit OS -> write to 64-bit registry view + $bk = [Microsoft.Win32.RegistryKey]::OpenBaseKey( + [Microsoft.Win32.RegistryHive]::LocalMachine, + [Microsoft.Win32.RegistryView]::Registry64 + ) + $k = $bk.CreateSubKey($KeyRel) + $k.SetValue($Name, 1, [Microsoft.Win32.RegistryValueKind]::DWord) + $k.Close() +} else { + if (-not (Test-Path -LiteralPath $KeyPS)) { New-Item -Path $KeyPS -Force | Out-Null } + New-ItemProperty -Path $KeyPS -Name $Name -Value 1 -PropertyType DWord -Force | Out-Null +} diff --git a/Packages/RegistryOnHKLM/readme.md b/Packages/RegistryOnHKLM/readme.md new file mode 100644 index 0000000..22737c5 --- /dev/null +++ b/Packages/RegistryOnHKLM/readme.md @@ -0,0 +1,10 @@ +# 🧩 HKLM Registry Write (64-bit) + +## 🔧 What it does +- Opens **HKLM** in the **64-bit registry view**. +- Creates/opens subkey +- Sets value + +## ✅ Prerequisites +- Run as **Administrator**. +- Writes to the **64-bit** hive; use `Registry32` if you need the 32-bit view. diff --git a/Packages/shutdown.ps1 b/Packages/shutdown.ps1 new file mode 100644 index 0000000..2955d15 --- /dev/null +++ b/Packages/shutdown.ps1 @@ -0,0 +1,37 @@ +#requires -version 5.1 +# Forced shutdown in 30 seconds with on-screen message. +# Works from 32-bit PowerShell on 64-bit Windows. Run as Administrator. + +$Message = 'Shutdown by Tanium' +$TimeoutSeconds = 30 + +# Admin check +$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) +if (-not $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { + Write-Error 'Run this script as Administrator.' + exit 1 +} + +# Pick correct shutdown.exe (Sysnative when 32-bit PS on 64-bit OS) +function Get-ShutdownExePath { + $sysnative = Join-Path $env:WINDIR 'Sysnative\shutdown.exe' + $system32 = Join-Path $env:WINDIR 'System32\shutdown.exe' + if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess -and (Test-Path $sysnative)) { + return $sysnative + } else { + return $system32 + } +} +$exe = Get-ShutdownExePath + +# Optional trace in Event Log +try { + $src = 'Tanium-Shutdown-PS' + if (-not [System.Diagnostics.EventLog]::SourceExists($src)) { + New-EventLog -LogName Application -Source $src -ErrorAction SilentlyContinue + } + Write-EventLog -LogName Application -Source $src -EntryType Information -EventId 10011 -Message $Message +} catch {} + +# Schedule forced shutdown with 30s countdown and message +& $exe /s /f /t $TimeoutSeconds /c $Message diff --git a/Provision/unattend.xml/readme.md b/Provision/unattend.xml/readme.md new file mode 100644 index 0000000..b49596b --- /dev/null +++ b/Provision/unattend.xml/readme.md @@ -0,0 +1,5 @@ +# Documentation + +For the full documentation, please visit the link below: + +[Windows Provisionning With Tanium](https://blog.wuibaille.fr/2024/09/windows-provisionning-with-tanium/) diff --git a/Provision/unattend.xml/unattend.xml b/Provision/unattend.xml/unattend.xml new file mode 100644 index 0000000..b15e5f3 --- /dev/null +++ b/Provision/unattend.xml/unattend.xml @@ -0,0 +1,114 @@ + + + + + true + + + + + + MyWorkgroup + + + + nomPC + MonOrganisation + MonOrganisation + true + Romance Standard Time + M7XTQ-FN8P6-TTKYV-9D4CC-J462D + + + www.google.fr + + + + + EnableAdmin + 1 + cmd /c net user Administrator /active:yes + + + UnfilterAdministratorToken + 2 + cmd /c reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v FilterAdministratorToken /t REG_DWORD /d 0 /f + + + disable user account page + 3 + reg add HKLM\Software\Microsoft\Windows\CurrentVersion\Setup\OOBE /v UnattendCreatedUser /t REG_DWORD /d 1 /f + + + disable async RunOnce + 4 + reg add HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer /v AsyncRunOnce /t REG_DWORD /d 0 /f + + + + + fr-fr;040c:0000040c + en-us + en-us + en-us + + + 0 + + "" + 1 + 9 + 9 + 1 + "" + "" + "" + Default + + + + 1 + + + + + + + Password1 + true</PlainText> + </AdministratorPassword> + </UserAccounts> + <AutoLogon> + <Enabled>true</Enabled> + <Username>Administrator</Username> + <Domain>.</Domain> + <Password> + <Value>Password1</Value> + <PlainText>true</PlainText> + </Password> + <LogonCount>1</LogonCount> + </AutoLogon> + <Display></Display> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <NetworkLocation>Work</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + <HideLocalAccountScreen>true</HideLocalAccountScreen> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <SkipMachineOOBE>true</SkipMachineOOBE> + <SkipUserOOBE>true</SkipUserOOBE> + </OOBE> + <RegisteredOrganization>MonOrganisation</RegisteredOrganization> + <RegisteredOwner>MonOrganisation</RegisteredOwner> + <TimeZone>Romance Standard Time</TimeZone> + </component> + <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <InputLocale>fr-fr;040c:0000040c</InputLocale> + <SystemLocale>en-us</SystemLocale> + <UILanguage>en-us</UILanguage> + <UserLocale>en-us</UserLocale> + </component> + </settings> + <cpi:offlineImage cpi:source="" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> +</unattend> diff --git a/README.md b/README.md index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0d2848f9a42dba7c8c4e86c21276b423a5d8acd2 100644 GIT binary patch literal 22 dcmezWPnki1A%r23A&()Gp_Cz)ftP`c0RT!M1jYaW literal 0 HcmV?d00001