#!/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. # Output formatting functions EXTENSION_CSV="csv" EXTENSION_JSON="json" EXTENSION_ASFF="asff-json" EXTENSION_TEXT="txt" EXTENSION_HTML="html" OUTPUT_DATE=$(date -u +"%Y%m%d%H%M%S") OUTPUT_DIR="${PROWLER_DIR}/output" # default output if none OUTPUT_FILE_NAME="${OUTPUT_DIR}/prowler-output-${ACCOUNT_NUM}-${OUTPUT_DATE}" HTML_LOGO_URL="https://github.com/toniblyx/prowler/" #HTML_LOGO_IMG="https://raw.githubusercontent.com/toniblyx/prowler/master/util/html/prowler-logo.png" HTML_LOGO_IMG="https://github.com/toniblyx/prowler/raw/2.4/util/html/prowler-logo-new.png" TIMESTAMP=$(get_iso8601_timestamp) PROWLER_PARAMETERS=$@ # Available parameters for outputs formats (implemented this in CSV from v2.4): # $PROFILE profile used to run Prowler (--profile in AWS CLI) # $ACCOUNT_NUM AWS Account ID # $REPREGION AWS region scanned # $TITLE_ID Numeric identifier of each check (1.2, 2.3, etc), originally based on CIS checks. # $CHECK_RESULT values can be PASS, FAIL, INFO or WARNING if whitelisted # $ITEM_SCORED corresponds to CHECK_SCORED, values can be Scored/Not Scored. This is CIS only, will be deprecated in Prowler. # $ITEM_LEVEL corresponds to CHECK_TYPE_ currently only for CIS Level 1, CIS Level 2 and Extras (all checks not part of CIS) # $TITLE_TEXT corresponds to CHECK_TITLE_ shows title of each check # $CHECK_RESULT_EXTENDED shows response of each check per resource like sg-123438 is open! # $CHECK_ASFF_COMPLIANCE_TYPE specify type from taxonomy https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format-type-taxonomy.html # $CHECK_SEVERITY severity Low, Medium, High, Critical # $CHECK_SERVICENAME AWS service name short name # $CHECK_ASFF_RESOURCE_TYPE values from https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html#asff-resources # $CHECK_ASFF_TYPE generic type from taxonomy here https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format-type-taxonomy.html # $CHECK_RISK text about risk # $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. # Ensure that output directory always exists when -M is used if [[ $MODE ]];then mkdir -p "${OUTPUT_DIR}" if [[ "${MODES[@]}" =~ "html" ]]; then addHtmlHeader > ${OUTPUT_FILE_NAME}.$EXTENSION_HTML HTML_REPORT_INIT="1" fi fi if [[ $PROFILE == "" ]];then PROFILE="ENV" fi textPass(){ CHECK_RESULT="PASS" CHECK_RESULT_EXTENDED="$1" if [[ "$QUIET" == 1 ]]; then return fi PASS_COUNTER=$((PASS_COUNTER+1)) if [[ $2 ]]; then REPREGION=$2 else 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_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" | tee -a ${OUTPUT_FILE_NAME}.$EXTENSION_CSV fi if [[ "${MODES[@]}" =~ "json" ]]; then generateJsonOutput "$1" "Pass" | tee -a ${OUTPUT_FILE_NAME}.$EXTENSION_JSON fi if [[ "${MODES[@]}" =~ "json-asff" ]]; then JSON_ASFF_OUTPUT=$(generateJsonAsffOutput "$1" "PASSED") echo "${JSON_ASFF_OUTPUT}" | tee -a $OUTPUT_FILE_NAME.$EXTENSION_ASFF if [[ "${SEND_TO_SECURITY_HUB}" -eq 1 ]]; then sendToSecurityHub "${JSON_ASFF_OUTPUT}" "${REPREGION}" fi fi if is_junit_output_enabled; then output_junit_success "$1" fi if [[ "${MODES[@]}" =~ "mono" ]]; then echo " $OK PASS!$NORMAL $1" | tee -a ${OUTPUT_FILE_NAME}.$EXTENSION_TEXT fi if [[ "${MODES[@]}" =~ "text" || "${MODES[@]}" =~ "mono" ]]; then echo " $OK PASS!$NORMAL $1" fi if [[ "${MODES[@]}" =~ "html" ]]; then generateHtmlOutput "$1" "PASS" fi } textInfo(){ CHECK_RESULT="INFO" CHECK_RESULT_EXTENDED="$1" if [[ "$QUIET" == 1 ]]; then return fi if [[ $2 ]]; then REPREGION=$2 else 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_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" | tee -a ${OUTPUT_FILE_NAME}.$EXTENSION_CSV fi if [[ "${MODES[@]}" =~ "json" ]]; then generateJsonOutput "$1" "Info" | tee -a ${OUTPUT_FILE_NAME}.${EXTENSION_JSON} fi if is_junit_output_enabled; then output_junit_info "$1" fi if [[ "${MODES[@]}" =~ "mono" ]]; then echo " $NOTICE INFO! $1 $NORMAL" | tee -a ${OUTPUT_FILE_NAME}.$EXTENSION_TEXT fi if [[ "${MODES[@]}" =~ "text" ]]; then echo " $NOTICE INFO! $1 $NORMAL" fi if [[ "${MODES[@]}" =~ "html" ]]; then generateHtmlOutput "$1" "INFO" fi } textFail(){ ## ignore whitelists for current check level="FAIL" colorcode="$BAD" while read -r i; do ignore_check_name="${i%%:*}" # Check name is everything up to the first : ignore_value="${i#*${CHECK_NAME}:}" # Ignore value is everything after the first : # Check to see if ignore value appears anywhere within log message. resource_value=".*${ignore_value}.*" if [[ ${ignore_check_name} != "${CHECK_NAME}" ]]; then # not for this check continue fi if [[ $1 =~ ${resource_value} ]]; then level="WARNING" colorcode="$WARNING" break fi done <<< "$IGNORES" # only set non-0 exit code on FAIL mode, WARN is ok if [[ "$level" == "FAIL" ]]; then FAIL_COUNTER=$((FAIL_COUNTER+1)) EXITCODE=3 fi CHECK_RESULT=$level CHECK_RESULT_EXTENDED="$1" if [[ $2 ]]; then REPREGION=$2 else 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_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" | tee -a ${OUTPUT_FILE_NAME}.$EXTENSION_CSV fi if [[ "${MODES[@]}" =~ "json" ]]; then generateJsonOutput "$1" "${level}" | tee -a ${OUTPUT_FILE_NAME}.${EXTENSION_JSON} fi if [[ "${MODES[@]}" =~ "json-asff" ]]; then JSON_ASFF_OUTPUT=$(generateJsonAsffOutput "$1" "${level}") echo "${JSON_ASFF_OUTPUT}" | tee -a ${OUTPUT_FILE_NAME}.${EXTENSION_ASFF} if [[ "${SEND_TO_SECURITY_HUB}" -eq 1 ]]; then sendToSecurityHub "${JSON_ASFF_OUTPUT}" "${REPREGION}" fi fi if is_junit_output_enabled; then if [[ "${level}" == "FAIL" ]]; then output_junit_failure "$1" elif [[ "${level}" == "WARNING" ]]; then output_junit_skipped "$1" fi fi if [[ "${MODES[@]}" =~ "mono" ]]; then echo " $colorcode ${level}! $1 $NORMAL" | tee -a ${OUTPUT_FILE_NAME}.$EXTENSION_TEXT fi if [[ "${MODES[@]}" =~ "text" ]]; then echo " $colorcode ${level}! $1 $NORMAL" fi if [[ "${MODES[@]}" =~ "html" ]]; then generateHtmlOutput "$1" "${level}" fi } textTitle(){ CHECKS_COUNTER=$((CHECKS_COUNTER+1)) TITLE_ID=$1 if [[ $NUMERAL ]]; then # Left-pad the check ID with zeros to simplify sorting, e.g. 1.1 -> 1.01 TITLE_ID=$(awk -F'.' '{ printf "%d.%02d", $1, $2 }' <<< "$TITLE_ID") 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 local group_ids if [[ -n "$5" ]]; then group_ids="$CYAN [$5] $NORMAL" fi if [[ "${MODES[@]}" =~ "csv" ]]; then >&2 echo "$TITLE_ID $TITLE_TEXT" | tee -a ${OUTPUT_FILE_NAME}.${EXTENSION_CSV} elif [[ "${MODES[@]}" =~ "json" || "${MODES[@]}" =~ "json-asff" ]]; then : else if [[ "$ITEM_SCORED" == "Scored" ]]; then echo -e "\n$BLUE $TITLE_ID $NORMAL $TITLE_TEXT $6 $group_ids " else echo -e "\n$PURPLE $TITLE_ID $TITLE_TEXT $6 $NORMAL $group_ids " fi fi } generateJsonOutput(){ local message=$1 local status=$2 jq -M -c \ --arg PROFILE "$PROFILE" \ --arg ACCOUNT_NUM "$ACCOUNT_NUM" \ --arg TITLE_TEXT "$TITLE_TEXT" \ --arg MESSAGE "$(echo -e "${message}" | sed -e 's/^[[:space:]]*//')" \ --arg STATUS "$status" \ --arg SEVERITY "$CHECK_SEVERITY" \ --arg SCORED "$ITEM_SCORED" \ --arg ITEM_LEVEL "$ITEM_LEVEL" \ --arg TITLE_ID "$TITLE_ID" \ --arg REPREGION "$REPREGION" \ --arg TYPE "$CHECK_ASFF_COMPLIANCE_TYPE" \ --arg TIMESTAMP "$(get_iso8601_timestamp)" \ --arg SERVICENAME "$CHECK_SERVICENAME" \ -n '{ "Profile": $PROFILE, "Account Number": $ACCOUNT_NUM, "Control": $TITLE_TEXT, "Message": $MESSAGE, "Severity": $SEVERITY, "Status": $STATUS, "Scored": $SCORED, "Level": $ITEM_LEVEL, "Control ID": $TITLE_ID, "Region": $REPREGION, "Timestamp": $TIMESTAMP, "Compliance": $TYPE, "Service": $SERVICENAME }' } generateJsonAsffOutput(){ # UNIQUE_ID must only contain characters from the unreserved characters set defined in section 2.3 of RFC-3986 # Replace any successive non-conforming characters with a single underscore local message=$1 local status=$2 if [[ "$status" == "FAIL" ]]; then status="FAILED" fi jq -M -c \ --arg ACCOUNT_NUM "$ACCOUNT_NUM" \ --arg TITLE_TEXT "$TITLE_TEXT" \ --arg MESSAGE "$(echo -e "${message}" | sed -e 's/^[[:space:]]*//')" \ --arg UNIQUE_ID "$(LC_ALL=C echo -e -n "${message}" | tr -cs '[:alnum:]._~-' '_')" \ --arg STATUS "$status" \ --arg SEVERITY "$(echo $CHECK_SEVERITY| awk '{ print toupper($0) }')" \ --arg TITLE_ID "$TITLE_ID" \ --arg CHECK_ID "$CHECK_ID" \ --arg TYPE "$CHECK_ASFF_COMPLIANCE_TYPE" \ --arg COMPLIANCE_RELATED_REQUIREMENTS "$CHECK_ASFF_COMPLIANCE_TYPE" \ --arg RESOURCE_TYPE "$ASFF_RESOURCE_TYPE" \ --arg REPREGION "$REPREGION" \ --arg TIMESTAMP "$(get_iso8601_timestamp)" \ --arg PROWLER_VERSION "$PROWLER_VERSION" \ --arg AWS_PARTITION "$AWS_PARTITION" \ -n '{ "SchemaVersion": "2018-10-08", "Id": "prowler-\($TITLE_ID)-\($ACCOUNT_NUM)-\($REPREGION)-\($UNIQUE_ID)", "ProductArn": "arn:\($AWS_PARTITION):securityhub:\($REPREGION)::product/prowler/prowler", "RecordState": "ACTIVE", "ProductFields": { "ProviderName": "Prowler", "ProviderVersion": $PROWLER_VERSION }, "GeneratorId": "prowler-\($CHECK_ID)", "AwsAccountId": $ACCOUNT_NUM, "Types": [ $TYPE ], "FirstObservedAt": $TIMESTAMP, "UpdatedAt": $TIMESTAMP, "CreatedAt": $TIMESTAMP, "Severity": { "Label": $SEVERITY }, "Title": $TITLE_TEXT, "Description": $MESSAGE, "Resources": [ { "Type": $RESOURCE_TYPE, "Id": "AWS::::Account:\($ACCOUNT_NUM)", "Partition": $AWS_PARTITION, "Region": $REPREGION } ], "Compliance": { "Status": $STATUS, "RelatedRequirements": [ $COMPLIANCE_RELATED_REQUIREMENTS ] } }' } generateHtmlOutput(){ local message=$1 local status=$2 if [[ $status == "INFO" ]];then echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo 'INFO' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SEVERITY'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ACCOUNT_NUM'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$REPREGION'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_TEXT'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$message'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_CAF_EPIC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_RISK'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_REMEDIATION'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_DOC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML fi if [[ $status == "PASS" ]];then echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo 'PASS' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SEVERITY'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ACCOUNT_NUM'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$REPREGION'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_TEXT'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$message'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_CAF_EPIC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_RISK'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_REMEDIATION'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_DOC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML fi if [[ $status == "FAIL" ]];then echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ' ' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo 'FAIL' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SEVERITY'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ACCOUNT_NUM'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$REPREGION'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_TEXT'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$message'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_CAF_EPIC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_RISK'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_REMEDIATION'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_DOC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML fi if [[ $status == "WARNING" ]];then echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo 'WARN' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SEVERITY'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ACCOUNT_NUM'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$REPREGION'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_TEXT'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$message'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_CAF_EPIC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_RISK'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo '

'$CHECK_REMEDIATION'

' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_DOC'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''>> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML fi }