mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-11 15:25:10 +00:00
Force the batch-import-findings AWS CLI call to be directed at the region the currently reporting resource is located in, as Security Hub enforces this requirement When checking that Security Hub is enabled, check for all regions that are in scope, e.g. all regions, unless '-f <region>' is used Fixes #618
359 lines
12 KiB
Bash
359 lines
12 KiB
Bash
#!/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"
|
|
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"
|
|
TIMESTAMP=$(get_iso8601_timestamp)
|
|
PROWLER_PARAMETERS=$@
|
|
|
|
# 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(){
|
|
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}PASS${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1" | 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" "INFORMATIONAL")
|
|
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(){
|
|
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}INFO${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1" | 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%:*}"
|
|
ignore_value="${i#*${CHECK_NAME}:}"
|
|
if [[ ${ignore_check_name} != "${CHECK_NAME}" ]]; then
|
|
# not for this check
|
|
continue
|
|
fi
|
|
if [[ $1 =~ .*"${ignore_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
|
|
|
|
if [[ $2 ]]; then
|
|
REPREGION=$2
|
|
else
|
|
REPREGION=$REGION
|
|
fi
|
|
|
|
if [[ "${MODES[@]}" =~ "csv" ]]; then
|
|
echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}${level}${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1" | 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}" "HIGH")
|
|
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 $group_ids"
|
|
else
|
|
echo -e "\n$PURPLE $TITLE_ID $TITLE_TEXT $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 SCORED "$ITEM_SCORED" \
|
|
--arg ITEM_LEVEL "$ITEM_LEVEL" \
|
|
--arg TITLE_ID "$TITLE_ID" \
|
|
--arg REPREGION "$REPREGION" \
|
|
--arg TIMESTAMP "$(get_iso8601_timestamp)" \
|
|
-n '{
|
|
"Profile": $PROFILE,
|
|
"Account Number": $ACCOUNT_NUM,
|
|
"Control": $TITLE_TEXT,
|
|
"Message": $MESSAGE,
|
|
"Status": $STATUS,
|
|
"Scored": $SCORED,
|
|
"Level": $ITEM_LEVEL,
|
|
"Control ID": $TITLE_ID,
|
|
"Region": $REPREGION,
|
|
"Timestamp": $TIMESTAMP,
|
|
}'
|
|
}
|
|
|
|
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
|
|
|
|
local severity=$3
|
|
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 "$severity" \
|
|
--arg TITLE_ID "$TITLE_ID" \
|
|
--arg TYPE "$ASFF_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):\($ACCOUNT_NUM):product/\($ACCOUNT_NUM)/default",
|
|
"ProductFields": {
|
|
"ProviderName": "Prowler",
|
|
"ProviderVersion": $PROWLER_VERSION
|
|
},
|
|
"GeneratorId": "prowler-\($PROWLER_VERSION)",
|
|
"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",
|
|
"Region": $REPREGION
|
|
}
|
|
],
|
|
"Compliance": {
|
|
"Status": $STATUS
|
|
}
|
|
}'
|
|
}
|
|
|
|
generateHtmlOutput(){
|
|
local message=$1
|
|
local status=$2
|
|
if [[ $status == "INFO" ]];then
|
|
echo '<tr class="table-info">' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td><i class="fas fa-info-circle"></i></td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>INFO</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ACCOUNT_NUM'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$REPREGION'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ITEM_LEVEL'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_ID'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_TEXT'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$message'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '</tr>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
fi
|
|
if [[ $status == "PASS" ]];then
|
|
echo '<tr class="p-3 mb-2 bg-success">' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td><i class="fas fa-thumbs-up"></i></td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>PASS</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ACCOUNT_NUM'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$REPREGION'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ITEM_LEVEL'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_ID'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_TEXT'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$message'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '</tr>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
fi
|
|
if [[ $status == "FAIL" ]];then
|
|
echo '<tr class="table-danger" >' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td> <i class="fas fa-thumbs-down"></i></td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>FAIL</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ACCOUNT_NUM'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$REPREGION'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ITEM_LEVEL'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_ID'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_TEXT'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$message'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '</tr>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
fi
|
|
if [[ $status == "WARNING" ]];then
|
|
echo '<tr class="table-warning">' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td><i class="fas fa-exclamation-triangle"></i></td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>WARN</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ACCOUNT_NUM'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$REPREGION'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$ITEM_LEVEL'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_ID'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$TITLE_TEXT'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '<td>'$message'</td>' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
echo '</tr>'>> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML
|
|
fi
|
|
}
|