Private/Get-PSUAppUsageStatus.ps1

<#
.SYNOPSIS
    Classifies the usage status of an Azure App Registration.
 
.DESCRIPTION
    Analyzes a hashtable of collected signals to determine whether an app is
    actively used, likely used, or unused. Returns a usage confidence level
    (High, Medium, Low) and the most recent usage date.
 
    Priority order for usage determination:
    1. Real sign-in dates (High confidence)
    2. Federated credentials / external system usage (High confidence)
    3. Configuration signals (roles, grants, assignments) (Medium confidence)
    4. No signal found (Low confidence - likely unused)
 
.PARAMETER Signals
    A hashtable containing all collected signals for the app. Expected keys:
    LastInteractiveSignInDate, LastSPSignInDate, AppRoleAssignmentCount,
    ConsumedByAppsCount, OAuthGrantCount, DirectoryRoleCount, AzureRBACRoleCount,
    FederatedCredentialCount, AssignedUsersCount, AssignedGroupsCount,
    UsedByExternalSystem.
 
.EXAMPLE
    $usage = Get-PSUAppUsageStatus -Signals $signalHashtable
    $usage.IsUnused # $false
    $usage.UsageConfidence # "High"
    $usage.LastUsedDate # 2026-01-15T10:30:00Z
 
    Classifies the usage status of an app with collected signals.
 
.OUTPUTS
    [Hashtable] with keys: IsUnused (bool), UsageConfidence (string),
    LastUsedDate (datetime/string), UnusedMoreThan1Year (bool), DaysSinceLastUse (int).
 
.NOTES
    Author: Lakshmanachari Panuganti
    Created: 7th March 2026
    Last Modified: 7th March 2026
    Version: 1.0
 
.LINK
    https://github.com/lakshmanachari-panuganti/OMG.PSUtilities/tree/main/OMG.PSUtilities.AzureCore
#>

function Get-PSUAppUsageStatus {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [hashtable]$Signals
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Classifying usage status"
    }

    process {
        # Step 1: Most recent confirmed sign-in date
        $candidates = @($Signals.LastInteractiveSignInDate, $Signals.LastSPSignInDate) |
            Where-Object { $_ -is [datetime] } |
            Sort-Object -Descending
        $LastUsedDate = if ($candidates.Count -gt 0) { $candidates[0] } else { $null }

        # Step 2: Any non-date usage signal
        $hasConfigSignal = (
            $Signals.AppRoleAssignmentCount -gt 0 -or
            $Signals.ConsumedByAppsCount -gt 0 -or
            $Signals.OAuthGrantCount -gt 0 -or
            $Signals.DirectoryRoleCount -gt 0 -or
            $Signals.AzureRBACRoleCount -gt 0 -or
            $Signals.FederatedCredentialCount -gt 0 -or
            $Signals.AssignedUsersCount -gt 0 -or
            $Signals.AssignedGroupsCount -gt 0
        )

        # Step 3: Classify with confidence
        if ($null -ne $LastUsedDate) {
            $IsUnused = $false
            $UsageConfidence = "High"
        } elseif ($Signals.UsedByExternalSystem) {
            $IsUnused = $false
            $UsageConfidence = "High"
            $LastUsedDate = "FederatedActive"
        } elseif ($hasConfigSignal) {
            $IsUnused = $false
            $UsageConfidence = "Medium"
            $LastUsedDate = "SignalFound-NoDate"
        } else {
            $IsUnused = $true
            $UsageConfidence = "Low"
            $LastUsedDate = "NoSignal"
        }

        # Step 4: Age classification
        $UnusedMoreThan1Year = $false
        if ($IsUnused) {
            $UnusedMoreThan1Year = $true
        } elseif ($LastUsedDate -is [datetime] -and $LastUsedDate -lt (Get-Date).AddDays(-365)) {
            $UnusedMoreThan1Year = $true
        }

        # Step 5: Days since last use
        $DaysSinceLastUse = if ($LastUsedDate -is [datetime]) {
            [int]((Get-Date) - $LastUsedDate).TotalDays
        } else { -1 }

        return @{
            IsUnused            = $IsUnused
            UsageConfidence     = $UsageConfidence
            LastUsedDate        = $LastUsedDate
            UnusedMoreThan1Year = $UnusedMoreThan1Year
            DaysSinceLastUse    = $DaysSinceLastUse
        }
    }
}