#!/usr/bin/env bash
# Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# A copy of the License is located at
#
# http://aws.amazon.com/apache2.0
#
# or in the "license" file accompanying this file. This file is distributed
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied. See the License for the specific language governing
# permissions and limitations under the License.

load_patterns() {
  git config --get-all secrets.patterns
  git config --get-all secrets.providers | while read -r cmd; do
    local result="$(export IFS=$'\n\t '; $cmd | tr -d $'\r')"
    if [ -n "${result}" ]; then
      echo "${result}"
    fi
  done
}

load_allowed() {
  git config --get-all secrets.allowed
  local gitallowed="$(git rev-parse --show-toplevel)/.gitallowed"
  if [ -e "$gitallowed" ]; then
    cat "$gitallowed" | awk 'NF && $1!~/^#/'
  fi
}

load_combined_patterns() {
  local patterns=$(load_patterns)
  local combined_patterns=''
  for pattern in $patterns; do
    combined_patterns=${combined_patterns}${pattern}"|"
  done
  combined_patterns=${combined_patterns%?}
  echo "$combined_patterns"
}

scan() {
  local files=("${@}") options=""
  [ "${SCAN_CACHED}" == 1 ] && options+="--cached"
  [ "${SCAN_UNTRACKED}" == 1 ] && options+=" --untracked"
  [ "${SCAN_NO_INDEX}" == 1 ] && options+=" --no-index"
  if [ ${#files[@]} -eq 0 ] || [ ! -z "${options}" ]; then
    output=$(git_grep $options "${files[@]}")
  else
    output=$(regular_grep "${files[@]}")
  fi
  process_output $? "${output}"
}

git_grep() {
  local options="$1"; shift
  local files=("${@}") combined_patterns=$(load_combined_patterns)
  [ -z "${combined_patterns}" ] && return 1
  GREP_OPTIONS= LC_ALL=C git grep -nwHEI ${options} "${combined_patterns}" -- "${files[@]}"
}

regular_grep() {
  local files=("${@}") patterns=$(load_patterns) action='skip'
  [ -z "${patterns}" ] && return 1
  [ ${RECURSIVE} -eq 1 ] && action="recurse"
  GREP_OPTIONS= LC_ALL=C grep -d "${action}" -nwHEI "${patterns}" "${files[@]}"
}

process_output() {
  local status="$1" output="$2"
  local allowed=$(load_allowed)
  case "$status" in
    0)
      [ -z "${allowed}" ] && echo "${output}" >&2 && return 1
      echo "${output}" | GREP_OPTIONS= LC_ALL=C grep -Ev "${allowed}" >&2 \
        && return 1 || return 0
      ;;
    1) return 0 ;;
    *) exit $status
  esac
}

scan_with_fn_or_die() {
  local fn="$1"; shift
  $fn "$@" && exit 0
  echo >&2
  echo "[ERROR] Matched one or more prohibited patterns" >&2
  echo >&2
  echo "Possible mitigations:" >&2
  echo "- Mark false positives as allowed using: git config --add secrets.allowed ..." >&2
  echo "- Mark false positives as allowed by adding regular expressions to .gitallowed at repository's root directory" >&2
  echo "- List your configured patterns: git config --get-all secrets.patterns" >&2
  echo "- List your configured allowed patterns: git config --get-all secrets.allowed" >&2
  echo "- Use --no-verify if this is a one-time false positive" >&2
  exit 1
}

