Monitoring Azure App Client Secrets and SSL Certificates Expiration

Step-by-step guide on how to integrate Microsoft Graph and NetCrunch to automatically detect expiring client secrets and certificates in your Azure apps—before they cause outages

Managing Azure applications at scale involves a hidden risk: expired client secrets and SSL certificates. These silent failures can lead to sudden authentication errors, service disruptions, and frustrating downtime.

This guide walks you through integrating PowerShell, Microsoft Graph API, and NetCrunch seamlessly. You’ll learn how to extract expiration data from Azure, transform it into actionable metrics, and configure NetCrunch to alert you before issues occur - so that you can stay one step ahead, every time.

1. Install Microsoft Graph Module

Open PowerShell as Administrator and run:

Install-Module Microsoft.Graph

2. Configure Azure (Microsoft Graph)

a. Log in to Azure Portal.

b. Assign required permissions (assuming you already have a registered application):

  • Go to Azure Active Directory → App registrations → [Your Application] → API permissions → Add permission → Microsoft Graph → Application permissions
  • Select Application.Read.All.
  • Click Add permissions.
  • Click Grant admin consent (requires Global Administrator role).

You need these credentials:

  • TenantId: from Azure AD Overview
  • AppId (Client ID): from App registration Overview
  • ClientSecret: existing secret from your app registration

3. PowerShell Script (Get-AppCredentialsExpiry.ps1)

An anonymized example script to fetch Client Secrets and SSL certificate expiry:

<#
.SYNOPSIS
     Retrieves Client Secret and SSL certificate expiration dates for all Microsoft Entra ID applications.
.DESCRIPTION
     This script connects to Microsoft Graph API, fetches all applications, and checks expiration dates
     of Client Secrets and SSL certificates. Results are saved to a JSON file in UTF-8 without BOM.
     .NOTES
WARNING: Hardcoding credentials in scripts is insecure. For production, use Azure Key Vault or environment variables.
#>

# Authentication data (REPLACE WITH YOUR VALUES)
$TenantId     = "7d9e17ec-0011-4642-ad4e-1351beed5880"       # e.g., "contoso.onmicrosoft.com" or GUID
$AppId        = "cc14f6ad-c967-431f-a702-1c4f00b62c04"       # Application (Client) ID
$ClientSecret = "0Wd8Q~UkSn.qUkRHqHYANWxz01c6K4ANx5TtRaC6"       # Secret value (visible only once when created)
$OutputFile   = "credentials_expiry_report.json"

# Function to get access token
function Get-AccessToken {
   param (
       [string]$TenantId,
       [string]$AppId,
       [string]$ClientSecret
     )

     $tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
     $body = @{
         grant_type    = "client_credentials"
         client_id     = $AppId
         client_secret = $ClientSecret
         scope         = "https://graph.microsoft.com/.default"
     }

    try {
       $response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body
       return $response.access_token
     } catch {
       Write-Error "Failed to get access token: $_"
       exit 1
   } 
}
# Function to fetch all applications
function Get-AllApplications {
         param (
             [string]$AccessToken
   )

        $headers = @{
            Authorization = "Bearer $AccessToken"
   }

   $url = "https://graph.microsoft.com/v1.0/applications"  # Corrected endpoint
   $allApps = @()

   try {
         do {
             $response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get
             $allApps += $response.value
             $url = $response.'@odata.nextLink'
      } while ($null -ne $url)

          return $allApps
   } catch {
        Write-Error "Failed to retrieve applications: $_"
        exit 1
   }
}

# Main execution
$accessToken = Get-AccessToken -TenantId $TenantId -AppId $AppId -ClientSecret $ClientSecret
$applications = Get-AllApplications -AccessToken $accessToken

# Prepare results
$result = @()

