Sometimes Azure DevOps search is enough.
You type a keyword, get a few results, open the file, and move on.
But sometimes you need more out of it.
Recently, we had to identify which integration flows were using a specific SFTP endpoint/account reference. The obvious first step was to use Azure DevOps search and look for a specific keyword across the project repositories.
Azure DevOps search found some results, but with one important limitation: all the results returned by the general search were pointing to the main branch.
That may be perfectly fine if main is the only branch being actively used. But in real projects, especially with many integrations, that is not always the case. Some deployment changes or environment-specific configurations may still live in dev, release, sit, or feature branches. And also when you have dozens of repositories and multiple active branches, checking this manually quickly becomes painful.
So the question was simple: how can we make sure we are not missing references that exist outside main?
The requirement:
Find all integration flows that reference a specific SFTP account/endpoint.
The keyword was something very specific, for example:
<search-term>
The approach
Instead of relying only on the Azure DevOps UI search, we used a PowerShell script to:
- Get all repositories from an Azure DevOps project.
- Clone them locally using a shallow clone.
- Fetch all remote branches.
- Search each branch for a specific keyword.
- Export the results to a CSV file.
This gives a much better overview of where the reference exists.
The script we used:
$organization = "https://dev.azure.com/<your-organization>"
$project = "<your-project>"
$root = "C:\Temp\AzureDevOps-Search"
$searchTerm = "<search-term>"
Write-Host "Starting Azure DevOps repository scan..." -ForegroundColor Cyan
# Check Azure CLI
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
Write-Host "Azure CLI is not installed or not available in PATH." -ForegroundColor Red
exit
}
# Check Git
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
Write-Host "Git is not installed or not available in PATH." -ForegroundColor Red
exit
}
# Ensure Azure DevOps extension exists
az extension add --name azure-devops --only-show-errors 2>$null
# Create local folder
New-Item -ItemType Directory -Force -Path $root | Out-Null
# Configure Azure DevOps defaults
az devops configure --defaults organization=$organization project=$project
# Get all repositories from the project
$repos = az repos list --query "[].{name:name,webUrl:webUrl}" -o json | ConvertFrom-Json
if (-not $repos) {
Write-Host "No repositories found. Check your Azure DevOps login, organization or project." -ForegroundColor Red
exit
}
$results = @()
foreach ($repo in $repos) {
$repoPath = Join-Path $root $repo.name
Write-Host ""
Write-Host "Processing repo: $($repo.name)" -ForegroundColor Cyan
if (!(Test-Path $repoPath)) {
Write-Host " Cloning repo..."
git clone --depth 1 --no-single-branch $repo.webUrl $repoPath
}
else {
Write-Host " Updating local repo..."
git -C $repoPath fetch --all --depth=1
}
$branches = git -C $repoPath for-each-ref --format="%(refname:short)" refs/remotes/origin |
Where-Object { $_ -ne "origin/HEAD" }
foreach ($branch in $branches) {
Write-Host " Searching branch: $branch"
$hits = git -C $repoPath grep -n -I -i --fixed-strings $searchTerm $branch -- 2>$null
foreach ($hit in $hits) {
if ($hit -match "^(?<branch>[^:]+):(?<file>[^:]+):(?<line>\d+):(?<text>.*)$") {
$results += [pscustomobject]@{
Repo = $repo.name
Branch = $matches.branch -replace "^origin/", ""
File = $matches.file
LineNumber = $matches.line
Match = $matches.text.Trim()
}
}
}
}
}
$outputFile = Join-Path $root "search-results.csv"
$results |
Sort-Object Repo, Branch, File, LineNumber -Unique |
Export-Csv $outputFile -NoTypeInformation -Encoding UTF8
Write-Host ""
Write-Host "Scan completed." -ForegroundColor Green
Write-Host "Results exported to:"
Write-Host $outputFile -ForegroundColor Green
What the script gives you?
As you can see by this powershell, the output is a CSV file with useful details such:
- Repo
- Branch
- File
- LineNumber
- Match
| Repo | Branch | File | LineNumber | Match |
|---|---|---|---|---|
<integration-code-repo> | <branch-name> | <deployment-file.yml> | 92 | <sftp-hostname-key-vault-reference> |
<integration-code-repo> | <branch-name> | <deployment-file.yml> | 93 | <sftp-username-key-vault-reference> |
<integration-code-repo> | <branch-name> | <deployment-file.yml> | 94 | <sftp-password-key-vault-reference> |
<integration-infra-repo> | <branch-name> | <pipeline-file.yml> | 143 | <sftp-hostname-secret-name> |
<integration-infra-repo> | <branch-name> | <pipeline-file.yml> | 144 | <sftp-password-secret-name> |
<integration-infra-repo> | <branch-name> | <pipeline-file.yml> | 145 | <sftp-username-secret-name> |
That means you can quickly answer questions like:
- Which integration uses this endpoint?
- In which repository?
- In which branch?
- In which deployment file?
- Which exact reference was found?
This is especially useful when the same integration has multiple branches such as:
main
dev
sit
release/*
feature/*
Is it safe?
The script does not change anything in Azure DevOps.
It only performs read operations:
az repos list
git clone
git fetch
git grep
Export-Csv
There is no git push, no commit, no delete, and no update to repositories, pipelines, or branches.
The only thing it creates is a local folder with cloned repositories and a CSV file with the results.
Important note
This approach covers repository-based files, such as:
YAML pipelines
deployment scripts
ARM/Bicep templates
Logic App deployment files
configuration files
However, it may not catch references stored outside Git, such as:
Azure DevOps Variable Groups
Classic Release Pipelines
Key Vault secrets
manually configured resources
So, for a complete audit, those areas may need a separate check.
In conclusion…
Azure DevOps search is great for a quick check.
But when you need to create a reliable inventory across many repositories and branches, a local scripted search gives you much more confidence.