#!/usr/bin/env bash

# Copyright 2018 Toni de la Fuente

# Prowler is a tool that provides automate auditing and hardening guidance of an
# AWS account. It is based on AWS-CLI commands. It follows some guidelines
# present in the CIS Amazon Web Services Foundations Benchmark at:
# https://d0.awsstatic.com/whitepapers/compliance/AWS_CIS_Foundations_Benchmark.pdf

# Contact the author at https://blyx.com/contact
# and open issues or ask questions at https://github.com/toniblyx/prowler


# All CIS based checks in checks folder are licensed under a Creative Commons
# Attribution-NonCommercial-ShareAlike 4.0 International Public License.
# The link to the license terms can be found at
# https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
#
# Any other piece of code is licensed as Apache License 2.0 as specified in
# each file. You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0

# Prowler - Iron Maiden
#
# Walking through the city, looking oh so pretty
# I've just got to find my way
# See the ladies flashing
# All there legs and lashes
# I've just got to find my way...

OPTRED="[1;31m"
OPTNORMAL="[0;39m"

# Set the defaults variables
PROWLER_VERSION=2.1.0
PROWLER_DIR=$(dirname "$0")

REGION=""
FILTERREGION=""
MAXITEMS=100
MONOCHROME=0
MODE="text"
QUIET=0
SEP=','
KEEPCREDREPORT=0
EXITCODE=0
SCRIPT_START_TIME=$( date -u +"%Y-%m-%dT%H:%M:%S%z" )
TITLE_ID=""
TITLE_TEXT="CALLER ERROR - UNSET TITLE"

# Command usage menu
usage(){
  echo "
USAGE:
      `basename $0` [ -p <profile> -r <region>  -h ]
  Options:
      -p <profile>        specify your AWS profile to use (i.e.: default)
      -r <region>         specify an AWS region to direct API requests to
                            (i.e.: us-east-1), all regions are checked anyway if the check requires it
      -c <check_id>       specify one or multiple check ids separated by commas, to see all available checks use "-l" option
                            (i.e.: "check11" for check 1.1 or "extra71,extra72" for extra check 71 and extra check 72)
      -g <group_id>       specify a group of checks by id, to see all available group of checks use "-L"
                            (i.e.: "check3" for entire section 3, "level1" for CIS Level 1 Profile Definitions or "forensics-ready")
      -f <filterregion>   specify an AWS region to run checks against
                            (i.e.: us-west-1)
      -m <maxitems>       specify the maximum number of items to return for long-running requests (default: 100)
      -M <mode>           output mode: text (default), mono, json, csv (separator is ","; data is on stdout; progress on stderr)
      -k                  keep the credential report
      -n                  show check numbers to sort easier
                            (i.e.: 1.01 instead of 1.1)
      -l                  list all available checks only (does not perform any check)
      -L                  list all groups (does not perform any check)
      -e                  exclude group extras
      -E                  execute all tests except a list of specified checks separated by comma (i.e. check21,check31)
      -b                  do not print Prowler banner
      -V                  show version number & exit
      -s                  show scoring report
      -x                  specify external directory with custom checks (i.e. /my/own/checks, files must start by "check")
      -q                  suppress info messages and passing test output
      -A                  account id for the account where to assume a role, requires -R and -T 
                            (i.e.: 123456789012)
      -R                  role name to assume in the account, requires -A and -T 
                            (i.e.: ProwlerRole)
      -T                  session durantion given to that role credentials in seconds, default 1h (3600) recommended 12h, requires -R and -T
                            (i.e.: 43200) 
      -h                  this help
  "
  exit
}

while getopts ":hlLkqp:r:c:g:f:m:M:E:enbVsx:A:R:T:" OPTION; do
   case $OPTION in
     h )
        usage
        EXITCODE=1
        exit $EXITCODE
        ;;
     l )
        PRINTCHECKSONLY=1
        ;;
     L )
        PRINTGROUPSONLY=1
        ;;
     k )
        KEEPCREDREPORT=1
        ;;
     p )
        PROFILE=$OPTARG
        ;;
     r )
        REGION_OPT=$OPTARG
        ;;
     c )
        CHECK_ID=$OPTARG
        ;;
     g )
        GROUP_ID_READ=$OPTARG
        ;;
     f )
        FILTERREGION=$OPTARG
        ;;
     m )
        MAXITEMS=$OPTARG
        ;;
     M )
        MODE=$OPTARG
        ;;
     n )
        NUMERAL=1
        ;;
     b )
        BANNER=0
        ;;
     e )
        EXTRAS=1
        ;;
     E )
        EXCLUDE_CHECK_ID=$OPTARG
        ;;
     V )
        echo "Prowler $PROWLER_VERSION"
        EXITCODE=0
        exit $EXITCODE
        ;;
     s )
        SCORING=1
        ;;
     x )
        EXTERNAL_CHECKS_PATH=$OPTARG
        ;;
     q )
        QUIET=1
        ;;
     A )
        ACCOUNT_TO_ASSUME=$OPTARG
        ;;
     R )
        ROLE_TO_ASSUME=$OPTARG
        ;;
     T )
        SESSION_DURATION_TO_ASSUME=$OPTARG
        ;;
     : )
        echo ""
        echo "$OPTRED ERROR!$OPTNORMAL  -$OPTARG requires an argument"
        usage
        EXITCODE=1
        exit $EXITCODE
        ;;
     ? )
        echo ""
        echo "$OPTRED ERROR!$OPTNORMAL Invalid option"
        usage
        EXITCODE=1
        exit $EXITCODE
        ;;
   esac