foreach ($app in $applications) {
    $appData = @{
             appId          = $app.appId
             displayName    = $app.displayName
             clientSecrets  = @()
             keyCredentials = @()  # SSL certificates
    }

# Process Client Secrets
if ($app.passwordCredentials) {
    foreach ($secret in $app.passwordCredentials) {
        $appData.clientSecrets += @{
             displayName    = $secret.displayName
             startDateTime = $secret.startDateTime
             endDateTime   = $secret.endDateTime
             daysUntilExpiry = if ($secret.endDateTime) { (New-TimeSpan -Start (Get-Date) -End ([DateTime]::Parse($secret.endDateTime))).Days } else { $null }
             isExpired     = if ($secret.endDateTime) { [DateTime]::Parse($secret.endDateTime) -lt (Get-Date) } else { $null }
        }
    }
}

# Process SSL certificates (Key Credentials)
if ($app.keyCredentials) {
    foreach ($cert in $app.keyCredentials) {
        $appData.keyCredentials += @{
            displayName    = $cert.displayName
            startDateTime = $cert.startDateTime
            endDateTime   = $cert.endDateTime
            type          = $cert.type  # e.g., "AsymmetricX509Cert"
            usage         = $cert.usage  # e.g., "Verify"
            daysUntilExpiry = if ($cert.endDateTime) { (New-TimeSpan -Start (Get-Date) -End ([DateTime]::Parse($cert.endDateTime))).Days } else { $null }
            isExpired     = if ($cert.endDateTime) { [DateTime]::Parse($cert.endDateTime) -lt (Get-Date) } else { $null }
        }
    }
}

$result += $appData
} 
# Save to JSON (UTF-8 without BOM)
try {
    $jsonContent = $result | ConvertTo-Json -Depth 10
    $outputPath = Join-Path -Path $PSScriptRoot -ChildPath $OutputFile
    [System.IO.File]::WriteAllText($outputPath, $jsonContent, [System.Text.UTF8Encoding]::new($false))
    Write-Host "Report saved to: $outputPath (UTF-8 without BOM)"
} catch {
    Write-Error "Failed to save file: $_"
    exit 1
}

4. NetCrunch Configuration

a. Open NetCrunch, go to Settings → Data Parsers

b. Create a new parser (JavaScript type) and paste:

const doc = typeof data === 'string' ? JSON.parse(data) : data;

result = result;

for (let i = 0; i < doc.length; i++) {
     const app = doc[i];
     const appName = app.displayName || `App${i}`;

     if (Array.isArray(app.clientSecrets)) {
         app.clientSecrets.forEach((secret, ix) => {
         if (secret.daysUntilExpiry !== undefined) {
            const days = Math.floor(secret.daysUntilExpiry);
            const counterName = 'Client Secret/Days To Expiration';
            const secretName = secret.displayName || `Secret${i + ix}`;
            result = result.counter(counterName, days, `${appName} - ${secretName}`);
            }
       });
   }

if (Array.isArray(app.keyCredentials)) {
    app.keyCredentials.forEach((cert, ix) => {
      if (cert.daysUntilExpiry !== undefined) {
            const days = Math.floor(cert.daysUntilExpiry);
            const certName = cert.displayName || `Cert #${ix + 1}`;
            const counterName = 'Certificates/Days To Expiration';
            result = result.counter(counterName, days, `${appName} - ${certName}`);
           }
       });
    }
}

c. In Test Data, paste the JSON content from credentials_expiry_report.json.

Alerting data parser

5. Add Script Sensor in NetCrunch

Important File Location Note:
Make sure that the PowerShell script (Get-AppCredentialsExpiry.ps1) and the resulting JSON file (credentials_expiry_report.json) are placed on the same machine where the NetCrunch Server is installed. This ensures that the Script Sensor can access the script and read the JSON output directly. The file names used here are examples and can be changed to suit your environment.

  • Select a Node for Azure Apps Secrets and SSL Certificates monitoring.

  • Add a new Script Sensor:

Script sensor settings
  • Sensor Name: e.g., AzureAppCredentialsMonitoring
  • Script Type: PowerShell
  • Specify the path and script name (e.g., Get-AppCredentialsExpiry.ps1).
  • Check Read result from file, specify the path to the JSON file (e.g., credentials_expiry_report.json).
  • Select the previously created data parser.

6. Configure Alerts

  • Add a new Alert:

    • Event Trigger: Monitoring Sensor Counter
script sensor settings - add event
  • Choose severity and description.
  • Select Counter → change "average value" to "value", set "less than" and enter threshold days (e.g., 30).
Script sensor alerting rule
  • Counter Selection:
    From the Performance Object, select Certificates or Client Secret. You can monitor a single application or all monitored applications by selecting the appropriate instance option (e.g., selecting All instances will monitor every application, whereas selecting a specific instance will limit the alert to that application).
script sensor settings add performance object
Select instance
  • Confirm your selection and create the Alert.

  • Associate your preferred Alerting Script if needed. Add alerting script

8. Completion

NetCrunch will now generate alerts when any Azure App Client Secret or SSL certificate approaches the expiration threshold you've configured, notifying you accordingly.

appazurecertificatesclientexpiration.script.sensorsecretssl

NetCrunch. Answers not just pictures

Maps → Alerts → Automation → Intelligence