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..946bf237d --- /dev/null +++ b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.ps1 @@ -0,0 +1,84 @@ +<# +.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 = "." +) + +# 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." + 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..6c657f8cd --- /dev/null +++ b/plugin/skills/azure-validate/references/scripts/scan-aspire-functions-secrets.sh @@ -0,0 +1,105 @@ +#!/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" + +# 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 file_contains "$CALL" "$file"; then + matches+=("$file") + fi +done < <(find "$ROOT" -type f -name "*.cs" -print0) + +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." + exit 0 +fi + +# Partition matching files by whether they already configure the setting. +needs_fix=() +configured=() +for file in "${matches[@]}"; do + if file_contains "$SETTING" "$file"; then + configured+=("$file") + else + needs_fix+=("$file") + fi +done + +if [ "${#needs_fix[@]}" -eq 0 ]; then + echo "VERDICT: ALREADY CONFIGURED" + echo "Every file that calls '$CALL' already sets '$SETTING':" + 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':" +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'." +exit 0