done

# Clean up any temp files when prowler quits unexpectedly
trap "{ rm -f /tmp/prowler*.policy.*; }" EXIT

. $PROWLER_DIR/include/colors
. $PROWLER_DIR/include/os_detector
. $PROWLER_DIR/include/aws_profile_loader
. $PROWLER_DIR/include/awscli_detector
. $PROWLER_DIR/include/outputs
. $PROWLER_DIR/include/csv_header
. $PROWLER_DIR/include/banner
. $PROWLER_DIR/include/whoami
. $PROWLER_DIR/include/credentials_report
. $PROWLER_DIR/include/scoring
. $PROWLER_DIR/include/python_detector
. $PROWLER_DIR/include/secrets_detector
. $PROWLER_DIR/include/check3x
. $PROWLER_DIR/include/assume_role

# Get a list of all available AWS Regions
REGIONS=$($AWSCLI ec2 describe-regions --query 'Regions[].RegionName' \
  --output text \
  $PROFILE_OPT \
  --region $REGION \
  --region-names $FILTERREGION)

# Load all of the groups of checks inside groups folder named as "groupNumber*"
for group in $(ls $PROWLER_DIR/groups/group[0-9]*|grep -v groupN_sample); do
	. "$group"
done

# Load all of the checks inside checks folder named as "check*"
# this includes also extra checks since they are "check_extraNN"
for checks in $(ls $PROWLER_DIR/checks/check*|grep -v check_sample); do
	. "$checks"
done

# include checks if external folder is specified
if [[ $EXTERNAL_CHECKS_PATH ]]; then
  for checks in $(ls $EXTERNAL_CHECKS_PATH/check*); do
	. "$checks"
  done
fi

# Function to show the title of the check
# using this way instead of arrays to keep bash3 (osx) and bash4(linux) compatibility
show_check_title() {
  local check_id=CHECK_ID_$1
  local check_title=CHECK_TITLE_$1
  local check_scored=CHECK_SCORED_$1
  local check_type=CHECK_TYPE_$1
  textTitle "${!check_id}" "${!check_title}" "${!check_scored}" "${!check_type}"
}

# Function to show the title of a group, by numeric id
show_group_title() {
	# when csv mode is used, no group tittle is shown
  if [[ "$MODE" != "csv" ]]; then
      textTitle "${GROUP_NUMBER[$1]}" "${GROUP_TITLE[$1]}" "NOT_SCORED" "SUPPORT"
  fi
}

# Function to execute the check
execute_check() {
	# See if this is an alternate name for a check
	# for example, we might have been passed 1.01 which is another name for 1.1
  local alternate_name_var=CHECK_ALTERNATE_$1
  local alternate_name=${!alternate_name_var}
  # Generate the credential report, only if it is group1 related which checks we
  # run so that the checks can safely assume it's available
	if [ ${alternate_name} ];then
    if [[ ${alternate_name} == check1* || ${alternate_name} == extra71 ]];then
      if [ ! -s $TEMP_REPORT_FILE ];then
        genCredReport
        saveReport
      fi
    fi
		show_check_title ${alternate_name}
		${alternate_name}
	else
    # Check to see if this is a real check
    local check_id_var=CHECK_ID_$1
    local check_id=${!check_id_var}
    if [ ${check_id} ]; then
      if [[ ${check_id} == 1* || ${check_id} == 7.1,7.01 ]];then
        if [ ! -s $TEMP_REPORT_FILE ];then
          genCredReport
          saveReport
        fi
      fi
		  show_check_title $1
		  $1
    else
      textFail "ERROR! Use a valid check name (i.e. check41 or extra71)";
      exit $EXITCODE
    fi
	fi
}

# Function to execute all checks in a group
execute_group() {
    show_group_title $1
    # run the checks in the group
    IFS=',' read -ra CHECKS <<< ${GROUP_CHECKS[$1]}

    # Exclude any checks specified
    if [[ -n ${2} ]]; then
        EXCLUDED_CHECKS=()
        NEW_CHECKS=()
        IFS=',' read -ra EXCLUDED_CHECKS <<< "${2},"
        for exc in ${EXCLUDED_CHECKS[@]} ; do
            for i in ${!CHECKS[@]} ; do
                [[ ${CHECKS[i]} = ${exc} ]] && unset CHECKS[i]
            done
        done
        unset EXCLUDED_CHECKS
    fi
    for i in ${CHECKS[@]}; do
        execute_check ${i}
    done
}

