Detecção de Aplicativos (App Registration) sem utilidade no Microsoft Entra ID

Quem administra um tenant Microsoft Entra ID de médio ou grande porte sabe que ele acumula aplicativos como uma gaveta acumula cabos: com o tempo, ninguém lembra mais para que servem. Cada App Registration esquecida — muitas vezes com secret ou certificado expirado, criada para um POC que nunca foi para frente — é, ao mesmo tempo, desperdício de inventário e superfície de ataque. Identidades de aplicação são alvo recorrente em incidentes, e um app abandonado com permissões amplas é exatamente o tipo de porta dos fundos que ninguém está vigiando.

Para resolver essa demanda clássica de governança de identidade, desenvolvi com apoio do GitHub Copilot e disponibilizei em meu GitHub um Runbook de Azure Automation em PowerShell 7.2 que faz a varredura periódica do tenant, classifica cada aplicativo por atividade de sign-in e estado de credencial, e envia um relatório formatado por e-mail (HTML + CSV anexo) para o grupo de administradores.

A cada execução, a automação responde a três perguntas que todo time de segurança deveria conseguir responder a qualquer momento:

  1. Quais apps nunca tiveram sign-in registrado?
  2. Quais apps não logam há mais de N dias (janela configurável: 10 / 60 / 90)?
  3. Qual o estado da credencial (secret/cert) de cada um — atual, expirando ou expirada?

Por que uma abordagem híbrida (Graph + Log Analytics)?

A primeira tentação é resolver isso com uma query KQL “pura” sobre a tabela AADServicePrincipalSignInLogs. O problema é fatal: o KQL só enxerga apps que já logaram depois que a Diagnostic Setting foi habilitada. Justamente os apps abandonados há meses — os candidatos mais óbvios à remoção — nunca aparecem nessa consulta.

A segunda tentação é usar só o Microsoft Graph, via relatório servicePrincipalSignInActivities. Esse endpoint, porém, é agregado e eventualmente consistente, com atraso de horas a dias — não reflete a realidade imediata.

A solução combina o melhor dos dois mundos:

Fonte Função Vantagem
Microsoft Graph (Get-MgApplication) Inventário completo Vê todos os apps, inclusive os nunca usados
Log Analytics (AADServicePrincipalSignInLogs) Último sign-in Dado fresco em minutos, sem latência

O cruzamento por AppId produz a classificação correta — Ativo / Inativo / Nunca logou — mesmo no dia em que um app loga pela primeira vez.

Pré-requisitos

Item Requisito
Licença Entra ID P1 ou P2 (para signInActivity e logs de sign-in de Service Principal)
Subscription Azure Com permissão para criar Automation Account e Log Analytics
Conta executora Privileged Role Administrator ou Global Admin (para conceder permissões de aplicação no Graph)
Mailbox de envio Caixa Exchange Online com licença ativa (E1/E3/E5 ou Exchange Plan)
Domínio Verificado no tenant (recomendado SPF/DKIM/DMARC para entrega)

Para organizar todos os recursos da solução, comecei criando um Resource Group dedicado — no meu caso, RG-APPINATIVOS. Manter Automation Account, Log Analytics e identidades sob o mesmo grupo facilita o controle de custo e a limpeza futura.

Resource Group dedicado para concentrar todos os recursos da solução.

Guia de implantação passo a passo

1. Diagnostic Setting do Entra ID

Tudo começa garantindo que os logs de sign-in cheguem ao Log Analytics. No portal, vá em Microsoft Entra ID → Monitoring → Diagnostic settings → Add diagnostic setting.

Diagnostic settings → Add diagnostic setting.

Dê um nome à configuração e marque as categorias de log necessárias:

  • AuditLogs
  • SignInLogs
  • ServicePrincipalSignInLogs
  • MicrosoftServicePrincipalSignInLogs
Nomeando a configuração e marcando AuditLogs, SignInLogs e ServicePrincipalSignInLogs.
Não esqueça do MicrosoftServicePrincipalSignInLogs — ele captura sign-ins de apps da própria Microsoft/M365.

Em Destination details, selecione Send to Log Analytics workspace e escolha (ou crie) o workspace de destino. No exemplo, usei o log-appinativos.

Destino: Send to Log Analytics workspace.

