#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) AUDIT="$SCRIPT_DIR/privacy_audit" ROOT=$(mktemp -d "${TMPDIR:-/tmp}/privacy-audit-test.XXXXXX") OUTSIDE=$(mktemp "${TMPDIR:-/tmp}/privacy-audit-outside.XXXXXX") OUTSIDE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/privacy-audit-outside-dir.XXXXXX") TRACKED_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/privacy-audit-tracked.XXXXXX") ENTERPRISE_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/privacy-audit-enterprise.XXXXXX") trap 'rm -rf "$ROOT" "$OUTSIDE" "$OUTSIDE_DIR" "$TRACKED_ROOT" "$ENTERPRISE_ROOT"' EXIT fail() { printf 'FAIL: %s\n' "$1" >&2 exit 1 } [ -x "$AUDIT" ] || fail 'privacy audit script missing or not executable' mkdir -p "$ROOT/app" "$ROOT/docs" printf '%s\n' 'https://hub.2.chatwoot.com/ping' > "$ROOT/app/forbidden.txt" set +e output=$( python3 "$AUDIT" --root "$ROOT" 2>&1 ) status=$? set -e [ "$status" -ne 0 ] || fail 'forbidden URL must fail the audit' printf '%s\n' "${output}" | grep -F 'app/forbidden.txt:1:hub-url' >/dev/null || fail 'audit did not report the forbidden file and line' rm "$ROOT/app/forbidden.txt" printf '%s\n' 'docs/audit-record.md' > "$ROOT/allowlist.txt" printf '%s\n' 'https://hub.2.chatwoot.com/ping' > "$ROOT/docs/audit-record.md" python3 "$AUDIT" --root "$ROOT" --allowlist-file "$ROOT/allowlist.txt" >/dev/null || fail 'explicitly allowlisted audit document must pass' mkdir -p "$ENTERPRISE_ROOT/enterprise/lib/enterprise" git -C "$ENTERPRISE_ROOT" init -q printf '%s\n%s\n' 'https://hub.2.chatwoot.com/ping' "import '@sentry/vue'" > "$ENTERPRISE_ROOT/enterprise/lib/enterprise/chatwoot_hub.rb" git -C "$ENTERPRISE_ROOT" add enterprise/lib/enterprise/chatwoot_hub.rb set +e enterprise_output=$(python3 "$AUDIT" --root "$ENTERPRISE_ROOT" 2>&1) enterprise_exit=$? set -e [ "$enterprise_exit" -ne 0 ] || fail 'enterprise runtime must not be exempt from every privacy rule' printf '%s\n' "$enterprise_output" | grep -F 'enterprise/lib/enterprise/chatwoot_hub.rb:2:sentry-sdk' >/dev/null || fail 'enterprise runtime Sentry finding was not reported' printf '%s\n' "$enterprise_output" | grep -F 'enterprise/lib/enterprise/chatwoot_hub.rb:1:hub-url' >/dev/null && fail 'intentional enterprise Hub boundary was not narrowly allowlisted' printf '%s\n' 'https://hub.2.chatwoot.com/ping' > "$OUTSIDE" set +e outside_allowlist_status=$(python3 "$AUDIT" --root "$ROOT" --allowlist-file "$OUTSIDE" >/dev/null 2>&1) outside_allowlist_exit=$? set -e [ "$outside_allowlist_exit" -eq 2 ] || fail 'allowlist outside root must return configuration error status 2' rm "$ROOT/docs/audit-record.md" ln -s "$OUTSIDE" "$ROOT/app/outside-link" set +e symlink_status=$(python3 "$AUDIT" --root "$ROOT" >/dev/null 2>&1) symlink_exit=$? set -e [ "$symlink_exit" -eq 0 ] || fail 'audit must not follow a symlink outside the root' mkdir -p "$ROOT/public" printf '%s\n' 'https://hub.2.chatwoot.com/ping' > "$OUTSIDE_DIR/bad.js" ln -s "$OUTSIDE_DIR" "$ROOT/public/vite" set +e built_symlink_output=$(python3 "$AUDIT" --root "$ROOT" 2>&1) built_symlink_exit=$? set -e [ "$built_symlink_exit" -eq 0 ] || fail 'audit must not traverse a symlinked built-artifact directory' mkdir -p "$ROOT/public/assets" printf '%s\n' 'https://hub.2.chatwoot.com/ping' > "$ROOT/public/assets/bundle.js" set +e built_output=$(python3 "$AUDIT" --root "$ROOT" 2>&1) built_exit=$? set -e [ "$built_exit" -ne 0 ] || fail 'audit must scan a real built artifact' printf '%s\n' "$built_output" | grep -F 'public/assets/bundle.js:1:hub-url' >/dev/null || fail 'audit did not report the built artifact finding' rm "$ROOT/public/assets/bundle.js" printf 'binary\0https://hub.2.chatwoot.com/ping\n' > "$ROOT/app/binary.dat" set +e binary_output=$(python3 "$AUDIT" --root "$ROOT" >/dev/null 2>&1) binary_status=$? set -e [ "$binary_status" -eq 0 ] || fail 'binary files must be skipped without failing the audit' rm "$ROOT/app/binary.dat" mkdir -p "$TRACKED_ROOT/app" git -C "$TRACKED_ROOT" init -q printf '%s\n' 'https://hub.2.chatwoot.com/ping' > "$OUTSIDE_DIR/tracked.js" ln -s "$OUTSIDE_DIR" "$TRACKED_ROOT/app/generated" git -C "$TRACKED_ROOT" add app/generated set +e tracked_symlink_output=$(python3 "$AUDIT" --root "$TRACKED_ROOT" 2>&1) tracked_symlink_exit=$? set -e [ "$tracked_symlink_exit" -eq 0 ] || fail 'audit must ignore tracked symlinks escaping the root' rm "$ROOT/app/outside-link" printf '%s\n' 'https://hub.2.chatwoot.com/ping' > "$ROOT/app/forbidden.txt" printf '%s\n' '../app/forbidden.txt' > "$ROOT/invalid-allowlist.txt" set +e invalid_allowlist_output=$(python3 "$AUDIT" --root "$ROOT" --allowlist-file "$ROOT/invalid-allowlist.txt" 2>&1) invalid_allowlist_exit=$? set -e [ "$invalid_allowlist_exit" -eq 2 ] || fail 'non-relative allowlist entries must return configuration error status 2' set +e report_output=$( python3 "$AUDIT" --root "$ROOT" --report-only 2>&1 ) report_status=$? set -e [ "$report_status" -eq 0 ] || fail 'report-only mode must not fail on known findings' printf '%s\n' "$report_output" | grep -F 'app/forbidden.txt:1:hub-url' >/dev/null || fail 'report-only mode did not report the finding' REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) set +e repo_report=$(python3 "$AUDIT" --root "$REPO_ROOT" --report-only 2>&1) repo_status=$? set -e [ "$repo_status" -eq 0 ] || fail 'report-only mode must scan a git repository without crashing' printf '%s\n' "$repo_report" | grep -F 'privacy_audit: REPORT-ONLY' >/dev/null || fail 'git-root report-only summary missing' printf 'PASS: privacy audit harness\n'