Remove Community Chatwoot Hub URL/push relay/sync/registration/event/changelog egress. changelog.js becomes a local empty-feed adapter (no axios/fetch/network) exporting the default ChangelogApi; links.js drops the Hub changelog URL. lib/chatwoot_hub.rb removes base_url/push_notification_url/billing_base_url/ instance_config/send_push/send_push_with_response; billing_url reads only an explicit CHATWOOT_BILLING_URL env, HTTPS-only with host and no userinfo, and never falls back to a Hub URL. Enterprise proprietary base_url override is preserved (spec uses singleton_class.instance_methods(false) for edition-safety). privacy_audit uses a narrow per-file/per-rule Enterprise exception (hub-url only) and privacy_audit_test.sh proves forbidden Enterprise runtime lines are still detected; deployment privacy guard unchanged. Approved by independent five-key review deleg_49d6ee2e (passed=true, blocking arrays empty).
128 lines
5.4 KiB
Bash
Executable File
128 lines
5.4 KiB
Bash
Executable File
#!/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'
|