pre_commit_hook() {
  SCAN_CACHED=1
  local files=() file rev="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
  git rev-parse --verify HEAD >/dev/null 2>&1 && rev="HEAD"
  while IFS= read -r file; do
    [ -n "$file" ] && files+=("$file")
  done <<< "$(git diff-index --diff-filter 'ACMU' --name-only --cached $rev --)"
  [ ${#files[@]} -eq 0 ] && exit 0
  scan_with_fn_or_die "scan" "${files[@]}"
}

commit_msg_hook() {
  scan_with_fn_or_die "scan" "$1"
}

prepare_commit_msg_hook() {
  case "$2,$3" in
    merge,)
      local git_head=$(env | grep GITHEAD)
      local sha="${git_head##*=}"
      local branch=$(git symbolic-ref HEAD)
      local dest="${branch#refs/heads/}"
      git log "${dest}".."${sha}" -p | scan_with_fn_or_die "scan" -
      ;;
  esac
}

add_config() {
  local key="$1"; shift
  local value="$@"
  if [ ${LITERAL} -eq 1 ]; then
    value=$(sed 's/[\.|$(){}?+*^]/\\&/g' <<< "${value}")
  fi
  if [ ${GLOBAL} -eq 1 ]; then
    git config --global --get-all $key | grep -Fq "${value}" && return 1
    git config --global --add "${key}" "${value}"
  else
    git config --get-all $key | grep -Fq "${value}" && return 1
    git config --add "${key}" "${value}"
  fi
}

register_aws() {
  local aws="(AWS|aws|Aws)?_?" quote="(\"|')" connect="\s*(:|=>|=)\s*"
  local opt_quote="${quote}?"
  add_config 'secrets.providers' 'bash .git-secrets/git-secrets --aws-provider'
  add_config 'secrets.patterns' '(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'
  add_config 'secrets.patterns' 'ABSK[A-Za-z0-9+/]{109,}=*'
  add_config 'secrets.patterns' 'bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t'
  add_config 'secrets.patterns' "${opt_quote}${aws}(SECRET|secret|Secret)?_?(ACCESS|access|Access)?_?(KEY|key|Key)${opt_quote}${connect}${opt_quote}[A-Za-z0-9/\+=]{40}${opt_quote}"
  add_config 'secrets.patterns' "${opt_quote}${aws}(ACCOUNT|account|Account)_?(ID|id|Id)?${opt_quote}${connect}${opt_quote}[0-9]{4}\-?[0-9]{4}\-?[0-9]{4}${opt_quote}"
  add_config 'secrets.allowed' 'AKIAIOSFODNN7EXAMPLE'
  add_config 'secrets.allowed' "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

aws_provider() {
  local fi="$1"
  [ -z "$fi" ] && fi=~/.aws/credentials
  if [ -f "$fi" ]; then
    awk -F "=" '/aws_access_key_id|aws_secret_access_key/ {print $2}' "$fi" \
      | tr -d ' "' \
      | sed 's/[]\.|$(){}?+*^]/\\&/g'
  fi
}

declare RECURSIVE=0 LITERAL=0 GLOBAL=0 ALLOWED=0
declare SCAN_CACHED=0 SCAN_NO_INDEX=0 SCAN_UNTRACKED=0

COMMAND="$1"
shift 1

while [ "$#" -ne 0 ]; do
  case "$1" in
    -r) RECURSIVE=1 ;;
    -l) LITERAL=1 ;;
    -a) ALLOWED=1 ;;
    --cached) SCAN_CACHED=1 ;;
    --no-index) SCAN_NO_INDEX=1 ;;
    --untracked) SCAN_UNTRACKED=1 ;;
    --global) GLOBAL=1 ;;
    --) shift; break ;;
  esac
  shift
done

case "${COMMAND}" in
  --register-aws) register_aws ;;
  --aws-provider) aws_provider "$1" ;;
  --pre_commit_hook|--pre-commit-hook) pre_commit_hook "$@" ;;
  --commit_msg_hook|--commit-msg-hook) commit_msg_hook "$@" ;;
  --prepare_commit_msg_hook|--prepare-commit-msg-hook) prepare_commit_msg_hook "$@" ;;
  --scan) scan_with_fn_or_die "scan" "$@" ;;
  --add)
    if [ ${ALLOWED} -eq 1 ]; then
      add_config "secrets.allowed" "$1"
    else
      add_config "secrets.patterns" "$1"
    fi
    ;;
  --list)
    if [ ${GLOBAL} -eq 1 ]; then
      git config --global --get-regex secrets.*
    else
      git config --get-regex secrets.*
    fi
    ;;
  *) echo "Usage: git-secrets [--scan|--register-aws|--pre_commit_hook|--commit_msg_hook|--list|--add]" >&2; exit 1 ;;
esac
