diff --git a/include/check11 b/include/check11 new file mode 100644 index 00000000..39e051a1 --- /dev/null +++ b/include/check11 @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +CHECK_ID[check11]="1.1,1.01" +CHECK_TITLE[check11]="Avoid the use of the root account (Scored)." +CHECK_SCORED[check11]="SCORED" +check11() { + # "Avoid the use of the root account (Scored)." + COMMAND11=$(cat $TEMP_REPORT_FILE| grep '' | cut -d, -f5,11,16 | sed 's/,/\ /g') + textTitle "$CHECK_ID" "$CHECK_TITLE" "SCORED" "LEVEL1" + textNotice "Root account last accessed (password key_1 key_2): $COMMAND11" +} +CHECK_ALTERNATE[check101]="check11" diff --git a/include/group1 b/include/group1 new file mode 100644 index 00000000..f77f5ae5 --- /dev/null +++ b/include/group1 @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +GROUP_ID[1]="group1" +GROUP_NUMBER[1]="1.0" +GROUP_TITLE[1]="Identity and Access Management" +GROUP_RUN_BY_DEFAULT[1]="Y" +GROUP_CHECKS[1]="check11" +textTitle "$GROUP_NUMBER" "$GROUP_TITLE" diff --git a/prowler2 b/prowler2 new file mode 100755 index 00000000..d7338f4b --- /dev/null +++ b/prowler2 @@ -0,0 +1,625 @@ +#!/usr/bin/env bash + +# Prowler is a tool that provides automate auditing and hardening guidance of an AWS account. +# It is based on AWS-CLI commands. It follows guidelines present in the CIS Amazon +# Web Services Foundations Benchmark at: +# https://d0.awsstatic.com/whitepapers/compliance/AWS_CIS_Foundations_Benchmark.pdf + +# This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 +# International Public License. The link to the license terms can be found at +# https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode +# +# Author: Toni de la Fuente - @ToniBlyx - https://blyx.com/contact + +# Prowler - Iron Maiden +# +# Walking through the city, looking oh so pretty +# I've just got to find my way +# See the ladies flashing +# All there legs and lashes +# I've just got to find my way... + +OPTRED="" +OPTNORMAL="" + +# Set the defaults for these getopts variables +REGION="us-east-1" +FILTERREGION="" +MAXITEMS=100 +MONOCHROME=0 +MODE="text" +SEP=',' +KEEPCREDREPORT=0 +EXITCODE=0 + +# Command usage menu +usage(){ + echo " +USAGE: + `basename $0` -p -r [ -h ] + + Options: + -p specify your AWS profile to use (i.e.: default) + -r specify an AWS region to direct API requests to + (i.e.: us-east-1), all regions are checked anyway + -c specify a check number or group from the AWS CIS benchmark + (i.e.: "check11" for check 1.1, "check3" for entire section 3, "level1" for CIS Level 1 Profile Definitions or "forensics-ready") + -f specify an AWS region to run checks against + (i.e.: us-west-1) + -m specify the maximum number of items to return for long-running requests (default: 100) + -M output mode: text (defalut), mono, csv (separator is ","; data is on stdout; progress on stderr) + -k keep the credential report + -n show check numbers to sort easier + (i.e.: 1.01 instead of 1.1) + -l list all available checks only (does not perform any check) + -e exclude extras + -h this help + " + exit +} + +while getopts ":hlkp:r:c:f:m:M:en" OPTION; do + case $OPTION in + h ) + usage + EXITCODE=1 + exit $EXITCODE + ;; + l ) + PRINTCHECKSONLY=1 + ;; + k ) + KEEPCREDREPORT=1 + ;; + p ) + PROFILE=$OPTARG + ;; + r ) + REGION=$OPTARG + ;; + c ) + CHECKNUMBER=$OPTARG + ;; + f ) + FILTERREGION=$OPTARG + ;; + m ) + MAXITEMS=$OPTARG + ;; + M ) + MODE=$OPTARG + ;; + n ) + NUMERAL=1 + ;; + e ) + EXTRAS=1 + ;; + : ) + echo "" + echo "$OPTRED ERROR!$OPTNORMAL -$OPTARG requires an argument" + usage + EXITCODE=1 + exit $EXITCODE + ;; + ? ) + echo "" + echo "$OPTRED ERROR!$OPTNORMAL Invalid option" + usage + EXITCODE=1 + exit $EXITCODE + ;; + esac +done + +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 +} + +printCsvHeader() { + >&2 echo "" + >&2 echo "Generating \"${SEP}\" delimited report on stdout for profile $PROFILE, account $ACCOUNT_NUM" + echo "PROFILE${SEP}ACCOUNT_NUM${SEP}REGION${SEP}TITLE_ID${SEP}RESULT${SEP}SCORED${SEP}LEVEL${SEP}TITLE_TEXT${SEP}NOTES" +} + +prowlerBanner() { + echo -e "$CYAN _" + echo -e " _ __ _ __ _____ _| | ___ _ __" + echo -e " | '_ \| '__/ _ \ \ /\ / / |/ _ \ '__|" + echo -e " | |_) | | | (_) \ V V /| | __/ |" + echo -e " | .__/|_| \___/ \_/\_/ |_|\___|_|" + echo -e " |_|$NORMAL$BLUE CIS based AWS Account Hardening Tool$NORMAL\n" + echo -e "$YELLOW Date: $(date)" +} + +# Get whoami in AWS, who is the user running this shell script +getWhoami(){ + ACCOUNT_NUM=$($AWSCLI sts get-caller-identity --output json $PROFILE_OPT --region $REGION --query "Account" | tr -d '"') + if [[ "$MODE" == "csv" ]]; then + CALLER_ARN_RAW=$($AWSCLI sts get-caller-identity --output json $PROFILE_OPT --region $REGION --query "Arn") + if [[ 255 -eq $? ]]; then + # Failed to get own identity ... exit + echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" + >&2 echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" + EXITCODE=2 + exit $EXITCODE + fi + CALLER_ARN=$(echo $CALLER_ARN_RAW | tr -d '"') + printCsvHeader + textTitle "0.0" "Show report generation info" "NOT_SCORED" "SUPPORT" + textNotice "ARN: $CALLER_ARN TIMESTAMP: $SCRIPT_START_TIME" + else + echo "" + echo "This report is being generated using credentials below:" + echo "" + echo -e "AWS-CLI Profile: $NOTICE[$PROFILE]$NORMAL AWS API Region: $NOTICE[$REGION]$NORMAL AWS Filter Region: $NOTICE[${FILTERREGION:-all}]$NORMAL\n" + if [[ $MONOCHROME -eq 1 ]]; then + echo "Caller Identity:" + $AWSCLI sts get-caller-identity --output text $PROFILE_OPT --region $REGION --query "Arn" + if [[ 255 -eq $? ]]; then + # Failed to get own identity ... exit + echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" + >&2 echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" + exit 2 + fi + echo "" + else + echo "Caller Identity:" + $AWSCLI sts get-caller-identity --output table $PROFILE_OPT --region $REGION + if [[ 255 -eq $? ]]; then + # Failed to get own identity ... exit + echo variable $PROFILE_OPT + echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" + >&2 echo "ERROR WITH $PROFILE CREDENTIALS - EXITING!" + EXITCODE=2 + exit $EXITCODE + fi + echo "" + fi + fi +} + +printColorsCode(){ + if [[ $MONOCHROME -eq 0 ]]; then + echo -e "\nColors Code for results: $NOTICE INFORMATIVE$NORMAL,$OK OK (RECOMMENDED VALUE)$NORMAL, $BAD WARNING (FIX REQUIRED)$NORMAL \n" + fi +} + +# Generate Credential Report +genCredReport() { + textTitle "0.1" "Generating AWS IAM Credential Report..." "NOT_SCORED" "SUPPORT" + until $( $AWSCLI iam generate-credential-report --output text --query 'State' $PROFILE_OPT --region $REGION |grep -q -m 1 "COMPLETE") ; do + sleep 1 + done +} + +# Save report to a file, decode it, deletion at finish and after every single check +saveReport(){ + $AWSCLI iam get-credential-report --query 'Content' --output text $PROFILE_OPT --region $REGION | decode_report > $TEMP_REPORT_FILE + if [[ $KEEPCREDREPORT -eq 1 ]]; then + textTitle "0.2" "Saving IAM Credential Report ..." "NOT_SCORED" "SUPPORT" + textNotice "IAM Credential Report saved in $TEMP_REPORT_FILE" + fi +} + +# Delete temporary report file +cleanTemp(){ + if [[ $KEEPCREDREPORT -ne 1 ]]; then + rm -fr $TEMP_REPORT_FILE + fi +} + +# Delete the temporary report file if we get interrupted/terminated +trap cleanTemp EXIT + +# Get a list of all available AWS Regions +REGIONS=$($AWSCLI ec2 describe-regions --query 'Regions[].RegionName' \ + --output text \ + $PROFILE_OPT \ + --region $REGION \ + --region-names $FILTERREGION) + +infoReferenceLong(){ + # Report review note: + echo -e "" + echo -e "For more information on the Prowler, feedback and issue reporting:" + echo -e "https://github.com/Alfresco/prowler" + echo -e "" + echo -e "For more information on the CIS benchmark:" + echo -e "https://benchmarks.cisecurity.org/tools2/amazon/CIS_Amazon_Web_Services_Foundations_Benchmark_v1.1.0.pdf" + +} + +callCheck(){ + if [[ $CHECKNUMBER ]];then + case "$CHECKNUMBER" in + check11|check101 ) execute_check check11;; + * ) + textWarn "ERROR! Use a valid check name (i.e. check41 or extra71)\n"; + esac + cleanTemp + exit $EXITCODE + fi +} + +# List only check tittles + +if [[ $PRINTCHECKSONLY == "1" ]]; then + prowlerBanner + show_all_titles + exit $EXITCODE +fi + +# Data Structures +# +# Groups +# ------ +# GROUP_NAME[X] = "groupname" +# GROUP_TITLE[X] = "Logging *****" +# GROUP_RUN_BY_DEFAULT[X] = "Y" // Whether this group is run by default +# GROUP_CHECKS[X] = "check11,check12" // etc. etc. +# +# Checks & Extras +# --------------- +# CHECK_TITLE[checkname] = "Title checkname" +# CHECK_ID[checkname] = '1.1,1.01' +# CHECK_SCORED[checkname] = 'SCORED' or 'NOT_SCORED' +# checkname() { +# // code of the function. The function should be named checkname +# } +# +# Check alternate names +# CHECK_ALTERNATE[alternatename] = "checkname" +# CHECK_ALTERNATE["check101"] = "check11" + +# # For group of checks arrays +# declare -a GROUP_NAME +# declare -a GROUP_TITLE +# declare -a GROUP_RUN_BY_DEFAULT +# declare -a GROUP_CHECKS +# +# # For checks +# declare -a CHECK_TITLE +# declare -a CHECK_ID +# declare -a CHECK_SCORED +# declare -a CHECK_ALTERNATE + +# Include all of the groups of checks inside include folder +for group in $(ls include/group*); do + . "$group" +done + +# Include all of the checks inside include folder +# this includes also extra check since they are "check_extraNN" +for checks in $(ls include/check*); do + . "$checks" +done + +# Function to show the title of the check +show_check_title() { + # This would just call textTitle + textTitle "${CHECK_ID[$1]} ${CHECK_TITLE[$1]} ${CHECK_SCORED[$1]}" +} + +# Function to show the title of a group, by numeric id +show_group_title() { + # This would also just call textTitle in the real prowler + textTitle ${GROUP_NUMBER[$1]} - ${GROUP_TITLE[$1]} +} + +# Function to execute the check +execute_check() { + # See if this is an alternate name for a check + # for example, we might have been passed 1.01 which is another name for 1.1 + if [ ${CHECK_ALTERNATE[$1]} ];then + show_check_title ${CHECK_ALTERNATE[$1]} + ${CHECK_ALTERNATE[$1]} + else + show_check_title $1 + $1 + fi +} + +# Function to execute all checks in a group +execute_group() { + show_group_title $1 + # run the checks in the group + IFS=',' read -ra CHECKS <<< ${GROUP_CHECKS[$1]} + for i in "${CHECKS[@]}"; do + execute_check $i + done +} + +# Function to execute group by name +execute_group_by_name() { + for i in ${#GROUP_NAME[@]}; do + if [ "${GROUP_NAME[$i]}" == "$1" ]; then + execute_group $i + fi + done +} + +# Function to execute all checks in all groups +execute_all() { + for i in ${#GROUP_TITLE[@]}; do + execute_group $i + done +} + +# Function to show the titles of everything +show_all_titles() { + for i in ${#GROUP_TITLE[@]}; do + show_group_title $i + # Display the title of the checks + IFS=',' read -ra CHECKS <<< ${GROUP_CHECKS[$i]} + for j in "${CHECKS[@]}"; do + show_check_title $j + done + done +} + +### All functions defined above ... run the workflow + +if [[ $MODE != "csv" ]]; then + prowlerBanner + printColorsCode +fi +getWhoami +genCredReport +saveReport +callCheck + +execute_all + + +if [[ ! $EXTRAS ]]; then + textTitle "7" "$TITLE7" "NOT_SCORED" "SUPPORT" + execute_group 7 +fi + +cleanTemp +exit $EXITCODE