← back to Ios Fleet Recording Qa

recording-qa.sh

160 lines

#!/usr/bin/env bash
# ios-fleet-recording-qa — pre-submission QA gate for App Store Guideline 2.1 device recordings.
# Rebuilds the readiness-probe the TK-11155 approved memos assumed existed (verify-recording + privacy-scan).
# Works for ALL 5 rejected apps (GovArbitrage, Homes on Spec, Beverly Hills Videos, Celebrity Signatures, 90210).
#
# Usage:
#   recording-qa.sh <video.mov> [app-name]     # run the gate, print human verdict
#   recording-qa.sh --json <video.mov>         # machine-readable verdict line
#   recording-qa.sh --approve <video.mov>      # AFTER a human PII eyeball, bind the review to the exact bytes
#
# Verdict codes (match the memo semantics):
#   TECH_FAIL                 too short / mostly black / unreadable
#   REJECTED_NOT_DEVICE       no iPhone/iPad capture metadata (simulator or re-encoded — Apple rejects for 2.1)
#   PRIVACY_FLAGGED           OCR found email/phone/address text in a frame -> MUST review/redo
#   TECH_PASS_PRIVACY_PENDING tech ok, OCR clean, but no human sign-off sidecar yet
#   READY_TO_SEND             tech ok + OCR clean + human sidecar present & matches current bytes
set -uo pipefail

MIN_SEC=20        # Apple/2.1 floor
WANT_SEC=30       # memo wants ~30-60s
FRAME_EVERY=2     # sample one frame every N seconds for OCR
BLACK_FRAC_FAIL=0.5

die(){ echo "recording-qa: $*" >&2; exit 2; }
command -v ffprobe >/dev/null || die "ffprobe missing (brew install ffmpeg)"
command -v ffmpeg  >/dev/null || die "ffmpeg missing"

MODE="run"; JSON=0
case "${1:-}" in
  --approve) MODE="approve"; shift;;
  --json)    JSON=1; shift;;
  "") die "usage: recording-qa.sh <video.mov> [app-name] | --json <v> | --approve <v>";;
esac
VID="${1:-}"; APP="${2:-app}"
[ -f "$VID" ] || die "no such file: $VID"
SIDE="${VID}.privacy-reviewed.sha256"

# ---- human sign-off path -------------------------------------------------
if [ "$MODE" = "approve" ]; then
  # refuse to bind a human review to a clip that doesn't already pass tech+OCR
  chk=$("$0" --json "$VID" 2>/dev/null)
  case "$chk" in
    *'"verdict":"PRIVACY_FLAGGED"'*|*'"verdict":"TECH_FAIL"'*|*'"verdict":"REJECTED_NOT_DEVICE"'*)
      echo "❌ refusing to approve — clip does not pass the gate:"; echo "   $chk"; exit 1;;
  esac
  shasum -a 256 "$VID" | awk '{print $1}' > "$SIDE"
  echo "✅ privacy review bound to bytes -> $SIDE"
  echo "   (re-run the gate; it should now report READY_TO_SEND)"
  exit 0
fi

# ---- technical checks -----------------------------------------------------
DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$VID" 2>/dev/null | cut -d. -f1); DUR=${DUR:-0}
W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width  -of csv=p=0 "$VID" 2>/dev/null); W=${W:-0}
H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "$VID" 2>/dev/null); H=${H:-0}
MODEL=$(ffprobe -v error -show_entries format_tags=com.apple.quicktime.model -of csv=p=0 "$VID" 2>/dev/null)
MAKE=$(ffprobe -v error -show_entries format_tags=com.apple.quicktime.make -of csv=p=0 "$VID" 2>/dev/null)

TECH_NOTES=(); VERDICT=""; DETAIL=""
[ "$DUR" -ge "$MIN_SEC" ] 2>/dev/null || { VERDICT="TECH_FAIL"; DETAIL="duration ${DUR}s < ${MIN_SEC}s floor"; }
[ "$H" -gt "$W" ] 2>/dev/null || TECH_NOTES+=("not portrait (${W}x${H}) — iPhone captures are portrait; confirm this is intended")
[ "$DUR" -lt "$WANT_SEC" ] 2>/dev/null && [ -z "$VERDICT" ] && TECH_NOTES+=("short: ${DUR}s (memo wants ~${WANT_SEC}-60s)")