Importante: os logs só começam a fluir após o Save. As tabelas (SigninLogs, AADServicePrincipalSignInLogs etc.) nascem no Log Analytics no primeiro evento ingerido — então elas só aparecerão na lista de Tables depois que houver sign-ins.

2. Workspace Log Analytics

Com o workspace recebendo dados, abra a lâmina de Overview dele para confirmar que está ativo e na região esperada.

Visão geral do workspace que armazenará os logs de sign-in.

Ajuste a retenção das tabelas para cobrir sua janela de análise. Para uma análise de 90 dias, defina retenção ≥ 90. Vá em Log Analytics workspace → Settings → Tables → Manage table em cada tabela relevante.

Tables do workspace — aqui se ajusta a retenção de cada tabela.
Analytics retention ajustada para 90 dias.
Retenção de 90 dias aplicada às tabelas de sign-in.

Dica: definir o padrão do workspace (em Usage and estimated costs → Data retention) é melhor do que ajustar tabela a tabela, porque tabelas novas — criadas quando o primeiro evento chegar — herdam o padrão automaticamente.

Por fim, anote o Workspace ID (Customer ID), que o Runbook vai usar. Se preferir o caminho via linha de comando, autentique-se no Azure CLI e recupere o GUID:

Autenticação no Azure CLI.
$ws = Get-AzOperationalInsightsWorkspace | Where-Object { $_.Name -eq "log-appinativos" }
$ws.CustomerId.Guid
O CustomerId.Guid é o Workspace ID que o Runbook consome.

3. Azure Automation Account

Crie o Automation Account em Create a resource → Automation. Aponte para o Resource Group da solução e escolha a região — no exemplo, aa-appinativos em Canada Central.

Criando o Automation Account no Resource Group da solução.

Na aba Advanced, garanta que a Managed Identity → System assigned esteja habilitada. É ela que autentica o Runbook sem armazenar nenhuma credencial.

Managed Identity atribuída pelo sistema — método recomendado, sem segredos.

Após criar, abra o recurso. Aqui acessaremos os menus de Modules, Runbooks e Identity (anote o Object (principal) ID da identidade — ele será usado para conceder permissões).

Visão geral do Automation Account criado.

4. Módulos do PowerShell

O Runbook depende de dois módulos do Graph. A ordem de importação importa, e o runtime precisa bater com o do Runbook (7.2): primeiro o Microsoft.Graph.Authentication, depois o Microsoft.Graph.Applications. Você pode usar a galeria do portal ou, de forma mais confiável, o Cloud Shell:

$rg = "RG-APPINATIVOS"
$aa = "AA-APPINATIVOS"
New-AzAutomationModule -ResourceGroupName $rg -AutomationAccountName $aa `
-Name "Microsoft.Graph.Authentication" `
-ContentLinkUri "https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Authentication" `
-RuntimeVersion "7.2"
# Aguarde 'Succeeded' antes de prosseguir:
Get-AzAutomationModule -ResourceGroupName $rg -AutomationAccountName $aa `
-Name "Microsoft.Graph.Authentication" -RuntimeVersion "7.2"
New-AzAutomationModule -ResourceGroupName $rg -AutomationAccountName $aa `
-Name "Microsoft.Graph.Applications" `
-ContentLinkUri "https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Applications" `
-RuntimeVersion "7.2"
Importando os módulos do Graph com runtime 7.2.
Aguarde o ProvisioningState chegar a Succeeded antes de seguir.

5. Permissões da Managed Identity

A managed identity precisa de quatro permissões de aplicação no Graph + uma role RBAC no workspace:

Permissão Tipo Para que serve
Application.Read.All Graph (App) Ler inventário de App Registrations
AuditLog.Read.All Graph (App) Ler logs de auditoria
Directory.Read.All Graph (App) Ler service principals
Mail.Send Graph (App) Enviar e-mail via Graph
Log Analytics Reader RBAC (workspace) Executar queries KQL no workspace

Autentique-se no Azure CLI e selecione a subscription correta:

Use o script scripts/01-grant-permissions.ps1 do repositório para conceder tudo de uma vez. Preencha o $miObjectId (o Object ID anotado no passo 3) e o nome do workspace:

# Permissões Graph (Application)
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All","Application.Read.All"
$miObjectId = "<OBJECT_ID_DA_MANAGED_IDENTITY>"
$graph = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$permissoes = "Application.Read.All","AuditLog.Read.All","Directory.Read.All","Mail.Send"
foreach ($p in $permissoes) {
$role = $graph.AppRoles | Where-Object { $_.Value -eq $p -and $_.AllowedMemberTypes -contains "Application" }
New-MgServicePrincipalAppRoleAssignment `
-ServicePrincipalId $miObjectId `
-PrincipalId $miObjectId `
-ResourceId $graph.Id `
-AppRoleId $role.Id
Write-Host "Concedido: $p" -ForegroundColor Green
}
# RBAC no workspace Log Analytics
$ws = Get-AzOperationalInsightsWorkspace | Where-Object { $_.Name -eq "log-appinativos" }
New-AzRoleAssignment `
-ObjectId $miObjectId `
-RoleDefinitionName "Log Analytics Reader" `
-Scope $ws.ResourceId
O script concede as quatro permissões do Graph e a role no workspace.
Preenchendo o Object ID da managed identity e o nome do workspace.
Rodando o script de concessão.

