#!/usr/bin/env bash # Prowler is a tool that provides automate auditing and hardening guidance of an AWS account. # It is based on AWS-CLI commands. It follows guidelines present in the CIS Amazon # Web Services Foundations Benchmark at: # https://d0.awsstatic.com/whitepapers/compliance/AWS_CIS_Foundations_Benchmark.pdf # This work is 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 # # Author: Toni de la Fuente - @ToniBlyx - https://blyx.com/contact # 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="" OPTNORMAL="" # Set the defaults for these getopts variables REGION="us-east-1" FILTERREGION="" MAXITEMS=100 MONOCHROME=0 MODE="text" SEP=',' KEEPCREDREPORT=0 EXITCODE=0 # Command usage menu usage(){ echo " USAGE: `basename $0` -p -r [ -h ] Options: -p specify your AWS profile to use (i.e.: default) -r specify an AWS region to direct API requests to (i.e.: us-east-1), all regions are checked anyway -c specify a check number or group from the AWS CIS benchmark (i.e.: "check11" for check 1.1, "check3" for entire section 3, "level1" for CIS Level 1 Profile Definitions or "forensics-ready") -f specify an AWS region to run checks against (i.e.: us-west-1) -m specify the maximum number of items to return for long-running requests (default: 100) -M output mode: text (defalut), mono, 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) -e exclude extras -h this help " exit } while getopts ":hlkp:r:c:f:m:M:en" OPTION; do case $OPTION in h ) usage EXITCODE=1 exit $EXITCODE ;; l ) PRINTCHECKSONLY=1 ;; k ) KEEPCREDREPORT=1 ;; p ) PROFILE=$OPTARG ;; r ) REGION=$OPTARG ;; c ) CHECKNUMBER=$OPTARG ;; f ) FILTERREGION=$OPTARG ;; m ) MAXITEMS=$OPTARG ;; M ) MODE=$OPTARG ;; n ) NUMERAL=1 ;; e ) EXTRAS=1 ;; : ) 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 . include/colors SCRIPT_START_TIME=$( date -u +"%Y-%m-%dT%H:%M:%S%z" ) # Functions to manage dates depending on OS if [ "$OSTYPE" == "linux-gnu" ] || [ "$OSTYPE" == "linux-musl" ]; then TEMP_REPORT_FILE=$(mktemp -t -p /tmp prowler.cred_report-XXXXXX) # function to compare in days, usage how_older_from_today date # date format %Y-%m-%d how_older_from_today() { DATE_TO_COMPARE=$1 TODAY_IN_DAYS=$(date -d "$(date +%Y-%m-%d)" +%s) DATE_FROM_IN_DAYS=$(date -d $DATE_TO_COMPARE +%s) DAYS_SINCE=$((($TODAY_IN_DAYS - $DATE_FROM_IN_DAYS )/60/60/24)) echo $DAYS_SINCE } # function to convert from timestamp to date, usage timestamp_to_date timestamp # output date format %Y-%m-%d timestamp_to_date() { # remove fractions of a second TIMESTAMP_TO_CONVERT=$(echo $1 | cut -f1 -d".") OUTPUT_DATE=$(date -d @$TIMESTAMP_TO_CONVERT +'%Y-%m-%d') echo $OUTPUT_DATE } decode_report() { base64 -d } elif [[ "$OSTYPE" == "darwin"* ]]; then # BSD/OSX commands compatibility TEMP_REPORT_FILE=$(mktemp -t prowler.cred_report-XXXXXX) how_older_from_today() { DATE_TO_COMPARE=$1 TODAY_IN_DAYS=$(date +%s) DATE_FROM_IN_DAYS=$(date -jf %Y-%m-%d $DATE_TO_COMPARE +%s) DAYS_SINCE=$((($TODAY_IN_DAYS - $DATE_FROM_IN_DAYS )/60/60/24)) echo $DAYS_SINCE } timestamp_to_date() { # remove fractions of a second TIMESTAMP_TO_CONVERT=$(echo $1 | cut -f1 -d".") OUTPUT_DATE=$(date -r $TIMESTAMP_TO_CONVERT +'%Y-%m-%d') echo $OUTPUT_DATE } decode_report() { base64 -D } elif [[ "$OSTYPE" == "cygwin" ]]; then # POSIX compatibility layer and Linux environment emulation for Windows TEMP_REPORT_FILE=$(mktemp -t -p /tmp prowler.cred_report-XXXXXX) how_older_from_today() { DATE_TO_COMPARE=$1 TODAY_IN_DAYS=$(date -d "$(date +%Y-%m-%d)" +%s) DATE_FROM_IN_DAYS=$(date -d $DATE_TO_COMPARE +%s) DAYS_SINCE=$((($TODAY_IN_DAYS - $DATE_FROM_IN_DAYS )/60/60/24)) echo $DAYS_SINCE } timestamp_to_date() { # remove fractions of a second TIMESTAMP_TO_CONVERT=$(echo $1 | cut -f1 -d".") OUTPUT_DATE=$(date -d @$TIMESTAMP_TO_CONVERT +'%Y-%m-%d') echo $OUTPUT_DATE } decode_report() { base64 -d } else echo "Unknown Operating System! Valid \$OSTYPE: linux-gnu, linux-musl, darwin* or cygwin" echo "Found: $OSTYPE" EXITCODE=1 exit $EXITCODE fi # It checks -p optoin first and use it as profile, if not -p provided then # check environment variables and if not, it checks and loads credentials from # instance profile (metadata server) if runs in an EC2 instance if [[ $PROFILE ]]; then PROFILE_OPT="--profile $PROFILE" else # if Prowler runs insinde an AWS instance with IAM instance profile attached INSTANCE_PROFILE=$(curl -s -m 1 http://169.254.169.254/latest/meta-data/iam/security-credentials/) if [[ $INSTANCE_PROFILE ]]; then AWS_ACCESS_KEY_ID=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/${INSTANCE_PROFILE} | grep AccessKeyId | cut -d':' -f2 | sed 's/[^0-9A-Z]*//g') AWS_SECRET_ACCESS_KEY_ID=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/${INSTANCE_PROFILE} | grep SecretAccessKey | cut -d':' -f2 | sed 's/[^0-9A-Za-z/+=]*//g') AWS_SESSION_TOKEN=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/${INSTANCE_PROFILE} grep Token| cut -d':' -f2 | sed 's/[^0-9A-Za-z/+=]*//g') fi if [[ $AWS_ACCESS_KEY_ID && $AWS_SECRET_ACCESS_KEY || $AWS_SESSION_TOKEN ]];then PROFILE="ENV" PROFILE_OPT="" else PROFILE="default" PROFILE_OPT="--profile $PROFILE" fi fi # AWS-CLI variables AWSCLI=$(which aws) if [ -z "${AWSCLI}" ]; then echo -e "\n$RED ERROR!$NORMAL AWS-CLI (aws command) not found. Make sure it is installed correctly and in your \$PATH\n" EXITCODE=1 exit $EXITCODE fi TITLE_ID="" TITLE_TEXT="CALLER ERROR - UNSET TITLE" ## Output formatting functions textOK(){ if [[ "$MODE" == "csv" ]]; then if [[ $2 ]]; then REPREGION=$2 else REPREGION=$REGION fi echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}PASS${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1" else echo " $OK OK! $NORMAL $1" fi } textNotice(){ if [[ "$MODE" == "csv" ]]; then if [[ $2 ]]; then REPREGION=$2 else REPREGION=$REGION fi echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}INFO${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1" else echo " $NOTICE INFO! $1 $NORMAL" fi } textWarn(){ EXITCODE=3 if [[ "$MODE" == "csv" ]]; then if [[ $2 ]]; then REPREGION=$2 else REPREGION=$REGION fi echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}WARNING${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1" else echo " $BAD WARNING! $1 $NORMAL" fi } textTitle(){ TITLE_ID=$1 if [[ $NUMERAL ]]; then TITLE_ID=$(echo $TITLE_ID | cut -d, -f2) else TITLE_ID=$(echo $TITLE_ID | cut -d, -f1) fi TITLE_TEXT=$2 case "$3" in 0|No|NOT_SCORED) ITEM_SCORED="Not Scored" ;; 1|Yes|SCORED) ITEM_SCORED="Scored" ;; *) ITEM_SCORED="Unspecified" ;; esac case "$4" in LEVEL1) ITEM_LEVEL="Level 1";; LEVEL2) ITEM_LEVEL="Level 2";; EXTRA) ITEM_LEVEL="Extra";; SUPPORT) ITEM_LEVEL="Support";; *) ITEM_LEVEL="Unspecified or Invalid";; esac if [[ "$MODE" == "csv" ]]; then >&2 echo "$TITLE_ID $TITLE_TEXT" else if [[ "$ITEM_SCORED" == "Scored" ]]; then echo -e "\n$BLUE $TITLE_ID $NORMAL $TITLE_TEXT" else echo -e "\n$PURPLE $TITLE_ID $TITLE_TEXT $NORMAL" fi fi } printCsvHeader() { >&2 echo "" >&2 echo "Generating \"${SEP}\" delimited report on stdout for profile $PROFILE, account $ACCOUNT_NUM" echo "PROFILE${SEP}ACCOUNT_NUM${SEP}REGION${SEP}TITLE_ID${SEP}RESULT${SEP}SCORED${SEP}LEVEL${SEP}TITLE_TEXT${SEP}NOTES" } prowlerBanner() { echo -e "$CYAN _" echo -e " _ __ _ __ _____ _| | ___ _ __" echo -e " | '_ \| '__/ _ \ \ /\ / / |/ _ \ '__|" echo -e " | |_) | | | (_) \ V V /| | __/ |" echo -e " | .__/|_| \___/ \_/\_/ |_|\___|_|" echo -e " |_|$NORMAL$BLUE CIS based AWS Account Hardening Tool$NORMAL\n" echo -e "$YELLOW Date: $(date)" } # Get whoami in AWS, who is the user running this shell script getWhoami(){ ACCOUNT_NUM=$($AWSCLI sts get-caller-identity --output json $PROFILE_OPT --region $REGION --query "Account" | tr -d '"') if [[ "$MODE" == "csv" ]]; then CALLER_ARN_RAW=$($AWSCLI sts get-caller-identity --output json $PROFILE_OPT --region $REGION --query "Arn") if [[ 255 -eq $? ]]; then # Failed to get own identity ... exit echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" >&2 echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" EXITCODE=2 exit $EXITCODE fi CALLER_ARN=$(echo $CALLER_ARN_RAW | tr -d '"') printCsvHeader textTitle "0.0" "Show report generation info" "NOT_SCORED" "SUPPORT" textNotice "ARN: $CALLER_ARN TIMESTAMP: $SCRIPT_START_TIME" else echo "" echo "This report is being generated using credentials below:" echo "" echo -e "AWS-CLI Profile: $NOTICE[$PROFILE]$NORMAL AWS API Region: $NOTICE[$REGION]$NORMAL AWS Filter Region: $NOTICE[${FILTERREGION:-all}]$NORMAL\n" if [[ $MONOCHROME -eq 1 ]]; then echo "Caller Identity:" $AWSCLI sts get-caller-identity --output text $PROFILE_OPT --region $REGION --query "Arn" if [[ 255 -eq $? ]]; then # Failed to get own identity ... exit echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" >&2 echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" exit 2 fi echo "" else echo "Caller Identity:" $AWSCLI sts get-caller-identity --output table $PROFILE_OPT --region $REGION if [[ 255 -eq $? ]]; then # Failed to get own identity ... exit echo variable $PROFILE_OPT echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" >&2 echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" EXITCODE=2 exit $EXITCODE fi echo "" fi fi } printColorsCode(){ if [[ $MONOCHROME -eq 0 ]]; then echo -e "\nColors Code for results: $NOTICE INFORMATIVE$NORMAL,$OK OK (RECOMMENDED VALUE)$NORMAL, $BAD WARNING (FIX REQUIRED)$NORMAL \n" fi } # Generate Credential Report genCredReport() { textTitle "0.1" "Generating AWS IAM Credential Report..." "NOT_SCORED" "SUPPORT" until $( $AWSCLI iam generate-credential-report --output text --query 'State' $PROFILE_OPT --region $REGION |grep -q -m 1 "COMPLETE") ; do sleep 1 done } # Save report to a file, decode it, deletion at finish and after every single check saveReport(){ $AWSCLI iam get-credential-report --query 'Content' --output text $PROFILE_OPT --region $REGION | decode_report > $TEMP_REPORT_FILE if [[ $KEEPCREDREPORT -eq 1 ]]; then textTitle "0.2" "Saving IAM Credential Report ..." "NOT_SCORED" "SUPPORT" textNotice "IAM Credential Report saved in $TEMP_REPORT_FILE" fi } # Delete temporary report file cleanTemp(){ if [[ $KEEPCREDREPORT -ne 1 ]]; then rm -fr $TEMP_REPORT_FILE fi } # Delete the temporary report file if we get interrupted/terminated trap cleanTemp EXIT # 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) infoReferenceLong(){ # Report review note: echo -e "" echo -e "For more information on the Prowler, feedback and issue reporting:" echo -e "https://github.com/Alfresco/prowler" echo -e "" echo -e "For more information on the CIS benchmark:" echo -e "https://benchmarks.cisecurity.org/tools2/amazon/CIS_Amazon_Web_Services_Foundations_Benchmark_v1.1.0.pdf" } callCheck(){ if [[ $CHECKNUMBER ]];then case "$CHECKNUMBER" in check11|check101 ) execute_check check11;; * ) textWarn "ERROR! Use a valid check name (i.e. check41 or extra71)\n"; esac cleanTemp exit $EXITCODE fi } # List only check tittles if [[ $PRINTCHECKSONLY == "1" ]]; then prowlerBanner show_all_titles exit $EXITCODE fi # Include all of the groups of checks inside include folder for group in $(ls groups/group*); do . "$group" done # Include all of the checks inside include folder # this includes also extra check since they are "check_extraNN" for checks in $(ls checks/check*); do . "$checks" done # Function to show the title of the check show_check_title() { # This would just call textTitle textTitle "${CHECK_ID[$1]}" "${CHECK_TITLE[$1]}" "${CHECK_SCORED[$1]}" "${CHECK_TYPE[$1]}" } # Function to show the title of a group, by numeric id show_group_title() { # This would also just call textTitle in the real prowler 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 if [ ${CHECK_ALTERNATE[$1]} ];then show_check_title ${CHECK_ALTERNATE[$1]} ${CHECK_ALTERNATE[$1]} else show_check_title $1 $1 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]} for i in "${CHECKS[@]}"; do execute_check $i done } # Function to execute group by name execute_group_by_name() { for i in ${#GROUP_NAME[@]}; do if [ "${GROUP_NAME[$i]}" == "$1" ]; then execute_group $i fi done } # Function to execute all checks in all groups execute_all() { for i in ${#GROUP_TITLE[@]}; do if [ "${GROUP_RUN_BY_DEFAULT[$i]}" == "Y" ]; then execute_group $i fi done } # Function to show the titles of everything show_all_titles() { for i in ${#GROUP_TITLE[@]}; do show_group_title $i # Display the title of the checks IFS=',' read -ra CHECKS <<< ${GROUP_CHECKS[$i]} for j in "${CHECKS[@]}"; do show_check_title $j done done } ### All functions defined above ... run the workflow if [[ $MODE != "csv" ]]; then prowlerBanner printColorsCode fi getWhoami genCredReport saveReport callCheck show_all_titles # if [[ ! $EXTRAS ]]; then # textTitle "7" "$TITLE7" "NOT_SCORED" "SUPPORT" # execute_group 7 # fi cleanTemp exit $EXITCODE