# Function to execute group by name
execute_group_by_id() {
  if [ "${GROUP_ID[$1]}" == "group1" ]; then
    genCredReport
    saveReport
  fi
	for i in "${!GROUP_ID[@]}"; do
		if [ "${GROUP_ID[$i]}" == "$1" ]; then
			execute_group ${i} $2
		fi
	done
}

# Function to execute all checks in all groups except extras if -e is invoked
execute_all() {
	for i in "${!GROUP_TITLE[@]}"; do
    if [[ $EXTRAS ]]; then
      GROUP_RUN_BY_DEFAULT[7]='N'
    fi
    if [ "${GROUP_RUN_BY_DEFAULT[$i]}" == "Y" ]; then
		  execute_group $i
    fi
	done
}

# Function to show the titles of everything
show_all_titles() {
  MAIN_GROUPS=(1 2 3 4 7)
  for i in "${MAIN_GROUPS[@]}"; do
		show_group_title $i
		# Display the title of the checks in groups 1,2,3,4 and 7
    # Any other group has checks in these groups
		IFS=',' read -ra CHECKS <<< ${GROUP_CHECKS[$i]}
		for j in ${CHECKS[@]}; do
			show_check_title $j
		done
	done
}

show_all_group_titles() {
	for i in "${!GROUP_TITLE[@]}"; do
		show_group_title $i
	done
}


# Function to execute all checks but exclude some of them
get_all_checks_without_exclusion() {
        CHECKS_EXCLUDED=()
        local CHECKS_TO_EXCLUDE=()
        local TOTAL_CHECKS=()
        #Get a list of checks to exclude
        IFS=',' read -ra E_CHECKS <<< "$1"
        for E_CHECK in "${E_CHECKS[@]}"; do
            CHECKS_TO_EXCLUDE+=($E_CHECK)
        done
        #Get a list of total checks available by ID
	for i in "${!GROUP_TITLE[@]}"; do
		#show_group_title $i
		IFS=',' read -ra CHECKS <<< ${GROUP_CHECKS[$i]}
		for j in ${CHECKS[@]}; do
                    TOTAL_CHECKS+=($CHECK_ID_$j)
		done
	done
        TOTAL_CHECKS=($(echo "${TOTAL_CHECKS[*]}" | tr ' ' '\n' | sort -u)) #removes duplicate and store the result as an array
        #Create a list that contains all checks but excluded ones
        for i in "${TOTAL_CHECKS[@]}"; do
            local COINCIDENCE=false
            for x in "${CHECKS_TO_EXCLUDE[@]}"; do
                if [[ "$i" == "$x" ]]; then
                    COINCIDENCE=true
                fi
            done
            if [[ "$COINCIDENCE" = false ]]; then
                CHECKS_EXCLUDED+=($i)
            fi
        done
}

### All functions defined above ... run the workflow
if [[ $MODE != "csv" ]]; then
  prowlerBanner
fi

# List only check tittles
if [[ $PRINTCHECKSONLY == "1" ]]; then
  show_all_titles
  exit $EXITCODE
fi

# List only group tittles
if [[ $PRINTGROUPSONLY == "1" ]]; then
  show_all_group_titles
  exit $EXITCODE
fi

# Check that jq is installed for JSON output
if [[ $MODE == "json" ]]; then
  . $PROWLER_DIR/include/jq_detector
fi

# Gather account data / test aws cli connectivity
getWhoami

# Execute group of checks if called with -g
if [[ $GROUP_ID_READ ]];then
  if [[ " ${GROUP_ID[@]} " =~ " ${GROUP_ID_READ} " ]]; then
    if [[ $MODE == "csv" ]]; then
      BANNER=0
    fi
    execute_group_by_id ${GROUP_ID_READ} ${EXCLUDE_CHECK_ID}
    cleanTemp
    scoring
    exit $EXITCODE
  else
    textFail "Use a valid check group ID i.e.: group1, extras, forensics-ready, etc."
    show_all_group_titles
    exit $EXITCODE
  fi
fi

# Get a list of total checks excluding a list provided by the user and overwrite CHECK_ID with the result
# if the list provided by the user contains an invalid check, this will be discarded.
# if the list provided by the user contains  just one argument and is invalid, then it will be discarded and all tests will be executed
if [[ ${EXCLUDE_CHECK_ID} ]];then
  get_all_checks_without_exclusion ${EXCLUDE_CHECK_ID}
  function join { local IFS="$1"; shift; echo "$*"; }
  CHECKS_EXCLUDED=$(join , "${CHECKS_EXCLUDED[@]}")
  CHECK_ID=${CHECKS_EXCLUDED}
fi

# Execute single check if called with -c
if [[ $CHECK_ID ]];then
  IFS=',' read -ra CHECKS <<< "$CHECK_ID"
  for CHECK in "${CHECKS[@]}"; do
    execute_check $CHECK
  done
  cleanTemp
  exit $EXITCODE
fi

if [[ $ACCOUNT_TO_ASSUME ]]; then
  # unset env variables with assumed role credentials
  unset AWS_ACCESS_KEY_ID
  unset AWS_SECRET_ACCESS_KEY
  unset AWS_SESSION_TOKEN
fi


execute_all
scoring
cleanTemp
exit $EXITCODE