Na primeira execução, o Microsoft Graph Command Line Tools pedirá consentimento para acessar as informações — aceite para prosseguir.

Consentimento do Microsoft Graph Command Line Tools.

Ao final, a saída confirma cada permissão concedida e exibe o Workspace Customer ID:

Todas as permissões concedidas e o Workspace Customer ID exibido.

Atenção ao Mail.Send: como permissão de aplicação, ele permite enviar como qualquer caixa do tenant. Em produção, aplique uma Application Access Policy do Exchange Online para restringir o envio à mailbox específica.

Propagação: permissões recém-concedidas podem levar 15–30 minutos para refletir no token. Se o primeiro teste retornar Forbidden, aguarde e tente de novo.

6. Importar o Runbook

No Automation Account, vá em Runbooks → Create a runbook.

Runbooks → Create a runbook.

Preencha o formulário:

  • Name: rb-appinativos
  • Runbook type: PowerShell
  • Runtime version: 7.2
Runbook PowerShell no runtime 7.2 — o mesmo dos módulos.

Cole o conteúdo de scripts/RB_AppsInativos.ps1 no editor e ajuste os parâmetros default no topo do script (ou passe-os depois via Test pane / Schedule):

param(
[int] $DiasSemUso = 90,
[string] $WorkspaceId = "<WORKSPACE_CUSTOMER_ID>",
[string] $Remetente = "automation@suaempresa.com",
[string] $Destinatario = "admins@suaempresa.com"
)
Editor do Runbook com o bloco param pronto para ajuste.

Clique em Publish.

Publicando o Runbook.
Runbook publicado e pronto para teste.
Visão geral do rb-appinativos.

7. Validação no Test Pane

No Runbook publicado, abra o Test pane, preencha os parâmetros e clique em Start.

Test pane: valide com parâmetros reais antes de agendar.

A saída esperada segue mais ou menos esta ordem:

Conectado ao Microsoft Graph.
Log Analytics: ultimo login carregado para N apps.
Inventario: N app registrations.
Inativos ou nunca logados: X de Y apps.
CSV gerado: X linhas, N bytes.
Email enviado para admins@suaempresa.com com anexo apps-inativos-YYYYMMDD.csv.

E o resultado chega na caixa de entrada: um e-mail HTML com tabela colorida classificando cada aplicativo, mais o CSV anexo para abrir no Excel.

Relatório de Aplicações Inativas entregue por e-mail (HTML + CSV anexo).

Confira também a pasta de spam — domínios sem SPF/DKIM completos costumam cair em spam no Hotmail/Gmail.

8. Agendamento

Para execução recorrente sem intervenção manual, vá em Schedules → Add a schedule.

Schedules → Add a schedule.

Defina nome, início, fuso e recorrência. No exemplo, criei o Recorrencia_30dias, mensal, no fuso de Brasília:

Agendamento recorrente — ajuste a periodicidade conforme sua política.

Depois, é só vincular o schedule ao Runbook (Link to schedule) e configurar os mesmos parâmetros do Test pane.

Agendamento ativo — o relatório passa a ser disparado automaticamente.

Como o script funciona (por dentro)

