<# .SYNOPSIS ¿Qué secretos y certificados de app registrations están vencidos o por vencer? .DESCRIPTION Recorre los registros de aplicaciones y revisa la fecha de vencimiento de cada secreto y certificado. Una credencial vencida suele tumbar una integración sin aviso, así que esto te da tiempo de rotarla. No lee ni exporta el valor de ningún secreto. Solo lectura: no cambia nada en el tenant. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .PARAMETER Dias Días hacia adelante para considerar una credencial por vencer. Por defecto, 30. .PARAMETER ZonaHoraria Zona horaria para mostrar las fechas. Por defecto, Central Standard Time (Mexico). .EXAMPLE .\get-secretos-apps-por-vencer.ps1 -Dias 60 -RutaCsv .\credenciales.csv .NOTES Permisos: Application.Read.All Rol mínimo: Lector global (Global Reader) Licencia: Microsoft Entra ID Free Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> [CmdletBinding()] param( [string]$RutaCsv, [ValidateRange(1, 36500)][int]$Dias = 30, [string]$ZonaHoraria = 'Central Standard Time (Mexico)' ) $ErrorActionPreference = 'Stop' $permisos = @('Application.Read.All') if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser' } $zona = [TimeZoneInfo]::FindSystemTimeZoneById($ZonaHoraria) Connect-MgGraph -Scopes $permisos -NoWelcome function ConvertTo-FechaUtc { param($Valor) # Graph puede dar texto o fecha; un secreto puede no traer vencimiento. if (-not $Valor) { return $null } if ($Valor -is [datetimeoffset]) { return $Valor.UtcDateTime } if ($Valor -is [datetime]) { if ($Valor.Kind -eq 'Unspecified') { return [datetime]::SpecifyKind($Valor, 'Utc') } else { return $Valor.ToUniversalTime() } } [datetimeoffset]::Parse("$Valor", [cultureinfo]::InvariantCulture).UtcDateTime } $ahora = [datetime]::UtcNow $limite = $ahora.AddDays($Dias) $uri = 'https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,passwordCredentials,keyCredentials&$top=999' $apps = while ($uri) { $pagina = Invoke-MgGraphRequest -Method GET -Uri $uri $pagina.value $uri = $pagina.'@odata.nextLink' } $filas = foreach ($app in $apps) { $credenciales = @( foreach ($c in $app.passwordCredentials) { @{ Tipo = 'Secreto'; Dato = $c } } foreach ($c in $app.keyCredentials) { @{ Tipo = 'Certificado'; Dato = $c } } ) foreach ($credencial in $credenciales) { $vence = ConvertTo-FechaUtc $credencial.Dato.endDateTime if (-not $vence -or $vence -gt $limite) { continue } [pscustomobject]@{ Aplicacion = $app.displayName AppId = $app.appId Tipo = $credencial.Tipo Credencial = $credencial.Dato.displayName KeyId = $credencial.Dato.keyId Estado = if ($vence -lt $ahora) { 'Vencido' } else { 'Por vencer' } Vence = [TimeZoneInfo]::ConvertTimeFromUtc($vence, $zona).ToString('yyyy-MM-dd HH:mm', [cultureinfo]::InvariantCulture) DiasRestantes = [int][math]::Floor(($vence - $ahora).TotalDays) } } } $filas = $filas | Sort-Object DiasRestantes Write-Host "Credenciales vencidas o que vencen en $Dias días: $(@($filas).Count)" if ($RutaCsv) { # Excel ejecutaría como fórmula un nombre que empiece con estos caracteres. foreach ($fila in $filas) { foreach ($p in $fila.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@]') { $p.Value = "'" + $p.Value } } } # UTF-8 con BOM para que Excel en español muestre bien los acentos. $filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM Write-Host "CSV guardado en $RutaCsv" } else { $filas | Format-Table -AutoSize }