shoc-pr-review-runner/scripts/redact-check.sh
Adam Moussa c3cd8f7765
feat: SHOC PR review runner, phase 1
Manually-dispatched GitHub Actions workflow that reviews SHOC pull requests in
a clean environment: exact-head checkout of shoc-frontend-new and shoc-backend,
clean build/test gates, a truthful evidence report, a single-shot Fireworks
review, deterministic output validation, and published artifacts. The runner
never writes to the product repositories or their pull requests.

The review checklists move here from the reviewers' local Cursor commands so
the instructions live outside both product repos.

Phase 1 does not provision a database, start either application, or run live
browser flows; the evidence report records those as NOT_RUN so a review cannot
claim them.

Security architecture: building a PR executes its author's code, so the
workflow is split. The gates job runs that code holding no Fireworks key and
revokes its App token first; the review job holds the key, executes no product
code, and re-checks out this repo fresh. Product checkouts live outside the
workspace, the App token is downscoped at mint time, gate results fail closed
on any duplicate key, changed files are read from git objects rather than the
filesystem, and the validator re-checks every claim against the gate table.
2026-07-29 12:05:38 -04:00

93 lines
3.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# Pre-upload artifact hygiene (spec §25): scan every staged artifact for the
# run's secret values and for generic credential patterns, and fail the upload
# on any hit. Secrets must never reach console logs, evidence files, prompts, or
# uploaded artifacts.
#
# Matching is deliberately broader than a literal grep: log formatters wrap long
# values across lines, and encoders re-shape them, so each artifact is also
# scanned in a whitespace-stripped form and against derived encodings of each
# secret. Archives are refused rather than scanned opaquely.
#
# Reads: ARTIFACTS_DIR, plus whichever secrets are in scope for this job. At
# least one of GH_TOKEN / FIREWORKS_API_KEY must be present — an empty scan set
# would pass vacuously and produce a green signal that proves nothing.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "$SCRIPT_DIR/lib.sh"
if [ -z "${GH_TOKEN:-}" ] && [ -z "${FIREWORKS_API_KEY:-}" ]; then
die "redaction check has no secrets to scan for — refusing to report clean (set GH_TOKEN and/or FIREWORKS_API_KEY)"
fi
hits=0
scanned=()
# Normalized copy of the artifact tree: newlines and spaces stripped, so a value
# wrapped across lines by a log formatter still matches.
norm_dir="$(mktemp -d)"
trap 'rm -rf "$norm_dir"' EXIT
while IFS= read -r -d '' f; do
case "$f" in
*.zip|*.gz|*.tgz|*.tar|*.bz2|*.xz|*.7z)
log "SECRET LEAK RISK: archive staged for upload cannot be scanned: $f"
hits=$((hits + 1))
continue
;;
esac
tr -d '\n\r \t' <"$f" >"$norm_dir/$(printf '%s' "$f" | md5sum | cut -d' ' -f1)" 2>/dev/null || true
done < <(find "$ARTIFACTS_DIR" -type f -print0)
# scan_value <label> <value> — checks the value and its common derived forms.
scan_value() {
local label="$1" value="$2"
[ -n "$value" ] || return 0
scanned+=("$label")
local -a forms=()
forms+=("$value")
forms+=("$(printf '%s' "$value" | base64 | tr -d '\n')")
forms+=("$(printf 'x-access-token:%s' "$value" | base64 | tr -d '\n')")
# URL-encoded form (only the characters that actually appear in tokens).
forms+=("$(printf '%s' "$value" | sed 's|/|%2F|g; s|+|%2B|g; s|=|%3D|g')")
local form found
for form in "${forms[@]}"; do
[ -n "$form" ] || continue
found="$(grep -rlF -- "$form" "$ARTIFACTS_DIR" 2>/dev/null || true)"
if [ -n "$found" ]; then
log "SECRET LEAK: $label found in artifact file(s):"
printf '%s\n' "$found" >&2
hits=$((hits + 1))
fi
# Whitespace-stripped scan catches line-wrapped occurrences.
found="$(grep -rlF -- "$(printf '%s' "$form" | tr -d '\n\r \t')" "$norm_dir" 2>/dev/null || true)"
if [ -n "$found" ]; then
log "SECRET LEAK: $label found (line-wrapped or whitespace-split) in a staged artifact"
hits=$((hits + 1))
fi
done
}
scan_value "GH_TOKEN (App installation token)" "${GH_TOKEN:-}"
scan_value "FIREWORKS_API_KEY" "${FIREWORKS_API_KEY:-}"
scan_value "SHOC_REVIEW_APP_PRIVATE_KEY" "${SHOC_REVIEW_APP_PRIVATE_KEY:-}"
# Generic credential patterns: catches secrets belonging to the PRODUCT repos
# (a PR touching .env or appsettings) that the runner knows nothing about.
generic_hits="$(grep -rlEI \
-e 'gh[pousr]_[A-Za-z0-9]{30,}' \
-e 'github_pat_[A-Za-z0-9_]{30,}' \
-e 'BEGIN [A-Z ]*PRIVATE KEY' \
-e 'AKIA[0-9A-Z]{16}' \
-e 'xox[baprs]-[A-Za-z0-9-]{10,}' \
"$ARTIFACTS_DIR" 2>/dev/null || true)"
if [ -n "$generic_hits" ]; then
log "SECRET LEAK: generic credential pattern found in artifact file(s):"
printf '%s\n' "$generic_hits" >&2
hits=$((hits + 1))
fi
if [ "$hits" -gt 0 ]; then
die "artifact redaction check failed: $hits leak indicator(s) — artifacts will not be uploaded"
fi
log "artifact redaction check clean (scanned for: ${scanned[*]} + generic credential patterns)"