From 9fc1935d6d0a9ae7b504a7e52b9a78a1665b9a83 Mon Sep 17 00:00:00 2001 From: "Leinoff, Alexander" Date: Tue, 15 May 2018 12:46:08 -0500 Subject: [PATCH 1/4] Add support for server side hooks Client side hooks are great, but rely on developers properly setting them up and not taking shortcuts. Server side hooks provide an opportunity to enforce security policies at a more global level. This commit adds an "update_hook" option which can be added as a serverside update hook. It scans the pushed commits for secrets. --- git-secrets | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/git-secrets b/git-secrets index 09f18e8..5198b1e 100755 --- a/git-secrets +++ b/git-secrets @@ -38,6 +38,7 @@ f,force --install overwrites hooks if the hook already exists l,literal --add and --add-allowed patterns are escaped so that they are literal a,allowed --add adds an allowed pattern instead of a prohibited pattern global Uses the --global git config +update_hook* update hook (internal only) commit_msg_hook* commit-msg hook (internal only) pre_commit_hook* pre-commit hook (internal only) prepare_commit_msg_hook* prepare-commit-msg hook (internal only)" @@ -61,11 +62,17 @@ load_patterns() { } load_allowed() { + local new_rev="$1" git config --get-all secrets.allowed local gitallowed="$(git rev-parse --show-toplevel)/.gitallowed" if [ -e "$gitallowed" ]; then cat $gitallowed | awk 'NF && $1!~/^#/' fi + if [ -n "${new_rev}" ]; then + git show ${new_rev}:.gitallowed 2>/dev/null | awk 'NF && $1!~/^#/' + # If there is a new commit being pushed to the server, read the .gitallowed from the new commit + # not the one that is already there + fi } # load patterns and combine them with | @@ -106,6 +113,38 @@ scan_history() { process_output $? "${output}" } + +# Scans commits that have been pushed to server through update hook +update_hook() { + local old_rev=$2 + local new_rev=$3 + + local new_branch=0 + local to_scan='' + + # Deal with weirdness on new branches. Thank you stack overflow! https://stackoverflow.com/a/19738143 + if [ "${old_rev}" = '0000000000000000000000000000000000000000' ]; then + new_branch=1 + to_scan=$(git rev-list $new_rev --not --branches=*) + fi + + local combined_patterns=$(load_combined_patterns) + + [ -z "${combined_patterns}" ] && return 0 + + # Looks for differences in commit range if not a new branch + if [ $new_branch -eq 0 ]; then + local to_scan=$(git log ${old_rev}..${new_rev} -G"${combined_patterns}" --pretty=%H) + fi + [ -z "${to_scan}" ] && return 0 + + # Scan through revisions with findings to normalize output + output=$(GREP_OPTIONS= LC_ALL=C git grep -nwHEI "${combined_patterns}" $to_scan) + process_output $? "${output}" "${new_rev}" +} + + + # Performs a git-grep, taking into account patterns and options. # Note: this function returns 1 on success, 0 on error. git_grep() { @@ -129,8 +168,8 @@ regular_grep() { # Takes into account allowed patterns, and if a bad match is found, # prints an error message and exits 1. process_output() { - local status="$1" output="$2" - local allowed=$(load_allowed) + local status="$1" output="$2" new_rev="$3" + local allowed=$(load_allowed $new_rev) case "$status" in 0) [ -z "${allowed}" ] && echo "${output}" >&2 && return 1 @@ -179,6 +218,7 @@ pre_commit_hook() { scan_with_fn_or_die "scan" "${files[@]}" } + # Determines if merging in a commit will introduce tainted history. prepare_commit_msg_hook() { case "$2,$3" in @@ -324,7 +364,7 @@ case "${COMMAND}" in --add-provider) add_config "secrets.providers" "$@" ;; --register-aws) register_aws ;; --aws-provider) aws_provider "$1" ;; - --commit_msg_hook|--pre_commit_hook|--prepare_commit_msg_hook) + --commit_msg_hook|--pre_commit_hook|--prepare_commit_msg_hook|--update_hook) ${COMMAND:2} "$@" ;; --add) From 38b48c378d4e9d773e12a406d72f7da40794699b Mon Sep 17 00:00:00 2001 From: Alexander Leinoff Date: Tue, 15 May 2018 12:58:10 -0500 Subject: [PATCH 2/4] Updates README with info for update_hook command --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index f830278..bbeeeb2 100644 --- a/README.rst +++ b/README.rst @@ -182,6 +182,10 @@ Each of these options must appear first on the command line. optionally provide the path to an ini file. +``--update_hook`` + This option can be used as part of a server side update hook by adding + `git-secrets --update_hook -- "$@"` to an update script on a git server + Options for ``--install`` ~~~~~~~~~~~~~~~~~~~~~~~~~ From c5106d2280b55d800d507800923c68069340a3b7 Mon Sep 17 00:00:00 2001 From: Aoyama Date: Fri, 7 Jan 2022 17:23:14 +0900 Subject: [PATCH 3/4] Add test for serverside hook --- test/update.bats | 131 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 test/update.bats diff --git a/test/update.bats b/test/update.bats new file mode 100644 index 0000000..1691514 --- /dev/null +++ b/test/update.bats @@ -0,0 +1,131 @@ +#!/usr/bin/env bats +load test_helper + +export TEST_REMOTE="$BATS_TMPDIR/test-remote.git" + +setup_remote() { + delete_remote + mkdir -p $TEST_REMOTE + cd $TEST_REMOTE + git init --bare + git config --local --add secrets.patterns '@todo' + git config --local --add secrets.patterns 'forbidden|me' + git config --local --add secrets.patterns '#hash' + cat <<-SCRIPT >> hooks/update +#!/usr/bin/env bash +$(cd $BATS_TEST_DIRNAME/..; pwd)/git-secrets --update_hook -- "\$@" +SCRIPT + chmod +x hooks/update + cd - +} + +delete_remote() { + [ -d $TEST_REMOTE ] && rm -rf $TEST_REMOTE || true +} + +alias_function() { + eval "${1}() $(declare -f ${2} | sed 1d)" +} + +alias_function _setup setup +setup() { + _setup + repo_run git config --unset-all secrets + setup_remote +} + +alias_function _teardown teardown +teardown() { + _teardown + delete_remote +} + +@test "Pushes branch contained allowed words" { + echo 'todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Create test.txt" + run git push $TEST_REMOTE master + [ $status -eq 0 ] +} + +@test "fails to push branch contained secret words" { + hashes=() + + echo '@todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Create test.txt" + hashes+=( $(git rev-parse HEAD) ) + + run git push $TEST_REMOTE master + [ $status -eq 1 ] + [[ "${lines[0]}" =~ ^remote:\ ${hashes[0]}:test.txt:1:@todo ]] + [[ "${lines[1]}" =~ ^remote:\ error:\ hook\ declined\ to\ update\ refs/heads/master ]] +} + +@test "fails to push branch when secret words got mixed in a commit" { + hashes=() + + cd $TEST_REPO + echo 'todo' > $TEST_REPO/test1.txt + echo '@todo' > $TEST_REPO/test2.txt + echo 'TODO' > $TEST_REPO/test3.txt + git add -A + git commit -m "Create files" + hashes+=( $(git rev-parse HEAD) ) + + run git push $TEST_REMOTE master + [ $status -eq 1 ] + [[ "${lines[0]}" =~ ^remote:\ ${hashes[0]}:test2.txt:1:@todo ]] + [[ "${lines[1]}" =~ ^remote:\ error:\ hook\ declined\ to\ update\ refs/heads/master ]] +} + +@test "fails to push branch even if secret words are fixed" { + hashes=() + + cd $TEST_REPO + echo 'todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Create test.txt" + hashes+=( $(git rev-parse HEAD) ) + + echo '@todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Update test.txt" + hashes+=( $(git rev-parse HEAD) ) + + echo 'todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Update test.txt" + hashes+=( $(git rev-parse HEAD) ) + + run git push $TEST_REMOTE master + [ $status -eq 1 ] + [[ "${lines[0]}" =~ ^remote:\ ${hashes[1]}:test.txt:1:@todo ]] + [[ "${lines[1]}" =~ ^remote:\ error:\ hook\ declined\ to\ update\ refs/heads/master ]] +} + +@test "Pushes branch when secret words set as allowed patterns" { + hashes=() + + cd $TEST_REMOTE + git config --local --add secrets.allowed '@todo' + + cd $TEST_REPO + echo 'todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Create test.txt" + hashes+=( $(git rev-parse HEAD) ) + + echo '@todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Update test.txt" + hashes+=( $(git rev-parse HEAD) ) + + echo 'todo' > $TEST_REPO/test.txt + git add -A + git commit -m "Update test.txt" + hashes+=( $(git rev-parse HEAD) ) + + run git push $TEST_REMOTE master + [ $status -eq 0 ] +} From f4c6565efc0ebbb367684ddaab8672b544495366 Mon Sep 17 00:00:00 2001 From: Aoyama Date: Fri, 14 Jan 2022 13:49:27 +0900 Subject: [PATCH 4/4] Fix test; Only check what is secret words --- test/update.bats | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/update.bats b/test/update.bats index 1691514..c78045b 100644 --- a/test/update.bats +++ b/test/update.bats @@ -58,8 +58,7 @@ teardown() { run git push $TEST_REMOTE master [ $status -eq 1 ] - [[ "${lines[0]}" =~ ^remote:\ ${hashes[0]}:test.txt:1:@todo ]] - [[ "${lines[1]}" =~ ^remote:\ error:\ hook\ declined\ to\ update\ refs/heads/master ]] + echo "$output" | grep -F "remote: ${hashes[0]}:test.txt:1:@todo" } @test "fails to push branch when secret words got mixed in a commit" { @@ -75,8 +74,7 @@ teardown() { run git push $TEST_REMOTE master [ $status -eq 1 ] - [[ "${lines[0]}" =~ ^remote:\ ${hashes[0]}:test2.txt:1:@todo ]] - [[ "${lines[1]}" =~ ^remote:\ error:\ hook\ declined\ to\ update\ refs/heads/master ]] + echo "$output" | grep -F "remote: ${hashes[0]}:test2.txt:1:@todo" } @test "fails to push branch even if secret words are fixed" { @@ -100,8 +98,7 @@ teardown() { run git push $TEST_REMOTE master [ $status -eq 1 ] - [[ "${lines[0]}" =~ ^remote:\ ${hashes[1]}:test.txt:1:@todo ]] - [[ "${lines[1]}" =~ ^remote:\ error:\ hook\ declined\ to\ update\ refs/heads/master ]] + echo "$output" | grep -F "remote: ${hashes[1]}:test.txt:1:@todo" } @test "Pushes branch when secret words set as allowed patterns" {