-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit
More file actions
executable file
·266 lines (231 loc) · 8.3 KB
/
git
File metadata and controls
executable file
·266 lines (231 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/usr/bin/env zsh
# Copyright 2026 Roblox Corporation
# SPDX-License-Identifier: MIT
COMMIT_COMMAND="commit"
SKIP_FLAG="--skip-secrets"
SKIP_GITLEAKS=false
DEBUG_FLAG="--debug"
DEBUG=false
TABLE_NAME="metrics"
RED='\033[0;31m'
RESET_COLOR='\033[0m'
MAX_RETRY_ATTEMPTS=100
METRICS_URL=
BIN_DIR=$(dirname "$0")
: "${SQLITE_DB:="$BIN_DIR/cached-metrics.sqlite"}"
# Fast path: skip all setup for non-commit commands or non-arm64 architecture.
# This is the hot path. The vast majority of git commands are not 'commit',
# so we exit as quickly as possible to minimize wrapper overhead.
if [[ $(uname -m) != "arm64" || "$1" != $COMMIT_COMMAND ]]; then
path=(${path:#$BIN_DIR})
exec git "$@"
fi
sqlite_query() {
sqlite3 -batch -noheader -cmd '.mode list' -cmd '.timeout 100' "$SQLITE_DB" $@
}
save_failed_metric() {
sqlite_query <<SQL
CREATE TABLE IF NOT EXISTS "$TABLE_NAME" (
id INTEGER PRIMARY KEY,
metric TEXT NOT NULL,
type TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
retry_attempts INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
UNIQUE(metric, type)
);
SQL
if [[ $# -eq 4 ]]; then
# We retried a metric and failed again, so increment the retry counter.
sqlite_query <<SQL
UPDATE "$TABLE_NAME"
SET retry_attempts = retry_attempts + 1
WHERE id = $4;
SQL
elif [[ $# -eq 3 ]]; then
# This is a new metric. Increase the count for this type of metric.
sqlite_query <<SQL
INSERT INTO "$TABLE_NAME" (metric, type, count, created_at)
VALUES ("$1", "$2", "$3", strftime('%s','now'))
ON CONFLICT(metric, type) DO UPDATE
SET count = count + "$3";
SQL
elif [[ $DEBUG == true ]]; then
echo "save_failed_metric: unexpected number of arguments: $#: $@"
fi
}
handle_metric() {
if [[ $# -lt 3 ]]; then
if [[ $DEBUG == true ]]; then
echo "handle_metric: unexpected number of arguments ($#): $@"
fi
return
fi
# We need to use a random label or else the metrics will be deduplicated.
RANDOM_LABEL_VALUE=$(cat /dev/urandom | base64 | tr -dc '0-9a-zA-Z' | head -c10)
# This command should be quick. If it fails, fail quickly.
curl --location $METRICS_URL \
--header 'Content-Type: application/json' \
--data "$1{type=\"$2\",random=\"$RANDOM_LABEL_VALUE\"} $3" \
--max-time 3 \
--connect-timeout 1
if [[ $? -ne 0 ]]; then
# Store metric for retry later.
save_failed_metric $@
elif [[ $# -ge 4 ]]; then
# This was a retry and it was successful. Delete the metric from the database.
sqlite_query "DELETE FROM $TABLE_NAME WHERE id = $4;"
fi
}
send_metric() {
if [[ -z "$METRICS_URL" ]]; then
return
fi
if [[ $DEBUG == true ]]; then
handle_metric $@ &
else
handle_metric $@ >/dev/null 2>&1 &
fi
}
# Try to send any metrics that failed the last time we tried to send them.
retry_failed_metrics() {
# Check if the expected database and table exist
if [[ ! -f "$SQLITE_DB" ]]; then
return
fi
if ! sqlite_query "SELECT name FROM sqlite_master WHERE type='table' AND name='$TABLE_NAME';" | grep -q "$TABLE_NAME"; then
return
fi
# Clean up entries we've given up on
sqlite_query "DELETE FROM $TABLE_NAME WHERE created_at < strftime('%s','now','-30 days') OR retry_attempts > $MAX_RETRY_ATTEMPTS;"
rows=$(sqlite_query "SELECT id, metric, type, count FROM $TABLE_NAME;")
if [[ -n "$rows" ]]; then
while IFS='|' read -r id metric type count; do
send_metric "$metric" "$type" $count "$id"
done <<< "$rows"
fi
}
ARGS=()
for ARG in "$@"; do
case "$ARG" in
$SKIP_FLAG)
SKIP_GITLEAKS=true
;;
$DEBUG_FLAG)
DEBUG=true
;;
*)
ARGS+=("$ARG")
;;
esac
done
if [[ $DEBUG == true ]]; then
set -x
fi
retry_failed_metrics &
# Remove $BIN_DIR from $PATH
REST_OF_PATH=$(echo "$PATH" | tr ':' '\n' | grep -xvF "$BIN_DIR" | paste -sd: -)
# This gets the directory that the actual script is running in. This is
# redundant in all cases except development, when the location of the script
# can be different from BIN_DIR for ease of use.
SCRIPT_DIR="${0:A:h}"
TOML="$SCRIPT_DIR/gitleaks.toml"
# Create temp file for Gitleaks' results report and configure it to be deleted on exit.
GITLEAKS_RESULTS_FILENAME=$(mktemp)
if [[ ! -f $GITLEAKS_RESULTS_FILENAME || $? -ne 0 ]]; then
report_error "Failed to create temp file for Gitleaks' results report" "failed_to_create_temp_file"
pass_command_through
fi
cleanup() {
rm -f "$GITLEAKS_RESULTS_FILENAME"
}
if [[ $DEBUG != true ]]; then
trap cleanup EXIT
fi
pass_command_through() {
export PATH=$REST_OF_PATH
if [[ $DEBUG == true ]]; then
# print which git we're sending them to for debugging purposes
which git
ls -la $(which git)
fi
cleanup
exec git $ARGS
}
wrap_text() {
fold -s -w "$(( COLUMNS < 80 ? COLUMNS : 80 ))"
}
report_error() {
echo -e "\033[33mError while checking git commit for secrets: $1
To run your command without checking secrets, append $SKIP_FLAG to your command:
<your git command> $SKIP_FLAG${RESET_COLOR}" | wrap_text
echo
send_metric "safe_git_push_error" "$2" 1
}
# Pass the command through if jq is not installed.
if ! command -v jq 2>&1 >/dev/null; then
pass_command_through
fi
# Pass the command through if the user wants us to.
if [[ $SKIP_GITLEAKS == true ]]; then
send_metric "safe_git_push_usage" "skip_secrets" 1
pass_command_through
fi
# If the files we need to perform the secrets check aren't present,
# fail open.
if [[ ! -f $TOML ]]; then
report_error "$TOML not found." "toml_not_found"
pass_command_through
fi
if ! command -v gitleaks 2>&1 >/dev/null; then
report_error "Gitleaks is not installed." "gitleaks_not_installed"
pass_command_through
fi
# get the content of the git commit except go.sum files
# See https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-pathspec
MODIFICATIONS=$(PATH=$REST_OF_PATH git diff --cached -- ':(exclude,top,glob)**/go.sum' | grep '^[+]' | grep -v '^+++' | sed 's/^+//')
# run gitleaks on the commit contents
GITLEAKS_STDOUT_FILENAME="/dev/null"
if [[ $DEBUG == true ]]; then
# Output the content of the commit so we can see what keywords, if any, triggered the scan
echo "Commit content:" $MODIFICATIONS
# Output Gitleaks's output to stdout since it reports errors we can't otherwise get.
GITLEAKS_STDOUT_FILENAME="/dev/stdout"
fi
# pipe output to /dev/null because it's just Gitleaks's logo and number of secrets caught.
echo "$MODIFICATIONS" | gitleaks stdin --report-path "$GITLEAKS_RESULTS_FILENAME" --report-format json -c "$TOML" &> $GITLEAKS_STDOUT_FILENAME
# We can't fail on error because if gitleaks finds secrets, it reports an error, and we
# can't differentiate this between an internal error like a malformed regex because
# gitleaks uses the same error code (1).
if [[ ! -f $GITLEAKS_RESULTS_FILENAME ]]; then
report_error "$GITLEAKS_RESULTS_FILENAME doesn't exist" "no_gitleaks_output"
pass_command_through
fi
if [[ $DEBUG == true ]]; then
cat $GITLEAKS_RESULTS_FILENAME
fi
LEAKED_SECRETS=$(jq -r '.[].Secret | select(. != null)' $GITLEAKS_RESULTS_FILENAME 2>&1)
if [[ $? -ne 0 ]]; then
echo $LEAKED_SECRETS
report_error "Unable to get secrets from $GITLEAKS_RESULTS_FILENAME" "cannot_get_gitleaks_output"
pass_command_through
fi
# Remove duplicate secrets
LEAKED_SECRETS=$(echo $LEAKED_SECRETS | sort -u)
if [[ ${#LEAKED_SECRETS} -eq 0 ]]; then
# No leaked secrets. Let the commit happen.
pass_command_through
fi
num_secrets=0
echo -e "${RED}Detected potential secrets in this commit.${RESET_COLOR}
Please remove the strings below from your repo if they're sensitive
and shouldn't be pushed to GitHub:\n"
while IFS= read -r SECRET; do
echo "\t"$SECRET
echo
(( num_secrets++ ))
done <<< $LEAKED_SECRETS
echo -e "If these aren't really secrets, you can bypass this check by appending $SKIP_FLAG to your command:
<your git command> $SKIP_FLAG" | wrap_text
send_metric "safe_git_push_usage" "secrets_detected" $num_secrets
exit 1