#!/bin/bash # # # Prowler multi-account assessment script: # Used to drive the assessment of AWS accounts via Prowler, post-processing the output reports # and optimizing the effort involved via automation. # # Script version: 2.96 # # Tunable parameters to review: # 1) PARALLELISM: Can be tuned to specify how many accounts to assess simultaneously. # The instance size must be adjusted appropriately. # Be aware of AWS Account level EC2 API Throttling limits and to execute this script in an account with minimal workloads. # r6i.xlarge can sustain 12 parallel assessments based on memory testing. # Utilize appropriately sized EC2 instance (8=r6i.large,12=r6i.xlarge, 16=r6i.2xlarge) # 2) AWSACCOUNT_LIST: Specify the accounts to be assessed using one of the supported methods: # Use the keyword allaccounts to generate a list of all accounts in the AWS Org # Use the keyword inputfile to read in AWS Account IDs from a file (If using this mode, must also set AWSACCOUNT_LIST_FILE) # Use a space separated list of AWS Account IDs # 3) AWSACCOUNT_LIST_FILE: If using AWSACCOUNT_LIST="inputfile", specify the path to the file # If the file is located in the /use/local/prowler directory, specify the filename, else specify the full path # Account IDs can be specified on one line (space separated) or one Account ID per line # 4) REGION_LIST: Specify regions (SPACE DELIMITED) if you wish to assess specific AWS regions # or leave allregions to include all AWS regions. # 5) IAM_CROSS_ACCOUNT_ROLE: The IAM Role name created for cross account access # 6) ACCOUNTID_WITH_NAME: By default, the value is true, the value of ACCOUNT_NUM column in the final report is populated with Account Name # in the format . Changing the value to false will produce the report with ACCOUNT_NUM=. # 7) S3_BUCKET: The S3 bucket which will be used for Prowler report upload. # This is set by default to the S3 bucket provisioned during deployment. # 8) CONSOLIDATED_REPORT: The name of the output report which does not have any grep filtering performed # Using .txt as 'CSV' output is semicolon delimited # 9) CONSOLIDATED_REPORT_FILTERED: The name of the output report which does have grep filtering performed to remove common errors. # Using .txt as 'CSV' output is semicolon delimited # This file is recommended to be used for reporting as know errors are removed and provide cleaner output # 10) The prowler command within the for loop can also be tuned to meet the needs of the assessment. # prowler -R arn:aws-partition:iam::$ACCOUNTID:role/$IAM_CROSS_ACCOUNT_ROLE -M csv json -T 43200 --verbose | tee output/stdout-$ACCOUNTID.txt 1>/dev/null # See Prowler documentation for all options. ######################################### #Variables which can be modified: (In most cases, scanning all accounts and all regions is preferred for a complete assessment) #Adjust PARALLELISM to adjust the number of parallel scans PARALLELISM="12" #Specify accounts to be assessed using one of the supported methods: AWSACCOUNT_LIST="allaccounts" #AWSACCOUNT_LIST="inputfile" #AWSACCOUNT_LIST="123456789012 210987654321" #If using AWSACCOUNT_LIST="inputfile", specify the path to the file: #AWSACCOUNT_LIST_FILE="file_with_account_ids #Specify the regions to have assessed (space separated) or use the keyword allregions to include all regions: REGION_LIST="allregions" #REGION_LIST="us-east-1 us-east-2" #Specify an IAM Role to use for cross account access in the target accounts (Execution Role): IAM_CROSS_ACCOUNT_ROLE="ProwlerExecRole" #Specify whether to output Account ID with Account Name in the final report. (set to false to disable) ACCOUNTID_WITH_NAME=true #S3 bucket where report will be uploaded S3_BUCKET="SetBucketName" #Consolidated output report without error filtering (Using .txt as 'CSV' output is semicolon delimited) CONSOLIDATED_REPORT=output/prowler-fullorgresults.txt #Consolidated output report with error filtering (Using .txt as 'CSV' output is semicolon delimited) (Recommended to be used for reporting) CONSOLIDATED_REPORT_FILTERED=output/prowler-fullorgresults-accessdeniedfiltered.txt ######################################### # CleanUp Last Ran Prowler Reports if they exist rm -rf output/* # Create output folder for first time scan with redirected stout mkdir -p output # Unset environment variables if they exist and utilize IAM Role attached to the EC2 instance unset_aws_environment() { unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN } unset_aws_environment # Determine the executing account AWS Number and Partition CALLER_IDENTITY_ARN=$(aws sts get-caller-identity --output text --query "Arn") AWSPARTITION=$(echo "$CALLER_IDENTITY_ARN" | cut -d: -f2) EXECACCOUNT=$(echo "$CALLER_IDENTITY_ARN" | cut -d: -f5) echo "" echo "AWS account Prowler is executing from: $EXECACCOUNT" echo "" # Assume Role in Managment account and export session credentials management_account_session() { AWSMANAGEMENT=$(aws organizations describe-organization --query Organization.MasterAccountId --output text) echo "AWS organization Management account: $AWSMANAGEMENT" unset_aws_environment ROLE_SESSION_CREDS=$(aws sts assume-role --role-arn arn:"$AWSPARTITION":iam::"$AWSMANAGEMENT":role/"$IAM_CROSS_ACCOUNT_ROLE" --role-session-name ProwlerRun --output json) AWS_ACCESS_KEY_ID=$(echo "$ROLE_SESSION_CREDS" | jq -r .Credentials.AccessKeyId) AWS_SECRET_ACCESS_KEY=$(echo "$ROLE_SESSION_CREDS" | jq -r .Credentials.SecretAccessKey) AWS_SESSION_TOKEN=$(echo "$ROLE_SESSION_CREDS" | jq -r .Credentials.SessionToken) export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN } #Monitor the number of background processes and return to task execution for loop when bg jobs are less than PARALLELISM limit process_monitor() { while [ "$(jobs | wc -l)" -ge $PARALLELISM ] do echo "Sleeping 20 seconds while waiting for active assessment queue to clear..." sleep 20 done } if [ "$AWSACCOUNT_LIST" = "allaccounts" ]; then # Lookup All Accounts in AWS Organization management_account_session ACCOUNTS_TO_PROCESS=$(aws organizations list-accounts --output text --query 'Accounts[?Status==`ACTIVE`].Id') echo "" elif [ "$AWSACCOUNT_LIST" = "inputfile" ]; then if [ -e $AWSACCOUNT_LIST_FILE ]; then echo "Reading External File: $AWSACCOUNT_LIST_FILE" ACCOUNTS_TO_PROCESS=$(cat $AWSACCOUNT_LIST_FILE) else echo "External file $AWSACCOUNT_LIST_FILE not located. Please validate the file/path and update the AWSACCOUNT_LIST_FILE variable." exit 0 fi else ACCOUNTS_TO_PROCESS=$AWSACCOUNT_LIST fi # Display account and region selection echo "" if [ "$AWSACCOUNT_LIST" = "allaccounts" ]; then echo "AWS Accounts being processed: All accounts in the AWS organization." echo "$ACCOUNTS_TO_PROCESS" else echo "AWS Accounts being processed: Specified AWS accounts below." echo "$ACCOUNTS_TO_PROCESS" fi echo "" echo "AWS regions being processed:" if [ "$REGION_LIST" == "allregions" ]; then echo "All AWS regions" else echo $REGION_LIST fi echo "" echo "Output from prowler assessments will be redirected to output/stdout-.txt and errors will be shown on the console" echo "tail -f these files to monitor progress of individual account assessments" echo "" echo "When using screen you can monitor assessments while executing by:" echo " 1) Create an additional window (Ctrl-a + c) and tail executed there. Use (Ctrl-a + n) with switch between windows" echo " or" echo " 2) The window where Prowler is executing is manually detached (Ctrl-a + d) and tail executed outside of screen. Screen can be resumed with screen -r" echo "" echo "As individual account assessments are completed, additional accounts will be assessed from the list" echo "" # Run Prowler against selected accounts and regions if [ "$REGION_LIST" == "allregions" ]; then for ACCOUNTID in $ACCOUNTS_TO_PROCESS; do test "$(jobs | wc -l)" -ge $PARALLELISM && process_monitor || true { # Unset AWS Profile Variables unset_aws_environment echo -e "Assessing AWS Account: $ACCOUNTID with all AWS regions using Role: $IAM_CROSS_ACCOUNT_ROLE on $(date)" # Run Prowler /usr/local/bin/prowler -R arn:$AWSPARTITION:iam::$ACCOUNTID:role/$IAM_CROSS_ACCOUNT_ROLE -M csv json html -T 43200 --verbose | tee output/stdout-$ACCOUNTID.txt 1>/dev/null } & done else for ACCOUNTID in $ACCOUNTS_TO_PROCESS; do test "$(jobs | wc -l)" -ge $PARALLELISM && process_monitor || true { # Unset AWS Profile Variables unset_aws_environment echo -e "Assessing AWS Account: $ACCOUNTID with regions: $REGION_LIST using Role: $IAM_CROSS_ACCOUNT_ROLE on $(date)" # Run Prowler with -f and scans regions specified in the $REGION_LIST variable /usr/local/bin/prowler -R arn:$AWSPARTITION:iam::$ACCOUNTID:role/$IAM_CROSS_ACCOUNT_ROLE -M csv json html -f $REGION_LIST -T 43200 --verbose | tee output/stdout-$ACCOUNTID.txt 1>/dev/null } & done fi # Wait for All Prowler Processes to finish wait echo "" echo "Prowler assessments have been completed against all accounts" echo "" #Unset the STS AssumeRole session and revert to permissions via the EC2 attached IAM Role unset_aws_environment # Prowler Output Post-Processing echo "======================================================================================" echo "Prowler Output Post-Processing" echo "======================================================================================" echo "" # Below logic is to reset the variable ACCOUNTID_WITH_NAME if $ACCOUNTID_WITH_NAME; then echo "ACCOUNTID_WITH_NAME flag is ON, verifying to ensure AWS Org. is configured and can be queried to get list of accounts." management_account_session IS_ACCOUNT_PART_OF_AWS_ORG=$(aws organizations describe-organization) if [ "$IS_ACCOUNT_PART_OF_AWS_ORG" == "" ]; then # Account where prowler is executed is not part of the AWS Organizations. # Change the value of the variable ACCOUNTID_WITH_NAME to false. echo "AWS Org was not found! Skipping report generation with Account Name (Resetting the flag ACCOUNTID_WITH_NAME to false)." ACCOUNTID_WITH_NAME=false fi # Verfiy AWS org. can be queried to get list of accounts. if $ACCOUNTID_WITH_NAME; then rm -f output/accts.txt # Delete previously generated accounts list file if exists. aws organizations list-accounts | jq -r '[.Accounts[] | {Account: .Id, Arn: .Arn, Email: .Email, Name: .Name, AccountName: (.Id + "-" + .Name), Status: .Status, JoinedMethod: .JoinedMethod, JoinedTimestamp: .JoinedTimestamp}]' | jq -r '(.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv' | sed 's/\"//g' > output/accts.txt if [ ! -f "output/accts.txt" ]; then echo "Failed getting list of Accounts from AWS Org! Skipping report generation with Account Name (Resetting the flag ACCOUNTID_WITH_NAME to false)." ACCOUNTID_WITH_NAME=false fi fi unset_aws_environment echo "Completed." echo "" fi if $ACCOUNTID_WITH_NAME; then # Concatenating all output csv files into a single file for use with Excel and replace account_num with echo "Concatenating all output csv files into a single file for use with Excel and replacing account_num with ..." counter=1 rm -f output/prowler-fullorgresults-temp.csv for fileName in output/prowler-*.csv ; do if [[ "$fileName" != "output/prowler-fullorgresults.csv" && "$fileName" != $CONSOLIDATED_REPORT_FILTERED && "$fileName" != "output/prowler-fullorgresults-with-acct-name.csv" && "$fileName" != "output/prowler-fullorgresults-raw.csv" ]]; then echo "Processing the file $fileName to replace AccountId with Name." acctId=$(echo "$fileName" | cut -d '-' -f3) acctName=$(awk -v var=$acctId '$1 == var {print $5}' FS=, output/accts.txt) acctEmail=$(awk -v var=$acctId '$1 == var {print $3}' FS=, output/accts.txt) if [[ "$counter" == "1" ]]; then # Header line awk 'NR==1 {print; exit}' $fileName > $CONSOLIDATED_REPORT ((counter+=1)) fi if [ "$acctName" == "" ]; then echo "Skipping Account Name replacement for the file $fileName, REASON: Account Name for the account $acctId not found in the file output/accts.txt" awk 'NR>1' $fileName >> output/prowler-fullorgresults-temp.csv else echo "Performing Account Name replacement for the file $fileName, ACCOUNT_NUM=$acctId with new value $acctName" awk 'NR>1' $fileName > output/PROCESS.csv acctNameCol=$(awk -F';' -vCOLM=ACCOUNT_NAME 'NR == 1 { for (i = 1; i <= NF; i++) { if ($i == COLM) { cidx = i; } } if (cidx <=0) { print -1; } else { print cidx; } fi; exit}' $fileName) acctEmailCol=$(awk -F';' -vCOLM=ACCOUNT_EMAIL 'NR == 1 { for (i = 1; i <= NF; i++) { if ($i == COLM) { cidx = i; } } if (cidx <=0) { print NF + 1; } else { print cidx; } fi; exit}' $fileName) if [ $acctNameCol -gt 0 ]; then awk -vFPAT='([^;]*)|("[^"]+")' -vNAME="$acctName" -vEMAIL="$acctEmail" "{$"$acctNameCol"=NAME;$"$acctEmailCol"=EMAIL;}1" OFS=';' output/PROCESS.csv >> output/prowler-fullorgresults-temp.csv else echo "Skipped Account Name replacement for the file $fileName, Reason: ACCOUNT_NAME column not found!" cat output/PROCESS.csv >> output/prowler-fullorgresults-temp.csv fi rm -f output/PROCESS.csv fi fi done cat output/prowler-fullorgresults-temp.csv | sort | uniq >> $CONSOLIDATED_REPORT; rm -f output/prowler-fullorgresults-temp.csv echo "Completed." echo "" else #Concatenating all output csv files into a single file for use with Excel echo "Concatenating all output csv files into a single file for use with Excel..." cat output/prowler-*.csv | sort | uniq > output/prowler-fullorgresults-raw.csv echo "Completed." echo "" # Move the final line in the file (Header) to the top for easier location in Excel awk '{a[NR]=$0} END {print a[NR]; for (i=1;i output/PROCESS.csv acctNameCol=$(awk -F';' -vCOLM=ACCOUNT_NAME 'NR == 1 { for (i = 1; i <= NF; i++) { if ($i == COLM) { cidx = i; } } if (cidx <=0) { print -1; } else { print cidx; } fi; exit}' output/PROCESS.csv) acctIdCol=$(awk -F';' -vCOLM=ACCOUNT_ID 'NR == 1 { for (i = 1; i <= NF; i++) { if ($i == COLM) { cidx = i; } } if (cidx <=0) { print -1; } else { print cidx; } fi; exit}' output/PROCESS.csv) if [ $acctNameCol -gt 0 ]; then awk -vFPAT='([^;]*)|("[^"]+")' "NR > 1 {$"$acctNameCol"=$"$acctIdCol";}1" OFS=';' output/PROCESS.csv > $CONSOLIDATED_REPORT else echo "Skipped Account Name replacement for the file $CONSOLIDATED_REPORT, Reason: ACCOUNT_NAME column not found!" cat output/PROCESS.csv > $CONSOLIDATED_REPORT fi rm -f output/PROCESS.csv # Remove the initial concatenated raw file rm -rf output/prowler-fullorgresults-raw.csv fi # end of if ACCOUNTID_WITH_NAME is true. #Perform processing to remove common "Access Denied" errors from output while preserving the "full" output echo "Creating an optional filtered version of the concatenate output for use with Excel..." grep -v -i 'Access Denied getting bucket\|Access Denied Trying to Get\|InvalidToken' $CONSOLIDATED_REPORT > $CONSOLIDATED_REPORT_FILTERED echo "Completed." echo "" #Call Python script to generate visualizations based on results python3 /usr/local/prowler/generateVisualizations.py #Zip output results into a single file for download (stdout-* includes stdout and can be reviewed for troubleshooting) OUTPUT_SUFFIX=$(date +%F-%H-%M) echo "Zipping output results into a single file for download. Output File: prowler_output.zip" zip -r prowler_output-$OUTPUT_SUFFIX.zip output/*.csv output/*.txt output/*.json output/*.html output/ResultsVisualizations-*/*.* echo "Completed." echo "" #Upload Prowler Report to S3 aws s3 cp prowler_output-$OUTPUT_SUFFIX.zip s3://$S3_BUCKET