Public/Get-IOPasswordOnlyAccounts.ps1
|
function Get-IOPasswordOnlyAccounts { <# .SYNOPSIS Identifies accounts used for non-interactive sign-ins that rely solely on passwords (potential service accounts without cert/managed identity). .EXAMPLE Get-IOPasswordOnlyAccounts .EXAMPLE Get-IOPasswordOnlyAccounts -ToCsv "password-only.csv" #> [CmdletBinding()] param( [ValidateRange(1, 365)] [int]$LookbackDays = 30, [string]$ToCsv ) $cmdName = $MyInvocation.MyCommand.Name Write-IOLog "Scanning for password-only accounts with non-interactive sign-ins in last $LookbackDays days..." -Level Info -Component $cmdName $results = [System.Collections.Generic.List[PSCustomObject]]::new() $since = [datetime]::UtcNow.AddDays(-$LookbackDays).ToString('yyyy-MM-ddTHH:mm:ssZ') $seenUsers = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Get non-interactive sign-ins (these often indicate service accounts) $filter = "createdDateTime ge $since" try { $signIns = Invoke-IOGraphRequest -Uri "v1.0/auditLogs/signIns?`$filter=$filter&`$select=userId,userPrincipalName,userDisplayName,appDisplayName,authenticationMethodsUsed,createdDateTime,signInEventTypes&`$top=500" -Top 500 } catch { # Sign-in logs require Entra ID P1/P2 — some tenants return 400 instead of 403 if ($_.Exception.Message -match '400|BadRequest|403|Forbidden') { Write-IOLog 'Non-interactive sign-in logs require Entra ID P1/P2 and AuditLog.Read.All.' -Level Warning -Component $cmdName return } throw } # Post-filter to non-interactive user sign-ins only $signIns = @($signIns | Where-Object { $evt = if ($_.PSObject.Properties['signInEventTypes']) { $_.signInEventTypes } else { @() } $evt -contains 'nonInteractiveUser' }) # Group by user and check their auth methods $userSignIns = $signIns | Group-Object -Property userId foreach ($group in $userSignIns) { $userId = $group.Name if ([string]::IsNullOrWhiteSpace($userId) -or $seenUsers.Contains($userId)) { continue } [void]$seenUsers.Add($userId) $sample = $group.Group[0] # Check what auth methods this user has registered try { $methods = Invoke-IOGraphRequest -Uri "v1.0/users/$userId/authentication/methods" -NoPagination -SkipConnectionCheck } catch { continue } $methodTypes = @($methods | ForEach-Object { $_.'@odata.type' } | Where-Object { $_ }) # Flag if only password method registered (no FIDO, cert, phone, authenticator) $strongMethods = @($methodTypes | Where-Object { $_ -notin @( '#microsoft.graph.passwordAuthenticationMethod', '#microsoft.graph.emailAuthenticationMethod' ) }) if ($strongMethods.Count -eq 0) { $results.Add([PSCustomObject]@{ DisplayName = $sample.userDisplayName UserPrincipalName = $sample.userPrincipalName UserId = $userId NonInteractiveApps = ($group.Group | Select-Object -ExpandProperty appDisplayName -Unique | Select-Object -First 5) -join '; ' SignInCount = $group.Count RegisteredMethods = ($methodTypes | ForEach-Object { $_.Split('.')[-1] -replace 'AuthenticationMethod','' }) -join '; ' Risk = if ($group.Count -gt 50) { 'Critical' } else { 'High' } Recommendation = 'Migrate to Managed Identity or Certificate auth' }) } } $sorted = $results | Sort-Object Risk, SignInCount -Descending Export-IOResult -Data $sorted -ToCsv $ToCsv -CommandName $cmdName } |