Checks/Applications/ARG.APP.002.ps1

Register-ArgusCheck @{
    Id          = 'ARG.APP.002'
    Title       = 'Client secrets expiring within {DaysAhead} days'
    Area        = 'Applications'
    Category    = 'Credential hygiene'
    Severity    = 'Medium'
    Description = 'Client secrets (`passwordCredentials`) of app registrations that expire within the next **{DaysAhead}** days. A secret counts as **replaced** when the same app already has another secret valid beyond that window; the check fails only for secrets without a replacement.'
    Rationale   = 'An expired secret stops the integration that uses it. Planned rotation, owned by someone, avoids outages discovered by users.'
    Remediation = @{
        Steps      = "1. Ask the application owner which service uses the secret.`n2. Add a new secret (or, better, a certificate or managed identity), update the service, and test it.`n3. Delete the old secret once nothing uses it."
        PowerShell = "# Sample only - review before running. Removes an old secret after the service was updated.`nConnect-MgGraph -Scopes 'Application.ReadWrite.All'`nRemove-MgApplicationPassword -ApplicationId '<ObjectId>' -KeyId '<KeyId>'"
        PortalUrl  = 'https://entra.microsoft.com/'
    }
    References  = @(
        @{ Title = 'Add and manage app credentials in Microsoft Entra ID'; Url = 'https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-credentials' }
        @{ Title = 'Security best practices for application properties'; Url = 'https://learn.microsoft.com/en-us/entra/identity-platform/security-best-practices-for-app-registration' }
    )
    Requires    = @{
        Services    = @('Graph')
        GraphScopes = @('Application.Read.All')
        DataSets    = @('Applications')
    }
    Parameters  = @{ DaysAhead = 30 }
    Tags        = @('Applications', 'Credentials')
    Test        = {
        param($Context)
        $window = $Context.Now.AddDays([int]$Context.Parameters.DaysAhead)
        $valid = @(Get-ArgusAppCredential -Application @($Context.Data['Applications']) -Type Secret | Where-Object { $_.End -and $_.End -gt $Context.Now })
        $findings = @($valid | Where-Object { $_.End -le $window } | ForEach-Object {
                $credential = $_
                $replaced = [bool]($valid | Where-Object { $_.ObjectId -eq $credential.ObjectId -and $_.KeyId -ne $credential.KeyId -and $_.End -gt $window })
                [pscustomobject]@{
                    AppName        = $credential.AppName
                    AppId          = $credential.AppId
                    CredentialName = $credential.CredentialName
                    Expires        = Format-ArgusDate $credential.End
                    DaysLeft       = [int][math]::Floor(($credential.End - $Context.Now).TotalDays)
                    Replaced       = if ($replaced) { 'Yes' } else { 'No' }
                    KeyId          = $credential.KeyId
                    ObjectId       = $credential.ObjectId
                }
            } | Sort-Object DaysLeft)
        $open = @($findings | Where-Object Replaced -EQ 'No').Count
        if ($findings.Count -eq 0) {
            return New-ArgusResult -Status Passed -EvaluatedCount $valid.Count -Summary 'No client secret expires within {DaysAhead} days ({EvaluatedCount} valid secrets evaluated).'
        }
        $status = if ($open -gt 0) { 'Failed' } else { 'Passed' }
        New-ArgusResult -Status $status -Findings $findings -EvaluatedCount $valid.Count `
            -Summary ("{AffectedCount} of {EvaluatedCount} client secrets expire within {DaysAhead} days; $open without a replacement secret.")
    }
}