<# .SYNOPSIS Exports Azure DevOps service connections with detailed service principal information to a CSV file. .DESCRIPTION This script retrieves all service connections from an Azure DevOps project and analyzes their associated service principals. It combines information from Azure DevOps REST API and Azure PowerShell cmdlets to provide comprehensive details about: - Service connection metadata (ID, name, status, authorization scheme) - Associated service principal details (Application ID, Object ID, display name) - Service principal credential expiration dates This is particularly useful for security auditing and monitoring service principal credential expiration to prevent service disruptions. .PARAMETER Token The Azure DevOps Personal Access Token (PAT) used for authentication. This token must have 'Service Connections (Read)' permissions to access service endpoint information. .PARAMETER Organization The Azure DevOps organization name. Defaults to "effectory" if not specified. .PARAMETER Project The Azure DevOps project name. Defaults to "Survey Software" if not specified. .EXAMPLE .\ServiceConnections.ps1 -Token "your-personal-access-token" Analyzes service connections using the default organization and project settings. .EXAMPLE .\ServiceConnections.ps1 -Token "your-pat-token" -Organization "myorg" -Project "MyProject" Analyzes service connections for a specific organization and project. .OUTPUTS Creates a timestamped CSV file in the current directory with the format: "yyyy-MM-dd HHmm serviceconnections.csv" The CSV contains the following columns: - Id: Service connection unique identifier - Name: Service connection display name - OperationStatus: Current operational status of the service connection - AuthorizationScheme: Authentication method used (e.g., ServicePrincipal, ManagedServiceIdentity) - ServicePrincipalApplicationId: Application (Client) ID of the associated service principal - ServicePrincipalObjectId: Object ID of the service principal in Azure AD - ServicePrincipalName: Display name of the service principal - ServicePrincipalEndDateTime: Expiration date of the service principal credentials .NOTES Author: Cloud Engineering Team Created: 2025 Requires: PowerShell 5.1 or later, Az PowerShell module Dependencies: Az PowerShell module must be installed and authenticated to Azure Prerequisites: - Install Az PowerShell module: Install-Module -Name Az - Connect to Azure: Connect-AzAccount - Azure DevOps Personal Access Token with Service Connections (Read) permissions - Appropriate Azure AD permissions to read service principal information The script combines two data sources: 1. Azure DevOps REST API - for service connection metadata 2. Azure PowerShell cmdlets - for detailed service principal information Security Considerations: - Monitor credential expiration dates to prevent service disruptions - Regular auditing of service connections helps maintain security posture - Service principals with expired credentials will cause deployment failures Error Handling: - If service principal information cannot be retrieved, those fields will be empty - The script continues processing even if individual service principals fail - Network or authentication errors are handled gracefully .LINK https://docs.microsoft.com/en-us/rest/api/azure/devops/serviceendpoint/endpoints https://docs.microsoft.com/en-us/powershell/module/az.resources/get-azadserviceprincipal #> param( [Parameter(Mandatory = $true, HelpMessage = "Azure DevOps Personal Access Token with Service Connections (Read) permissions")] [string]$Token, [Parameter(Mandatory = $false, HelpMessage = "Azure DevOps organization name")] [string]$Organization = "effectory", [Parameter(Mandatory = $false, HelpMessage = "Azure DevOps project name")] [string]$Project = "Survey Software" ) # Define a class to structure service connection information with service principal details class ServiceConnection { [string] $Id = "" # Service connection unique identifier [string] $Name = "" # Service connection display name [string] $OperationStatus = "" # Current operational status [string] $AuthorizationScheme = "" # Authentication method (e.g., ServicePrincipal) [string] $ServicePrincipalApplicationId = "" # Application (Client) ID of service principal [string] $ServicePrincipalObjectId = "" # Object ID of service principal in Azure AD [string] $ServicePrincipalName = "" # Display name of service principal [string] $ServicePrincipalEndDateTime = "" # Expiration date of service principal credentials } # Generate timestamped filename for the output CSV [string] $date = Get-Date -Format "yyyy-MM-dd HHmm" $fileName = ".\$date serviceconnections.csv" # Display script execution banner Write-Host "========================================================================================================================================================================" Write-Host "Analyzing Azure DevOps service connections and associated service principals" Write-Host "Organization: $Organization, Project: $Project" Write-Host "Output file: $fileName" Write-Host "Note: This script requires Azure PowerShell (Az module) to be installed and authenticated" Write-Host "========================================================================================================================================================================" # Prepare authentication for Azure DevOps REST API calls # Personal Access Token must be base64 encoded with a colon prefix $encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($Token)")) # URL-encode the project name for API calls $projectEncoded = $Project -replace " ", "%20" # Check if Azure PowerShell is available and user is authenticated Write-Host "Verifying Azure PowerShell authentication..." -ForegroundColor Yellow try { $azContext = Get-AzContext if (-not $azContext) { Write-Host "ERROR: Not authenticated to Azure. Please run 'Connect-AzAccount' first." -ForegroundColor Red exit 1 } Write-Host "Azure authentication verified - Tenant: $($azContext.Tenant.Id)" -ForegroundColor Green } catch { Write-Host "ERROR: Azure PowerShell module not found. Please install with 'Install-Module -Name Az'" -ForegroundColor Red exit 1 } # Retrieve service connections from Azure DevOps Write-Host "Fetching service connections from Azure DevOps..." -ForegroundColor Yellow $url="https://dev.azure.com/$Organization/$projectEncoded/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4" $head = @{ Authorization =" Basic $encodedToken" } $response = Invoke-RestMethod -Uri $url -Method GET -Headers $head Write-Host "Found $($response.count) service connections" -ForegroundColor Green # Initialize array to store service connection analysis results [ServiceConnection[]]$Result = @() # Process each service connection to gather detailed information $response.value | ForEach-Object { Write-Host "Analyzing service connection: $($_.name)" -ForegroundColor Cyan # Create new service connection object and populate basic information [ServiceConnection] $serviceConnection = [ServiceConnection]::new() $serviceConnection.Id = $_.id $serviceConnection.Name = $_.name $serviceConnection.OperationStatus = $_.operationStatus $serviceConnection.AuthorizationScheme = $_.authorization.scheme Write-Host " Authorization scheme: $($_.authorization.scheme)" -ForegroundColor Gray Write-Host " Operation status: $($_.operationStatus)" -ForegroundColor Gray # Extract service principal information if available $principalid = $_.authorization.parameters.serviceprincipalid if ($null -ne $principalid) { Write-Host " Retrieving service principal details..." -ForegroundColor Gray try { # Get service principal information from Azure AD $principal = Get-AzADServicePrincipal -ApplicationId $principalid -ErrorAction Stop $credential = Get-AzADAppCredential -ApplicationId $principalid -ErrorAction Stop $serviceConnection.ServicePrincipalApplicationId = $principalid $serviceConnection.ServicePrincipalObjectId = $principal.Id $serviceConnection.ServicePrincipalName = $principal.DisplayName # Handle multiple credentials - get the latest expiration date if ($credential) { $latestExpiration = ($credential | Sort-Object EndDateTime -Descending | Select-Object -First 1).EndDateTime $serviceConnection.ServicePrincipalEndDateTime = $latestExpiration Write-Host " Service Principal: $($principal.DisplayName)" -ForegroundColor Gray Write-Host " Application ID: $principalid" -ForegroundColor Gray Write-Host " Credential expires: $latestExpiration" -ForegroundColor Gray # Warn about expiring credentials (within 30 days) if ($latestExpiration -and $latestExpiration -lt (Get-Date).AddDays(30)) { Write-Host " WARNING: Credential expires within 30 days!" -ForegroundColor Red } } else { Write-Host " No credentials found for this service principal" -ForegroundColor Yellow } } catch { Write-Host " Error retrieving service principal details: $($_.Exception.Message)" -ForegroundColor Red # Keep the Application ID even if other details fail $serviceConnection.ServicePrincipalApplicationId = $principalid } } else { Write-Host " No service principal associated with this connection" -ForegroundColor Gray } # Add service connection to results array $Result += $serviceConnection } # Export results to CSV file $Result | Export-Csv -Path $fileName -NoTypeInformation # Calculate and display summary statistics $totalConnections = $Result.Count $connectionsWithServicePrincipals = ($Result | Where-Object { $_.ServicePrincipalApplicationId -ne "" }).Count $connectionsWithoutServicePrincipals = $totalConnections - $connectionsWithServicePrincipals # Check for expiring credentials (within 30 days) $expiringCredentials = $Result | Where-Object { $_.ServicePrincipalEndDateTime -ne "" -and [DateTime]::Parse($_.ServicePrincipalEndDateTime) -lt (Get-Date).AddDays(30) } # Check for expired credentials $expiredCredentials = $Result | Where-Object { $_.ServicePrincipalEndDateTime -ne "" -and [DateTime]::Parse($_.ServicePrincipalEndDateTime) -lt (Get-Date) } # Display completion summary Write-Host "========================================================================================================================================================================" Write-Host "Service connection analysis completed successfully!" -ForegroundColor Green Write-Host "" Write-Host "SUMMARY STATISTICS:" -ForegroundColor Cyan Write-Host "Total service connections: $totalConnections" -ForegroundColor Yellow Write-Host "Connections with service principals: $connectionsWithServicePrincipals" -ForegroundColor Yellow Write-Host "Connections without service principals: $connectionsWithoutServicePrincipals" -ForegroundColor Yellow if ($expiredCredentials.Count -gt 0) { Write-Host "" Write-Host "EXPIRED CREDENTIALS (IMMEDIATE ATTENTION REQUIRED):" -ForegroundColor Red $expiredCredentials | ForEach-Object { Write-Host " - $($_.Name): $($_.ServicePrincipalName) (expired $($_.ServicePrincipalEndDateTime))" -ForegroundColor Red } } if ($expiringCredentials.Count -gt 0) { Write-Host "" Write-Host "EXPIRING CREDENTIALS (WITHIN 30 DAYS):" -ForegroundColor Yellow $expiringCredentials | ForEach-Object { Write-Host " - $($_.Name): $($_.ServicePrincipalName) (expires $($_.ServicePrincipalEndDateTime))" -ForegroundColor Yellow } } Write-Host "" Write-Host "Output file: $fileName" -ForegroundColor Yellow Write-Host "========================================================================================================================================================================"