From b95d0764fc172d3ff53bf806271b5845b67f7345 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Wed, 15 Jul 2026 14:53:33 -0700 Subject: [PATCH 1/2] feat: replace azure-validate Aspire Functions secret-storage scan with a script Replace the inline bash + PowerShell detection one-liners in aspire-functions-secrets.md with a tested cross-platform script pair (scan-aspire-functions-secrets.{sh,ps1}) that scans *.cs for AddAzureFunctionsProject, checks each match for AzureWebJobsSecretStorageType, and prints a single NOT APPLICABLE / ALREADY CONFIGURED / FIX REQUIRED verdict. Only the scan + verdict is scripted; the source fix stays in prose. Fixes #2505 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6bffc710-2569-48cd-8d36-b3447f7106ff --- .../references/aspire-functions-secrets.md | 30 +++--- .../scripts/scan-aspire-functions-secrets.ps1 | 83 +++++++++++++++++ .../scripts/scan-aspire-functions-secrets.sh | 91 +++++++++++++++++++ 3 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 create mode 100644 plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh diff --git a/plugin/skills/azure-validate/references/aspire-functions-secrets.md b/plugin/skills/azure-validate/references/aspire-functions-secrets.md index c0d47a39b..529bafd28 100644 --- a/plugin/skills/azure-validate/references/aspire-functions-secrets.md +++ b/plugin/skills/azure-validate/references/aspire-functions-secrets.md @@ -14,33 +14,25 @@ This check is required when **all** of these are true: ## Detection -Search for `AddAzureFunctionsProject` in the AppHost source file(s): +Run the scan script — it finds `*.cs` files that call `AddAzureFunctionsProject`, checks whether each one already sets `AzureWebJobsSecretStorageType`, and prints a single verdict so you don't have to parse raw grep output. +**Bash** — [`scripts/scan-aspire-functions-secrets.sh`](scripts/scan-aspire-functions-secrets.sh): ```bash -grep -rn "AddAzureFunctionsProject" . --include="*.cs" +./scripts/scan-aspire-functions-secrets.sh [directory] # directory defaults to . ``` -**PowerShell:** +**PowerShell** — [`scripts/scan-aspire-functions-secrets.ps1`](scripts/scan-aspire-functions-secrets.ps1): ```powershell -Get-ChildItem -Recurse -Filter "*.cs" | Select-String "AddAzureFunctionsProject" -List +.\scripts\scan-aspire-functions-secrets.ps1 [-Path ] # -Path defaults to . ``` -If found, check whether `AzureWebJobsSecretStorageType` is already configured in those same file(s): +The script prints one of three verdicts: -```bash -# Check only the AppHost file(s) that contain AddAzureFunctionsProject -find . -name "*.cs" -path "*AppHost*" -print0 | xargs -0 grep -l "AddAzureFunctionsProject" 2>/dev/null | xargs grep -l "AzureWebJobsSecretStorageType" -``` - -**PowerShell:** -```powershell -Get-ChildItem -Recurse -Filter "*.cs" | - Where-Object { $_.FullName -match "AppHost" } | - Select-String "AddAzureFunctionsProject" -List | - ForEach-Object { Select-String "AzureWebJobsSecretStorageType" -Path $_.Path } -``` - -**If `AddAzureFunctionsProject` is present but `AzureWebJobsSecretStorageType` is NOT configured in the same file → fix is required.** +| Verdict | Meaning | Action | +|---------|---------|--------| +| `NOT APPLICABLE` | No `AddAzureFunctionsProject` call found | Skip this check | +| `ALREADY CONFIGURED` | Every matching file already sets `AzureWebJobsSecretStorageType` | No change required | +| `FIX REQUIRED` | Matching file(s) call `AddAzureFunctionsProject` but omit the setting (file/line listed) | Apply the **Fix** below to each listed file | ## Fix diff --git a/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 new file mode 100644 index 000000000..9758c704b --- /dev/null +++ b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 @@ -0,0 +1,83 @@ +<# +.SYNOPSIS + Aspire + Azure Functions secret-storage pre-provisioning scan. +.DESCRIPTION + Decides whether the AzureWebJobsSecretStorageType=Files fix is required by + scanning C# source for the Aspire Functions builder call and the setting. + + Logic: + 1. Find *.cs files that call AddAzureFunctionsProject. + 2. For each, check whether the same file already sets AzureWebJobsSecretStorageType. + 3. Files with the call but missing the setting -> fix required. + + Prints a single verdict — NOT APPLICABLE, ALREADY CONFIGURED, or FIX REQUIRED + (with the matching file(s) and line(s)). Exit code is 0 for every verdict; + a non-zero exit only indicates a usage/environment error. +.PARAMETER Path + Directory to scan. Defaults to the current directory. +.EXAMPLE + .\scan-aspire-functions-secrets.ps1 + # Scan the current directory +.EXAMPLE + .\scan-aspire-functions-secrets.ps1 -Path ./src + # Scan a specific directory +#> +param( + [string]$Path = "." +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path -LiteralPath $Path -PathType Container)) { + Write-Error "'$Path' is not a directory." + exit 2 +} + +$call = "AddAzureFunctionsProject" +$setting = "AzureWebJobsSecretStorageType" + +# Collect *.cs files that reference AddAzureFunctionsProject. +$matches = Get-ChildItem -Path $Path -Recurse -File -Filter "*.cs" | + Where-Object { Select-String -Path $_.FullName -SimpleMatch -Pattern $call -Quiet } + +if (-not $matches) { + Write-Output "VERDICT: NOT APPLICABLE" + Write-Output "No '$call' call found in any *.cs file under '$Path'." + Write-Output "The Functions secret-storage check does not apply - skip it." + exit 0 +} + +# Partition matching files by whether they already configure the setting. +$needsFix = @() +$configured = @() +foreach ($file in $matches) { + if (Select-String -Path $file.FullName -SimpleMatch -Pattern $setting -Quiet) { + $configured += $file + } else { + $needsFix += $file + } +} + +if ($needsFix.Count -eq 0) { + Write-Output "VERDICT: ALREADY CONFIGURED" + Write-Output "Every file that calls '$call' already sets '$setting':" + foreach ($file in $configured) { + $line = (Select-String -Path $file.FullName -SimpleMatch -Pattern $setting | + Select-Object -First 1).LineNumber + Write-Output " - $($file.FullName) (line $line)" + } + Write-Output "No change required." + exit 0 +} + +Write-Output "VERDICT: FIX REQUIRED" +Write-Output "The following file(s) call '$call' but do NOT set '$setting':" +foreach ($file in $needsFix) { + $line = (Select-String -Path $file.FullName -SimpleMatch -Pattern $call | + Select-Object -First 1).LineNumber + Write-Output " - $($file.FullName) (line $line)" +} +Write-Output "" +Write-Output "Add .WithEnvironment(`"$setting`", `"Files`") to the AddAzureFunctionsProject" +Write-Output "builder chain in each file above BEFORE running 'azd provision'." +exit 0 diff --git a/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh new file mode 100644 index 000000000..7764c84d2 --- /dev/null +++ b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# scan-aspire-functions-secrets.sh +# Aspire + Azure Functions secret-storage pre-provisioning scan. +# +# Decides whether the AzureWebJobsSecretStorageType=Files fix is required by +# scanning C# source for the Aspire Functions builder call and the setting. +# +# Logic: +# 1. Find *.cs files that call AddAzureFunctionsProject. +# 2. For each, check whether the same file already sets AzureWebJobsSecretStorageType. +# 3. Files with the call but missing the setting -> fix required. +# +# Usage: +# ./scan-aspire-functions-secrets.sh [directory] +# +# Examples: +# ./scan-aspire-functions-secrets.sh # Scan current directory +# ./scan-aspire-functions-secrets.sh ./src # Scan a specific directory +# +# Output: a single verdict — NOT APPLICABLE, ALREADY CONFIGURED, or FIX REQUIRED +# (with the matching file(s) and line(s)). Exit code is 0 for every verdict; +# a non-zero exit only indicates a usage/environment error. + +set -euo pipefail + +ROOT="${1:-.}" + +if [ ! -d "$ROOT" ]; then + echo "ERROR: '$ROOT' is not a directory." >&2 + exit 2 +fi + +CALL="AddAzureFunctionsProject" +SETTING="AzureWebJobsSecretStorageType" + +# Collect *.cs files that reference AddAzureFunctionsProject (NUL-delimited for safe paths). +matches="" +while IFS= read -r -d '' file; do + if grep -q "$CALL" "$file"; then + matches+="$file"$'\n' + fi +done < <(find "$ROOT" -type f -name "*.cs" -print0) + +# Strip trailing newline. +matches="${matches%$'\n'}" + +if [ -z "$matches" ]; then + echo "VERDICT: NOT APPLICABLE" + echo "No '$CALL' call found in any *.cs file under '$ROOT'." + echo "The Functions secret-storage check does not apply — skip it." + exit 0 +fi + +# Partition matching files by whether they already configure the setting. +needs_fix="" +configured="" +while IFS= read -r file; do + [ -n "$file" ] || continue + if grep -q "$SETTING" "$file"; then + configured+="$file"$'\n' + else + needs_fix+="$file"$'\n' + fi +done <<< "$matches" + +needs_fix="${needs_fix%$'\n'}" +configured="${configured%$'\n'}" + +if [ -z "$needs_fix" ]; then + echo "VERDICT: ALREADY CONFIGURED" + echo "Every file that calls '$CALL' already sets '$SETTING':" + while IFS= read -r file; do + [ -n "$file" ] || continue + line=$(grep -n "$SETTING" "$file" | head -n1 | cut -d: -f1) + echo " - $file (line $line)" + done <<< "$configured" + echo "No change required." + exit 0 +fi + +echo "VERDICT: FIX REQUIRED" +echo "The following file(s) call '$CALL' but do NOT set '$SETTING':" +while IFS= read -r file; do + [ -n "$file" ] || continue + line=$(grep -n "$CALL" "$file" | head -n1 | cut -d: -f1) + echo " - $file (line $line)" +done <<< "$needs_fix" +echo "" +echo "Add .WithEnvironment(\"$SETTING\", \"Files\") to the AddAzureFunctionsProject" +echo "builder chain in each file above BEFORE running 'azd provision'." +exit 0 From 2ae06f7738b00889ba343f4ca16c483f28e467b2 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Thu, 16 Jul 2026 11:18:53 -0700 Subject: [PATCH 2/2] fix: address PR review feedback on Aspire Functions secret scan scripts Bash: - Use arrays instead of a newline-joined string so file paths are genuinely NUL-safe (paths with newlines/spaces are preserved end to end). - Use fixed-string grep (-F) for the literal markers. - Treat grep read errors (exit code 2) as a fatal environment error (exit 3) instead of silently reporting no match, since this is a critical pre-provision scan. PowerShell: - Remove the $ErrorActionPreference = "Stop" line so it defaults to "Continue"; Write-Error is then non-terminating and the explicit exit codes (e.g. exit 2 for a bad directory) are honored. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6bffc710-2569-48cd-8d36-b3447f7106ff --- .../scripts/scan-aspire-functions-secrets.ps1 | 3 +- .../scripts/scan-aspire-functions-secrets.sh | 74 +++++++++++-------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 index 9758c704b..946bf237d 100644 --- a/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 +++ b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 @@ -26,7 +26,8 @@ param( [string]$Path = "." ) -$ErrorActionPreference = "Stop" +# Leave $ErrorActionPreference at its default ("Continue") so that Write-Error +# below is non-terminating and the explicit `exit` codes are honored. if (-not (Test-Path -LiteralPath $Path -PathType Container)) { Write-Error "'$Path' is not a directory." diff --git a/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh index 7764c84d2..6c657f8cd 100644 --- a/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh +++ b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh @@ -33,18 +33,40 @@ fi CALL="AddAzureFunctionsProject" SETTING="AzureWebJobsSecretStorageType" -# Collect *.cs files that reference AddAzureFunctionsProject (NUL-delimited for safe paths). -matches="" +# file_contains +# Returns 0 if the file contains the literal string, 1 if not. +# A grep read error (exit code 2) is fatal: this is a critical pre-provision +# scan, so a file we cannot read must not be silently reported as "no match". +file_contains() { + local pattern="$1" file="$2" rc + if grep -Fq -- "$pattern" "$file"; then + return 0 + fi + rc=$? + if [ "$rc" -eq 2 ]; then + echo "ERROR: failed to read '$file' while scanning for '$pattern'." >&2 + exit 3 + fi + return 1 +} + +# first_line +# Prints the 1-based line number of the first literal match (or nothing). +first_line() { + grep -Fn -- "$1" "$2" | head -n1 | cut -d: -f1 +} + +# Collect *.cs files that reference AddAzureFunctionsProject into an array. +# find ... -print0 + read -d '' keeps paths intact even if they contain +# newlines or spaces (genuinely NUL-safe, unlike a newline-joined string). +matches=() while IFS= read -r -d '' file; do - if grep -q "$CALL" "$file"; then - matches+="$file"$'\n' + if file_contains "$CALL" "$file"; then + matches+=("$file") fi done < <(find "$ROOT" -type f -name "*.cs" -print0) -# Strip trailing newline. -matches="${matches%$'\n'}" - -if [ -z "$matches" ]; then +if [ "${#matches[@]}" -eq 0 ]; then echo "VERDICT: NOT APPLICABLE" echo "No '$CALL' call found in any *.cs file under '$ROOT'." echo "The Functions secret-storage check does not apply — skip it." @@ -52,39 +74,31 @@ if [ -z "$matches" ]; then fi # Partition matching files by whether they already configure the setting. -needs_fix="" -configured="" -while IFS= read -r file; do - [ -n "$file" ] || continue - if grep -q "$SETTING" "$file"; then - configured+="$file"$'\n' +needs_fix=() +configured=() +for file in "${matches[@]}"; do + if file_contains "$SETTING" "$file"; then + configured+=("$file") else - needs_fix+="$file"$'\n' + needs_fix+=("$file") fi -done <<< "$matches" - -needs_fix="${needs_fix%$'\n'}" -configured="${configured%$'\n'}" +done -if [ -z "$needs_fix" ]; then +if [ "${#needs_fix[@]}" -eq 0 ]; then echo "VERDICT: ALREADY CONFIGURED" echo "Every file that calls '$CALL' already sets '$SETTING':" - while IFS= read -r file; do - [ -n "$file" ] || continue - line=$(grep -n "$SETTING" "$file" | head -n1 | cut -d: -f1) - echo " - $file (line $line)" - done <<< "$configured" + for file in "${configured[@]}"; do + echo " - $file (line $(first_line "$SETTING" "$file"))" + done echo "No change required." exit 0 fi echo "VERDICT: FIX REQUIRED" echo "The following file(s) call '$CALL' but do NOT set '$SETTING':" -while IFS= read -r file; do - [ -n "$file" ] || continue - line=$(grep -n "$CALL" "$file" | head -n1 | cut -d: -f1) - echo " - $file (line $line)" -done <<< "$needs_fix" +for file in "${needs_fix[@]}"; do + echo " - $file (line $(first_line "$CALL" "$file"))" +done echo "" echo "Add .WithEnvironment(\"$SETTING\", \"Files\") to the AddAzureFunctionsProject" echo "builder chain in each file above BEFORE running 'azd provision'."