# real-device signal: iOS screen recordings carry com.apple.quicktime.model=iPhone/iPad.
# NOTE: this tag is legitimately STRIPPED by AirDrop / any re-encode, so treat its ABSENCE
# as a warning (not a hard reject). Hard-reject only on a positive simulator tell.
DEVICE_OK=0; SIM_TELL=0
case "$MODEL$MAKE" in *iPhone*|*iPad*|*Apple*) DEVICE_OK=1;; esac
case "$VID" in *[Ss]imulator*|*-sim.*|*_sim.*|*.sim.*) SIM_TELL=1;; esac

# black-frame detection over the whole clip
BLACK=$(ffmpeg -nostats -i "$VID" -vf "blackdetect=d=0.3:pic_th=0.98" -an -f null - 2>&1 \
        | awk -F'black_duration:' '/black_duration/{s+=$2} END{printf "%d", s+0}')
BLACKFRAC=$(awk -v b="${BLACK:-0}" -v d="${DUR:-1}" 'BEGIN{ if(d<=0)d=1; printf "%.2f", b/d }')
awk -v f="$BLACKFRAC" -v t="$BLACK_FRAC_FAIL" 'BEGIN{exit !(f>t)}' && [ -z "$VERDICT" ] && {
  VERDICT="TECH_FAIL"; DETAIL="mostly black (${BLACKFRAC} of clip) — bad capture"; }

if [ -z "$VERDICT" ] && [ "$SIM_TELL" -eq 1 ]; then
  VERDICT="REJECTED_NOT_DEVICE"; DETAIL="filename indicates a simulator clip — Apple rejects simulator recordings for 2.1"
elif [ "$DEVICE_OK" -eq 0 ]; then
  TECH_NOTES+=("physical-device capture UNCONFIRMED (no iPhone/iPad metadata — fine if AirDropped/edited; NOT fine if it's a simulator clip — eyeball it)")
fi

# ---- privacy / PII OCR scan ----------------------------------------------
PII_HITS=(); CRED_HITS=(); FRAMES=0
if [ -z "$VERDICT" ] && command -v tesseract >/dev/null 2>&1; then
  TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
  ffmpeg -nostats -loglevel error -i "$VID" -vf "fps=1/${FRAME_EVERY}" "$TMP/f_%04d.png" 2>/dev/null || true
  EMAIL='[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
  PHONE='(\+?1[-. ]?)?\(?[0-9]{3}\)?[-. ][0-9]{3}[-. ][0-9]{4}'
  ADDR='[0-9]{1,6} +([A-Z][a-z]+ ){1,3}(St|Street|Ave|Avenue|Rd|Road|Blvd|Dr|Drive|Ln|Lane|Ct|Court|Way|Pl|Place|Blvd)\b'
  # credential lens (TK-11155): a clip can be PII-clean yet expose a plaintext
  # username / API key / backend host. Apple's 2.1 item #4 ASKS for demo
  # credentials, so this WARNS (conscious decision) — it never hard-fails.
  CRED_USER='(User ?name|Username|Login|User ID)[: ]+[A-Za-z0-9._-]{3,}'
  CRED_SECRET='(api[ _-]?key|secret|bearer|token|authorization)[: ]+[A-Za-z0-9._-]{8,}'
  CRED_BASIC='Basic [A-Za-z0-9+/=]{12,}'
  CRED_HOST='https?://[A-Za-z0-9.-]*(agentabrams\.com|localhost|127\.0\.0\.1)[^ ]*'
  for f in "$TMP"/f_*.png; do
    [ -e "$f" ] || continue
    FRAMES=$((FRAMES+1))
    TXT=$(tesseract "$f" stdout 2>/dev/null | tr -s ' ')
    for pat in "$EMAIL:email" "$PHONE:phone" "$ADDR:street-address"; do
      rx="${pat%:*}"; label="${pat##*:}"
      m=$(printf '%s' "$TXT" | grep -oE "$rx" | head -1)
      [ -n "$m" ] && PII_HITS+=("$(basename "$f") $label: $m")
    done
    # credential labels sit on their OWN OCR line above the value
    # ("Username\n\nadmin"), so match against a newline-flattened copy.
    TXTF=$(printf '%s' "$TXT" | tr '\n' ' ' | tr -s ' ')
    for pat in "$CRED_USER:username" "$CRED_SECRET:api-secret" "$CRED_BASIC:basic-auth" "$CRED_HOST:backend-host"; do
      rx="${pat%:*}"; label="${pat##*:}"
      m=$(printf '%s' "$TXTF" | grep -oiE "$rx" | head -1)
      # drop label-followed-by-ordinary-word noise ("Login with", "Password required")
      case "$(printf '%s' "$m" | awk '{print tolower($NF)}')" in
        with|to|or|and|using|required|screen|button|here|below|above|optional|failed|error|settings) m="";; esac
      # a masked value (dots/asterisks) is fine — only flag a VISIBLE one
      case "$m" in *"..."*|*"***"*|*"•"*) m="";; esac
      [ -n "$m" ] && CRED_HITS+=("$(basename "$f") $label: $m")
    done
  done
  if [ "${#PII_HITS[@]}" -gt 0 ]; then VERDICT="PRIVACY_FLAGGED"; DETAIL="OCR found PII in ${#PII_HITS[@]} frame(s) — redo or crop"; fi
  CRED_NOTE=""
  [ "${#CRED_HITS[@]}" -gt 0 ] && CRED_NOTE=" · ${#CRED_HITS[@]} credential/backend string(s) VISIBLE — confirm intentional (Apple 2.1 #4 asks for demo credentials) or crop"