Cada bloco do RB_AppsInativos.ps1 tem uma responsabilidade isolada:

Bloco O que faz
1. Connect-MgGraph -Identity Autentica como managed identity no Graph
2. Token do Log Analytics Pega token via IDENTITY_ENDPOINT (não usa IMDS, que não funciona no sandbox do Automation)
3. Query KQL via REST union entre AADServicePrincipalSignInLogs e SigninLogs → último login por AppId
4. Inventário Get-MgApplication -All traz todos os App Registrations + credenciais
5. Classificação Cruza inventário com hash de sign-ins; define Status e Credencial
6. CSV em memória Gera CSV com separador ; e BOM UTF-8 (Excel PT-BR friendly)
7. HTML + anexo Monta o body HTML com tabela colorida e anexa o CSV em base64
8. sendMail via REST Pega token fresco do Graph via IDENTITY_ENDPOINT (o Connect-MgGraph pode usar token em cache sem Mail.Send)

Vale destacar duas particularidades técnicas que costumam tropeçar quem reproduz esse padrão. Primeiro, o uso do IDENTITY_ENDPOINT em vez do clássico 169.254.169.254: o endpoint IMDS só funciona em VMs e Hybrid Workers; no sandbox padrão do Azure Automation, a managed identity expõe o token por duas variáveis de ambiente (IDENTITY_ENDPOINT e IDENTITY_HEADER) — é o método oficial e suportado. Segundo, pegar um token fresco antes do sendMail: como o Connect-MgGraph -Identity cacheia o token, se o Mail.Send foi concedido depois desse cache, o token em uso não tem o scope e o Graph retorna Forbidden. Um token novo via REST elimina o problema.

Resolução de problemas

Sintoma Causa provável Solução
Connect-MgGraph is not recognized Módulos não importados ou runtime errado Passo 4: confirme runtime 7.2 e status Available
Tabelas de sign-in não existem Diagnostic Setting não salva ou sem eventos Passo 1; force um sign-in para criar a tabela
Query KQL retorna No results Janela curta ou logs ainda não ingeridos Aguarde 5–30 min após o primeiro sign-in
Invalid URI: hostname could not be parsed Rodou bloco do Runbook no Cloud Shell IDENTITY_ENDPOINT só existe no sandbox
sendMail → Forbidden Mail.Send não propagou ou fora do token Aguarde 30 min ou use token fresco via REST
sendMail → MailboxNotEnabledForRESTAPI Remetente sem licença Exchange Atribua licença E1/E3/E5 ou use outra caixa
E-mail cai no spam Domínio sem SPF/DKIM/DMARC Configure os registros DNS do domínio do remetente

Boas práticas de produção

Antes de levar isso para o ambiente real, alguns cuidados fazem diferença. Restrinja o Mail.Send com uma Application Access Policy no Exchange Online, limitando a managed identity a enviar apenas pela mailbox de automação — sem isso, ela pode tecnicamente enviar como qualquer caixa. Quarentene antes de excluir: o relatório identifica candidatos, mas a remoção deve ser por etapas — detectar (este Runbook), desativar o app por 30 dias (Update-MgServicePrincipal -AccountEnabled:$false) e, sem incidentes, excluir. Filtre apps de sistema (Microsoft, Office etc.) pelo publisherDomain ou por uma whitelist, para não poluir o relatório. Monitore o próprio Runbook com um alerta no Azure Monitor para falhas do job — se ele quebrar em silêncio, a higiene de identidade volta a degradar. E, por fim, versione via Source Control, integrando o Automation Account ao repositório do GitHub.

Contribua com a comunidade!

Este repositório é open-source (licença MIT) e focado na evolução contínua dos profissionais de nuvem. Se você identificar uma melhoria — filtro de apps de sistema, suporte a múltiplos destinatários e grupos do M365, tabela em Application Insights / Workbook do Sentinel, ou até uma variante que desativa automaticamente apps com mais de 180 dias inativos — fique à vontade para abrir uma Issue ou enviar um Pull Request.

Se este projeto te ajudou a fortalecer a governança de identidade do seu tenant, acesse o link github.com/ErickMedeiros/entra-apps-inativos, deixe uma ⭐ e ajude a fazer com que ele chegue a mais profissionais!

Forte abraço pessoal 👊