Treinamento de PowerShell
Um guia prático para escrever scripts de dispositivo que funcionam na primeira vez, em mil máquinas e não só na VM do laboratório: onde o script roda, o que ele pode imprimir, como testar e como automatizar pela API.
1.Como o agente roda o seu script
No ControlUp for Desktops quem executa o script é o agente instalado em cada dispositivo, no momento definido pelo gatilho. O script não recebe argumentos (a exceção são os gatilhos de sessão, no módulo 3) e conversa com a plataforma só pela saída padrão (stdout): o que estiver entre os marcadores vira dado ou evento; o resto é ignorado.
### SIP DATA ###Linhas de dados (JSON) que vão para um índice personalizado: viram gráfico, filtro, alerta e exportação.### SIP EVENT ###Texto curto que aparece na aba de eventos do dispositivo: bom para contar o que aconteceu ou o que falhou.Os dados de uma execução só são guardados quando três coisas são verdade ao mesmo tempo:
- a opção Sends Data está ligada e aponta para um índice;
- o script termina dentro do tempo limite;
- nada foi escrito em stderr.
Se o script escrever qualquer coisa em stderr, o agente descarta os dados daquela execução sem avisar. É a causa número 1 de "o meu índice está vazio". O módulo 6 mostra como evitar.
2.Contexto de execução e PowerShell 5.1
O contexto define com que conta o script roda e, portanto, o que ele consegue ler e alterar.
| Você precisa de… | Use o contexto |
|---|---|
| HKLM, serviços, software instalado para todos, BitLocker, TPM, log de segurança | System: LocalSystem, sessão 0, sem área de trabalho |
HKCU, %APPDATA%, %LOCALAPPDATA%, aplicativos por usuário, mostrar uma mensagem | User: só nos gatilhos que permitem esse contexto |
| O HKCU de todos os usuários logados, a partir do System | System percorrendo Registry::HKEY_USERS: fique com os SIDs que começam com S-1-5-21- ou S-1-12-1- e pule as chaves _Classes. Nunca carregue o NTUSER.DAT com reg load. |
O agente usa o Windows PowerShell 5.1
- Primeira linha sempre:
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8, para acentos e nomes saírem certos no JSON. - Nada de
param()no topo. O agente acrescenta linhas antes do seu código, e umparam()que não é a primeira instrução quebra com 'param' is not recognized. Se precisar de valores do gatilho, leia$args. - Código-fonte só com caracteres ASCII. Aspas curvas e travessões colados do Word ou de páginas da web quebram o 5.1, até dentro de comentários. Escreva comentários e nomes em inglês simples.
- Sem recursos do PowerShell 7, como
??, o operador ternário ouForEach-Object -Parallel.
3.Gatilhos e configurações
| Gatilho | Quando usar e o que observar |
|---|---|
| Custom Action (System ou User) | Sob demanda, pelo menu do dispositivo, por um alerta ou pela API. Só roda em dispositivos online. Scripts disparados por alerta precisam estar salvos como Custom Action. |
| Short / Long Interval Timer | Coleta periódica; os intervalos são da organização (Configuration › Settings › Agent › Trigger Intervals). Não permite Overwrite. |
| Once Per Day | O agente confere a cada 15 minutos e roda se não rodou nas últimas 24 horas. Não tem horário fixo. |
| Logon, Explorer Started, Logon Complete, Logoff, Lock, Unlock, Disconnect, Reconnect | Só Windows. No Logoff só há contexto System e cerca de 4 segundos. |
| SIP Service Start / Stop | A cada inicialização ou parada do agente (5 segundos para a saída). O Start roda a cópia em cache: uma alteração recém-salva pode ficar de fora. |
| Network Change | Quando a rede muda; rajadas são agrupadas em cerca de 1 segundo. |
| Process Started | Só Windows, pelo nome do executável (nome.exe). Pode disparar duas vezes: o script precisa aguentar rodar de novo. |
Argumentos: nenhum, como regra. Os gatilhos de sessão e o Process Started passam o SID do usuário (e, na maioria, um ID de logon) em $args; o Logon – New Session passa só o SID; os alertas não passam nada.
Configurações que você preenche no console
- Sends Data e o nome do índice (um índice por script).
- Overwrite: ligado guarda só a linha atual de cada dispositivo (estado); desligado guarda o histórico (eventos).
- Timeout: com
0o script dispara e o agente não espera, então nada é coletado. - Run only when online e Run only when idle (o limite de CPU ociosa fica nas configurações avançadas).
- Permission group para scripts que alteram o dispositivo, e Device tag para começar sempre por um piloto.
Se um alerta dispara ao mesmo tempo em mais dispositivos do que o limite da organização (raiz quadrada do número de dispositivos, entre 50 e 500), os scripts ligados a ele não rodam. Teste remediação por alerta num grupo pequeno.
4.Dados: SIP DATA
Entre os marcadores vai só um array JSON de objetos, sem rótulos nem linhas de depuração. Se aparecer qualquer outra coisa ali, o bloco não é lido, mesmo com a execução marcada como sucesso.
### SIP DATA BEGINS ###
[{"schema_ver":1,"drive":"C:","free_mb":51210,"pct_free":21}]
### SIP DATA ENDS ###Regras para o índice não se perder
- Só números inteiros. Decimais são arredondados. Mude a unidade e diga no nome:
_ms,_mb,_pct_x100. - O primeiro valor define o tipo do campo. Converta números explicitamente: um número que chegou como texto uma vez nunca vira gráfico.
- Nomes em
snake_case, minúsculos e para sempre. Renomear um campo divide o histórico em dois. - Linhas planas, sem objetos dentro de objetos.
- Todo campo em toda execução. Quando não der para ler, use
-1ou"unknown", nunca um valor que pareça real e nunca deixe o campo de fora. Incluaschema_ver. - Estados como palavras (
"running", não4) e datas em UTC no formatoyyyy-MM-ddTHH:mm:ssZ. - Vai criar um alerta sobre o índice? Ele precisa ter
device_id,device_typeedevice_name. - Dispositivos offline guardam os dados por até 28 dias, por padrão (no máximo 31), e enviam quando voltam.
Montando as linhas no PowerShell
Use [ordered]@{} para cada linha e uma lista para juntar. Na hora de imprimir, passe a lista com -InputObject: com o pipe, uma lista de uma linha só vira um objeto, não um array, e o bloco deixa de ser válido.
$rows = New-Object System.Collections.Generic.List[object]
$rows.Add([ordered]@{
schema_ver = 1
service = 'Spooler'
state = "$((Get-Service -Name Spooler -ErrorAction Stop).Status)"
checked_utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
})
Write-Output '### SIP DATA BEGINS ###'
Write-Output (ConvertTo-Json -InputObject $rows.ToArray() -Compress)
Write-Output '### SIP DATA ENDS ###'5.Eventos: SIP EVENT
- Linhas simples ou, melhor, linhas
chave=valor, que são fáceis de filtrar e de usar em alertas. - No máximo 4.000 caracteres. Corte de propósito e diga quantas linhas ficaram de fora.
- Eventos não ficam guardados offline. O que precisa sobreviver vai no SIP DATA.
- O título do evento é o nome do script, então dê nomes que se leiam bem, como
Disk free - report.
function Write-SipEvent {
param([string[]]$Lines, [int]$MaxChars = 3800)
$kept = New-Object System.Collections.Generic.List[string]
$used = 0
for ($i = 0; $i -lt $Lines.Count; $i++) {
$line = $Lines[$i]
if ($line.Length -gt 300) { $line = $line.Substring(0, 297) + '...' }
if ($used + $line.Length + 2 -gt $MaxChars) {
$kept.Add("Truncated=$($Lines.Count - $i) more line(s) not shown")
break
}
$kept.Add($line)
$used += $line.Length + 2
}
Write-Output '### SIP EVENT BEGINS ###'
foreach ($k in $kept) { Write-Output $k }
Write-Output '### SIP EVENT ENDS ###'
}
Write-SipEvent -Lines @('Result=would_fix', 'Script=Fast startup - audit', 'Before=1', 'After=0')Uma função declarada mais abaixo no script pode usar param() normalmente: a regra do módulo 2 vale só para o topo do arquivo.
6.Erros sem stderr
- Use
-ErrorAction Stopdentro de umtryem tudo o que pode falhar, e transforme a falha num SIP EVENT.SilentlyContinuenão silencia tudo: oGet-WinEventnum log que não existe ainda escreve em stderr. - Executáveis nativos: mande o stderr para longe com
2>$nulle confira$LASTEXITCODE. Escrevasc.exe, nuncasc, que no PowerShell é apelido deSet-Content. - Prefira CIM a WMI, sempre com limite de tempo:
Get-CimInstance ... -OperationTimeoutSec 20 -ErrorAction Stop. - Nunca consulte
Win32_Product: ela dispara verificação e reparo de pacotes MSI. Para software instalado, leia as chavesUninstallnas visões de 64 e 32 bits. - Logs de eventos:
Get-WinEvent -FilterHashtablecom-MaxEvents, lendo$e.Properties[n].Valueem vez deMessage(que é traduzido e lento). "Nenhum evento encontrado" é um zero de verdade, não um erro. - No caminho de falha, termine com
exit 1; sem isso o histórico de execuções mostra sucesso.
Armadilhas do PowerShell 5.1
| O que acontece | Como evitar |
|---|---|
| Enumerações viram números no JSON | Converta para texto: "$($svc.Status)" |
Datas viram \/Date(...)\/ | Formate você mesmo em UTC: .ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') |
[int]2.5 dá 2 (arredondamento bancário) | Use [math]::Floor quando não pode arredondar para cima |
$var -eq $null falha com listas | Ponha o $null à esquerda: $null -eq $var |
| Windows 10 e 11 com o mesmo nome em alguns campos | Decida pelo número do build: 22000 ou mais é Windows 11 |
7.Remediação segura
Script que altera o dispositivo começa sempre em modo auditoria: ele só diz o que faria. Quando o cliente aprovar, você liga a correção. Assim a primeira execução em produção não muda nada.
- Em auditoria, informe
would_fixe um evento com a mudança exata que seria feita. - Com a correção ligada, mude só se estiver errado, leia de novo para confirmar e informe
fixedoufix_failed. - Estado desconhecido nunca é motivo para mudar nada.
- Guarde
mode,actederesultem campos separados, e garanta que rodar duas vezes seguidas é seguro. - Orçamento de tempo: dê ao script cerca de metade do tempo limite do console, confira um cronômetro dentro dos laços e marque o que não terminou como
out_of_time, ainda imprimindo os dados.
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$Remediate = $false # audit mode until the customer approves
$key = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power'
$name = 'HiberbootEnabled'
try {
$before = (Get-ItemProperty -Path $key -Name $name -ErrorAction Stop).$name
$result = 'compliant'; $acted = $false
if ($before -eq 1) {
if ($Remediate) {
Set-ItemProperty -Path $key -Name $name -Value 0 -Type DWord -ErrorAction Stop
$after = (Get-ItemProperty -Path $key -Name $name -ErrorAction Stop).$name
$result = if ($after -eq 0) { 'fixed' } else { 'fix_failed' }
$acted = $true
} else {
$result = 'would_fix'
}
}
$row = [ordered]@{
schema_ver = 1
mode = if ($Remediate) { 'fix' } else { 'audit' }
acted = $acted
result = $result
before = [int]$before
}
Write-Output '### SIP DATA BEGINS ###'
Write-Output (ConvertTo-Json -InputObject @($row) -Compress)
Write-Output '### SIP DATA ENDS ###'
if ($result -ne 'compliant') {
Write-Output '### SIP EVENT BEGINS ###'
Write-Output "Result=$result"
Write-Output "Action=Set $name to 0"
Write-Output '### SIP EVENT ENDS ###'
}
}
catch {
Write-Output '### SIP EVENT BEGINS ###'
Write-Output 'Result=unknown'
Write-Output "Error=$($_.Exception.Message)"
Write-Output '### SIP EVENT ENDS ###'
exit 1
}8.Testar antes de publicar
- Numa janela comum do Windows PowerShell 5.1. Rode o arquivo e veja a saída.
- Como SYSTEM. Abra um PowerShell com
psexec -s -i powershell.exe(Sysinternals) e rode de novo: é assim que o agente roda no contexto System. - Confira o contrato com o teste abaixo: stderr vazio, código de saída e um JSON válido entre os marcadores.
- Piloto com device tag. Salve no console, aplique a uma tag com um ou dois dispositivos e confira Configuration › Data, a aba de eventos do dispositivo e o índice
builtin_script_audit. Só então amplie a tag.
$script = '.\disk-free.ps1'
$out = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script 2> .\stderr.txt
"Exit code: $LASTEXITCODE"
if ((Get-Item .\stderr.txt).Length -gt 0) {
'STDERR IS NOT EMPTY - the agent would discard this run:'
Get-Content .\stderr.txt
}
$text = $out -join "`n"
if ($text -match '(?s)### SIP DATA BEGINS ###\s*(.*?)\s*### SIP DATA ENDS ###') {
$rows = $Matches[1] | ConvertFrom-Json
"Rows: $(@($rows).Count)"
$rows | Format-Table -AutoSize
} else {
'No SIP DATA block found'
}9.Automatizar com a API
A API serve para o lado de administração: ler índices, exportar relatórios, disparar scripts em lote. Esses scripts rodam numa estação de administração ou num servidor de automação, nunca nos dispositivos.
- Endereço base
https://api.controlup.com/edge/apie o cabeçalhoAuthorization: Bearer <chave>. O endereço antigo por tenant, comx-api-key, foi desativado. - A chave tem as permissões dadas diretamente ao usuário que a criou, não as que vêm de grupos.
- Limites: 200 requisições por minuto por organização e 60 por usuário. Com HTTP 429, espere o
Retry-Aftere tente de novo. - A chave fica numa variável de ambiente ou num cofre de segredos, nunca dentro de um arquivo de script. No PowerShell 5.1, ligue o TLS 1.2 antes da primeira chamada.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if (-not $env:CU_API_KEY) { throw 'Set the CU_API_KEY environment variable first' }
$base = 'https://api.controlup.com/edge/api'
$headers = @{ Authorization = "Bearer $env:CU_API_KEY" }
# Rows of the disk_free index (up to 10,000; use the export endpoints for more)
$response = Invoke-RestMethod -Method Get -Uri "$base/data/disk_free" -Headers $headers
$response | ConvertTo-Json -Depth 5Rodar um script pela API
- Ache o script em
GET /scriptspelo nome exato: a busca por "parecido" devolve vários, e você quer um só. - A ação de execução é a
RUN_SCRIPT_V2, enviada a um dispositivo (POST /devices/{id}/actions) ou a vários (POST /devices/action, com a lista dedevice_ids). O campodataé um texto JSON, como{"script_id":"…","run_as_system":true,"run_as_user":false,"run_all_users":false,"session_id":-1}. - A resposta 202 quer dizer "na fila", não "deu certo". Confirme no índice
builtin_script_auditou nos eventos. - Em lote: grupos de 100 dispositivos, uma pausa curta entre eles, e o primeiro teste sempre com um
$DryRun = $trueque só lista o que faria. - Para relatórios grandes use
export.csvouexport.ndjsondo índice. Exportações têm nomes de dispositivos: guarde em local com acesso controlado.
Os nomes exatos dos campos de cada chamada estão na referência da API; confira lá antes de colocar uma automação em produção.
✦Exercício: espaço livre em disco
Junte tudo num script completo: uma linha por disco fixo, com tamanho, espaço livre e percentual, uma vez por dia, no contexto System.
<#
.SYNOPSIS
Reports free space on every fixed drive.
.NOTES
Platform : Windows | Language: PowerShell 5.1
Context : System
Trigger : Once Per Day
Index : disk_free (overwrite on)
Version : 1.0
#>
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
try {
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
$disks = @(Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -OperationTimeoutSec 20 -ErrorAction Stop)
$rows = New-Object System.Collections.Generic.List[object]
foreach ($d in $disks) {
$sizeMb = [math]::Floor([double]$d.Size / 1MB)
$freeMb = [math]::Floor([double]$d.FreeSpace / 1MB)
$pct = if ($sizeMb -gt 0) { [int][math]::Floor($freeMb * 100 / $sizeMb) } else { -1 }
$rows.Add([ordered]@{
schema_ver = 1
drive = [string]$d.DeviceID
size_mb = [int64]$sizeMb
free_mb = [int64]$freeMb
pct_free = $pct
checked_utc = $now
})
}
Write-Output '### SIP DATA BEGINS ###'
Write-Output (ConvertTo-Json -InputObject $rows.ToArray() -Compress)
Write-Output '### SIP DATA ENDS ###'
}
catch {
Write-Output '### SIP EVENT BEGINS ###'
Write-Output 'Result=Failure'
Write-Output 'Script=Disk free - report'
Write-Output "Error=$($_.Exception.Message)"
Write-Output '### SIP EVENT ENDS ###'
exit 1
}| No console | Valor |
|---|---|
| Plataforma e linguagem | Windows · PowerShell |
| Contexto | System |
| Gatilho | Once Per Day |
| Sends Data · índice | Ligado · disk_free |
| Overwrite | Ligado (estado atual de cada disco) |
| Timeout | 60 segundos |
| Device tag | Piloto com dois dispositivos |
Exemplo do que sai (os números são ilustrativos):
### SIP DATA BEGINS ###
[{"schema_ver":1,"drive":"C:","size_mb":243582,"free_mb":51210,"pct_free":21,"checked_utc":"2026-10-29T12:00:00Z"}]
### SIP DATA ENDS ###Faça uma versão para alertas: acrescente device_id, device_type e device_name e crie um alerta para pct_free abaixo de 10.
✓Checklist final
0 de 10 conferidos