|
| 1 | +#!/bin/sh |
| 2 | +# |
| 3 | +# Guards the App Configuration exports and the catalog that describes them. |
| 4 | +# |
| 5 | +# Config/*.json scanned for credentials |
| 6 | +# app-config.yaml validated against its own embedded schema |
| 7 | +# |
| 8 | +# Enable for your clone with: |
| 9 | +# git config core.hooksPath .githooks |
| 10 | +# |
| 11 | +# Validates the STAGED content, not the working tree, so a problem cannot slip through by |
| 12 | +# being fixed in the file after `git add`. |
| 13 | +# |
| 14 | +# Bypass with `git commit --no-verify`. CI runs the same checks either way. |
| 15 | + |
| 16 | +set -e |
| 17 | + |
| 18 | +staged_exports=$(git diff --cached --name-only --diff-filter=ACM \ |
| 19 | + | grep -E '^Config/.*\.json$' || true) |
| 20 | +staged_catalog=$(git diff --cached --name-only --diff-filter=ACM \ |
| 21 | + | grep -E '^app-config\.yaml$' || true) |
| 22 | + |
| 23 | +if [ -z "$staged_exports" ] && [ -z "$staged_catalog" ]; then |
| 24 | + exit 0 |
| 25 | +fi |
| 26 | + |
| 27 | +# Probe that the interpreter actually runs. On Windows, `python3` often resolves |
| 28 | +# to the Microsoft Store alias, which exists on PATH but exits non-zero -- so |
| 29 | +# `command -v` alone picks an interpreter that cannot execute anything. |
| 30 | +PYTHON="" |
| 31 | +for candidate in python3 python py; do |
| 32 | + if command -v "$candidate" >/dev/null 2>&1 && |
| 33 | + "$candidate" -c "import sys" >/dev/null 2>&1; then |
| 34 | + PYTHON="$candidate" |
| 35 | + break |
| 36 | + fi |
| 37 | +done |
| 38 | + |
| 39 | +if [ -z "$PYTHON" ]; then |
| 40 | + echo "pre-commit: no working python found, skipping App Config checks." |
| 41 | + echo "pre-commit: CI will still run them." |
| 42 | + exit 0 |
| 43 | +fi |
| 44 | + |
| 45 | +# Materialize staged blobs so the checks see exactly what is being committed. |
| 46 | +tmpdir=$(mktemp -d) |
| 47 | +trap 'rm -rf "$tmpdir"' EXIT |
| 48 | + |
| 49 | +stage_blob() { |
| 50 | + dest="$tmpdir/$(basename "$1")" |
| 51 | + git show ":$1" > "$dest" |
| 52 | + echo "$dest" |
| 53 | +} |
| 54 | + |
| 55 | +if [ -n "$staged_exports" ]; then |
| 56 | + files="" |
| 57 | + for path in $staged_exports; do |
| 58 | + files="$files $(stage_blob "$path")" |
| 59 | + done |
| 60 | + |
| 61 | + # --strict matches appconfig-secret-scan.yml, so the hook cannot pass something CI will |
| 62 | + # reject. Errors block either way; --strict is only about warnings - a secret-shaped key |
| 63 | + # holding a literal, a duplicate (key, label), a malformed entry. Those are worth stopping |
| 64 | + # for here because these files go to a public repository, where a false positive costs a |
| 65 | + # --no-verify and a false negative costs a rotated credential and permanent git history. |
| 66 | + # |
| 67 | + # It does not make the two identical: this scans the staged blob, CI scans all three files |
| 68 | + # on disk. A warning in a file you did not stage still passes here and fails there. |
| 69 | + # shellcheck disable=SC2086 |
| 70 | + if ! "$PYTHON" Scripts/AzureAppConfig/validate_aac_secrets.py --strict $files; then |
| 71 | + echo "" |
| 72 | + echo "pre-commit: BLOCKED -- staged App Config export failed the secret scan." |
| 73 | + echo "pre-commit: An ERROR is a credential: move it into Key Vault and reference it," |
| 74 | + echo "pre-commit: then re-stage. A WARNING is something worth a look rather than" |
| 75 | + echo "pre-commit: certainly wrong; it blocks here because CI runs --strict too." |
| 76 | + echo "pre-commit: If the finding is wrong, commit with --no-verify." |
| 77 | + exit 1 |
| 78 | + fi |
| 79 | +fi |
| 80 | + |
| 81 | +if [ -n "$staged_catalog" ]; then |
| 82 | + catalog=$(stage_blob "$staged_catalog") |
| 83 | + if ! "$PYTHON" Scripts/AzureAppConfig/validate_app_config_schema.py "$catalog"; then |
| 84 | + echo "" |
| 85 | + echo "pre-commit: BLOCKED -- staged app-config.yaml does not match its own schema." |
| 86 | + echo "pre-commit: Fix the entries above, or commit with --no-verify." |
| 87 | + exit 1 |
| 88 | + fi |
| 89 | +fi |
| 90 | + |
| 91 | +# The required-key check is deliberately NOT run here. It compares the catalog against all |
| 92 | +# three stores, so it would fail on gaps a commit did not introduce and cannot fix. CI reports |
| 93 | +# it instead. |
| 94 | + |
| 95 | +exit 0 |
0 commit comments