#!/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 / 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), 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 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" ] || [ "$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 } # List of checks IDs and Titles TITLE1="Identity and Access Management ****************************************" ID11="1.1,1.01" TITLE11="Avoid the use of the root account (Scored)." ID12="1.2,1.02" TITLE12="Ensure multi-factor authentication (MFA) is enabled for all IAM users that have a console password (Scored)" ID13="1.3,1.03" TITLE13="Ensure credentials unused for 90 days or greater are disabled (Scored)" ID14="1.4,1.04" TITLE14="Ensure access keys are rotated every 90 days or less (Scored)" # also checked by Security Monkey ID15="1.5,1.05" TITLE15="Ensure IAM password policy requires at least one uppercase letter (Scored)" ID16="1.6,1.06" TITLE16="Ensure IAM password policy require at least one lowercase letter (Scored)" ID17="1.7,1.07" TITLE17="Ensure IAM password policy require at least one symbol (Scored)" ID18="1.8,1.08" TITLE18="Ensure IAM password policy require at least one number (Scored)" ID19="1.9,1.09" TITLE19="Ensure IAM password policy requires minimum length of 14 or greater (Scored)" ID110="1.10" TITLE110="Ensure IAM password policy prevents password reuse: 24 or greater (Scored)" ID111="1.11" TITLE111="Ensure IAM password policy expires passwords within 90 days or less (Scored)" ID112="1.12" TITLE112="Ensure no root account access key exists (Scored)" ID113="1.13" TITLE113="Ensure MFA is enabled for the root account (Scored)" ID114="1.14" TITLE114="Ensure hardware MFA is enabled for the root account (Scored)" ID115="1.15" TITLE115="Ensure security questions are registered in the AWS account (Not Scored)" ID116="1.16" TITLE116="Ensure IAM policies are attached only to groups or roles (Scored)" ID117="1.17" TITLE117="Enable detailed billing (Scored)" ID118="1.18" TITLE118="Ensure IAM Master and IAM Manager roles are active (Scored)" ID119="1.19" TITLE119="Maintain current contact details (Scored)" ID120="1.20" TITLE120="Ensure security contact information is registered (Scored)" ID121="1.21" TITLE121="Ensure IAM instance roles are used for AWS resource access from instances (Not Scored)" ID122="1.22" TITLE122="Ensure a support role has been created to manage incidents with AWS Support (Scored)" ID123="1.23" TITLE123="Do not setup access keys during initial user setup for all IAM users that have a console password (Not Scored)" ID124="1.24" TITLE124="Ensure IAM policies that allow full \"*:*\" administrative privileges are not created (Scored)" TITLE2="Logging ***************************************************************" ID21="2.1,2.01" TITLE21="Ensure CloudTrail is enabled in all regions (Scored)" ID22="2.2,2.02" TITLE22="Ensure CloudTrail log file validation is enabled (Scored)" ID23="2.3,2.03" TITLE23="Ensure the S3 bucket CloudTrail logs to is not publicly accessible (Scored)" ID24="2.4,2.04" TITLE24="Ensure CloudTrail trails are integrated with CloudWatch Logs (Scored)" ID25="2.5,2.05" TITLE25="Ensure AWS Config is enabled in all regions (Scored)" ID26="2.6,2.06" TITLE26="Ensure S3 bucket access logging is enabled on the CloudTrail S3 bucket (Scored)" ID27="2.7,2.07" TITLE27="Ensure CloudTrail logs are encrypted at rest using KMS CMKs (Scored)" ID28="2.8,2.08" TITLE28="Ensure rotation for customer created CMKs is enabled (Scored)" TITLE3="Monitoring ************************************************************" ID31="3.1,3.01" TITLE31="Ensure a log metric filter and alarm exist for unauthorized API calls (Scored)" ID32="3.2,3.02" TITLE32="Ensure a log metric filter and alarm exist for Management Console sign-in without MFA (Scored)" ID33="3.3,3.03" TITLE33="Ensure a log metric filter and alarm exist for usage of root account (Scored)" ID34="3.4,3.04" TITLE34="Ensure a log metric filter and alarm exist for IAM policy changes (Scored)" ID35="3.5,3.05" TITLE35="Ensure a log metric filter and alarm exist for CloudTrail configuration changes (Scored)" ID36="3.6,3.06" TITLE36="Ensure a log metric filter and alarm exist for AWS Management Console authentication failures (Scored)" ID37="3.7,3.07" TITLE37="Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created CMKs (Scored)" ID38="3.8,3.08" TITLE38="Ensure a log metric filter and alarm exist for S3 bucket policy changes (Scored)" ID39="3.9,3.09" TITLE39="Ensure a log metric filter and alarm exist for AWS Config configuration changes (Scored)" ID310="3.10" TITLE310="Ensure a log metric filter and alarm exist for security group changes (Scored)" ID311="3.11" TITLE311="Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL) (Scored)" ID312="3.12" TITLE312="Ensure a log metric filter and alarm exist for changes to network gateways (Scored)" ID313="3.13" TITLE313="Ensure a log metric filter and alarm exist for route table changes (Scored)" ID314="3.14" TITLE314="Ensure a log metric filter and alarm exist for VPC changes (Scored)" ID315="3.15" TITLE315="Ensure appropriate subscribers to each SNS topic (Not Scored)" TITLE4="Networking ************************************************************" ID41="4.1,4.01" TITLE41="Ensure no security groups allow ingress from 0.0.0.0/0 to port 22 (Scored)" ID42="4.2,4.02" TITLE42="Ensure no security groups allow ingress from 0.0.0.0/0 to port 3389 (Scored)" ID43="4.3,4.03" TITLE43="Ensure VPC Flow Logging is Enabled in all VPCs (Scored)" ID44="4.4,4.04" TITLE44="Ensure the default security group of every VPC restricts all traffic (Scored)" ID45="4.5,4.05" TITLE45="Ensure routing tables for VPC peering are \"least access\" (Not Scored)" TITLE7="Extras ****************************************************************" ID71="7.1,7.01" TITLE71="Ensure users with AdministratorAccess policy have MFA tokens enabled (Not Scored) (Not part of CIS benchmark)" ID72="7.2,7.02" TITLE72="Ensure there are no EBS Snapshots set as Public (Not Scored) (Not part of CIS benchmark)" 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)" ID74="7.4,7.04" TITLE74="Ensure there are no Security Groups without ingress filtering being used (Not Scored) (Not part of CIS benchmark)" ID75="7.5,7.05" TITLE75="Ensure there are no Security Groups not being used (Not Scored) (Not part of CIS benchmark)" ID76="7.6,7.06" TITLE76="Ensure there are no EC2 AMIs set as Public (Not Scored) (Not part of CIS benchmark)" ID77="7.7,7.07" TITLE77="Ensure there are no ECR repositories set as Public (Not Scored) (Not part of CIS benchmark)" ID78="7.8,7.08" TITLE78="Ensure there are no Public Accessible RDS instances (Not Scored) (Not part of CIS benchmark)" ID79="7.9,7.09" TITLE79="Check for internet facing Elastic Load Balancers (Not Scored) (Not part of CIS benchmark)" ID710="7.10,7.10" TITLE710="Check for internet facing EC2 Instances (Not Scored) (Not part of CIS benchmark)" ID711="7.11,7.11" TITLE711="Check for Publicly Accessible Redshift Clusters (Not Scored) (Not part of CIS benchmark)" ID712="7.12,7.12" TITLE712="Check if Amazon Macie is enabled (Not Scored) (Not part of CIS benchmark)" ID713="7.13,7.13" TITLE713="Check if GuardDuty is enabled (Not Scored) (Not part of CIS benchmark)" ID714="7.14,7.14" TITLE714="Check if CloudFront distributions have logging enabled (Not Scored) (Not part of CIS benchmark)" ID715="7.15,7.15" TITLE715="Check if Elasticsearch Service domains have logging enabled (Not Scored) (Not part of CIS benchmark)" ID716="7.16,7.16" TITLE716="Check if Elasticsearch Service domains allow open access (Not Scored) (Not part of CIS benchmark)" ID717="7.17,7.17" TITLE717="Check if Elastic Load Balancers have logging enabled (Not Scored) (Not part of CIS benchmark)" ID718="7.18,7.18" TITLE718="Check if S3 buckets have server access logging enabled (Not Scored) (Not part of CIS benchmark)" ID719="7.19,7.19" TITLE719="Check if Route53 hosted zones are logging queries to CloudWatch Logs (Not Scored) (Not part of CIS benchmark)" ID720="7.20,7.20" TITLE720="Check if Lambda functions invoke API operations are being recorded by CloudTrail (Not Scored) (Not part of CIS benchmark)" ID721="7.21,7.21" TITLE721="Check if Redshift cluster has audit logging enabled (Not Scored) (Not part of CIS benchmark)" ID722="7.22,7.22" TITLE722="Check if API Gateway has logging enabled (Not Scored) (Not part of CIS benchmark)" 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" } check11(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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 --output text --assignment-status Assigned --query 'VirtualMFADevices[*].[SerialNumber]' | grep '^arn:aws:iam::[0-9]\{12\}:mfa/root-account-mfa-device$') if [[ "$COMMAND114" ]]; then textWarn "Only 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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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 --query 'KeyMetadata.[KeyId, KeyManager]'|grep -v 'AWS'|awk '{ print $1 }'; 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) if [[ "$CHECK_KMS_KEY_ROTATION" == "True" ]];then textOK "Key $key in Region $regx is set correctly" 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(){ # "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 | tr '\011' '\012' | grep $group | 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 $3}') METRICFILTER_SET=$($AWSCLI logs describe-metric-filters --log-group-name $group $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --output text | grep METRICFILTERS | awk 'BEGIN {IGNORECASE=1}; /UnauthorizedOperation/ || /AccessDenied/ {print $3};') if [[ $METRICFILTER_SET ]];then for metric in $METRICFILTER_SET; do metric_name=$($AWSCLI logs describe-metric-filters $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --log-group-name $group --filter-name-prefix $metric --output text --query 'metricFilters[0].metricTransformations[0].metricName') HAS_ALARM_ASSOCIATED=$($AWSCLI cloudwatch describe-alarms $PROFILE_OPT --region $CLOUDWATCH_LOGGROUP_REGION --query 'MetricAlarms[?MetricName==`'$metric_name'`]' --output text) if [[ $HAS_ALARM_ASSOCIATED ]];then CHECK31OK="$CHECK31OK $group:$metric" else CHECK31WARN="$CHECK31WARN $group:$metric" fi done else CHECK31WARN="$CHECK31WARN $group" fi done if [[ $CHECK31OK ]]; then for group in $CHECK31OK; do metric=${group#*:} group=${group%:*} textOK "CloudWatch group $group found with metric filter $metric and alarms set for Unauthorized Operation and Access Denied" done fi if [[ $CHECK31WARN ]]; then for group in $CHECK31WARN; do case $group in *:*) metric=${group#*:} group=${group%:*} textWarn "CloudWatch group $group found with metric filter $metric but no alarms associated" ;; *) textWarn "CloudWatch group $group found but no metric filters or alarms associated" esac done fi else textWarn "No CloudWatch group found for CloudTrail events" fi } check32(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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 AWS Management Console authentication failures" 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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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 --query 'SecurityGroups[?length(IpPermissions[?((FromPort==null && ToPort==null) || (FromPort<=`22` && 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(){ # "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 --query 'SecurityGroups[?length(IpPermissions[?((FromPort==null && ToPort==null) || (FromPort<=`3389` && ToPort>=`3389`)) && 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 3389 TCP open to 0.0.0.0/0" "$regx" fi done } check43(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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(){ # "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 } extra76(){ # "Ensure there are no EC2 AMIs set as Public (Not Scored) (Not part of CIS benchmark)" textTitle "$ID76" "$TITLE76" "NOT_SCORED" "EXTRA" textNotice "Looking for AMIs in all regions... " for regx in $REGIONS; do LIST_OF_PUBLIC_AMIS=$($AWSCLI ec2 describe-images --owners self $PROFILE_OPT --region $regx --filters "Name=is-public,Values=true" --query 'Images[*].{ID:ImageId}' --output text) if [[ $LIST_OF_PUBLIC_AMIS ]];then for ami in $LIST_OF_PUBLIC_AMIS; do textWarn "$regx: $ami is currently Public!" "$regx" done else textOK "$regx: No Public AMIs found" "$regx" fi done } extra77(){ # "Ensure there are no ECR repositories set as Public (Not Scored) (Not part of CIS benchmark)" textTitle "$ID77" "$TITLE77" "NOT_SCORED" "EXTRA" textNotice "Looking for ECR repos in all regions... " for regx in $REGIONS; do LIST_OF_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $regx --query 'repositories[*].{Name:repositoryName}' --output text) for ecr_repo in $LIST_OF_ECR_REPOS; do TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-ecr-repo.policy.XXXXXXXXXX) $AWSCLI ecr get-repository-policy --repository-name $ecr_repo $PROFILE_OPT --region $regx --output text > $TEMP_POLICY_FILE 2> /dev/null # check if the policy has Principal as * CHECK_ECR_REPO_ALLUSERS_POLICY=$(cat $TEMP_POLICY_FILE | 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_ECR_REPO_ALLUSERS_POLICY ]];then textWarn "$regx: $ecr_repo policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$regx" else textOK "$regx: $ecr_repo is not open" "$regx" fi done rm -fr $TEMP_POLICY_FILE done } extra78(){ # "Ensure there are no Public Accessible RDS instances (Not Scored) (Not part of CIS benchmark)" textTitle "$ID78" "$TITLE78" "NOT_SCORED" "EXTRA" textNotice "Looking for RDS instances in all regions... " for regx in $REGIONS; do LIST_OF_RDS_PUBLIC_INSTANCES=$($AWSCLI rds describe-db-instances $PROFILE_OPT --region $regx --query 'DBInstances[?PubliclyAccessible==`true`].[DBInstanceIdentifier,Endpoint.Address]' --output text) if [[ $LIST_OF_RDS_PUBLIC_INSTANCES ]];then while read -r rds_instance;do RDS_NAME=$(echo $rds_instance | awk '{ print $1; }') RDS_DNSNAME=$(echo $rds_instance | awk '{ print $2; }') textWarn "$regx: RDS instance: $RDS_NAME at $RDS_DNSNAME is set as Publicly Accessible!" "$regx" done <<< "$LIST_OF_RDS_PUBLIC_INSTANCES" else textOK "$regx: no Publicly Accessible RDS instances found" "$regx" fi done } extra79(){ # "Check for internet facing Elastic Load Balancers (Not Scored) (Not part of CIS benchmark)" textTitle "$ID79" "$TITLE79" "NOT_SCORED" "EXTRA" textNotice "Looking for Elastic Load Balancers in all regions... " for regx in $REGIONS; do LIST_OF_PUBLIC_ELBS=$($AWSCLI elb describe-load-balancers $PROFILE_OPT --region $regx --query 'LoadBalancerDescriptions[?Scheme == `internet-facing`].[LoadBalancerName,DNSName]' --output text) LIST_OF_PUBLIC_ELBSV2=$($AWSCLI elbv2 describe-load-balancers $PROFILE_OPT --region $regx --query 'LoadBalancers[?Scheme == `internet-facing`].[LoadBalancerName,DNSName]' --output text) LIST_OF_ALL_ELBS=$( echo $LIST_OF_PUBLIC_ELBS; echo $LIST_OF_PUBLIC_ELBSV2) LIST_OF_ALL_ELBS_PER_LINE=$( echo $LIST_OF_ALL_ELBS| xargs -n2 ) if [[ $LIST_OF_ALL_ELBS ]];then while read -r elb;do ELB_NAME=$(echo $elb | awk '{ print $1; }') ELB_DNSNAME=$(echo $elb | awk '{ print $2; }') textWarn "$regx: ELB: $ELB_NAME at DNS: $ELB_DNSNAME is internet-facing!" "$regx" done <<< "$LIST_OF_ALL_ELBS_PER_LINE" else textOK "$regx: no Internet Facing ELBs found" "$regx" fi done } extra710(){ # "Check for internet facing EC2 Instances (Not Scored) (Not part of CIS benchmark)" textTitle "$ID710" "$TITLE710" "NOT_SCORED" "EXTRA" textNotice "Looking for instances in all regions... " for regx in $REGIONS; do LIST_OF_PUBLIC_INSTANCES=$($AWSCLI ec2 describe-instances $PROFILE_OPT --region $regx --query 'Reservations[*].Instances[?PublicIpAddress].[InstanceId,PublicIpAddress]' --output text) if [[ $LIST_OF_PUBLIC_INSTANCES ]];then while read -r instance;do INSTANCE_ID=$(echo $instance | awk '{ print $1; }') PUBLIC_IP=$(echo $instance | awk '{ print $2; }') textWarn "$regx: Instance: $INSTANCE_ID at IP: $PUBLIC_IP is internet-facing!" "$regx" done <<< "$LIST_OF_PUBLIC_INSTANCES" else textOK "$regx: no Internet Facing EC2 Instances found" "$regx" fi done } extra711(){ # "Check for Publicly Accessible Redshift Clusters (Not Scored) (Not part of CIS benchmark)" textTitle "$ID711" "$TITLE711" "NOT_SCORED" "EXTRA" textNotice "Looking for Reshift clusters in all regions... " for regx in $REGIONS; do LIST_OF_PUBLIC_REDSHIFT_CLUSTERS=$($AWSCLI redshift describe-clusters $PROFILE_OPT --region $regx --query 'Clusters[?PubliclyAccessible == `true`].[ClusterIdentifier,Endpoint.Address]' --output text) if [[ $LIST_OF_PUBLIC_REDSHIFT_CLUSTERS ]];then while read -r cluster;do CLUSTER_ID=$(echo $cluster | awk '{ print $1; }') CLUSTER_ENDPOINT=$(echo $cluster | awk '{ print $2; }') textWarn "$regx: Cluster: $CLUSTER_ID at Endpoint: $CLUSTER_ENDPOINT is publicly accessible!" "$regx" done <<< "$LIST_OF_PUBLIC_REDSHIFT_CLUSTERS" else textOK "$regx: no Publicly Accessible Redshift Clusters found" "$regx" fi done } extra712(){ # "Check if Amazon Macie is enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID712" "$TITLE712" "NOT_SCORED" "EXTRA" textNotice "No API commands available to check if Macie is enabled," textNotice "just looking if IAM Macie related permissions exist. " MACIE_IAM_ROLES_CREATED=$($AWSCLI iam list-roles $PROFILE_OPT --query 'Roles[*].Arn'|grep AWSMacieServiceCustomer|wc -l) if [[ $MACIE_IAM_ROLES_CREATED -eq 2 ]];then textOK "Macie related IAM roles exist, so it might be enabled. Check it out manually." else textWarn "No Macie related IAM roles found. It is most likely not to be enabled" fi } extra713(){ # "Check if GuardDuty is enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID713" "$TITLE713" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_GUARDDUTY_DETECTORS=$($AWSCLI guardduty list-detectors $PROFILE_OPT --region $regx --output text |cut -f2) if [[ $LIST_OF_GUARDDUTY_DETECTORS ]];then while read -r detector;do DETECTOR_ENABLED=$($AWSCLI guardduty get-detector --detector-id $detector $PROFILE_OPT --region $regx --output text| cut -f3|grep ENABLED) if [[ $DETECTOR_ENABLED ]]; then textOK "$regx: GuardDuty detector $detector enabled" "$regx" else textWarn "$regx: GuardDuty detector $detector configured but suspended" "$regx" fi done <<< "$LIST_OF_GUARDDUTY_DETECTORS" else textWarn "$regx: GuardDuty detector not configured!" "$regx" fi done } extra714(){ # "Check if CloudFront distributions have logging enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID714" "$TITLE714" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_DISTRIBUTIONS=$($AWSCLI cloudfront list-distributions $PROFILE_OPT --region $regx --query 'DistributionList.Items[].Id' --output text |grep -v "^None") if [[ $LIST_OF_DISTRIBUTIONS ]]; then for cdn in $LIST_OF_DISTRIBUTIONS;do CDN_LOG_ENABLED=$($AWSCLI cloudfront get-distribution $PROFILE_OPT --region $regx --id "$cdn" --query 'Distribution.DistributionConfig.Logging.Enabled' | grep true) if [[ $CDN_LOG_ENABLED ]];then textOK "$regx: CDN $cdn logging enabled" "$regx" else textWarn "$regx: CDN $cdn logging disabled!" "$regx" fi done else textNotice "$regx: No CDN configured" "$regx" fi done } extra715(){ # "Check if Elasticsearch Service domains have logging enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID715" "$TITLE715" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_DOMAINS=$($AWSCLI es list-domain-names $PROFILE_OPT --region $regx --query DomainNames --output text) if [[ $LIST_OF_DOMAINS ]]; then for domain in $LIST_OF_DOMAINS;do SEARCH_SLOWLOG_ENABLED=$($AWSCLI es describe-elasticsearch-domain-config --domain-name $domain $PROFILE_OPT --region $regx --query DomainConfig.LogPublishingOptions.Options.SEARCH_SLOW_LOGS.Enabled --output text |grep -v ^None|grep -v ^False) if [[ $SEARCH_SLOWLOG_ENABLED ]];then textOK "$regx: ElasticSearch Service domain $domain SEARCH_SLOW_LOGS enabled" "$regx" else textWarn "$regx: ElasticSearch Service domain $domain SEARCH_SLOW_LOGS disabled!" "$regx" fi INDEX_SLOWLOG_ENABLED=$($AWSCLI es describe-elasticsearch-domain-config --domain-name $domain $PROFILE_OPT --region $regx --query DomainConfig.LogPublishingOptions.Options.INDEX_SLOW_LOGS.Enabled --output text |grep -v ^None|grep -v ^False) if [[ $INDEX_SLOWLOG_ENABLED ]];then textOK "$regx: ElasticSearch Service domain $domain INDEX_SLOW_LOGS enabled" "$regx" else textWarn "$regx: ElasticSearch Service domain $domain INDEX_SLOW_LOGS disabled!" "$regx" fi done else textNotice "$regx: No Elasticsearch Service domain found" "$regx" fi done } extra716(){ # "Check if Elasticsearch Service domains allow open access (Not Scored) (Not part of CIS benchmark)" textTitle "$ID716" "$TITLE716" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_DOMAINS=$($AWSCLI es list-domain-names $PROFILE_OPT --region $regx --query DomainNames --output text) if [[ $LIST_OF_DOMAINS ]]; then for domain in $LIST_OF_DOMAINS;do CHECK_IF_MEMBER_OF_VPC=$($AWSCLI es describe-elasticsearch-domain-config --domain-name $domain $PROFILE_OPT --region $regx --query DomainConfig.VPCOptions.Options.VPCId --output text|grep -v ^None) if [[ ! $CHECK_IF_MEMBER_OF_VPC ]];then TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-es-domain.policy.XXXXXXXXXX) $AWSCLI es describe-elasticsearch-domain-config --domain-name $domain $PROFILE_OPT --region $regx --query DomainConfig.AccessPolicies.Options --output text > $TEMP_POLICY_FILE 2> /dev/null # check if the policy has Principal as * CHECK_ES_DOMAIN_ALLUSERS_POLICY=$(cat $TEMP_POLICY_FILE | 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_ES_DOMAIN_ALLUSERS_POLICY ]];then textWarn "$regx: $domain policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$regx" else textOK "$regx: $domain is not open" "$regx" fi else textOK "$regx: $domain is in a VPC" "$regx" fi done fi textNotice "$regx: No Elasticsearch Service domain found" "$regx" rm -fr $TEMP_POLICY_FILE done } extra717(){ # "Check if Elastic Load Balancers have logging enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID717" "$TITLE717" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_ELBS=$($AWSCLI elb describe-load-balancers $PROFILE_OPT --region $regx --query 'LoadBalancerDescriptions[*].LoadBalancerName' --output text|xargs -n1) LIST_OF_ELBSV2=$($AWSCLI elbv2 describe-load-balancers $PROFILE_OPT --region $regx --query 'LoadBalancers[*].LoadBalancerArn' --output text|xargs -n1) if [[ $LIST_OF_ELBS || $LIST_OF_ELBSV2 ]]; then if [[ $LIST_OF_ELBS ]]; then for elb in $LIST_OF_ELBS; do CHECK_ELBS_LOG_ENABLED=$($AWSCLI elb describe-load-balancer-attributes $PROFILE_OPT --region $regx --load-balancer-name $elb --query 'LoadBalancerAttributes.AccessLog.Enabled'|grep "^true") if [[ $CHECK_ELBS_LOG_ENABLED ]]; then textOK "$regx: $elb has access logs to S3 configured" "$regx" else textWarn "$regx: $elb has not configured access logs" "$regx" fi done fi if [[ $LIST_OF_ELBSV2 ]]; then for elbarn in $LIST_OF_ELBSV2; do CHECK_ELBSV2_LOG_ENABLED=$($AWSCLI elbv2 describe-load-balancer-attributes $PROFILE_OPT --region $regx --load-balancer-arn $elbarn --query Attributes[*] --output text|grep "^access_logs.s3.enabled"|cut -f2|grep true) ELBV2_NAME=$(echo $elbarn|cut -d\/ -f3) if [[ $CHECK_ELBSV2_LOG_ENABLED ]]; then textOK "$regx: $ELBV2_NAME has access logs to S3 configured" "$regx" else textWarn "$regx: $ELBV2_NAME has not configured access logs" "$regx" fi done fi else textNotice "$regx: No ELBs found" "$regx" fi done } extra718(){ # "Check if S3 buckets have server access logging enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID718" "$TITLE718" "NOT_SCORED" "EXTRA" LIST_OF_BUCKETS=$($AWSCLI s3api list-buckets $PROFILE_OPT --query Buckets[*].Name --output text|xargs -n1) if [[ $LIST_OF_BUCKETS ]]; then for bucket in $LIST_OF_BUCKETS;do BUCKET_SERVER_LOG_ENABLED=$($AWSCLI s3api get-bucket-logging --bucket $bucket $PROFILE_OPT --query [LoggingEnabled] --output text|grep -v "^None$") if [[ $BUCKET_SERVER_LOG_ENABLED ]];then textOK "Bucket $bucket has server access logging enabled" else textWarn "Bucket $bucket has server access logging disabled!" fi done else textNotice "No S3 Buckets found" fi } extra719(){ # "Check if Route53 hosted zones are logging queries to CloudWatch Logs (Not Scored) (Not part of CIS benchmark)" textTitle "$ID719" "$TITLE719" "NOT_SCORED" "EXTRA" LIST_OF_HOSTED_ZONES=$($AWSCLI route53 list-hosted-zones $PROFILE_OPT --query HostedZones[*].Id --output text|xargs -n1) if [[ $LIST_OF_HOSTED_ZONES ]]; then for hostedzoneid in $LIST_OF_HOSTED_ZONES;do HOSTED_ZONE_QUERY_LOG_ENABLED=$($AWSCLI route53 list-query-logging-configs --hosted-zone-id $hostedzoneid $PROFILE_OPT --query QueryLoggingConfigs[*].CloudWatchLogsLogGroupArn --output text|cut -d: -f7) if [[ $HOSTED_ZONE_QUERY_LOG_ENABLED ]];then textOK "Route53 hosted zone Id $hostedzoneid has query logging enabled in Log Group $HOSTED_ZONE_QUERY_LOG_ENABLED" else textWarn "Route53 hosted zone Id $hostedzoneid has query logging disabled!" fi done else textNotice "No Route53 hosted zones found" fi } extra720(){ # "Check if Lambda functions invoke API operations are being recorded by CloudTrail (Not Scored) (Not part of CIS benchmark)" textTitle "$ID720" "$TITLE720" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_FUNCTIONS=$($AWSCLI lambda list-functions $PROFILE_OPT --region $regx --query Functions[*].FunctionName --output text) if [[ $LIST_OF_FUNCTIONS ]]; then for lambdafunction in $LIST_OF_FUNCTIONS;do LIST_OF_TRAILS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $regx --query trailList[?HomeRegion==\`$regx\`].Name --output text) if [[ $LIST_OF_TRAILS ]]; then for trail in $LIST_OF_TRAILS; do FUNCTION_ENABLED_IN_TRAIL=$($AWSCLI cloudtrail get-event-selectors $PROFILE_OPT --trail-name $trail --region $regx --query "EventSelectors[*].DataResources[?Type == \`AWS::Lambda::Function\`].Values" --output text |xargs -n1| grep -E "^arn:aws:lambda.*function:$lambdafunction$") if [[ $FUNCTION_ENABLED_IN_TRAIL ]]; then textOK "$regx: Lambda function $lambdafunction enabled in trail $trail" "$regx" else textWarn "$regx: Lambda function $lambdafunction NOT enabled in trail $trail" "$regx" fi done # LIST_OF_MULTIREGION_TRAILS=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $regx --query "trailList[?IsMultiRegionTrail == \`true\`].Name" --output text) # if [[ $LIST_OF_MULTIREGION_TRAILS ]]; then # for trail in $LIST_OF_MULTIREGION_TRAILS; do # REGION_OF_TRAIL=$($AWSCLI cloudtrail describe-trails $PROFILE_OPT --region $regx --query "trailList[?IsMultiRegionTrail == \`true\` && Name == \`$trail\` ].HomeRegion" --output text) # FUNCTION_ENABLED_IN_THIS_REGION=$($AWSCLI cloudtrail get-event-selectors $PROFILE_OPT --trail-name $trail --region $REGION_OF_TRAIL --query "EventSelectors[*].DataResources[?Type == \`AWS::Lambda::Function\`].Values" --output text |xargs -n1| grep -E "^arn:aws:lambda.*function:$lambdafunction$") # if [[ $FUNCTION_ENABLED_IN_THIS_REGION ]]; then # textOK "$regx: Lambda function $lambdafunction enabled in trail $trail" "$regx" # else # textWarn "$regx: Lambda function $lambdafunction NOT enabled in trail $trail" "$regx" # fi # done # else # textWarn "$regx: Lambda function $lambdafunction is not being recorded!" "$regx" # fi else textWarn "$regx: Lambda function $lambdafunction is not being recorded no CloudTrail found!" "$regx" fi done else textNotice "$regx: No Lambda functions found" "$regx" fi done } extra721(){ # "Check if Redshift cluster has audit logging enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID721" "$TITLE721" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_REDSHIFT_CLUSTERS=$($AWSCLI redshift describe-clusters $PROFILE_OPT --region $regx --query 'Clusters[*].ClusterIdentifier' --output text) if [[ $LIST_OF_REDSHIFT_CLUSTERS ]]; then for redshiftcluster in $LIST_OF_REDSHIFT_CLUSTERS;do REDSHIFT_LOG_ENABLED=$($AWSCLI redshift describe-logging-status $PROFILE_OPT --region $regx --cluster-identifier $redshiftcluster --query LoggingEnabled --output text | grep True) if [[ $REDSHIFT_LOG_ENABLED ]];then REDSHIFT_LOG_ENABLED_BUCKET=$($AWSCLI redshift describe-logging-status $PROFILE_OPT --region $regx --cluster-identifier $redshiftcluster --query BucketName --output text) textOK "$regx: Redshift cluster $redshiftcluster has audit logging enabled to bucket $REDSHIFT_LOG_ENABLED_BUCKET" "$regx" else textWarn "$regx: Redshift cluster $redshiftcluster logging disabled!" "$regx" fi done else textNotice "$regx: No Redshift cluster configured" "$regx" fi done } extra722(){ # "Check if API Gateway has logging enabled (Not Scored) (Not part of CIS benchmark)" textTitle "$ID722" "$TITLE722" "NOT_SCORED" "EXTRA" for regx in $REGIONS; do LIST_OF_API_GW=$($AWSCLI apigateway get-rest-apis $PROFILE_OPT --region $regx --query items[*].id --output text) if [[ $LIST_OF_API_GW ]];then for apigwid in $LIST_OF_API_GW;do API_GW_NAME=$($AWSCLI apigateway get-rest-apis $PROFILE_OPT --region $regx --query "items[?id==\`$apigwid\`].name" --output text) CHECK_STAGES_NAME=$($AWSCLI apigateway get-stages $PROFILE_OPT --region $regx --rest-api-id $apigwid --query "item[*].stageName" --output text) if [[ $CHECK_STAGES_NAME ]];then for stagname in $CHECK_STAGES_NAME;do CHECK_STAGE_METHOD_LOGGING=$($AWSCLI apigateway get-stages $PROFILE_OPT --region $regx --rest-api-id $apigwid --query "item[?stageName == \`$stagname\` ].methodSettings" --output text|awk '{ print $1" log level "$6}') if [[ $CHECK_STAGE_METHOD_LOGGING ]];then textOK "$regx: API Gateway $API_GW_NAME has stage logging enabled for $CHECK_STAGE_METHOD_LOGGING" "$regx" else textWarn "$regx: API Gateway $API_GW_NAME logging disabled for stage $stagname!" "$regx" fi done else textWarn "$regx: No Stage name found for $API_GW_NAME" "$regx" fi done else textNotice "$regx: No API Gateway found" "$regx" fi 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;; extra76|extra706 ) extra76;; extra77|extra707 ) extra77;; extra78|extra708 ) extra78;; extra79|extra709 ) extra79;; extra710 ) extra710;; extra711 ) extra711;; extra712 ) extra712;; extra713 ) extra713;; extra714 ) extra714;; extra715 ) extra715;; extra716 ) extra716;; extra717 ) extra717;; extra718 ) extra718;; extra719 ) extra719;; extra720 ) extra720;; extra721 ) extra721;; extra722 ) extra722;; ## 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;extra76;extra77;extra78; extra79;extra710;extra711;extra712;extra713;extra714;extra715;extra716; extra717;extra718;extra719;extra720;extra721;extra722 ;; forensics-ready ) check21;check22;check23;check24;check25;check26;check27; check43; extra712;extra713;extra714;extra715;extra717;extra718;extra719; extra720;extra721;extra722 ;; * ) 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 textTitle "1" "$TITLE1" "NOT_SCORED" "SUPPORT" textTitle "$ID11" "$TITLE11" "SCORED" "LEVEL1" textTitle "$ID12" "$TITLE12" "SCORED" "LEVEL1" textTitle "$ID13" "$TITLE13" "SCORED" "LEVEL1" textTitle "$ID14" "$TITLE14" "SCORED" "LEVEL1" textTitle "$ID15" "$TITLE15" "SCORED" "LEVEL1" textTitle "$ID16" "$TITLE16" "SCORED" "LEVEL1" textTitle "$ID17" "$TITLE17" "SCORED" "LEVEL1" textTitle "$ID18" "$TITLE18" "SCORED" "LEVEL1" textTitle "$ID19" "$TITLE19" "SCORED" "LEVEL1" textTitle "$ID110" "$TITLE110" "SCORED" "LEVEL1" textTitle "$ID111" "$TITLE111" "SCORED" "LEVEL1" textTitle "$ID112" "$TITLE112" "SCORED" "LEVEL1" textTitle "$ID113" "$TITLE113" "SCORED" "LEVEL1" textTitle "$ID114" "$TITLE114" "SCORED" "LEVEL1" textTitle "$ID115" "$TITLE115" "NOT_SCORED" "LEVEL2" textTitle "$ID116" "$TITLE116" "SCORED" "LEVEL1" textTitle "$ID117" "$TITLE117" "SCORED" "LEVEL1" textTitle "$ID118" "$TITLE118" "SCORED" "LEVEL1" textTitle "$ID119" "$TITLE119" "SCORED" "LEVEL1" textTitle "$ID120" "$TITLE120" "SCORED" "LEVEL1" textTitle "$ID121" "$TITLE121" "NOT_SCORED" "LEVEL2" textTitle "$ID122" "$TITLE122" "SCORED" "LEVEL1" textTitle "$ID123" "$TITLE123" "NOT_SCORED" "LEVEL1" textTitle "$ID124" "$TITLE124" "SCORED" "LEVEL1" textTitle "2" "$TITLE2" "NOT_SCORED" "SUPPORT" textTitle "$ID21" "$TITLE21" "SCORED" "LEVEL1" textTitle "$ID22" "$TITLE22" "SCORED" "LEVEL2" textTitle "$ID23" "$TITLE23" "SCORED" "LEVEL1" textTitle "$ID24" "$TITLE24" "SCORED" "LEVEL1" textTitle "$ID25" "$TITLE25" "SCORED" "LEVEL1" textTitle "$ID26" "$TITLE26" "SCORED" "LEVEL1" textTitle "$ID27" "$TITLE27" "SCORED" "LEVEL2" textTitle "$ID28" "$TITLE28" "SCORED" "LEVEL2" textTitle "3" "$TITLE3" "NOT_SCORED" "SUPPORT" textTitle "$ID31" "$TITLE31" "SCORED" "LEVEL1" textTitle "$ID32" "$TITLE32" "SCORED" "LEVEL1" textTitle "$ID33" "$TITLE33" "SCORED" "LEVEL1" textTitle "$ID34" "$TITLE34" "SCORED" "LEVEL1" textTitle "$ID35" "$TITLE35" "SCORED" "LEVEL1" textTitle "$ID36" "$TITLE36" "SCORED" "LEVEL2" textTitle "$ID37" "$TITLE37" "SCORED" "LEVEL2" textTitle "$ID38" "$TITLE38" "SCORED" "LEVEL1" textTitle "$ID39" "$TITLE39" "SCORED" "LEVEL2" textTitle "$ID310" "$TITLE310" "SCORED" "LEVEL2" textTitle "$ID311" "$TITLE311" "SCORED" "LEVEL2" textTitle "$ID312" "$TITLE312" "SCORED" "LEVEL1" textTitle "$ID313" "$TITLE313" "SCORED" "LEVEL1" textTitle "$ID314" "$TITLE314" "SCORED" "LEVEL1" textTitle "$ID315" "$TITLE315" "NOT_SCORED" "LEVEL1" textTitle "4" "$TITLE4" "NOT_SCORED" "SUPPORT" textTitle "$ID41" "$TITLE41" "SCORED" "LEVEL1" textTitle "$ID42" "$TITLE42" "SCORED" "LEVEL1" textTitle "$ID43" "$TITLE43" "SCORED" "LEVEL2" textTitle "$ID44" "$TITLE44" "SCORED" "LEVEL2" textTitle "$ID45" "$TITLE45" "NOT_SCORED" "LEVEL2" textTitle "7" "$TITLE7" "NOT_SCORED" "SUPPORT" textTitle "$ID71" "$TITLE71" "NOT_SCORED" "EXTRA" textTitle "$ID72" "$TITLE72" "NOT_SCORED" "EXTRA" textTitle "$ID73" "$TITLE73" "NOT_SCORED" "EXTRA" textTitle "$ID74" "$TITLE74" "NOT_SCORED" "EXTRA" textTitle "$ID75" "$TITLE75" "NOT_SCORED" "EXTRA" textTitle "$ID76" "$TITLE76" "NOT_SCORED" "EXTRA" textTitle "$ID77" "$TITLE77" "NOT_SCORED" "EXTRA" textTitle "$ID78" "$TITLE78" "NOT_SCORED" "EXTRA" textTitle "$ID79" "$TITLE79" "NOT_SCORED" "EXTRA" textTitle "$ID710" "$TITLE710" "NOT_SCORED" "EXTRA" textTitle "$ID711" "$TITLE711" "NOT_SCORED" "EXTRA" textTitle "$ID712" "$TITLE712" "NOT_SCORED" "EXTRA" textTitle "$ID713" "$TITLE713" "NOT_SCORED" "EXTRA" textTitle "$ID714" "$TITLE714" "NOT_SCORED" "EXTRA" textTitle "$ID715" "$TITLE715" "NOT_SCORED" "EXTRA" textTitle "$ID716" "$TITLE716" "NOT_SCORED" "EXTRA" textTitle "$ID717" "$TITLE717" "NOT_SCORED" "EXTRA" textTitle "$ID718" "$TITLE718" "NOT_SCORED" "EXTRA" textTitle "$ID719" "$TITLE719" "NOT_SCORED" "EXTRA" textTitle "$ID720" "$TITLE720" "NOT_SCORED" "EXTRA" textTitle "$ID721" "$TITLE721" "NOT_SCORED" "EXTRA" textTitle "$ID722" "$TITLE722" "NOT_SCORED" "EXTRA" exit $EXITCODE fi ### All functions defined above ... run the workflow if [[ $MODE != "csv" ]]; then prowlerBanner printColorsCode fi getWhoami genCredReport saveReport callCheck 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 textTitle "2" "$TITLE2" "NOT_SCORED" "SUPPORT" check21 check22 check23 check24 check25 check26 check27 check28 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 textTitle "4" "$TITLE4" "NOT_SCORED" "SUPPORT" check41 check42 check43 check44 check45 if [[ ! $EXTRAS ]]; then textTitle "7" "$TITLE7" "NOT_SCORED" "SUPPORT" extra71 extra72 extra73 extra74 extra75 extra76 extra77 extra78 extra79 extra710 extra711 extra712 extra713 extra714 extra715 extra716 extra717 extra718 extra719 extra720 extra721 extra722 fi cleanTemp exit $EXITCODE