From 365b396f9a96e1941bca95ad6021d91a4f3ce9f0 Mon Sep 17 00:00:00 2001 From: Toni de la Fuente Date: Wed, 16 Mar 2022 16:27:19 +0100 Subject: [PATCH] feat(metadata): Include account metadata in Prowler assessments (#1049) * Add support for organizations accounts metadata part 1 * Add support for organizations accounts metadata part 2 * Add gathering account metadata from org * chore(prowler): get accounts metadata Use assume_role backing up normal assumed credentials to assume management account and then restore it to old ones * fix(orgs metadata): deleted assume_role_orgs * refactor(organization_metadata) Reformulate to extract AWS Organizations metadata * doc(org_metadata): include required -R in usage * docs(org-metadata): Update README Co-authored-by: n4ch04 Co-authored-by: Pepe Fagoaga --- README.md | 24 ++++++++++++++++++++ include/assume_role | 1 - include/csv_header | 5 +---- include/organizations_metadata | 41 ++++++++++++++++++++++++++++++++++ include/outputs | 23 +++++++++++++++---- prowler | 31 ++++++++++++++++++++++++- 6 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 include/organizations_metadata diff --git a/README.md b/README.md index c247d5c9..51d32994 100644 --- a/README.md +++ b/README.md @@ -365,6 +365,30 @@ for accountId in $ACCOUNTS_IN_ORGS; do ./prowler -A $accountId -R RemoteRoleToAs ``` Usig the same for loop it can be scanned a list of accounts with a variable like `ACCOUNTS_LIST='11111111111 2222222222 333333333'` +### Get AWS Account details from your AWS Organization: + +From Prowler v2.8, you can get additional information of the scanned account in CSV and JSON outputs. When scanning a single account you get the Account ID as part of the output. Now, if you have AWS Organizations and are scanning multiple accounts using the assume role functionality, Prowler can get your account details like Account Name, Email, ARN, Organization ID and Tags and you will have them next to every finding in the CSV and JSON outputs. +In order to do that you can use the new option `-O `, requires `-R ` and also needs permissions `organizations:ListAccounts*` and `organizations:ListTagsForResource`. See the following sample command: +``` +./prowler -R ProwlerScanRole -A 111111111111 -O 222222222222 -M json,csv +``` +In that command Prowler will scan the account `111111111111` assuming the role `ProwlerScanRole` and getting the account details from the AWS Organizatiosn management account `222222222222` assuming the same role `ProwlerScanRole` for that and creating two reports with those details in JSON and CSV. + +In the JSON output below (redacted) you can see tags coded in base64 to prevent breaking CSV or JSON due to its format: + +```json + "Account Email": "my-prod-account@domain.com", + "Account Name": "my-prod-account", + "Account ARN": "arn:aws:organizations::222222222222:account/o-abcde1234/111111111111", + "Account Organization": "o-abcde1234", + "Account tags": "\"eyJUYWdzIjpasf0=\"" +``` +The additional fields in CSV header output are as follow: + +```csv +ACCOUNT_DETAILS_EMAIL,ACCOUNT_DETAILS_NAME,ACCOUNT_DETAILS_ARN,ACCOUNT_DETAILS_ORG,ACCOUNT_DETAILS_TAGS +``` + ### GovCloud Prowler runs in GovCloud regions as well. To make sure it points to the right API endpoint use `-r` to either `us-gov-west-1` or `us-gov-east-1`. If not filter region is used it will look for resources in both GovCloud regions by default: diff --git a/include/assume_role b/include/assume_role index 7f94b4fd..b169d993 100644 --- a/include/assume_role +++ b/include/assume_role @@ -92,7 +92,6 @@ cleanSTSAssumeFile() { } backupInitialAWSCredentials() { - if [[ $(printenv AWS_ACCESS_KEY_ID) && $(printenv AWS_SECRET_ACCESS_KEY) && $(printenv AWS_SESSION_TOKEN) ]]; then INITIAL_AWS_ACCESS_KEY_ID=$(printenv AWS_ACCESS_KEY_ID) INITIAL_AWS_SECRET_ACCESS_KEY=$(printenv AWS_SECRET_ACCESS_KEY) diff --git a/include/csv_header b/include/csv_header index 1c01f93d..980e8636 100644 --- a/include/csv_header +++ b/include/csv_header @@ -13,8 +13,5 @@ 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}CHECK_RESULT${SEP}ITEM_SCORED${SEP}ITEM_LEVEL${SEP}TITLE_TEXT${SEP}CHECK_RESULT_EXTENDED${SEP}CHECK_ASFF_COMPLIANCE_TYPE${SEP}CHECK_SEVERITY${SEP}CHECK_SERVICENAME${SEP}CHECK_ASFF_RESOURCE_TYPE${SEP}CHECK_ASFF_TYPE${SEP}CHECK_RISK${SEP}CHECK_REMEDIATION${SEP}CHECK_DOC${SEP}CHECK_CAF_EPIC${SEP}CHECK_RESOURCE_ID${SEP}PROWLER_START_TIME" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV - # echo "PROFILE${SEP}ACCOUNT_NUM${SEP}REGION${SEP}TITLE_ID${SEP}RESULT${SEP}SCORED${SEP}LEVEL${SEP}TITLE_TEXT${SEP}NOTES${SEP}COMPLIANCE${SEP}SEVERITY${SEP}SERVICENAME" | tee -a $OUTPUT_FILE_NAME.$EXTENSION_CSV + echo "PROFILE${SEP}ACCOUNT_NUM${SEP}REGION${SEP}TITLE_ID${SEP}CHECK_RESULT${SEP}ITEM_SCORED${SEP}ITEM_LEVEL${SEP}TITLE_TEXT${SEP}CHECK_RESULT_EXTENDED${SEP}CHECK_ASFF_COMPLIANCE_TYPE${SEP}CHECK_SEVERITY${SEP}CHECK_SERVICENAME${SEP}CHECK_ASFF_RESOURCE_TYPE${SEP}CHECK_ASFF_TYPE${SEP}CHECK_RISK${SEP}CHECK_REMEDIATION${SEP}CHECK_DOC${SEP}CHECK_CAF_EPIC${SEP}CHECK_RESOURCE_ID${SEP}PROWLER_START_TIME${SEP}ACCOUNT_DETAILS_EMAIL${SEP}ACCOUNT_DETAILS_NAME${SEP}ACCOUNT_DETAILS_ARN${SEP}ACCOUNT_DETAILS_ORG${SEP}ACCOUNT_DETAILS_TAGS" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV } diff --git a/include/organizations_metadata b/include/organizations_metadata new file mode 100644 index 00000000..bb51df83 --- /dev/null +++ b/include/organizations_metadata @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Prowler - the handy cloud security tool (copyright 2018) by Toni de la Fuente +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +# Gets account details with a given ACCOUNT_ID. +# Prowler requires organizations:ListAccounts* and organizations:ListTagsForResource +# in the management account in order to get that data. SecurityAudit managed policy includes them. + +# Account Tags are in json format with comma, however they are converted to Base64 +# in order to avoid breaking the CSV or JSON. To use them a post-processor is needed. + +get_orgs_account_details(){ + echo " Prowler is getting details from the AWS Organizations Management Account: ${MANAGEMENT_ACCOUNT_ID}..." + # Assume role to recover AWS Organizations metadata + assume_role + + # The following code requires organizations:ListTagsForResource + ACCOUNTS_DETAILS=$($AWSCLI $PROFILE_OPT --region "${REGION}" organizations list-accounts --output json 2>&1) + if ! grep -q -E 'AccessDenied|UnauthorizedOperation|AuthorizationError' <<< "${ACCOUNTS_DETAILS}" + then + # Prowler gets only ACTIVE accounts details + ACCOUNT_DETAILS_EMAIL=$(jq -r --arg ACCOUNT_ID "${ACCOUNT_NUM}" '.Accounts[] | select(.Status == "ACTIVE") | select(.Id == $ACCOUNT_ID) | "\(.Email)"' <<< "${ACCOUNTS_DETAILS}") + ACCOUNT_DETAILS_NAME=$(jq -r --arg ACCOUNT_ID "${ACCOUNT_NUM}" '.Accounts[] | select(.Status == "ACTIVE") | select(.Id == $ACCOUNT_ID) | "\(.Name)"' <<< "${ACCOUNTS_DETAILS}") + ACCOUNT_DETAILS_ARN=$(jq -r --arg ACCOUNT_ID "${ACCOUNT_NUM}" '.Accounts[] | select(.Status == "ACTIVE") | select(.Id == $ACCOUNT_ID) | "\(.Arn)"' <<< "${ACCOUNTS_DETAILS}") + ACCOUNT_DETAILS_ORG=$(jq -r --arg ACCOUNT_ID "${ACCOUNT_NUM}" '.Accounts[] | select(.Status == "ACTIVE") | select(.Id == $ACCOUNT_ID) | "\(.Arn)"' <<< "${ACCOUNTS_DETAILS}" | awk -F/ '{ print $2 }') + ACCOUNT_DETAILS_TAGS=$($AWSCLI $PROFILE_OPT --region "${REGION}" organizations list-tags-for-resource --resource-id "${MANAGEMENT_ACCOUNT_ID}" --output json | jq -c '. | @base64' 2>&1) + else + # textFail "${regx}: Access Denied trying to list AWS Organization accounts. Prowler requires organizations:List*" "$regx" + textInfo "Access Denied trying to list AWS Organization accounts. Prowler requires organizations:List*" + exit 1 + fi +} diff --git a/include/outputs b/include/outputs index 05d075a2..fece23a4 100644 --- a/include/outputs +++ b/include/outputs @@ -64,6 +64,11 @@ PROWLER_PARAMETERS=$@ # $CHECK_REMEDIATION text about remediation # $CHECK_DOC link to related documentation # $CHECK_CAF_EPIC it can be Logging and Monitoring, IAM, Data Protection, Infrastructure Security. Incident Response is not included since CAF has not specific checks on it logs enablement are part of Logging and Monitoring. +# $ACCOUNT_DETAILS_EMAIL +# $ACCOUNT_DETAILS_NAME +# $ACCOUNT_DETAILS_ARN +# $ACCOUNT_DETAILS_ORG +# $ACCOUNT_DETAILS_TAGS # Ensure that output directory always exists when -M is used if [[ $MODE ]];then @@ -102,7 +107,7 @@ textPass(){ REPREGION=$REGION fi if [[ "${MODES[@]}" =~ "csv" ]]; then - echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}$CHECK_RESULT${SEP}$ITEM_SCORED${SEP}$ITEM_CIS_LEVEL${SEP}$TITLE_TEXT${SEP}$CHECK_RESULT_EXTENDED${SEP}$CHECK_ASFF_COMPLIANCE_TYPE${SEP}$CHECK_SEVERITY${SEP}$CHECK_SERVICENAME${SEP}$CHECK_ASFF_RESOURCE_TYPE${SEP}$CHECK_ASFF_TYPE${SEP}$CHECK_RISK${SEP}$CHECK_REMEDIATION${SEP}$CHECK_DOC${SEP}$CHECK_CAF_EPIC${SEP}$CHECK_RESOURCE_ID${SEP}$PROWLER_START_TIME" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV + echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}$CHECK_RESULT${SEP}$ITEM_SCORED${SEP}$ITEM_CIS_LEVEL${SEP}$TITLE_TEXT${SEP}$CHECK_RESULT_EXTENDED${SEP}$CHECK_ASFF_COMPLIANCE_TYPE${SEP}$CHECK_SEVERITY${SEP}$CHECK_SERVICENAME${SEP}$CHECK_ASFF_RESOURCE_TYPE${SEP}$CHECK_ASFF_TYPE${SEP}$CHECK_RISK${SEP}$CHECK_REMEDIATION${SEP}$CHECK_DOC${SEP}$CHECK_CAF_EPIC${SEP}$CHECK_RESOURCE_ID${SEP}$PROWLER_START_TIME${SEP}$ACCOUNT_DETAILS_EMAIL${SEP}$ACCOUNT_DETAILS_NAME${SEP}$ACCOUNT_DETAILS_ARN${SEP}$ACCOUNT_DETAILS_ORG${SEP}$ACCOUNT_DETAILS_TAGS" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV fi if [[ "${MODES[@]}" =~ "json" ]]; then generateJsonOutput "$1" "Pass" "$CHECK_RESOURCE_ID" >> ${OUTPUT_FILE_NAME}.$EXTENSION_JSON @@ -144,7 +149,7 @@ textInfo(){ REPREGION=$REGION fi if [[ "${MODES[@]}" =~ "csv" ]]; then - echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}$CHECK_RESULT${SEP}$ITEM_SCORED${SEP}$ITEM_CIS_LEVEL${SEP}$TITLE_TEXT${SEP}$CHECK_RESULT_EXTENDED${SEP}$CHECK_ASFF_COMPLIANCE_TYPE${SEP}$CHECK_SEVERITY${SEP}$CHECK_SERVICENAME${SEP}$CHECK_ASFF_RESOURCE_TYPE${SEP}$CHECK_ASFF_TYPE${SEP}$CHECK_RISK${SEP}$CHECK_REMEDIATION${SEP}$CHECK_DOC${SEP}$CHECK_CAF_EPIC${SEP}$CHECK_RESOURCE_ID${SEP}$PROWLER_START_TIME" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV + echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}$CHECK_RESULT${SEP}$ITEM_SCORED${SEP}$ITEM_CIS_LEVEL${SEP}$TITLE_TEXT${SEP}$CHECK_RESULT_EXTENDED${SEP}$CHECK_ASFF_COMPLIANCE_TYPE${SEP}$CHECK_SEVERITY${SEP}$CHECK_SERVICENAME${SEP}$CHECK_ASFF_RESOURCE_TYPE${SEP}$CHECK_ASFF_TYPE${SEP}$CHECK_RISK${SEP}$CHECK_REMEDIATION${SEP}$CHECK_DOC${SEP}$CHECK_CAF_EPIC${SEP}$CHECK_RESOURCE_ID${SEP}$PROWLER_START_TIME${SEP}$ACCOUNT_DETAILS_EMAIL${SEP}$ACCOUNT_DETAILS_NAME${SEP}$ACCOUNT_DETAILS_ARN${SEP}$ACCOUNT_DETAILS_ORG${SEP}$ACCOUNT_DETAILS_TAGS" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV fi if [[ "${MODES[@]}" =~ "json" ]]; then generateJsonOutput "$1" "Info" "$CHECK_RESOURCE_ID" >> ${OUTPUT_FILE_NAME}.${EXTENSION_JSON} @@ -209,7 +214,7 @@ textFail(){ fi if [[ "${MODES[@]}" =~ "csv" ]]; then - echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}$CHECK_RESULT${SEP}$ITEM_SCORED${SEP}$ITEM_CIS_LEVEL${SEP}$TITLE_TEXT${SEP}$CHECK_RESULT_EXTENDED${SEP}$CHECK_ASFF_COMPLIANCE_TYPE${SEP}$CHECK_SEVERITY${SEP}$CHECK_SERVICENAME${SEP}$CHECK_ASFF_RESOURCE_TYPE${SEP}$CHECK_ASFF_TYPE${SEP}$CHECK_RISK${SEP}$CHECK_REMEDIATION${SEP}$CHECK_DOC${SEP}$CHECK_CAF_EPIC${SEP}$CHECK_RESOURCE_ID${SEP}$PROWLER_START_TIME" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV + echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}$CHECK_RESULT${SEP}$ITEM_SCORED${SEP}$ITEM_CIS_LEVEL${SEP}$TITLE_TEXT${SEP}$CHECK_RESULT_EXTENDED${SEP}$CHECK_ASFF_COMPLIANCE_TYPE${SEP}$CHECK_SEVERITY${SEP}$CHECK_SERVICENAME${SEP}$CHECK_ASFF_RESOURCE_TYPE${SEP}$CHECK_ASFF_TYPE${SEP}$CHECK_RISK${SEP}$CHECK_REMEDIATION${SEP}$CHECK_DOC${SEP}$CHECK_CAF_EPIC${SEP}$CHECK_RESOURCE_ID${SEP}$PROWLER_START_TIME${SEP}$ACCOUNT_DETAILS_EMAIL${SEP}$ACCOUNT_DETAILS_NAME${SEP}$ACCOUNT_DETAILS_ARN${SEP}$ACCOUNT_DETAILS_ORG${SEP}$ACCOUNT_DETAILS_TAGS" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV fi if [[ "${MODES[@]}" =~ "json" ]]; then generateJsonOutput "$1" "${level}" "$CHECK_RESOURCE_ID">> ${OUTPUT_FILE_NAME}.${EXTENSION_JSON} @@ -301,6 +306,11 @@ generateJsonOutput(){ --arg CHECK_REMEDIATION "$CHECK_REMEDIATION" \ --arg CHECK_DOC "$CHECK_DOC" \ --arg CHECK_RESOURCE_ID "$resource_id" \ + --arg ACCOUNT_DETAILS_EMAIL "$ACCOUNT_DETAILS_EMAIL" \ + --arg ACCOUNT_DETAILS_NAME "$ACCOUNT_DETAILS_NAME" \ + --arg ACCOUNT_DETAILS_ARN "$ACCOUNT_DETAILS_ARN" \ + --arg ACCOUNT_DETAILS_ORG "$ACCOUNT_DETAILS_ORG" \ + --arg ACCOUNT_DETAILS_TAGS "$ACCOUNT_DETAILS_TAGS" \ -n '{ "Profile": $PROFILE, "Account Number": $ACCOUNT_NUM, @@ -319,7 +329,12 @@ generateJsonOutput(){ "Risk": $CHECK_RISK, "Remediation": $CHECK_REMEDIATION, "Doc link": $CHECK_DOC, - "Resource ID": $CHECK_RESOURCE_ID + "Resource ID": $CHECK_RESOURCE_ID, + "Account Email": $ACCOUNT_DETAILS_EMAIL, + "Account Name": $ACCOUNT_DETAILS_NAME, + "Account ARN": $ACCOUNT_DETAILS_ARN, + "Account Organization": $ACCOUNT_DETAILS_ORG, + "Account tags": $ACCOUNT_DETAILS_TAGS }' } diff --git a/prowler b/prowler index 58d34931..d0460d5b 100755 --- a/prowler +++ b/prowler @@ -110,13 +110,15 @@ USAGE: -z Failed checks do not trigger exit code 3. -Z Specify one or multiple check ids separated by commas that will trigger exit code 3 if they fail. Unspecified checks will not trigger exit code 3. This will override "-z". (i.e.: "-Z check11,check12" will cause check11 and/or check12 to trigger exit code 3) + -O Specify AWS Organizations management account ID. Used to get account details, requires -R. + (requires organizations:ListAccounts* and organizations:ListTagsForResource) -V Show version number & exit. -h This help. " exit } -while getopts ":hlLkqp:r:c:C:g:f:m:M:E:x:enbVsSI:A:R:T:w:N:o:B:D:F:zZ:" OPTION; do +while getopts ":hlLkqp:r:c:C:g:f:m:M:E:x:enbVsSI:A:R:T:w:N:o:B:D:F:zZ:O:" OPTION; do case $OPTION in h ) usage @@ -223,6 +225,10 @@ while getopts ":hlLkqp:r:c:C:g:f:m:M:E:x:enbVsSI:A:R:T:w:N:o:B:D:F:zZ:" OPTION; Z ) FAILED_CHECK_FAILED_SCAN_LIST=$OPTARG ;; + O ) + MANAGEMENT_ACCOUNT_ID=$OPTARG + ;; + : ) echo "" echo "$OPTRED ERROR!$OPTNORMAL -$OPTARG requires an argument" @@ -287,6 +293,7 @@ unset AWS_DEFAULT_OUTPUT . $PROWLER_DIR/include/connection_tests . $PROWLER_DIR/include/securityhub_integration . $PROWLER_DIR/include/junit_integration +. $PROWLER_DIR/include/organizations_metadata # Parses the check file into CHECK_ID's. if [[ -n "$CHECK_FILE" ]]; then @@ -644,6 +651,28 @@ if is_junit_output_enabled; then prepare_junit_output fi +# First, check AWS Organizations Metadata +if [[ -n "${MANAGEMENT_ACCOUNT_ID}" && -n "${ROLE_TO_ASSUME}" ]] +then + # Backing up initial credentials + backupInitialAWSCredentials + + # Backing up initial account to assume + INITIAL_ACCOUNT_TO_ASSUME="${ACCOUNT_TO_ASSUME}" + + # Set the new account to assume to recover AWS Organizations Metadata + ACCOUNT_TO_ASSUME="${MANAGEMENT_ACCOUNT_ID}" + + # Recover AWS Organizations Metadata + get_orgs_account_details + + # Restoring account to assume to -A field after getting account metadata + ACCOUNT_TO_ASSUME="${INITIAL_ACCOUNT_TO_ASSUME}" + + # Restoring initial credentials + restoreInitialAWSCredentials +fi + # Gather account data / test aws cli connectivity getWhoami if [[ -n "${ACCOUNT_TO_ASSUME}" || -n "${ROLE_TO_ASSUME}" ]]; then