How to Export Azure DevOps Service Connection Details using PowerShell

I’ve recently been looking to report on and remove old unneeded Azure Service Connections found in Azure DevOps and found that any actual information on how to do this or existing scripts to do it for me were sorely lacking. As such I’ve developed a working example, which can be found in the article below.

The script below will export a CSV file containing all Service Connections which your user has access to, including the following details –

  • Connection Name
  • Last Used
  • Client Secret Expiry
  • Created By
  • Created By Email
  • Description
  • Authorization Scheme
  • Is Shared
  • Is Ready
  • Azure Subscription
  • Enterprise Application Name
  • Connection ID
  • Subscription ID
  • Enterprise App ID
  • Registered App ID (SPN ID)

This is all performed using the latest Graph API for PowerShell and a brilliant API wrapper of the DevOps API called AzDevOps.

$orgName = '-------' # Name of the DevOps organisation
$api = '7.1-preview.4' # The version of the API to utilize

$personalAccessToken = '----------' #Create a Personal access Token in DevOps

# Install the AzDevOps module if required
if (-not(Get-Module -ListAvailable -Name AzDevOps)) {
    Install-Module AzDevOps
} 

# Connect to Azure DevOps & Azure
Connect-AdoOrganization -Orgname $orgName -PAT $personalAccessToken -ApiVersion $api
Connect-MgGraph -Scopes 'Application.Read.All'

# Get all available projects from ADO
$AdoProjects = Get-AdoProject

# Retrieve the Service Endpoints (Service connections) of all projects
$Result = @()
for($x=0; $x -lt $AdoProjects.length; $x++) {
    $requestUri = "https://dev.azure.com/$orgName/$($AdoProjects[$x].name)/_apis/serviceendpoint/endpoints?api-version=$api"
    $Result += (Invoke-AdoEndpoint -Uri ([System.Uri]::new($requestUri)) -Method Get -Headers $Global:azDevOpsHeader).value
    Write-Host "Retrieved $($Result.count) results total"
}

# Filter down to the Azure RM Service Endpoints
$azureEndpoints = $Result | Where-Object {$_.type -eq 'azurerm'}

$output = @()
$ErrorActionPreference = "Stop"
$output = for($i=0; $i -lt $azureEndPoints.count; $i++) {
    $record = $azureEndpoints[$i]
    # Get the usage history of each Service Endpoint, hardcoded api-version 7.0 otherwise it returns an error
    $historyResult = Invoke-AdoEndpoint -Uri ([System.Uri]::new("https://dev.azure.com/$orgName/$project/_apis/serviceendpoint/$($record.id)/executionhistory?api-version=7.0")) -Method Get -Headers $Global:azDevOpsHeader
    Write-Host "Retrieved $($historyResult.count) Usage records for $($record.name)"

    if($historyResult.count -gt 0) {
        $lastUsed = $historyResult[0].value.data[0].finishTime.Substring(0, 10)
    } else {
        $lastUsed = "Never"
    }
            
    # Get the Enterprise App name
    try {
        if($record.data.appObjectId -ne $null) {
            $enterpriseApp = Get-MgApplication -ApplicationId $record.data.appObjectId -ErrorAction Stop
            $registeredApp = Get-MgServicePrincipal -ServicePrincipalId $record.data.spnObjectId -ErrorAction Stop | select *
        } else {
            $enterpriseApp = "appObjectId is blank for $($record.name)"
            $registeredApp = "appObjectId is blank for $($record.name)"
        }

    } catch [System.Exception] {
        $enterpriseApp = "Enterprise Application not found"
        $registeredApp = "Registered Application not found"
    } catch {
        $message = $_.Exception.Message + " - " + $_.Exception.GetType().FullName
        Write-Error $message
        Write-Host "at $i"
    }

    $credentialExpiry = if($enterpriseApp.PasswordCredentials -ne $null) {($enterpriseApp.PasswordCredentials | sort-object EndDateTime -Descending | select EndDateTime -First 1).EndDateTime} else {$enterpriseApp}
    [pscustomobject]@{
        connectionName = $record.name
        lastUsed = $lastused
        clientExpiry = $credentialExpiry
        clientExpired = if($credentialExpiry -lt (Get-Date) -or $credentialExpiry.GetType().Name -ne 'DateTime') {$true} else {$false}
        createdBy = $record.createdBy.displayName
        createdByEmail = $record.createdBy.uniqueName
        description = $record.description
        authorization = $record.authorization.scheme
        isShared = $record.isShared
        isReady = $record.isReady
        subscriptionName = $record.data.subscriptionName
        enterpriseAppName = if($enterpriseApp.DisplayName -ne $null) {$enterpriseApp.DisplayName} else {$enterpriseApp}
        
        connectionId = $record.id
        subscriptionId = $record.data.subscriptionId
        enterpriseAppId = $record.data.appObjectId
        registeredAppId = if($registeredApp.AppId -ne $null) {$registeredApp.AppId} else {$registeredApp}
    }
}

$date = Get-Date
$output | export-csv -NoClobber -NoTypeInformation "C:\temp\report-$($date.Year)$($date.Month)$($date.Day)$($date.Millisecond).csv"

Disconnect-MgGraph
Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.