Skip to content

Commit a0ee223

Browse files
authored
Preserve supplied Sourcebot install ID on first boot (#1648)
* fix: preserve supplied Sourcebot install ID * fix: safely serialize supplied install IDs * fix: restrict telemetry redirects to HTTPS
1 parent b95ad90 commit a0ee223

3 files changed

Lines changed: 211 additions & 22 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
6+
TEST_ROOT=$(mktemp -d)
7+
trap 'rm -rf "$TEST_ROOT"' EXIT
8+
9+
FAKE_BIN="$TEST_ROOT/bin"
10+
UUIDGEN_LOG="$TEST_ROOT/uuidgen.log"
11+
CURL_PAYLOAD_FILE="$TEST_ROOT/curl-payload.json"
12+
CURL_REDIRECT_POLICY_LOG="$TEST_ROOT/curl-redirect-policy.log"
13+
mkdir -p "$FAKE_BIN"
14+
: > "$UUIDGEN_LOG"
15+
: > "$CURL_REDIRECT_POLICY_LOG"
16+
17+
cat > "$FAKE_BIN/uuidgen" <<'EOF'
18+
#!/usr/bin/env bash
19+
if [[ "${UUIDGEN_SHOULD_FAIL:-false}" == "true" ]]; then
20+
exit 1
21+
fi
22+
printf 'called\n' >> "$UUIDGEN_LOG"
23+
printf 'generated-install-id\n'
24+
EOF
25+
26+
cat > "$FAKE_BIN/curl" <<'EOF'
27+
#!/usr/bin/env bash
28+
follow_redirects=false
29+
proto_redir=""
30+
payload=""
31+
method="GET"
32+
33+
while (($# > 0)); do
34+
case "$1" in
35+
-L|--location)
36+
follow_redirects=true
37+
;;
38+
--proto-redir)
39+
shift
40+
proto_redir="$1"
41+
;;
42+
-d|--data)
43+
shift
44+
payload="$1"
45+
method="POST"
46+
;;
47+
-X|--request)
48+
shift
49+
method="$1"
50+
;;
51+
esac
52+
shift
53+
done
54+
55+
if [[ -z "$payload" ]]; then
56+
exit 1
57+
fi
58+
printf '%s\n' "$payload" > "$CURL_PAYLOAD_FILE"
59+
60+
if [[ "${CURL_REDIRECT_STATUS:-}" =~ ^30(7|8)$ && "${CURL_REDIRECT_URL:-}" == http://* ]]; then
61+
printf '%s|%s|%s|%s\n' "$CURL_REDIRECT_STATUS" "$follow_redirects" "$proto_redir" "$method" >> "$CURL_REDIRECT_POLICY_LOG"
62+
if [[ "$follow_redirects" == "true" && "$proto_redir" != "=https" ]]; then
63+
printf 'followed insecure redirect\n' >> "$CURL_REDIRECT_POLICY_LOG"
64+
fi
65+
exit 1
66+
fi
67+
EOF
68+
69+
cat > "$FAKE_BIN/yarn" <<'EOF'
70+
#!/usr/bin/env bash
71+
exit 0
72+
EOF
73+
74+
cat > "$FAKE_BIN/mkdir" <<'EOF'
75+
#!/usr/bin/env bash
76+
exit 0
77+
EOF
78+
79+
cat > "$FAKE_BIN/supervisord" <<'EOF'
80+
#!/usr/bin/env bash
81+
printf '%s\n' "$SOURCEBOT_INSTALL_ID" > "$RESULT_FILE"
82+
EOF
83+
84+
chmod +x "$FAKE_BIN/uuidgen" "$FAKE_BIN/curl" "$FAKE_BIN/yarn" "$FAKE_BIN/mkdir" "$FAKE_BIN/supervisord"
85+
86+
run_entrypoint() {
87+
local data_dir="$1"
88+
local result_file="$2"
89+
shift 2
90+
91+
env \
92+
PATH="$FAKE_BIN:$PATH" \
93+
UUIDGEN_LOG="$UUIDGEN_LOG" \
94+
UUIDGEN_SHOULD_FAIL="false" \
95+
CURL_PAYLOAD_FILE="$CURL_PAYLOAD_FILE" \
96+
CURL_REDIRECT_POLICY_LOG="$CURL_REDIRECT_POLICY_LOG" \
97+
CURL_REDIRECT_STATUS="" \
98+
CURL_REDIRECT_URL="" \
99+
RESULT_FILE="$result_file" \
100+
DATA_CACHE_DIR="$data_dir" \
101+
DATABASE_URL="postgresql://test" \
102+
REDIS_URL="redis://test" \
103+
SOURCEBOT_ENCRYPTION_KEY="test-encryption-key" \
104+
AUTH_SECRET="test-auth-secret" \
105+
AUTH_URL="http://localhost:3000" \
106+
SOURCEBOT_TELEMETRY_DISABLED="true" \
107+
"$@" \
108+
/bin/sh "$REPO_ROOT/entrypoint.sh" >/dev/null
109+
}
110+
111+
assert_equals() {
112+
local description="$1"
113+
local actual="$2"
114+
local expected="$3"
115+
116+
if [[ "$actual" != "$expected" ]]; then
117+
echo "FAIL: $description"
118+
echo "Expected: $expected"
119+
echo "Actual: $actual"
120+
exit 1
121+
fi
122+
}
123+
124+
supplied_data="$TEST_ROOT/supplied-data"
125+
supplied_result="$TEST_ROOT/supplied-result"
126+
supplied_id=$'supplied"install\\id\nsecond-line'
127+
mkdir -p "$supplied_data"
128+
run_entrypoint "$supplied_data" "$supplied_result" SOURCEBOT_INSTALL_ID="$supplied_id" SOURCEBOT_TELEMETRY_DISABLED="false" POSTHOG_PAPIK="test-project-key" CURL_REDIRECT_STATUS="307" CURL_REDIRECT_URL="http://insecure.example/capture/"
129+
assert_equals "uses the supplied install ID on first boot" "$(<"$supplied_result")" "$supplied_id"
130+
assert_equals "persists the supplied install ID as valid JSON" "$(jq -r '.install_id' "$supplied_data/.installedv3")" "$supplied_id"
131+
assert_equals "does not generate an ID when one is supplied" "$(wc -l < "$UUIDGEN_LOG" | tr -d ' ')" "0"
132+
if ! jq -e --arg expected "$supplied_id" \
133+
'.event == "install" and .distinct_id == $expected and .api_key == "test-project-key"' \
134+
"$CURL_PAYLOAD_FILE" >/dev/null; then
135+
echo "FAIL: install telemetry payload did not safely encode the supplied install ID"
136+
exit 1
137+
fi
138+
assert_equals "blocks an HTTP 307 redirect without changing the POST method" "$(sed -n '1p' "$CURL_REDIRECT_POLICY_LOG")" "307|true|=https|POST"
139+
140+
# Exercise the next-boot read and upgrade telemetry paths with the same escaped ID.
141+
jq -n --arg install_id "$supplied_id" \
142+
'{version: "previous-version", install_id: $install_id}' > "$supplied_data/.installedv3"
143+
supplied_restart_result="$TEST_ROOT/supplied-restart-result"
144+
run_entrypoint "$supplied_data" "$supplied_restart_result" SOURCEBOT_INSTALL_ID="conflicting-install-id" SOURCEBOT_TELEMETRY_DISABLED="false" POSTHOG_PAPIK="test-project-key" CURL_REDIRECT_STATUS="308" CURL_REDIRECT_URL="http://insecure.example/capture/"
145+
assert_equals "reads the escaped install ID on the next boot" "$(<"$supplied_restart_result")" "$supplied_id"
146+
if ! jq -e --arg expected "$supplied_id" \
147+
'.event == "upgrade" and .distinct_id == $expected and .api_key == "test-project-key"' \
148+
"$CURL_PAYLOAD_FILE" >/dev/null; then
149+
echo "FAIL: upgrade telemetry payload did not safely encode the persisted install ID"
150+
exit 1
151+
fi
152+
assert_equals "blocks an HTTP 308 redirect without changing the POST method" "$(sed -n '2p' "$CURL_REDIRECT_POLICY_LOG")" "308|true|=https|POST"
153+
assert_equals "never follows an insecure telemetry redirect" "$(wc -l < "$CURL_REDIRECT_POLICY_LOG" | tr -d ' ')" "2"
154+
155+
generated_data="$TEST_ROOT/generated-data"
156+
generated_result="$TEST_ROOT/generated-result"
157+
mkdir -p "$generated_data"
158+
run_entrypoint "$generated_data" "$generated_result" SOURCEBOT_INSTALL_ID=""
159+
assert_equals "generates an install ID when none is supplied" "$(<"$generated_result")" "generated-install-id"
160+
assert_equals "persists the generated install ID" "$(jq -r '.install_id' "$generated_data/.installedv3")" "generated-install-id"
161+
assert_equals "generates exactly one install ID" "$(wc -l < "$UUIDGEN_LOG" | tr -d ' ')" "1"
162+
163+
existing_data="$TEST_ROOT/existing-data"
164+
existing_result="$TEST_ROOT/existing-result"
165+
mkdir -p "$existing_data"
166+
printf '{"version":"existing-version","install_id":"persisted-install-id"}\n' > "$existing_data/.installedv3"
167+
run_entrypoint "$existing_data" "$existing_result" SOURCEBOT_INSTALL_ID="conflicting-install-id"
168+
assert_equals "keeps the persisted install ID after first boot" "$(<"$existing_result")" "persisted-install-id"
169+
assert_equals "does not generate another ID after first boot" "$(wc -l < "$UUIDGEN_LOG" | tr -d ' ')" "1"
170+
171+
failed_data="$TEST_ROOT/failed-data"
172+
failed_result="$TEST_ROOT/failed-result"
173+
mkdir -p "$failed_data"
174+
if run_entrypoint "$failed_data" "$failed_result" SOURCEBOT_INSTALL_ID="" UUIDGEN_SHOULD_FAIL="true"; then
175+
echo "FAIL: entrypoint succeeded when install ID generation failed"
176+
exit 1
177+
fi
178+
if [[ -e "$failed_data/.installedv3" ]]; then
179+
echo "FAIL: entrypoint left an invalid first-run file after install ID generation failed"
180+
exit 1
181+
fi
182+
183+
echo "Entrypoint install ID tests passed."

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
run: .github/scripts/test-cve-remediation.sh
2121
- name: Test Zoekt sync automation
2222
run: .github/scripts/testZoektSync.sh
23+
- name: Test entrypoint install ID behavior
24+
run: .github/scripts/test-entrypoint-install-id.sh
2325

2426
test:
2527
runs-on: ubuntu-latest

entrypoint.sh

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -146,48 +146,52 @@ fi
146146
FIRST_RUN_FILE="$DATA_CACHE_DIR/.installedv3"
147147

148148
if [ ! -f "$FIRST_RUN_FILE" ]; then
149-
touch "$FIRST_RUN_FILE"
150-
export SOURCEBOT_INSTALL_ID=$(uuidgen)
149+
if [ -z "$SOURCEBOT_INSTALL_ID" ]; then
150+
SOURCEBOT_INSTALL_ID=$(uuidgen)
151+
fi
152+
export SOURCEBOT_INSTALL_ID
151153

152154
# If this is our first run, send a `install` event to PostHog
153155
# (if telemetry is enabled)
154156
if [ "$SOURCEBOT_TELEMETRY_DISABLED" = "false" ]; then
155-
if ! ( curl -L --output /dev/null --silent --fail --header "Content-Type: application/json" -d '{
156-
"api_key": "'"$POSTHOG_PAPIK"'",
157-
"event": "install",
158-
"distinct_id": "'"$SOURCEBOT_INSTALL_ID"'",
159-
"properties": {
160-
"sourcebot_version": "'"$SOURCEBOT_VERSION"'"
161-
}
162-
}' https://us.i.posthog.com/capture/ ) then
157+
INSTALL_EVENT_PAYLOAD=$(jq -n \
158+
--arg api_key "$POSTHOG_PAPIK" \
159+
--arg distinct_id "$SOURCEBOT_INSTALL_ID" \
160+
--arg sourcebot_version "$SOURCEBOT_VERSION" \
161+
'{api_key: $api_key, event: "install", distinct_id: $distinct_id, properties: {sourcebot_version: $sourcebot_version}}')
162+
163+
if ! ( curl -L --proto-redir '=https' --output /dev/null --silent --fail --header "Content-Type: application/json" -d "$INSTALL_EVENT_PAYLOAD" https://us.i.posthog.com/capture/ ) then
163164
echo -e "\e[33m[Warning] Failed to send install event.\e[0m"
164165
fi
165166
fi
166167
else
167-
export SOURCEBOT_INSTALL_ID=$(cat "$FIRST_RUN_FILE" | jq -r '.install_id')
168-
PREVIOUS_VERSION=$(cat "$FIRST_RUN_FILE" | jq -r '.version')
168+
SOURCEBOT_INSTALL_ID=$(jq -r '.install_id' "$FIRST_RUN_FILE")
169+
export SOURCEBOT_INSTALL_ID
170+
PREVIOUS_VERSION=$(jq -r '.version' "$FIRST_RUN_FILE")
169171

170172
# If the version has changed, we assume an upgrade has occurred.
171173
if [ "$PREVIOUS_VERSION" != "$SOURCEBOT_VERSION" ]; then
172174
echo -e "\e[34m[Info] Upgraded from version $PREVIOUS_VERSION to $SOURCEBOT_VERSION\e[0m"
173175

174176
if [ "$SOURCEBOT_TELEMETRY_DISABLED" = "false" ]; then
175-
if ! ( curl -L --output /dev/null --silent --fail --header "Content-Type: application/json" -d '{
176-
"api_key": "'"$POSTHOG_PAPIK"'",
177-
"event": "upgrade",
178-
"distinct_id": "'"$SOURCEBOT_INSTALL_ID"'",
179-
"properties": {
180-
"from_version": "'"$PREVIOUS_VERSION"'",
181-
"to_version": "'"$SOURCEBOT_VERSION"'"
182-
}
183-
}' https://us.i.posthog.com/capture/ ) then
177+
UPGRADE_EVENT_PAYLOAD=$(jq -n \
178+
--arg api_key "$POSTHOG_PAPIK" \
179+
--arg distinct_id "$SOURCEBOT_INSTALL_ID" \
180+
--arg from_version "$PREVIOUS_VERSION" \
181+
--arg to_version "$SOURCEBOT_VERSION" \
182+
'{api_key: $api_key, event: "upgrade", distinct_id: $distinct_id, properties: {from_version: $from_version, to_version: $to_version}}')
183+
184+
if ! ( curl -L --proto-redir '=https' --output /dev/null --silent --fail --header "Content-Type: application/json" -d "$UPGRADE_EVENT_PAYLOAD" https://us.i.posthog.com/capture/ ) then
184185
echo -e "\e[33m[Warning] Failed to send upgrade event.\e[0m"
185186
fi
186187
fi
187188
fi
188189
fi
189190

190-
echo "{\"version\": \"$SOURCEBOT_VERSION\", \"install_id\": \"$SOURCEBOT_INSTALL_ID\"}" > "$FIRST_RUN_FILE"
191+
jq -n \
192+
--arg version "$SOURCEBOT_VERSION" \
193+
--arg install_id "$SOURCEBOT_INSTALL_ID" \
194+
'{version: $version, install_id: $install_id}' > "$FIRST_RUN_FILE"
191195

192196
# Run a Database migration
193197
echo -e "\e[34m[Info] Running database migration...\e[0m"

0 commit comments

Comments
 (0)