fi

# ---- final verdict --------------------------------------------------------
if [ -z "$VERDICT" ]; then
  if [ -f "$SIDE" ] && [ "$(shasum -a 256 "$VID" | awk '{print $1}')" = "$(cat "$SIDE" 2>/dev/null)" ]; then
    VERDICT="READY_TO_SEND"; DETAIL="tech PASS + OCR clean (${FRAMES} frames) + human sidecar matches"
  else
    VERDICT="TECH_PASS_PRIVACY_PENDING"; DETAIL="tech PASS + OCR clean (${FRAMES} frames); needs human eyeball then: recording-qa.sh --approve '$VID'"
  fi
fi

if [ "$JSON" -eq 1 ]; then
  printf '{"app":"%s","file":"%s","verdict":"%s","duration_s":%s,"dims":"%dx%d","device_meta":"%s","black_frac":%s,"ocr_frames":%d,"pii_hits":%d,"cred_hits":%d,"detail":"%s"}\n' \
    "$APP" "$VID" "$VERDICT" "${DUR:-0}" "${W:-0}" "${H:-0}" "${MODEL:-none}" "$BLACKFRAC" "$FRAMES" "${#PII_HITS[@]}" "${#CRED_HITS[@]}" "$DETAIL${CRED_NOTE:-}"
  case "$VERDICT" in READY_TO_SEND|TECH_PASS_PRIVACY_PENDING) exit 0;; *) exit 1;; esac
fi

echo "════════ recording-qa · $APP ════════"
echo "file       : $VID"
echo "duration   : ${DUR}s   dims: ${W}x${H}   device: ${MODEL:-<none>} ${MAKE:-}"
echo "black-frac : $BLACKFRAC   ocr-frames: $FRAMES"
for n in "${TECH_NOTES[@]:-}"; do [ -n "$n" ] && echo "note       : $n"; done
for h in "${PII_HITS[@]:-}"; do [ -n "$h" ] && echo "⚠️ PII     : $h"; done
for h in "${CRED_HITS[@]:-}"; do [ -n "$h" ] && echo "🔑 CRED    : $h"; done
echo "────────────────────────────────────"
echo "VERDICT    : $VERDICT"
echo "             $DETAIL${CRED_NOTE:-}"
case "$VERDICT" in
  READY_TO_SEND)             echo "➡️  attach this clip in the ASC Resolution Center reply."; exit 0;;
  TECH_PASS_PRIVACY_PENDING) echo "➡️  eyeball it frame-by-frame; if clean run: recording-qa.sh --approve '$VID'"; exit 0;;
  PRIVACY_FLAGGED)           echo "➡️  do NOT upload — re-record or crop out the PII."; exit 1;;
  REJECTED_NOT_DEVICE)       echo "➡️  capture on a PHYSICAL iPhone (Control Center screen record), not the simulator."; exit 1;;
  *)                         echo "➡️  re-capture."; exit 1;;
esac