ControlUp for Desktops · scripts de dispositivo

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.

Windows PowerShell 5.1SIP DATASIP EVENTAPI da ControlUpcerca de 45 min

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.

1 · ConsoleVocê salva o script e escolhe gatilho, contexto, índice e tempo limite.
2 · Agente no dispositivoRoda o script quando o gatilho dispara e lê o que ele escreveu no stdout.
3 · PlataformaGuarda os blocos marcados e mostra em dashboards, alertas e na API.
### 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.
Regra de ouro

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çaSystem: LocalSystem, sessão 0, sem área de trabalho
HKCU, %APPDATA%, %LOCALAPPDATA%, aplicativos por usuário, mostrar uma mensagemUser: só nos gatilhos que permitem esse contexto
O HKCU de todos os usuários logados, a partir do SystemSystem 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 um param() 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 ou ForEach-Object -Parallel.

3.Gatilhos e configurações

GatilhoQuando 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 TimerColeta periódica; os intervalos são da organização (Configuration › Settings › Agent › Trigger Intervals). Não permite Overwrite.
Once Per DayO 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, ReconnectSó Windows. No Logoff só há contexto System e cerca de 4 segundos.
SIP Service Start / StopA 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 ChangeQuando a rede muda; rajadas são agrupadas em cerca de 1 segundo.
Process StartedSó 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 0 o 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.
Freio de alertas

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.

Formato da saída
### 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 -1 ou "unknown", nunca um valor que pareça real e nunca deixe o campo de fora. Inclua schema_ver.
  • Estados como palavras ("running", não 4) e datas em UTC no formato yyyy-MM-ddTHH:mm:ssZ.
  • Vai criar um alerta sobre o índice? Ele precisa ter device_id, device_type e device_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.

PowerShell
$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.
PowerShell · evento com tamanho controlado
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 Stop dentro de um try em tudo o que pode falhar, e transforme a falha num SIP EVENT. SilentlyContinue não silencia tudo: o Get-WinEvent num log que não existe ainda escreve em stderr.
  • Executáveis nativos: mande o stderr para longe com 2>$null e confira $LASTEXITCODE. Escreva sc.exe, nunca sc, que no PowerShell é apelido de Set-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 chaves Uninstall nas visões de 64 e 32 bits.
  • Logs de eventos: Get-WinEvent -FilterHashtable com -MaxEvents, lendo $e.Properties[n].Value em vez de Message (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 aconteceComo evitar
Enumerações viram números no JSONConverta 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 listasPonha o $null à esquerda: $null -eq $var
Windows 10 e 11 com o mesmo nome em alguns camposDecida 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_fix e 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 fixed ou fix_failed.
  • Estado desconhecido nunca é motivo para mudar nada.
  • Guarde mode, acted e result em 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.
PowerShell · exemplo: desligar a inicialização rápida
[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

  1. Numa janela comum do Windows PowerShell 5.1. Rode o arquivo e veja a saída.
  2. Como SYSTEM. Abra um PowerShell com psexec -s -i powershell.exe (Sysinternals) e rode de novo: é assim que o agente roda no contexto System.
  3. Confira o contrato com o teste abaixo: stderr vazio, código de saída e um JSON válido entre os marcadores.
  4. 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.
PowerShell · teste local do contrato de saída
$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/api e o cabeçalho Authorization: Bearer <chave>. O endereço antigo por tenant, com x-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-After e 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.
PowerShell · ler um índice personalizado
[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 5

Rodar um script pela API

  • Ache o script em GET /scripts pelo 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 de device_ids). O campo data é 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_audit ou nos eventos.
  • Em lote: grupos de 100 dispositivos, uma pausa curta entre eles, e o primeiro teste sempre com um $DryRun = $true que só lista o que faria.
  • Para relatórios grandes use export.csv ou export.ndjson do í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.

disk-free.ps1
<#
.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 consoleValor
Plataforma e linguagemWindows · PowerShell
ContextoSystem
GatilhoOnce Per Day
Sends Data · índiceLigado · disk_free
OverwriteLigado (estado atual de cada disco)
Timeout60 segundos
Device tagPiloto com dois dispositivos

Exemplo do que sai (os números são ilustrativos):

Saída esperada
### 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 ###
Para ir além

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

Referências