Files
prowler/prowler2
Toni de la Fuente 2f761f62a6 new folder structure
2018-03-20 10:56:37 -04:00

545 lines
16 KiB
Bash
Executable File
Raw Blame History

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