#!/bin/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 / Alfresco Software Inc. # 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... # Exit if a pipeline results in an error. # set -ue # set -o pipefail # set -vx # Exits if any error is found # set -e 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) -c specify a check number or group from the AWS CIS benchmark (i.e.: check11 for check 1.1, extra71, check3 for entire section 3 or level1 for CIS Level 1 Profile Definitions) -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 (default), mono, csv (separator is \"${SEP}\"; 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) -h this help " exit } while getopts ":hkp:r:c:f:m:M:n" OPTION; do case $OPTION in h ) usage EXITCODE=1 exit $EXITCODE ;; k ) KEEPCREDREPORT=1 ;; p ) PROFILE=$OPTARG ;; r ) REGION=$OPTARG ;; c ) CHECKNUMBER=$OPTARG ;; f ) FILTERREGION=$OPTARG ;; m ) MAXITEMS=$OPTARG ;; M ) MODE=$OPTARG ;; n ) NUMERAL=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 if [[ $MODE != "mono" && $MODE != "text" && $MODE != "csv" ]]; then echo "" echo "$OPTRED ERROR!$OPTNORMAL Invalid output mode. Choose text, mono, or csv." usage EXITCODE=1 exit $EXITCODE fi if [[ "$MODE" == "mono" || "$MODE" == "csv" ]]; then MONOCHROME=1 fi if [[ $MONOCHROME -eq 1 ]]; then # Colors NORMAL='' WARNING='' # Bad (red) SECTION='' # Section (yellow) NOTICE='' # Notice (yellow) OK='' # Ok (green) BAD='' # Bad (red) CYAN='' BLUE='' BROWN='' DARKGRAY='' GRAY='' GREEN='' MAGENTA='' PURPLE='' RED='' YELLOW='' WHITE='' else # Colors # NOTE: Your editor may NOT show the 0x1b / escape character left of the '[' NORMAL="" WARNING="" # Bad (red) SECTION="" # Section (yellow) NOTICE="" # Notice (yellow) OK="" # Ok (green) BAD="" # Bad (red) CYAN="" BLUE="" BROWN="" DARKGRAY="" GRAY="" GREEN="" MAGENTA="" PURPLE="" RED="" YELLOW="" WHITE="" fi SCRIPT_START_TIME=$( date -u +"%Y-%m-%dT%H:%M:%S%z" ) # Functions to manage dates depending on OS if [[ "$OSTYPE" == "linux-gnu" ]]; then # 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 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 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" EXITCODE=1 exit $EXITCODE fi # Check environment if profile provided reads it from creds file, then instance profile # if not profile provided loads it from environment variables if [[ $PROFILE ]]; then PROFILE_OPT="--profile $PROFILE" if [[ ! -f ~/.aws/credentials ]]; then echo -e "\n$RED ERROR!$NORMAL AWS credentials file not found (~/.aws/credentials). Run 'aws configure' first. \n" return 1 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 fi else 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(){ TEMP_REPORT_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-XXXXX.cred_report ) $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" } check11(){ ID11="1.1,1.01" TITLE11="Avoid the use of the root account (Scored)." COMMAND11=$(cat $TEMP_REPORT_FILE| grep '' | cut -d, -f5,11,16 | sed 's/,/\ /g') textTitle "$ID11" "$TITLE11" "SCORED" "LEVEL1" textNotice "Root account last accessed (password key_1 key_2): $COMMAND11" } check12(){ ID12="1.2,1.02" TITLE12="Ensure multi-factor authentication (MFA) is enabled for all IAM users that have a console password (Scored)" # List users with password enabled COMMAND12_LIST_USERS_WITH_PASSWORD_ENABLED=$(cat $TEMP_REPORT_FILE|awk -F, '{ print $1,$4 }' |grep true | awk '{ print $1 }') COMMAND12=$( for i in $COMMAND12_LIST_USERS_WITH_PASSWORD_ENABLED; do cat $TEMP_REPORT_FILE|awk -F, '{ print $1,$8 }' |grep "$i " |grep false | awk '{ print $1 }' done) textTitle "$ID12" "$TITLE12" "SCORED" "LEVEL1" if [[ $COMMAND12 ]]; then for u in $COMMAND12; do textWarn "User $u has Password enabled but MFA disabled" done else textOK "No users found with Password enabled and MFA disabled" fi } check13(){ ID13="1.3,1.03" TITLE13="Ensure credentials unused for 90 days or greater are disabled (Scored)" textTitle "$ID13" "$TITLE13" "SCORED" "LEVEL1" COMMAND12_LIST_USERS_WITH_PASSWORD_ENABLED=$(cat $TEMP_REPORT_FILE|awk -F, '{ print $1,$4 }' |grep true | awk '{ print $1 }') if [[ $COMMAND12_LIST_USERS_WITH_PASSWORD_ENABLED ]]; then COMMAND13=$( for i in $COMMAND12_LIST_USERS_WITH_PASSWORD_ENABLED; do cat $TEMP_REPORT_FILE|awk -F, '{ print $1,$5 }' |grep $i| awk '{ print $1 }'|tr '\n' ' '; done) # list of users that have used password USERS_PASSWORD_USED=$($AWSCLI iam list-users --query "Users[?PasswordLastUsed].UserName" --output text $PROFILE_OPT --region $REGION) if [[ $USERS_PASSWORD_USED ]]; then # look for users with a password last used more or equal to 90 days for i in $USERS_PASSWORD_USED; do DATEUSED=$($AWSCLI iam list-users --query "Users[?UserName=='$i'].PasswordLastUsed" --output text $PROFILE_OPT --region $REGION | cut -d'T' -f1) HOWOLDER=$(how_older_from_today $DATEUSED) if [ $HOWOLDER -gt "90" ];then textWarn "User \"$i\" has not logged in during the last 90 days " else textOK "User \"$i\" found with credentials used in the last 90 days" fi done fi else textOK "No users found with password enabled" fi } check14(){ ID14="1.4,1.04" TITLE14="Ensure access keys are rotated every 90 days or less (Scored)" # also checked by Security Monkey LIST_OF_USERS_WITH_ACCESS_KEY1=$(cat $TEMP_REPORT_FILE| awk -F, '{ print $1, $9 }' |grep "\ true" | awk '{ print $1 }') LIST_OF_USERS_WITH_ACCESS_KEY2=$(cat $TEMP_REPORT_FILE| awk -F, '{ print $1, $14 }' |grep "\ true" | awk '{ print $1 }') textTitle "$ID14" "$TITLE14" "SCORED" "LEVEL1" C14_NUM_USERS1=0 C14_NUM_USERS2=0 if [[ $LIST_OF_USERS_WITH_ACCESS_KEY1 ]]; then # textWarn "Users with access key 1 older than 90 days:" for user in $LIST_OF_USERS_WITH_ACCESS_KEY1; do # check access key 1 DATEROTATED1=$(cat $TEMP_REPORT_FILE | grep -v user_creation_time | grep "^${user},"| awk -F, '{ print $10 }' | grep -v "N/A" | awk -F"T" '{ print $1 }') HOWOLDER=$(how_older_from_today $DATEROTATED1) if [ $HOWOLDER -gt "90" ];then textWarn " $user has not rotated access key1 in over 90 days " C14_NUM_USERS1=$(expr $C14_NUM_USERS1 + 1) fi done if [[ $C14_NUM_USERS1 -eq 0 ]]; then textOK "No users with access key 1 older than 90 days." fi else textOK "No users with access key 1." fi if [[ $LIST_OF_USERS_WITH_ACCESS_KEY2 ]]; then # textWarn "Users with access key 2 older than 90 days:" for user in $LIST_OF_USERS_WITH_ACCESS_KEY2; do # check access key 2 DATEROTATED2=$(cat $TEMP_REPORT_FILE | grep -v user_creation_time | grep "^${user},"| awk -F, '{ print $10 }' | grep -v "N/A" | awk -F"T" '{ print $1 }') HOWOLDER=$(how_older_from_today $DATEROTATED2) if [ $HOWOLDER -gt "90" ];then textWarn " $user has not rotated access key2. " C14_NUM_USERS2=$(expr $C14_NUM_USERS2 + 1) fi done if [[ $C14_NUM_USERS2 -eq 0 ]]; then textOK "No users with access key 2 older than 90 days." fi else textOK "No users with access key 2." fi } check15(){ ID15="1.5,1.05" TITLE15="Ensure IAM password policy requires at least one uppercase letter (Scored)" COMMAND15=$($AWSCLI iam get-account-password-policy $PROFILE_OPT --region $REGION --output json --query 'PasswordPolicy.RequireUppercaseCharacters' 2> /dev/null) # must be true textTitle "$ID15" "$TITLE15" "SCORED" "LEVEL1" if [[ "$COMMAND15" == "true" ]];then textOK "Password Policy requires upper case" else textWarn "Password Policy missing upper-case requirement" fi } check16(){ ID16="1.6,1.06" TITLE16="Ensure IAM password policy require at least one lowercase letter (Scored)" COMMAND16=$($AWSCLI iam get-account-password-policy $PROFILE_OPT --region $REGION --output json --query 'PasswordPolicy.RequireLowercaseCharacters' 2> /dev/null) # must be true textTitle "$ID16" "$TITLE16" "SCORED" "LEVEL1" if [[ "$COMMAND16" == "true" ]];then textOK "Password Policy requires lower case" else textWarn "Password Policy missing lower-case requirement" fi } check17(){ ID17="1.7,1.07" TITLE17="Ensure IAM password policy require at least one symbol (Scored)" COMMAND17=$($AWSCLI iam get-account-password-policy $PROFILE_OPT --region $REGION --output json --query 'PasswordPolicy.RequireSymbols' 2> /dev/null) # must be true textTitle "$ID17" "$TITLE17" "SCORED" "LEVEL1" if [[ "$COMMAND17" == "true" ]];then textOK "Password Policy requires symbol" else textWarn "Password Policy missing symbol requirement" fi } check18(){ ID18="1.8,1.08" TITLE18="Ensure IAM password policy require at least one number (Scored)" COMMAND18=$($AWSCLI iam get-account-password-policy $PROFILE_OPT --region $REGION --output json --query 'PasswordPolicy.RequireNumbers' 2> /dev/null) # must be true textTitle "$ID18" "$TITLE18" "SCORED" "LEVEL1" if [[ "$COMMAND18" == "true" ]];then textOK "Password Policy requires number" else textWarn "Password Policy missing number requirement" fi } check19(){ ID19="1.9,1.09" TITLE19="Ensure IAM password policy requires minimum length of 14 or greater (Scored)" COMMAND19=$($AWSCLI iam get-account-password-policy $PROFILE_OPT --region $REGION --output json --query 'PasswordPolicy.MinimumPasswordLength' 2> /dev/null) textTitle "$ID19" "$TITLE19" "SCORED" "LEVEL1" if [[ $COMMAND19 -gt "13" ]];then textOK "Password Policy requires more than 13 characters" else textWarn "Password Policy missing or weak length requirement" fi } check110(){ ID110="1.10" TITLE110="Ensure IAM password policy prevents password reuse: 24 or greater (Scored)" COMMAND110=$($AWSCLI iam get-account-password-policy $PROFILE_OPT --region $REGION --query 'PasswordPolicy.PasswordReusePrevention' --output text 2> /dev/null) textTitle "$ID110" "$TITLE110" "SCORED" "LEVEL1" if [[ $COMMAND110 ]];then if [[ $COMMAND110 -gt "23" ]];then textOK "Password Policy limits reuse" else textWarn "Password Policy has weak reuse requirement (lower than 24)" fi else textWarn "Password Policy missing reuse requirement" fi } check111(){ ID111="1.11" TITLE111="Ensure IAM password policy expires passwords within 90 days or less (Scored)" COMMAND111=$($AWSCLI iam get-account-password-policy $PROFILE_OPT --region $REGION --output json | grep MaxPasswordAge | awk -F: '{ print $2 }'|sed 's/\ //g'|sed 's/,/ /g' 2> /dev/null) textTitle "$ID111" "$TITLE111" "SCORED" "LEVEL1" if [[ $COMMAND111 ]];then if [ "$COMMAND111" == "90" ];then textOK "Password Policy includes expiration" fi else textWarn "Password expiration not set or set greater than 90 days " fi } check112(){ ID112="1.12" TITLE112="Ensure no root account access key exists (Scored)" # ensure the access_key_1_active and access_key_2_active fields are set to FALSE. ROOTKEY1=$(cat $TEMP_REPORT_FILE |grep root_account|awk -F',' '{ print $9 }') ROOTKEY2=$(cat $TEMP_REPORT_FILE |grep root_account|awk -F',' '{ print $14 }') textTitle "$ID112" "$TITLE112" "SCORED" "LEVEL1" if [ "$ROOTKEY1" == "false" ];then textOK "No access key 1 found for root" else textWarn "Found access key 1 for root " fi if [ "$ROOTKEY2" == "false" ];then textOK "No access key 2 found for root" else textWarn "Found access key 2 for root " fi } check113(){ ID113="1.13" TITLE113="Ensure MFA is enabled for the root account (Scored)" COMMAND113=$($AWSCLI iam get-account-summary $PROFILE_OPT --region $REGION --output json --query 'SummaryMap.AccountMFAEnabled') textTitle "$ID113" "$TITLE113" "SCORED" "LEVEL1" if [ "$COMMAND113" == "1" ]; then textOK "Virtual MFA is enabled for root" else textWarn "MFA is not ENABLED for root account " fi } check114(){ ID114="1.14" TITLE114="Ensure hardware MFA is enabled for the root account (Scored)" COMMAND113=$($AWSCLI iam get-account-summary $PROFILE_OPT --region $REGION --output json --query 'SummaryMap.AccountMFAEnabled') textTitle "$ID114" "$TITLE114" "SCORED" "LEVEL1" if [ "$COMMAND113" == "1" ]; then COMMAND114=$($AWSCLI iam list-virtual-mfa-devices $PROFILE_OPT --region $REGION --query 'VirtualMFADevices' --output text|grep :root |wc -l) if [ "$COMMAND114" == "1" ]; then textOK "Virtual MFA is enabled for root" else textOK "Hardware MFA is enabled for root " fi else textWarn "MFA is not ENABLED for root account " fi } check115(){ ID115="1.15" TITLE115="Ensure security questions are registered in the AWS account (Not Scored)" textTitle "$ID115" "$TITLE115" "NOT_SCORED" "LEVEL2" textNotice "No command available for check 1.15 " textNotice "Login to the AWS Console as root & click on the Account " textNotice "Name -> My Account -> Configure Security Challenge Questions " } check116(){ ID116="1.16" TITLE116="Ensure IAM policies are attached only to groups or roles (Scored)" textTitle "$ID116" "$TITLE116" "SCORED" "LEVEL1" LIST_USERS=$($AWSCLI iam list-users --query 'Users[*].UserName' --output text $PROFILE_OPT --region $REGION) C116_NUM_USERS=0 for user in $LIST_USERS;do USER_POLICY=$($AWSCLI iam list-attached-user-policies --output text $PROFILE_OPT --region $REGION --user-name $user) if [[ $USER_POLICY ]]; then textWarn "$user has policy directly attached " C116_NUM_USERS=$(expr $C116_NUM_USERS + 1) fi done if [[ $C116_NUM_USERS -eq 0 ]]; then textOK "No policies attached to users." fi } check117(){ ID117="1.17" TITLE117="Enable detailed billing (Scored)" # No command available textTitle "$ID117" "$TITLE117" "SCORED" "LEVEL1" textNotice "No command available for check 1.17 " textNotice "See section 1.17 on the CIS Benchmark guide for details " } check118(){ ID118="1.18" TITLE118="Ensure IAM Master and IAM Manager roles are active (Scored)" textTitle "$ID118" "$TITLE118" "SCORED" "LEVEL1" FINDMASTERANDMANAGER=$($AWSCLI iam list-roles $PROFILE_OPT --region $REGION --query "Roles[*].{RoleName:RoleName}" --output text | grep -E 'Master|Manager'| tr '\n' ' ') if [[ $FINDMASTERANDMANAGER ]];then textNotice "Found next roles as possible IAM Master and IAM Manager candidates: " textNotice "$FINDMASTERANDMANAGER " textNotice "run the commands below to check their policies with section 1.18 in the guide..." for role in $FINDMASTERANDMANAGER;do # find inline policies in found roles INLINEPOLICIES=$($AWSCLI iam list-role-policies --role-name $role $PROFILE_OPT --region $REGION --query "PolicyNames[*]" --output text) for policy in $INLINEPOLICIES;do textNotice "INLINE: $AWSCLI iam get-role-policy --role-name $role --policy-name $policy $PROFILE_OPT --region $REGION --output json" done # find attached policies in found roles ATTACHEDPOLICIES=$($AWSCLI iam list-attached-role-policies --role-name $role $PROFILE_OPT --region $REGION --query "AttachedPolicies[*]" --output text) for policy in $ATTACHEDPOLICIES;do textNotice "ATTACHED: $AWSCLI iam get-role-policy --role-name $role --policy-name $policy $PROFILE_OPT --region $REGION --output json" done done else textWarn "IAM Master and IAM Manager roles not found" fi } check119(){ ID119="1.19" TITLE119="Maintain current contact details (Scored)" # No command available textTitle "$ID119" "$TITLE119" "SCORED" "LEVEL1" textNotice "No command available for check 1.19 " textNotice "See section 1.19 on the CIS Benchmark guide for details " } check120(){ ID120="1.20" TITLE120="Ensure security contact information is registered (Scored)" # No command available textTitle "$ID120" "$TITLE120" "SCORED" "LEVEL1" textNotice "No command available for check 1.20 " textNotice "See section 1.20 on the CIS Benchmark guide for details " } check121(){ ID121="1.21" TITLE121="Ensure IAM instance roles are used for AWS resource access from instances (Not Scored)" textTitle "$ID121" "$TITLE121" "NOT_SCORED" "LEVEL2" textNotice "No command available for check 1.21 " textNotice "See section 1.21 on the CIS Benchmark guide for details " } check122(){ ID122="1.22" TITLE122="Ensure a support role has been created to manage incidents with AWS Support (Scored)" textTitle "$ID122" "$TITLE122" "SCORED" "LEVEL1" SUPPORTPOLICYARN=$($AWSCLI iam list-policies --query "Policies[?PolicyName == 'AWSSupportAccess'].Arn" $PROFILE_OPT --region $REGION --output text) if [[ $SUPPORTPOLICYARN ]];then for policyarn in $SUPPORTPOLICYARN;do POLICYUSERS=$($AWSCLI iam list-entities-for-policy --policy-arn $SUPPORTPOLICYARN $PROFILE_OPT --region $REGION --output json) if [[ $POLICYUSERS ]];then textOK "Support Policy attached to $policyarn" for user in $(echo "$POLICYUSERS" | grep UserName | cut -d'"' -f4) ; do textNotice "User $user has support access via $policyarn" done # textNotice "Make sure your team can create a Support case with AWS " else textWarn "Support Policy not applied to any Group / User / Role " fi done else textWarn "No Support Policy found" fi } check123(){ ID123="1.23" TITLE123="Do not setup access keys during initial user setup for all IAM users that have a console password (Not Scored)" textTitle "$ID123" "$TITLE123" "NOT_SCORED" "LEVEL1" LIST_USERS=$($AWSCLI iam list-users --query 'Users[*].UserName' --output text $PROFILE_OPT --region $REGION) # List of USERS with KEY1 last_used_date as N/A LIST_USERS_KEY1_NA=$(for user in $LIST_USERS; do grep "^${user}," $TEMP_REPORT_FILE|awk -F, '{ print $1,$11 }'|grep N/A |awk '{ print $1 }'; done) LIST_USERS_KEY1_ACTIVE=$(for user in $LIST_USERS_KEY1_NA; do grep "^${user}," $TEMP_REPORT_FILE|awk -F, '{ print $1,$9 }'|grep "true$"|awk '{ print $1 }'|sed 's/[[:blank:]]+/,/g' ; done) if [[ $LIST_USERS_KEY1_ACTIVE ]]; then for user in $LIST_USERS_KEY1_ACTIVE; do textNotice "$user has never used Access Key 1" done else textOK "No users found with Access Key 1 never used" fi # List of USERS with KEY2 last_used_date as N/A LIST_USERS_KEY2_NA=$(for user in $LIST_USERS; do grep "^${user}," $TEMP_REPORT_FILE|awk -F, '{ print $1,$16 }'|grep N/A |awk '{ print $1 }' ; done) LIST_USERS_KEY2_ACTIVE=$(for user in $LIST_USERS_KEY2_NA; do grep "^${user}," $TEMP_REPORT_FILE|awk -F, '{ print $1,$14 }'|grep "true$" |awk '{ print $1 }' ; done) if [[ $LIST_USERS_KEY2_ACTIVE ]]; then for user in $LIST_USERS_KEY2_ACTIVE; do textNotice "$user has never used Access Key 2" done else textOK "No users found with Access Key 2 never used" fi } check124(){ ID124="1.24" TITLE124="Ensure IAM policies that allow full \"*:*\" administrative privileges are not created (Scored)" textTitle "$ID124" "$TITLE124" "SCORED" "LEVEL1" LIST_CUSTOM_POLICIES=$($AWSCLI iam list-policies --output text $PROFILE_OPT --region $REGION|grep 'arn:aws:iam::[0-9]\{12\}:'|awk '{ print $2 }') if [[ $LIST_CUSTOM_POLICIES ]]; then textNotice "Looking for custom policies: (skipping default policies - it may take few seconds...)" for policy in $LIST_CUSTOM_POLICIES; do POLICY_VERSION=$($AWSCLI iam list-policies $PROFILE_OPT --region $REGION --query 'Policies[*].[Arn,DefaultVersionId]' --output text |awk "\$1 == \"$policy\" { print \$2 }") POLICY_WITH_FULL=$($AWSCLI iam get-policy-version --output text --policy-arn $policy --version-id $POLICY_VERSION --query "PolicyVersion.Document.Statement[?Effect == 'Allow' && contains(Resource, '*') && contains (Action, '*')]" $PROFILE_OPT --region $REGION) if [[ $POLICY_WITH_FULL ]]; then POLICIES_ALLOW_LIST="$POLICIES_ALLOW_LIST $policy" fi done if [[ $POLICIES_ALLOW_LIST ]]; then textNotice "List of custom policies: " for policy in $POLICIES_ALLOW_LIST; do textNotice "Policy $policy allows \"*:*\"" done else textOK "No custom policy found that allow full \"*:*\" administrative privileges" fi else textOK "No custom policies found" fi } check21(){ ID21="2.1,2.01" TITLE21="Ensure CloudTrail is enabled in all regions (Scored)" textTitle "$ID21" "$TITLE21" "SCORED" "LEVEL1" LIST_OF_TRAILS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].Name' --output text) if [[ $LIST_OF_TRAILS ]];then for trail in $LIST_OF_TRAILS;do MULTIREGION_TRAIL_STATUS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].IsMultiRegionTrail' --output text --trail-name-list $trail) if [[ "$MULTIREGION_TRAIL_STATUS" == 'False' ]];then textWarn "$trail trail in $REGION is not enabled in multi region mode" else textOK "$trail trail in $REGION is enabled for all regions" fi done else textWarn "No CloudTrail trails found!" fi } check22(){ ID22="2.2,2.02" TITLE22="Ensure CloudTrail log file validation is enabled (Scored)" textTitle "$ID22" "$TITLE22" "SCORED" "LEVEL2" LIST_OF_TRAILS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].Name' --output text) if [[ $LIST_OF_TRAILS ]];then for trail in $LIST_OF_TRAILS;do LOGFILEVALIDATION_TRAIL_STATUS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].LogFileValidationEnabled' --output text --trail-name-list $trail) if [[ "$LOGFILEVALIDATION_TRAIL_STATUS" == 'False' ]];then textWarn "$trail trail in $REGION has not log file validation enabled" else textOK "$trail trail in $REGION has log file validation enabled" fi done else textWarn "No CloudTrail trails found!" fi } check23(){ ID23="2.3,2.03" TITLE23="Ensure the S3 bucket CloudTrail logs to is not publicly accessible (Scored)" textTitle "$ID23" "$TITLE23" "SCORED" "LEVEL1" CLOUDTRAILBUCKET=$($AWSCLI cloudtrail describe-trails --query 'trailList[*].S3BucketName' --output text $PROFILE_OPT --region $REGION) if [[ $CLOUDTRAILBUCKET ]];then for bucket in $CLOUDTRAILBUCKET;do CLOUDTRAILBUCKET_HASALLPERMISIONS=$($AWSCLI s3api get-bucket-acl --bucket $bucket --query 'Grants[?Grantee.URI==`http://acs.amazonaws.com/groups/global/AllUsers`]' $PROFILE_OPT --region $REGION --output text) if [[ $CLOUDTRAILBUCKET_HASALLPERMISIONS ]];then textWarn "check your $bucket CloudTrail bucket ACL and Policy!" else textOK "Bucket $bucket is set correctly" fi done else textWarn "No CloudTrail bucket found!" fi } check24(){ ID24="2.4,2.04" TITLE24="Ensure CloudTrail trails are integrated with CloudWatch Logs (Scored)" textTitle "$ID24" "$TITLE24" "SCORED" "LEVEL1" TRAILS_AND_REGIONS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].{Name:Name, HomeRegion:HomeRegion}' --output text | tr "\t" ',') if [[ $TRAILS_AND_REGIONS ]];then for reg_trail in $TRAILS_AND_REGIONS;do trail=$(echo $reg_trail | cut -d',' -f2) TRAIL_REGION=$(echo $reg_trail | cut -d',' -f1) LATESTDELIVERY_TIMESTAMP=$($AWSCLI cloudtrail get-trail-status --name $trail $PROFILE_OPT --region $TRAIL_REGION --query 'LatestCloudWatchLogsDeliveryTime' --output text|grep -v None) if [[ ! $LATESTDELIVERY_TIMESTAMP ]];then textWarn "$trail trail is not logging in the last 24h or not configured (it is in $TRAIL_REGION)" else LATESTDELIVERY_DATE=$(timestamp_to_date $LATESTDELIVERY_TIMESTAMP) HOWOLDER=$(how_older_from_today $LATESTDELIVERY_DATE) if [ $HOWOLDER -gt "1" ];then textWarn "$trail trail is not logging in the last 24h or not configured (it is in $TRAIL_REGION)" else textOK "$trail trail has been logging during the last 24h (it is in $TRAIL_REGION)" fi fi done else textWarn "No CloudTrail trails found!" fi } check25(){ ID25="2.5,2.05" TITLE25="Ensure AWS Config is enabled in all regions (Scored)" textTitle "$ID25" "$TITLE25" "SCORED" "LEVEL1" for regx in $REGIONS; do CHECK_AWSCONFIG_STATUS=$($AWSCLI configservice get-status $PROFILE_OPT --region $regx --output json| grep "recorder: ON") if [[ $CHECK_AWSCONFIG_STATUS ]];then textOK "Region $regx has AWS Config recorder: ON" "$regx" else textWarn "Region $regx has AWS Config disabled or not configured" "$regx" fi done } check26(){ ID26="2.6,2.06" TITLE26="Ensure S3 bucket access logging is enabled on the CloudTrail S3 bucket (Scored)" textTitle "$ID26" "$TITLE26" "SCORED" "LEVEL1" CLOUDTRAILBUCKET=$($AWSCLI cloudtrail describe-trails --query 'trailList[*].S3BucketName' --output text $PROFILE_OPT --region $REGION) if [[ $CLOUDTRAILBUCKET ]];then for bucket in $CLOUDTRAILBUCKET;do CLOUDTRAILBUCKET_LOGENABLED=$($AWSCLI s3api get-bucket-logging --bucket $bucket $PROFILE_OPT --region $REGION --query 'LoggingEnabled.TargetBucket' --output text|grep -v None) if [[ $CLOUDTRAILBUCKET_LOGENABLED ]];then textOK "Bucket access logging enabled in $bucket" else textWarn "access logging is not enabled in $bucket CloudTrail S3 bucket!" fi done else textWarn "CloudTrail bucket not found!" fi } check27(){ ID27="2.7,2.07" TITLE27="Ensure CloudTrail logs are encrypted at rest using KMS CMKs (Scored)" textTitle "$ID27" "$TITLE27" "SCORED" "LEVEL2" CLOUDTRAILNAME=$($AWSCLI cloudtrail describe-trails --query 'trailList[*].Name' --output text $PROFILE_OPT --region $REGION) if [[ $CLOUDTRAILNAME ]];then for trail in $CLOUDTRAILNAME;do CLOUDTRAILENC_ENABLED=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --trail $trail --query 'trailList[*].KmsKeyId' --output text) if [[ $CLOUDTRAILENC_ENABLED ]];then textOK "KMS key found for $trail" else textWarn "encryption is not enabled in your CloudTrail trail $trail (KMS key not found)!" fi done else textWarn "CloudTrail bucket doesn't exist!" fi } check28(){ ID28="2.8,2.08" TITLE28="Ensure rotation for customer created CMKs is enabled (Scored)" textTitle "$ID28" "$TITLE28" "SCORED" "LEVEL2" for regx in $REGIONS; do CHECK_KMS_KEYLIST=$($AWSCLI kms list-keys $PROFILE_OPT --region $regx --output text --query 'Keys[*].KeyId') if [[ $CHECK_KMS_KEYLIST ]];then CHECK_KMS_KEYLIST_NO_DEFAULT=$(for key in $CHECK_KMS_KEYLIST ; do $AWSCLI kms describe-key --key-id $key $PROFILE_OPT --region $regx --output text|grep -v 'Default master key that protects my ACM private keys when no other key is defined'|awk '{ print $3 }'|awk -F'/' '{ print $2 }'; done) for key in $CHECK_KMS_KEYLIST_NO_DEFAULT; do CHECK_KMS_KEY_TYPE=$($AWSCLI kms describe-key --key-id $key $PROFILE_OPT --region $regx --query 'KeyMetadata.Origin' | sed 's/["]//g') if [[ "$CHECK_KMS_KEY_TYPE" == "EXTERNAL" ]];then textOK "Key $key in Region $regx Customer Uploaded Key Material." "$regx" else CHECK_KMS_KEY_ROTATION=$($AWSCLI kms get-key-rotation-status --key-id $key $PROFILE_OPT --region $regx --output text) #CHECK_KMS_DEFAULT_KEY=$($AWSCLI kms describe-key --key-id $key $PROFILE_OPT --region $regx --query 'KeyMetadata.Description' | sed -n '/Default master key that protects my ACM private keys when no other key is defined /p'|| echo "False") if [[ "$CHECK_KMS_KEY_ROTATION" == "True" ]];then textOK "Key $key in Region $regx is set correctly" elif [[ "$CHECK_KMS_KEY_ROTATION" == "False" && $CHECK_KMS_DEFAULT_KEY ]];then textNotice "Region $regx key $key is an AWS default master key and cannot be deleted nor modified." "$regx" else textWarn "Key $key in Region $regx is not set to rotate!!!" "$regx" fi fi done else textNotice "Region $regx doesn't have encryption keys" "$regx" fi done } check31(){ ID31="3.1,3.01" TITLE31="Ensure a log metric filter and alarm exist for unauthorized API calls (Scored)" textTitle "$ID31" "$TITLE31" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text| tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | awk '/UnauthorizedOperation/ || /AccessDenied/ {print $1" "$2}') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /UnauthorizedOperation/ || /AccessDenied/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms set for Unauthorized Operation and Access Denied" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check32(){ ID32="3.2,3.02" TITLE32="Ensure a log metric filter and alarm exist for Management Console sign-in without MFA (Scored)" textTitle "$ID32" "$TITLE32" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' |grep filterPattern|grep MFAUsed| awk '/ConsoleLogin/ && (/additionalEventData.MFAUsed.*\!=.*\"Yes/) {print $1}') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /ConsoleLogin/ || /MFAUsed/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms set for sign-in Console without MFA enabled" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check33(){ ID33="3.3,3.03" TITLE33="Ensure a log metric filter and alarm exist for usage of root account (Scored)" textTitle "$ID33" "$TITLE33" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION |grep -E 'userIdentity.*Root.*AwsServiceEvent') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /userIdentity/ || /Root/ || /AwsServiceEvent/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms set for usage of root account" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check34(){ ID34="3.4,3.04" TITLE34="Ensure a log metric filter and alarm exist for IAM policy changes (Scored)" textTitle "$ID34" "$TITLE34" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'DeleteGroupPolicy.*DeleteRolePolicy.*DeleteUserPolicy.*PutGroupPolicy.*PutRolePolicy.*PutUserPolicy.*CreatePolicy.*DeletePolicy.*CreatePolicyVersion.*DeletePolicyVersion.*AttachRolePolicy.*DetachRolePolicy.*AttachUserPolicy.*DetachUserPolicy.*AttachGroupPolicy.*DetachGroupPolicy') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /DeletePolicy/ || /DeletePolicies/ || /Policies/ || /Policy/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for IAM policy changes" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check35(){ ID35="3.5,3.05" TITLE35="Ensure a log metric filter and alarm exist for CloudTrail configuration changes (Scored)" textTitle "$ID35" "$TITLE35" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'CreateTrail.*UpdateTrail.*DeleteTrail.*StartLogging.*StopLogging') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /TrailChange/ || /Trails/ || /CreateTrail/ || /UpdateTrail/ || /DeleteTrail/ || /StartLogging/ || /StopLogging/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for CloudTrail configuration changes" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check36(){ ID36="3.6,3.06" TITLE36="Ensure a log metric filter and alarm exist for AWS Management Console authentication failures (Scored)" textTitle "$ID36" "$TITLE36" "SCORED" "LEVEL2" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'ConsoleLogin.*Failed') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /FailedLogin/ || /ConsoleLogin/ || /Failed/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for usage of root account" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check37(){ ID37="3.7,3.07" TITLE37="Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs (Scored)" textTitle "$ID37" "$TITLE37" "SCORED" "LEVEL2" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'kms.amazonaws.com.*DisableKey.*ScheduleKeyDeletion') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /DisableKey/ || /ScheduleKeyDeletion/ || /kms/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for changes of customer created CMKs" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check38(){ ID38="3.8,3.08" TITLE38="Ensure a log metric filter and alarm exist for S3 bucket policy changes (Scored)" textTitle "$ID38" "$TITLE38" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 's3.amazonaws.com.*PutBucketAcl.*PutBucketPolicy.*PutBucketCors.*PutBucketLifecycle.*PutBucketReplication.*DeleteBucketPolicy.*DeleteBucketCors.*DeleteBucketLifecycle.*DeleteBucketReplication') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /S3/ || /BucketPolicy/ || /BucketPolicies/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for S3 bucket policy changes" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check39(){ ID39="3.9,3.09" TITLE39="Ensure a log metric filter and alarm exist for AWS Config configuration changes (Scored)" textTitle "$ID39" "$TITLE39" "SCORED" "LEVEL2" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'config.amazonaws.com.*StopConfigurationRecorder.*DeleteDeliveryChannel.*PutDeliveryChannel.*PutConfigurationRecorder') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /config/ || /ConfigurationRecorder/ || /DeliveryChannel/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for AWS Config configuration changes" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check310(){ ID310="3.10" TITLE310="Ensure a log metric filter and alarm exist for security group changes (Scored)" textTitle "$ID310" "$TITLE310" "SCORED" "LEVEL2" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'AuthorizeSecurityGroupIngress.*AuthorizeSecurityGroupEgress.*RevokeSecurityGroupIngress.*RevokeSecurityGroupEgress.*CreateSecurityGroup.*DeleteSecurityGroup') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /SecurityGroup/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for security group changes" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check311(){ ID311="3.11" TITLE311="Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL) (Scored)" textTitle "$ID311" "$TITLE311" "SCORED" "LEVEL2" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'CreateNetworkAcl.*CreateNetworkAclEntry.*DeleteNetworkAcl.*DeleteNetworkAclEntry.*ReplaceNetworkAclEntry.*ReplaceNetworkAclAssociation') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /NetworkAcl/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for changes to NACLs" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check312(){ ID312="3.12" TITLE312="Ensure a log metric filter and alarm exist for changes to network gateways (Scored)" textTitle "$ID312" "$TITLE312" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'CreateCustomerGateway.*DeleteCustomerGateway.*AttachInternetGateway.*CreateInternetGateway.*DeleteInternetGateway.*DetachInternetGateway') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /InternetGateway/ || /CustomerGateway/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for changes to network gateways" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check313(){ ID313="3.13" TITLE313="Ensure a log metric filter and alarm exist for route table changes (Scored)" textTitle "$ID313" "$TITLE313" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'CreateRoute.*CreateRouteTable.*ReplaceRoute.*ReplaceRouteTableAssociation.*DeleteRouteTable.*DeleteRoute.*DisassociateRouteTable') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /Route/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for route table changes" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check314(){ ID314="3.14" TITLE314="Ensure a log metric filter and alarm exist for VPC changes (Scored)" textTitle "$ID314" "$TITLE314" "SCORED" "LEVEL1" CLOUDWATCH_GROUP=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | tr '\011' '\012' | awk -F: '{ print $7 }') if [[ $CLOUDWATCH_GROUP ]];then for group in $CLOUDWATCH_GROUP; do CLOUDWATCH_LOGGROUP_REGION=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $REGION --query 'trailList[*].CloudWatchLogsLogGroupArn' --output text | awk -F: '{ print $4 }') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'metricFilters' | grep -E 'CreateVpc.*DeleteVpc.*ModifyVpcAttribute.*AcceptVpcPeeringConnection.*CreateVpcPeeringConnection.*DeleteVpcPeeringConnection.*RejectVpcPeeringConnection.*AttachClassicLinkVpc.*DetachClassicLinkVpc.*DisableVpcClassicLink.*EnableVpcClassicLink') if [[ $METRICFILTER_SET ]];then HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[].MetricName' --output text | awk 'BEGIN {IGNORECASE=1}; /VPC/;') if [[ $HAS_ALARM_ASSOCIATED ]];then textOK "CloudWatch group $group found with metric filters and alarms for VPC changes" else textWarn "CloudWatch group $group found with metric filters but no alarms associated" fi else textWarn "CloudWatch group $group found but no metric filters or alarms associated" fi done else textWarn "No CloudWatch group found for CloudTrail events" fi } check315(){ ID315="3.15" TITLE315="Ensure appropriate subscribers to each SNS topic (Not Scored)" textTitle "$ID315" "$TITLE315" "NOT_SCORED" "LEVEL1" CAN_SNS_LIST_SUBS=1 for regx in $REGIONS; do TOPICS_LIST=$($AWSCLI sns list-topics $PROFILE_OPT --region $regx --output text --query 'Topics[*].TopicArn') ntopics=$(echo $TOPICS_LIST | wc -w ) if [[ $TOPICS_LIST && $CAN_SNS_LIST_SUBS -eq 1 ]];then textNotice "Region $regx has $ntopics topics" "$regx" for topic in $TOPICS_LIST; do TOPIC_SHORT=$(echo $topic | awk -F: '{ print $6 }') CHECK_TOPIC_LIST=$($AWSCLI sns list-subscriptions-by-topic --topic-arn $topic $PROFILE_OPT --region $regx --query 'Subscriptions[*].{Endpoint:Endpoint,Protocol:Protocol}' --output text --max-items $MAXITEMS 2> /dev/null) if [[ $? -eq 255 ]]; then # Permission error export CAN_SNS_LIST_SUBS=0 ntopics=$(echo $TOPICS_LIST | wc -w ) textNotice "Region $regx / $ntopics Topics / Subscriptions NO_PERMISSION" "$regx" break; fi if [[ "Z" != "Z${CHECK_TOPIC_LIST}" ]]; then printf '%s\n' "$CHECK_TOPIC_LIST" | while IFS= read -r dest ; do textNotice "Region $regx / Topic $TOPIC_SHORT / Subscription $dest" "$regx" done else textWarn "Region $regx / Topic $TOPIC_SHORT / Subscription NONE" "$regx" fi done elif [[ $CAN_SNS_LIST_SUBS -eq 0 ]]; then textNotice "Region $regx has $ntopics topics - unable to list subscribers" "$regx" # break else textOK "Region $regx has 0 topics" "$regx" fi done } check41(){ ID41="4.1,4.01" TITLE41="Ensure no security groups allow ingress from 0.0.0.0/0 to port 22 (Scored)" textTitle "$ID41" "$TITLE41" "SCORED" "LEVEL1" for regx in $REGIONS; do SG_LIST=$($AWSCLI ec2 describe-security-groups --filters "Name=ip-permission.to-port,Values=22" --query 'SecurityGroups[?length(IpPermissions[?ToPort==`22` && contains(IpRanges[].CidrIp, `0.0.0.0/0`)]) > `0`].{GroupId:GroupId}' $PROFILE_OPT --region $regx --output text) if [[ $SG_LIST ]];then for SG in $SG_LIST;do textWarn "Found Security Group: $SG open to 0.0.0.0/0 in Region $regx" "$regx" done else textOK "No Security Groups found in $regx with port 22 TCP open to 0.0.0.0/0" "$regx" fi done } check42(){ ID42="4.2,4.02" TITLE42="Ensure no security groups allow ingress from 0.0.0.0/0 to port 3389 (Scored)" textTitle "$ID42" "$TITLE42" "SCORED" "LEVEL1" for regx in $REGIONS; do SG_LIST=$($AWSCLI ec2 describe-security-groups --filters "Name=ip-permission.to-port,Values=3389" --query 'SecurityGroups[?length(IpPermissions[?ToPort==`3389` && contains(IpRanges[].CidrIp, `0.0.0.0/0`)]) > `0`].{GroupName: GroupName}' $PROFILE_OPT --region $regx --output text) if [[ $SG_LIST ]];then for SG in $SG_LIST;do textWarn "Found Security Group: $SG open to 0.0.0.0/0 in Region $regx" "$regx" done else textOK "No Security Groups found in $regx with port 3389 TCP open to 0.0.0.0/0" "$regx" fi done } check43(){ ID43="4.3,4.03" TITLE43="Ensure VPC Flow Logging is Enabled in all VPCs (Scored)" textTitle "$ID43" "$TITLE43" "SCORED" "LEVEL2" for regx in $REGIONS; do CHECK_FL=$($AWSCLI ec2 describe-flow-logs $PROFILE_OPT --region $regx --query 'FlowLogs[?FlowLogStatus==`ACTIVE`].LogGroupName' --output text) if [[ $CHECK_FL ]];then for FL in $CHECK_FL;do textOK "VPCFlowLog is enabled for LogGroupName: $FL in Region $regx" "$regx" done else textWarn "No VPCFlowLog has been found in Region $regx" "$regx" fi done } check44(){ ID44="4.4,4.04" TITLE44="Ensure the default security group of every VPC restricts all traffic (Scored)" textTitle "$ID44" "$TITLE44" "SCORED" "LEVEL2" for regx in $REGIONS; do CHECK_SGDEFAULT=$($AWSCLI ec2 describe-security-groups $PROFILE_OPT --region $regx --filters Name=group-name,Values='default' --query 'SecurityGroups[*].{IpPermissions:IpPermissions,IpPermissionsEgress:IpPermissionsEgress,GroupId:GroupId}' --output text |grep 0.0.0.0) if [[ $CHECK_SGDEFAULT ]];then textWarn "Default Security Groups found that allow 0.0.0.0 IN or OUT traffic in Region $regx" "$regx" else textOK "No Default Security Groups open to 0.0.0.0 found in Region $regx" "$regx" fi done } check45(){ #set -xe ID45="4.5,4.05" TITLE45="Ensure routing tables for VPC peering are \"least access\" (Not Scored)" textTitle "$ID45" "$TITLE45" "NOT_SCORED" "LEVEL2" textNotice "Looking for VPC peering in all regions... " for regx in $REGIONS; do LIST_OF_VPCS_PEERING_CONNECTIONS=$($AWSCLI ec2 describe-vpc-peering-connections --output text $PROFILE_OPT --region $regx --query 'VpcPeeringConnections[*].VpcPeeringConnectionId') if [[ $LIST_OF_VPCS_PEERING_CONNECTIONS ]];then textNotice "$regx: $LIST_OF_VPCS_PEERING_CONNECTIONS - review routing tables" "$regx" #LIST_OF_VPCS=$($AWSCLI ec2 describe-vpcs $PROFILE_OPT --region $regx --query 'Vpcs[*].VpcId' --output text) #aws ec2 describe-route-tables --filter "Name=vpc-id,Values=vpc-0213e864" --query "RouteTables[*].{RouteTableId:RouteTableId, VpcId:VpcId, Routes:Routes, AssociatedSubnets:Associations[*].SubnetId}" $PROFILE_OPT --region $regx # for vpc in $LIST_OF_VPCS; do # VPCS_WITH_PEERING=$($AWSCLI ec2 describe-route-tables --filter "Name=vpc-id,Values=$vpc" $PROFILE_OPT --region $regx --query "RouteTables[*].{RouteTableId:RouteTableId, VpcId:VpcId, Routes:Routes, AssociatedSubnets:Associations[*].SubnetId}" |grep GatewayId|grep pcx-) # done #echo $VPCS_WITH_PEERING else textOK "$regx: No VPC peering found" "$regx" fi done } extra71(){ # set -x ID71="7.1,7.01" TITLE71="Ensure users with AdministratorAccess policy have MFA tokens enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID71" "$TITLE71" "NOT_SCORED" "EXTRA" ADMIN_GROUPS='' AWS_GROUPS=$($AWSCLI $PROFILE_OPT iam list-groups --output text --query 'Groups[].GroupName') for grp in $AWS_GROUPS; do # aws --profile onlinetraining iam list-attached-group-policies --group-name Administrators --query 'AttachedPolicies[].PolicyArn' | grep 'arn:aws:iam::aws:policy/AdministratorAccess' # list-attached-group-policies CHECK_ADMIN_GROUP=$($AWSCLI $PROFILE_OPT iam list-attached-group-policies --group-name $grp --output json --query 'AttachedPolicies[].PolicyArn' | grep 'arn:aws:iam::aws:policy/AdministratorAccess') if [[ $CHECK_ADMIN_GROUP ]]; then ADMIN_GROUPS="$ADMIN_GROUPS $grp" textNotice "$grp group provides administrative access" ADMIN_USERS=$($AWSCLI $PROFILE_OPT iam get-group --group-name $grp --output json --query 'Users[].UserName' | grep '"' | cut -d'"' -f2 ) for auser in $ADMIN_USERS; do # users in group are Administrators # users # check for user MFA device in credential report USER_MFA_ENABLED=$( cat $TEMP_REPORT_FILE | grep "^$auser," | cut -d',' -f8) if [[ "true" == $USER_MFA_ENABLED ]]; then textOK "$auser / MFA Enabled / admin via group $grp" else textWarn "$auser / MFA DISABLED / admin via group $grp" fi done else textNotice "$grp group provides non-administrative access" fi done # set +x } extra72(){ #set -x ID72="7.2,7.02" TITLE72="Ensure there are no EBS Snapshots set as Public (Not Scored) (Not part of CIS benchmark)" textTitle "$ID72" "$TITLE72" "NOT_SCORED" "EXTRA" textNotice "Looking for EBS Snapshots in all regions... " for regx in $REGIONS; do LIST_OF_EBS_SNAPSHOTS=$($AWSCLI ec2 describe-snapshots $PROFILE_OPT --region $regx --owner-ids $ACCOUNT_NUM --output text --query 'Snapshots[*].{ID:SnapshotId}' --max-items $MAXITEMS | grep -v None 2> /dev/null) for snapshot in $LIST_OF_EBS_SNAPSHOTS; do SNAPSHOT_IS_PUBLIC=$($AWSCLI ec2 describe-snapshot-attribute $PROFILE_OPT --region $regx --output text --snapshot-id $snapshot --attribute createVolumePermission --query "CreateVolumePermissions[?Group=='all']") if [[ $SNAPSHOT_IS_PUBLIC ]];then textWarn "$regx: $snapshot is currently Public!" "$regx" else textOK "$regx: $snapshot is not Public" "$regx" fi done done } extra73(){ #set -x ID73="7.3,7.03" TITLE73="Ensure there are no S3 buckets open to the Everyone or Any AWS user (Not Scored) (Not part of CIS benchmark)" textTitle "$ID73" "$TITLE73" "NOT_SCORED" "EXTRA" textNotice "Looking for open S3 Buckets (ACLs and Policies) in all regions... " ALL_BUCKETS_LIST=$($AWSCLI s3api list-buckets --query 'Buckets[*].{Name:Name}' $PROFILE_OPT --region $REGION --output text) for bucket in $ALL_BUCKETS_LIST; do BUCKET_LOCATION=$($AWSCLI s3api get-bucket-location --bucket $bucket $PROFILE_OPT --region $REGION --output text) if [[ "None" == $BUCKET_LOCATION ]]; then BUCKET_LOCATION="us-east-1" fi if [[ "EU" == $BUCKET_LOCATION ]]; then BUCKET_LOCATION="eu-west-1" fi # check if AllUsers is in the ACL as Grantee CHECK_BUCKET_ALLUSERS_ACL=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query "Grants[?Grantee.URI == 'http://acs.amazonaws.com/groups/global/AllUsers']" --output text |grep -v GRANTEE) CHECK_BUCKET_ALLUSERS_ACL_SINGLE_LINE=$(echo -ne $CHECK_BUCKET_ALLUSERS_ACL) # check if AuthenticatedUsers is in the ACL as Grantee, they will have access with sigened URL only CHECK_BUCKET_AUTHUSERS_ACL=$($AWSCLI s3api get-bucket-acl $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --query "Grants[?Grantee.URI == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers']" --output text |grep -v GRANTEE) CHECK_BUCKET_AUTHUSERS_ACL_SINGLE_LINE=$(echo -ne $CHECK_BUCKET_AUTHUSERS_ACL) # to prevent error NoSuchBucketPolicy first clean the output controlling stderr TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-${bucket}.policy.XXXXXXXXXX) $AWSCLI s3api get-bucket-policy $PROFILE_OPT --region $BUCKET_LOCATION --bucket $bucket --output text --query Policy > $TEMP_POLICY_FILE 2> /dev/null # check if the S3 policy has Principal as * CHECK_BUCKET_ALLUSERS_POLICY=$(cat $TEMP_POLICY_FILE | sed -e 's/[{}]/''/g' | awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}'|awk '/Principal/ && !skip { print } { skip = /Deny/} '|grep ^\"Principal|grep \*) if [[ $CHECK_BUCKET_ALLUSERS_ACL || $CHECK_BUCKET_AUTHUSERS_ACL || $CHECK_BUCKET_ALLUSERS_POLICY ]];then if [[ $CHECK_BUCKET_ALLUSERS_ACL ]];then textWarn "$BUCKET_LOCATION: $bucket bucket is open to the Internet (Everyone) with permissions: $CHECK_BUCKET_ALLUSERS_ACL_SINGLE_LINE" "$regx" fi if [[ $CHECK_BUCKET_AUTHUSERS_ACL ]];then textWarn "$BUCKET_LOCATION: $bucket bucket is open to Authenticated users (Any AWS user) with permissions: $CHECK_BUCKET_AUTHUSERS_ACL_SINGLE_LINE" "$regx" fi if [[ $CHECK_BUCKET_ALLUSERS_POLICY ]];then textWarn "$BUCKET_LOCATION: $bucket bucket policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$regx" fi else textOK "$BUCKET_LOCATION: $bucket bucket is not open" "$regx" fi rm -fr $TEMP_POLICY_FILE done } extra74(){ #set -x ID74="7.4,7.04" TITLE74="Ensure there are no Security Groups without ingress filtering being used (Not Scored) (Not part of CIS benchmark)" textTitle "$ID74" "$TITLE74" "NOT_SCORED" "EXTRA" textNotice "Looking for Security Groups in all regions... " for regx in $REGIONS; do LIST_OF_SECURITYGROUPS=$($AWSCLI ec2 describe-security-groups $PROFILE_OPT --region $regx --filters "Name=ip-permission.cidr,Values=0.0.0.0/0" --query "SecurityGroups[].[GroupId]" --output text --max-items $MAXITEMS) for SG_ID in $LIST_OF_SECURITYGROUPS; do SG_NO_INGRESS_FILTER=$($AWSCLI ec2 describe-network-interfaces $PROFILE_OPT --region $regx --filters "Name=group-id,Values=$SG_ID" --query "length(NetworkInterfaces)" --output text) if [[ $SG_NO_INGRESS_FILTER -ne 0 ]];then textWarn "$regx: $SG_ID has not ingress filtering and it is being used!" "$regx" else textNotice "$regx: $SG_ID has not ingress filtering but it is no being used" "$regx" fi done done } extra75(){ #set -x ID75="7.5,7.05" TITLE75="Ensure there are no Security Groups not being used (Not Scored) (Not part of CIS benchmark)" textTitle "$ID75" "$TITLE75" "NOT_SCORED" "EXTRA" textNotice "Looking for Security Groups in all regions... " for regx in $REGIONS; do LIST_OF_SECURITYGROUPS=$($AWSCLI ec2 describe-security-groups $PROFILE_OPT --region $regx --query "SecurityGroups[].[GroupId]" --output text --max-items $MAXITEMS) for SG_ID in $LIST_OF_SECURITYGROUPS; do SG_NOT_USED=$($AWSCLI ec2 describe-network-interfaces $PROFILE_OPT --region $regx --filters "Name=group-id,Values=$SG_ID" --query "length(NetworkInterfaces)" --output text) if [[ $SG_NOT_USED -eq 0 ]];then textWarn "$regx: $SG_ID is not being used!" "$regx" else textOK "$regx: $SG_ID is being used" "$regx" fi done done } callCheck(){ if [[ $CHECKNUMBER ]];then case "$CHECKNUMBER" in check11|check101 ) check11;; check12|check102 ) check12;; check13|check103 ) check13;; check14|check104 ) check14;; check15|check105 ) check15;; check16|check106 ) check16;; check17|check107 ) check17;; check18|check108 ) check18;; check19|check109 ) check19;; check110 ) check110;; check111 ) check111;; check112 ) check112;; check113 ) check113;; check114 ) check114;; check115 ) check115;; check116 ) check116;; check117 ) check117;; check118 ) check118;; check119 ) check119;; check120 ) check120;; check121 ) check121;; check122 ) check122;; check123 ) check123;; check124 ) check124;; check21|check201 ) check21;; check22|check202 ) check22;; check23|check203 ) check23;; check24|check204 ) check24;; check25|check205 ) check25;; check26|check206 ) check26;; check27|check207 ) check27;; check28|check208 ) check28;; check31|check301 ) check31;; check32|check302 ) check32;; check33|check303 ) check33;; check34|check304 ) check34;; check35|check305 ) check35;; check36|check306 ) check36;; check37|check307 ) check37;; check38|check308 ) check38;; check39|check309 ) check39;; check310 ) check310;; check311 ) check311;; check312 ) check312;; check313 ) check313;; check314 ) check314;; check315 ) check315;; check41|check401 ) check41;; check42|check402 ) check42;; check43|check403 ) check43;; check44|check404 ) check44;; check45|check405 ) check45;; extra71|extra701 ) extra71;; extra72|extra702 ) extra72;; extra73|extra703 ) extra73;; extra74|extra704 ) extra74;; extra75|extra705 ) extra75;; ## Groups of Checks check1 ) check11;check12;check13;check14;check15;check16;check17;check18; check19;check110;check111;check112;check113;check114;check115; check116;check117;check118;check119;check120;check121;check122; check123;check124; ;; check2 ) check21;check22;check23;check24;check25;check26;check27;check28 ;; check3 ) check31;check32;check33;check34;check35;check36;check37;check38; check39;check310;check311;check312;check313;check314;check315 ;; check4 ) check41;check42;check43;check44;check45 ;; level1 ) check11;check12;check13;check14;check15;check16;check17;check18; check19;check110;check111;check112;check113;check115;check116;check117; check118;check119;check120;check122;check123;check124;check21;check23; check24;check25;check26;check31;check32;check33;check34;check35; check38;check312;check313;check314;check315;check41;check42 ;; level2 ) check11;check12;check13;check14;check15;check16;check17;check18; check19;check110;check111;check112;check113;check114;check115;check116; check117;check118;check119;check120;check121;check122;check123;check124; check21;check22;check23;check24;check25;check26;check27;check28;check31; check32;check33;check34;check35;check36;check37;check38;check39; check310;check311;check312;check313;check314;check315;check41;check42; check43;check44;check45 ;; extras ) extra71;extra72;extra73;extra74;extra75 ;; * ) textWarn "ERROR! Use a valid check name (i.e. check41 or extra71)\n"; esac cleanTemp exit $EXITCODE fi } ### All functions defined above ... run the workflow if [[ $MODE != "csv" ]]; then prowlerBanner printColorsCode fi getWhoami genCredReport saveReport callCheck TITLE1="Identity and Access Management ****************************************" textTitle "1" "$TITLE1" "NOT_SCORED" "SUPPORT" check11 check12 check13 check14 check15 check16 check17 check18 check19 check110 check111 check112 check113 check114 check115 check116 check117 check118 check119 check120 check121 check122 check123 check124 TITLE2="Logging ***************************************************************" textTitle "2" "$TITLE2" "NOT_SCORED" "SUPPORT" check21 check22 check23 check24 check25 check26 check27 check28 TITLE3="Monitoring ************************************************************" textTitle "3" "$TITLE3" "NOT_SCORED" "SUPPORT" # 3 Monitoring check commands / Mostly covered by SecurityMonkey check31 check32 check33 check34 check35 check36 check37 check38 check39 check310 check311 check312 check313 check314 check315 TITLE4="Networking ************************************************************" textTitle "4" "$TITLE4" "NOT_SCORED" "SUPPORT" check41 check42 check43 check44 check45 TITLE7="Extras ************************************************************" textTitle "7" "$TITLE7" "NOT_SCORED" "SUPPORT" extra71 extra72 extra73 extra74 extra75 cleanTemp exit $EXITCODE