diff --git a/README.md b/README.md index 6f3fdd1dd8..b87e85d884 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,7 @@ Jump into the section for the database provider you want to use and follow the r - Mac -> `brew install libpq` - Ubuntu -> `sudo apt-get install postgresql-client ` - RHEL/Centos -> `sudo yum install postgresql10` + - Configure a `~/.pgpass` file into the root folder of the user that is going to launch Prowler ([pgpass file doc](https://www.postgresql.org/docs/current/libpq-pgpass.html)), including an extra field at the end of the line, separated by `:`, to name the table, for instance: `hostname:port:database:username:password:prowler_findings` - Create a table in your PostgreSQL database to store the Prowler's data. You can use the following SQL statement to create the table: @@ -372,6 +373,7 @@ account_details_org TEXT, account_details_tags TEXT ); ``` + - Execute Prowler with `-d` flag, for example: `./prowler -M csv -d postgres` > *Note*: This command creates a `csv` output file and stores the Prowler output in the configured PostgreSQL DB. It's an example, `-d` flag **does not** require `-M` to run. diff --git a/include/colors b/include/colors index 43c84b7054..637bfd8684 100644 --- a/include/colors +++ b/include/colors @@ -12,58 +12,19 @@ # specific language governing permissions and limitations under the License. -IFS=',' read -ra MODES <<< "${MODE}" -for MODE in "${MODES[@]}"; do - if [[ "$MODE" != "mono" && "$MODE" != "text" && "$MODE" != "csv" && "$MODE" != "json" && "$MODE" != "json-asff" && "$MODE" != "junit-xml" && "$MODE" != "html" ]]; then - echo -e "${OPTRED}ERROR!$OPTNORMAL Invalid output mode. Choose text, mono, csv, json, json-asff, junit-xml or html. ./prowler -h for help" - EXITCODE=1 - exit $EXITCODE - fi - if [[ "$MODE" == "mono" || "$MODE" == "csv" || "$MODE" == "json" || "$MODE" == "json-asff" ]]; then - MONOCHROME=1 - fi -done - -if [[ $MONOCHROME -eq 1 ]]; then - # Colors - NORMAL='' - WARNING='' # Bad (red) - SECTION='' # Section (yellow) - NOTICE='' # Notice (yellow) - OK='' # Ok (green) - BAD='' # Bad (red) - CYAN='' - BLUE='' - BROWN='' - DARKGRAY='' - GRAY='' - GREEN='' - MAGENTA='' - PURPLE='' - RED='' - YELLOW='' - WHITE='' -else - # Colors - # NOTE: Your editor may NOT show the 0x1b / escape character left of the '[' - NORMAL="" - WARNING="" # Warning (brown) - SECTION="" # Section (yellow) - NOTICE="" # Notice (yellow) - OK="" # Ok (green) - BAD="" # Bad (red) - CYAN="" - BLUE="" - BROWN="" - DARKGRAY="" - GRAY="" - GREEN="" - MAGENTA="" - PURPLE="" - RED="" - YELLOW="" - WHITE="" -fi +# Colors +# NOTE: Your editor may NOT show the 0x1b / escape character left of the '[' +NORMAL="" +WARNING="" # Warning (brown) +NOTICE="" # Notice (yellow) +OK="" # Ok (green) +BAD="" # Bad (red) +CYAN="" +BLUE="" +BROWN="" +MAGENTA="" +RED="" +YELLOW="" printColorsCode(){ if [[ $MONOCHROME -eq 0 ]]; then diff --git a/include/db_connector b/include/db_connector index bfa3d6ddbc..be72b72e8d 100755 --- a/include/db_connector +++ b/include/db_connector @@ -28,7 +28,6 @@ postgresql_connector () { CSV_REGISTRY="${1}" psql -q -U "${PSQL_USER}" -h "${PSQL_HOSTNAME}" -c "copy ${PSQL_TABLE} from stdin with null as E'\'\'' delimiter ','" <<< "${CSV_REGISTRY}" - } db_exit_abnormally() { diff --git a/include/html_report b/include/html_report index cf7218a6ff..7789d420b7 100644 --- a/include/html_report +++ b/include/html_report @@ -18,7 +18,7 @@ addHtmlHeader() { PROFILE="ENV" fi if [[ -z $HTML_REPORT_INIT ]]; then - cat <> "${OUTPUT_FILE_NAME}"."${EXTENSION_HTML}" diff --git a/include/outputs b/include/outputs index 69c5764938..6ad37e4346 100644 --- a/include/outputs +++ b/include/outputs @@ -11,45 +11,12 @@ # 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 -if [[ $OUTPUT_DIR_CUSTOM ]]; then - # output mode has to be set to other than text - if [[ ! " ${MODES[@]} " =~ " text " || ${check_id} == 7.1 || ${check_id} == 7.74 ]]; then - if [[ ! -d $OUTPUT_DIR_CUSTOM ]]; then - echo "$OPTRED ERROR!$OPTNORMAL directory \"$OUTPUT_DIR_CUSTOM\" does not exist." - exit 1 - else - OUTPUT_DIR=$OUTPUT_DIR_CUSTOM - fi - else - echo "$OPTRED ERROR!$OPTNORMAL - Mode (-M) has to be set as well. Use -h for help." - exit 1 - fi -fi -if [ -z ${OUTPUT_FILE_NAME+x} ]; then - OUTPUT_FILE_NAME="${OUTPUT_DIR}/prowler-output-${ACCOUNT_NUM}-${OUTPUT_DATE}" -else - OUTPUT_FILE_NAME="${OUTPUT_DIR}/$OUTPUT_FILE_NAME" -fi -HTML_LOGO_URL="https://github.com/prowler-cloud/prowler/" -HTML_LOGO_IMG="https://github.com/prowler-cloud/prowler/raw/master/util/html/prowler-logo-new.png" -TIMESTAMP=$(get_iso8601_timestamp) -PROWLER_PARAMETERS=$@ - - # Available parameters for outputs formats (implemented this in CSV from v2.4): +# Output format # $PROFILE profile used to run Prowler (--profile in AWS CLI) # $ACCOUNT_NUM AWS Account ID -# $REPREGION AWS region scanned +# $REGION_FROM_CHECK 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 allowlisted # $ITEM_SCORED corresponds to CHECK_SCORED, values can be Scored/Not Scored. This is CIS only, will be deprecated in Prowler. @@ -71,249 +38,227 @@ PROWLER_PARAMETERS=$@ # $ACCOUNT_DETAILS_ORG # $ACCOUNT_DETAILS_TAGS +EXTENSION_CSV="csv" +EXTENSION_JSON="json" +EXTENSION_ASFF="asff.json" +EXTENSION_TEXT="txt" +EXTENSION_HTML="html" +HTML_LOGO_URL="https://github.com/prowler-cloud/prowler/" +HTML_LOGO_IMG="https://github.com/prowler-cloud/prowler/raw/master/util/html/prowler-logo-new.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" +# Set exitcode for codebuild/jenkins-like integrations +set_exitcode () { + if [ "${FAILED_CHECK_FAILED_SCAN}" == 1 ] && [ -z "${FAILED_CHECK_FAILED_SCAN_LIST}" ] + then + EXITCODE=3 + export EXITCODE fi - if [[ "${MODES[@]}" =~ "csv" ]]; then - printCsvHeader + if [[ "${FAILED_CHECK_FAILED_SCAN_LIST}" =~ ${CHECK_NAME} ]] + then + EXITCODE=3 + export EXITCODE fi -fi +} -# textInfo "HTML report will be saved: ${OUTPUT_FILE_NAME}.$EXTENSION_HTML" -# textInfo "JSON ASFF report will be saved: ${OUTPUT_FILE_NAME}.$EXTENSION_ASFF" -# textInfo "CSV report will be saved: ${OUTPUT_FILE_NAME}.$EXTENSION_CSV" -# textInfo "JSON report will be saved: ${OUTPUT_FILE_NAME}.$EXTENSION_JSON" - -if [[ $PROFILE == "" ]];then - PROFILE="ENV" -fi - -textPass(){ - CHECK_RESULT="PASS" - CHECK_RESULT_EXTENDED="$1" - CHECK_RESOURCE_ID="$3" - - if [[ "$QUIET" == 1 ]]; then - return - fi - - PASS_COUNTER=$((PASS_COUNTER+1)) - if [[ $2 ]]; then - REPREGION=$2 - else - REPREGION=$REGION - fi - - CSV_LINE="${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/,/--}" - - if [[ "${MODES[@]}" =~ "csv" ]]; then - # Writting csv file replacing commas - echo "${CSV_LINE}" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV - fi - if [[ "${MODES[@]}" =~ "json" ]]; then - generateJsonOutput "$1" "Pass" "$CHECK_RESOURCE_ID" >> ${OUTPUT_FILE_NAME}.$EXTENSION_JSON - fi - if [[ "${MODES[@]}" =~ "json-asff" ]]; then - JSON_ASFF_OUTPUT=$(generateJsonAsffOutput "$1" "PASSED" "$CHECK_RESOURCE_ID") - echo "${JSON_ASFF_OUTPUT}" >> $OUTPUT_FILE_NAME.$EXTENSION_ASFF - if [[ "${SEND_TO_SECURITY_HUB}" -eq 1 ]]; then - sendToSecurityHub "${JSON_ASFF_OUTPUT}" "${REPREGION}" +# Add header to certain output files readed from mode +output_files_init() { + for MODE in "${MODES[@]}"; + do + if [ "${MODE}" == 'html' ] + then + addHtmlHeader + HTML_REPORT_INIT="1" + export HTML_REPORT_INIT fi + if [ "${MODE}" == 'csv' ] + then + printCsvHeader + fi + done +} + +# Check if resource checked is allowlisted +allowlist_check() { + # Checked value is the whole log message that comes as argument + CHECKED_VALUE=${1} + # Default value of result is Fail + CHECK_RESULT='FAIL' + ## ignore allowlists for current check + while read -r excluded_item; do + # IGNORE_CHECK_NAME is the check with resources allowlisted + # RESOURCE_VALUE is what it comes after 'CHECK_NAME:' + IGNORE_CHECK_NAME=$(awk -F ":" '{print $1}' <<< "${excluded_item}") + RESOURCE_VALUE=$(awk -F "${CHECK_NAME}:" '{print $2}' <<< "${excluded_item}") + if [[ "${IGNORE_CHECK_NAME}" == "${CHECK_NAME}" ]] + then + if [[ "${CHECKED_VALUE}" =~ ${RESOURCE_VALUE} ]] + then + CHECK_RESULT="WARNING" + break + fi + fi + + done <<< "$IGNORES" + echo "${CHECK_RESULT}" +} + +is_quiet() { + IS_QUIET="${1}" + if [ "${IS_QUIET}" -eq 1 ] + then + return 0 + else + return 1 fi - if is_junit_output_enabled; then - output_junit_success "$1" +} + +general_output() { + CHECK_RESULT="${1}" + CHECK_RESULT_EXTENDED="${2}" + CHECK_RESOURCE_ID="${3}" + REGION_FROM_CHECK="${4}" + CSV_LINE="${PROFILE/,/--}${SEP}${ACCOUNT_NUM/,/--}${SEP}${REGION_FROM_CHECK/,/--}${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/,/--}" + COLOR_CODE="$OK" + + # Check if color is different than normal (pass) + if [ "${CHECK_RESULT}" == 'INFO' ] + then + COLOR_CODE="${NOTICE}" + elif [ "${CHECK_RESULT}" == 'FAIL' ] + then + COLOR_CODE="${BAD}" + elif [ "${CHECK_RESULT}" == 'WARNING' ] + then + COLOR_CODE="${WARNING}" fi - if [[ "${MODES[@]}" =~ "mono" ]]; then - echo " $OK PASS!$NORMAL $1" >> ${OUTPUT_FILE_NAME}.$EXTENSION_TEXT - fi - # if [[ "${MODES[@]}" =~ "text" || "${MODES[@]}" =~ "mono" ]]; then - # runs showing console output no mater what output is selected - echo " $OK PASS!$NORMAL $1" - # fi - if [[ "${MODES[@]}" =~ "html" ]]; then - generateHtmlOutput "$1" "PASS" + # Check if region is passed + if [ -z "${REGION_FROM_CHECK}" ] + then + REGION_FROM_CHECK=$REGION fi + # Iterating over input modes + for MODE in "${MODES[@]}"; + do + if [ "${MODE}" == 'html' ] + then + generateHtmlOutput "${CHECK_RESULT_EXTENDED}" "${CHECK_RESULT}" + elif [ "${MODE}" == 'csv' ] + then + echo "${CSV_LINE}" >> "${OUTPUT_FILE_NAME}"."${EXTENSION_CSV}" + elif [ "${MODE}" == 'json' ] + then + generateJsonOutput "${CHECK_RESULT_EXTENDED}" "${CHECK_RESULT}" "$CHECK_RESOURCE_ID" >> "${OUTPUT_FILE_NAME}"."${EXTENSION_JSON}" + elif [ "${MODE}" == 'json-asff' ] + then + JSON_ASFF_OUTPUT=$(generateJsonAsffOutput "${CHECK_RESULT_EXTENDED}" "PASSED" "$CHECK_RESOURCE_ID") + echo "${JSON_ASFF_OUTPUT}" >> "${OUTPUT_FILE_NAME}"."${EXTENSION_ASFF}" + if [[ "${SEND_TO_SECURITY_HUB}" -eq 1 ]]; then + sendToSecurityHub "${JSON_ASFF_OUTPUT}" "${REGION_FROM_CHECK}" + fi + elif is_junit_output_enabled + then + if [ "${CHECK_RESULT}" == 'PASS' ] + then + output_junit_success "${CHECK_RESULT_EXTENDED}" + elif [ "${CHECK_RESULT}" == 'INFO' ] + then + output_junit_info "${CHECK_RESULT_EXTENDED}" + elif [ "${CHECK_RESULT}" == 'FAIL' ] + then + output_junit_failure "${CHECK_RESULT_EXTENDED}" + elif [ "${CHECK_RESULT}" == 'WARNING' ] + then + output_junit_skipped "${CHECK_RESULT_EXTENDED}" + fi + elif [ "${MODE}" == 'mono' ] + then + echo " $COLOR_CODE ${CHECK_RESULT}! $NORMAL ${CHECK_RESULT_EXTENDED}">> "${OUTPUT_FILE_NAME}"."${EXTENSION_TEXT}" + fi + done + + # Print in screen + echo " $COLOR_CODE ${CHECK_RESULT}!$NORMAL ${CHECK_RESULT_EXTENDED}" #checking database provider if [[ ${DATABASE_PROVIDER} == 'postgresql' ]] then postgresql_connector "${CSV_LINE}" fi +} + +textPass(){ + CHECK_RESULT="PASS" + CHECK_RESULT_EXTENDED="${1}" + CHECK_RESOURCE_ID="${3}" + CHECK_REGION="${2}" + + if is_quiet "${QUIET}" + then + return + fi + general_output "${CHECK_RESULT}" "${CHECK_RESULT_EXTENDED}" "${CHECK_RESOURCE_ID}" "${CHECK_REGION}" } textInfo(){ CHECK_RESULT="INFO" - CHECK_RESULT_EXTENDED="$1" - CHECK_RESOURCE_ID="$3" + CHECK_RESULT_EXTENDED="${1}" + CHECK_RESOURCE_ID="${3}" + CHECK_REGION="${2}" - if [[ "$QUIET" == 1 ]]; then + if is_quiet "${QUIET}" + then return fi - if [[ $2 ]]; then - REPREGION=$2 - else - REPREGION=$REGION - fi - - CSV_LINE="${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/,/--}" - - if [[ "${MODES[@]}" =~ "csv" ]]; then - # Writing csv file replacing commas - echo "${CSV_LINE}" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV - fi - if [[ "${MODES[@]}" =~ "json" ]]; then - generateJsonOutput "$1" "Info" "$CHECK_RESOURCE_ID" >> ${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" >> ${OUTPUT_FILE_NAME}.$EXTENSION_TEXT - fi - # if [[ "${MODES[@]}" =~ "text" ]]; then - echo " $NOTICE INFO! $1 $NORMAL" - # fi - if [[ "${MODES[@]}" =~ "html" ]]; then - generateHtmlOutput "$1" "INFO" "$CHECK_RESOURCE_ID" - fi - - #checking database provider - if [[ ${DATABASE_PROVIDER} == 'postgresql' ]] - then - postgresql_connector "${CSV_LINE}" - fi + general_output "${CHECK_RESULT}" "${CHECK_RESULT_EXTENDED}" "${CHECK_RESOURCE_ID}" "${CHECK_REGION}" } textFail(){ - ## ignore allowlists for current check - level="FAIL" - colorcode="$BAD" - while read -r excluded_item; do - # ignore_check_name is the check with resources allowlisted - ignore_check_name=$(awk -F ":" '{print $1}' <<< "${excluded_item}") - # Resource value is what it comes after CHECK_NAME: : - resource_value=$(awk -F "$CHECK_NAME:" '{print $2}' <<< "${excluded_item}") - # Checked value is the whole log message that comes as argument - checked_value=$1 - if [[ "${ignore_check_name}" != "${CHECK_NAME}" ]]; then - # not for this check - continue - fi - # To set WARNING flag checked_value have to include value of resource_value - # If it is treated as only expanse (*[]*) will not detect regex like [:alpha:] - if [[ "${checked_value}" =~ ${resource_value} ]]; then - level="WARNING" - colorcode="${WARNING}" - break - fi - done <<< "$IGNORES" + + CHECK_RESULT='FAIL' + CHECK_RESULT_EXTENDED="${1}" + CHECK_RESOURCE_ID="${3}" + CHECK_REGION="${2}" + # Check if resources are whitelisted + CHECK_RESULT=$(allowlist_check "${CHECK_RESULT_EXTENDED}") + # only set non-0 exit code on FAIL mode, WARN is ok - if [[ "$level" == "FAIL" ]]; then - FAIL_COUNTER=$((FAIL_COUNTER+1)) - if [ "$FAILED_CHECK_FAILED_SCAN" == 1 ] && [ -z "$FAILED_CHECK_FAILED_SCAN_LIST" ] ; then - EXITCODE=3 - fi - if [[ "${FAILED_CHECK_FAILED_SCAN_LIST[@]}" =~ "$CHECK_NAME" ]]; then - EXITCODE=3 - fi - fi - - CHECK_RESULT=$level - CHECK_RESULT_EXTENDED="$1" - CHECK_RESOURCE_ID="$3" - - if [[ $2 ]]; then - REPREGION=$2 - else - REPREGION=$REGION - fi - - CSV_LINE="${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/,/--}" - - if [[ "${MODES[@]}" =~ "csv" ]]; then - # Writing csv file replacing commas - echo "${CSV_LINE}" >> ${OUTPUT_FILE_NAME}.$EXTENSION_CSV - fi - if [[ "${MODES[@]}" =~ "json" ]]; then - generateJsonOutput "$1" "${level}" "$CHECK_RESOURCE_ID">> ${OUTPUT_FILE_NAME}.${EXTENSION_JSON} - fi - if [[ "${MODES[@]}" =~ "json-asff" ]]; then - JSON_ASFF_OUTPUT=$(generateJsonAsffOutput "$1" "${level}" "$CHECK_RESOURCE_ID") - echo "${JSON_ASFF_OUTPUT}" >> ${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" >> ${OUTPUT_FILE_NAME}.$EXTENSION_TEXT - fi - # if [[ "${MODES[@]}" =~ "text" ]]; then - echo " $colorcode ${level}! $1 $NORMAL" - # fi - if [[ "${MODES[@]}" =~ "html" ]]; then - generateHtmlOutput "$1" "${level}" "$CHECK_RESOURCE_ID" - fi - - #checking database provider - if [[ ${DATABASE_PROVIDER} == 'postgresql' ]] + if [[ "${CHECK_RESULT}" == "FAIL" ]] then - postgresql_connector "${CSV_LINE}" + set_exitcode fi + + general_output "${CHECK_RESULT}" "${CHECK_RESULT_EXTENDED}" "${CHECK_RESOURCE_ID}" "${CHECK_REGION}" } textTitle(){ - CHECKS_COUNTER=$((CHECKS_COUNTER+1)) - TITLE_ID="$1" + + TITLE_ID="${1}" + TITLE_TEXT="${2}" + CHECK_ASFF_COMP_TYPE="${6}" + 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 local CHECK_SERVICENAME="$MAGENTA$3$NORMAL" local CHECK_SEVERITY="$BROWN[$4]$NORMAL" - case "$6" in + case "${CHECK_ASFF_COMP_TYPE}" in LEVEL1) ITEM_CIS_LEVEL="CIS Level 1";; LEVEL2) ITEM_CIS_LEVEL="CIS Level 2";; EXTRA) ITEM_CIS_LEVEL="Extra";; SUPPORT) ITEM_CIS_LEVEL="Support";; *) ITEM_CIS_LEVEL="Unspecified or Invalid";; esac - - local group_ids - # if [[ -n "$4" ]]; then - group_ids="$CYAN[$5]$NORMAL" - # fi - - # if [[ "${MODES[@]}" =~ "csv" ]]; then - # >&2 echo "$TITLE_ID $TITLE_TEXT" >> ${OUTPUT_FILE_NAME}.${EXTENSION_CSV} - # elif [[ "${MODES[@]}" =~ "json" || "${MODES[@]}" =~ "json-asff" ]]; then - # : - # else - # # if [[ "$ITEM_SCORED" == "Scored" ]]; then - # echo -e "$TITLE_ID $CHECK_SERVICENAME $TITLE_TEXT $CHECK_SEVERITY $group_ids " - echo -e "$TITLE_ID $TITLE_TEXT - $CHECK_SERVICENAME $CHECK_SEVERITY" - # # else - # # echo -e "\n$PURPLE $TITLE_ID $TITLE_TEXT $6 $NORMAL $group_ids " - # # fi - # fi + + echo -e "$TITLE_ID $TITLE_TEXT - $CHECK_SERVICENAME $CHECK_SEVERITY" } generateJsonOutput(){ @@ -330,7 +275,7 @@ generateJsonOutput(){ --arg SCORED "$ITEM_SCORED" \ --arg ITEM_CIS_LEVEL "$ITEM_CIS_LEVEL" \ --arg TITLE_ID "$TITLE_ID" \ - --arg REPREGION "$REPREGION" \ + --arg REGION_FROM_CHECK "$REGION_FROM_CHECK" \ --arg TYPE "$CHECK_ASFF_COMPLIANCE_TYPE" \ --arg TIMESTAMP "$(get_iso8601_timestamp)" \ --arg SERVICENAME "$CHECK_SERVICENAME" \ @@ -354,7 +299,7 @@ generateJsonOutput(){ "Scored": $SCORED, "Level": $ITEM_CIS_LEVEL, "Control ID": $TITLE_ID, - "Region": $REPREGION, + "Region": $REGION_FROM_CHECK, "Timestamp": $TIMESTAMP, "Compliance": $TYPE, "Service": $SERVICENAME, @@ -399,15 +344,15 @@ generateJsonAsffOutput(){ --arg TYPE "$CHECK_ASFF_COMPLIANCE_TYPE" \ --arg COMPLIANCE_RELATED_REQUIREMENTS "$CHECK_ASFF_COMPLIANCE_TYPE" \ --arg RESOURCE_TYPE "$CHECK_ASFF_RESOURCE_TYPE" \ - --arg REPREGION "$REPREGION" \ + --arg REGION_FROM_CHECK "$REGION_FROM_CHECK" \ --arg TIMESTAMP "$(get_iso8601_timestamp)" \ --arg PROWLER_VERSION "$PROWLER_VERSION" \ --arg AWS_PARTITION "$AWS_PARTITION" \ --arg CHECK_RESOURCE_ID "$resource_id" \ -n '{ "SchemaVersion": "2018-10-08", - "Id": "prowler-\($TITLE_ID)-\($ACCOUNT_NUM)-\($REPREGION)-\($UNIQUE_ID)", - "ProductArn": "arn:\($AWS_PARTITION):securityhub:\($REPREGION)::product/prowler/prowler", + "Id": "prowler-\($TITLE_ID)-\($ACCOUNT_NUM)-\($REGION_FROM_CHECK)-\($UNIQUE_ID)", + "ProductArn": "arn:\($AWS_PARTITION):securityhub:\($REGION_FROM_CHECK)::product/prowler/prowler", "RecordState": "ACTIVE", "ProductFields": { "ProviderName": "Prowler", @@ -432,7 +377,7 @@ generateJsonAsffOutput(){ "Type": $RESOURCE_TYPE, "Id": $CHECK_RESOURCE_ID, "Partition": $AWS_PARTITION, - "Region": $REPREGION + "Region": $REGION_FROM_CHECK } ], "Compliance": { @@ -466,7 +411,7 @@ generateHtmlOutput(){ echo ' '$status'' >> ${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 ' '$REGION_FROM_CHECK'' >> ${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 diff --git a/prowler b/prowler index 47cbe93426..fa05cf4101 100755 --- a/prowler +++ b/prowler @@ -41,6 +41,7 @@ EXITCODE=0 SEND_TO_SECURITY_HUB=0 FAILED_CHECK_FAILED_SCAN=1 PROWLER_START_TIME=$( date -u +"%Y-%m-%dT%H:%M:%S%z" ) +OUTPUT_DATE=$(date -u +"%Y%m%d%H%M%S") TITLE_ID="" TITLE_TEXT="CALLER ERROR - UNSET TITLE" ALLOWLIST_FILE="" @@ -309,8 +310,7 @@ unset AWS_DEFAULT_OUTPUT . $PROWLER_DIR/include/db_connector - - +#Parsing input options # Parses the check file into CHECK_ID's. if [[ -n "$CHECK_FILE" ]]; then if [[ -f $CHECK_FILE ]]; then @@ -369,11 +369,66 @@ then fi fi +# Check modes +if [ -n "${MODE}" ] +then + IFS=',' read -ra MODES <<< "${MODE}" + for MODE in "${MODES[@]}"; do + if [[ "$MODE" != "mono" && "$MODE" != "text" && "$MODE" != "csv" && "$MODE" != "json" && "$MODE" != "json-asff" && "$MODE" != "junit-xml" && "$MODE" != "html" ]]; then + echo -e "${OPTRED}ERROR!$OPTNORMAL Invalid output mode. Choose text, mono, csv, json, json-asff, junit-xml or html. ./prowler -h for help" + EXITCODE=1 + exit $EXITCODE + fi + done +fi + +#Check output dir custom +if [ -n "${OUTPUT_DIR_CUSTOM}" ] +then + # If dir custom, mode has to be enabled (if you provide a custom directory it's because you want to store something) + if [ "${MODE}" != 'text' ] + then + # Check if custom folder exists + if [ ! -d "${OUTPUT_DIR_CUSTOM}" ] + then + echo "$OPTRED ERROR!$OPTNORMAL directory \"$OUTPUT_DIR_CUSTOM\" does not exist." + exit 1 + else + OUTPUT_DIR=$OUTPUT_DIR_CUSTOM + fi + else + echo "$OPTRED ERROR!$OPTNORMAL - When using custom directory output Mode (-M) has to be set as well. Use -h for help." + exit 1 + fi + # If custom output dir is not provided output dir -> default + else + OUTPUT_DIR="${PROWLER_DIR}/output" + if [ ! -d "${OUTPUT_DIR}" ] + then + mkdir -p "${OUTPUT_DIR}" + fi + fi + + # End of input parameters tests + # Pre-process allowlist file if supplied if [[ -n "$ALLOWLIST_FILE" ]]; then allowlist fi +# Create output file name +if [ -n "${OUTPUT_FILE_NAME}" ] +then + OUTPUT_FILE_NAME="${OUTPUT_DIR}/$OUTPUT_FILE_NAME" +else + OUTPUT_FILE_NAME="${OUTPUT_DIR}/prowler-output-${ACCOUNT_NUM}-${OUTPUT_DATE}" +fi + +# Add headers to certain output files +output_files_init + + + # Load all of the groups of checks inside groups folder named as "groupNumber*" for group in $(ls $PROWLER_DIR/groups/group[0-9]*|grep -v groupN_sample); do . "$group" diff --git a/util/html/generate-html-from-csv.sh b/util/html/generate-html-from-csv.sh index 5c55d096eb..3fe078edfb 100755 --- a/util/html/generate-html-from-csv.sh +++ b/util/html/generate-html-from-csv.sh @@ -169,14 +169,14 @@ unset HTML_REPORT_INIT } addHtmlHeader > ${OUTPUT_FILE_NAME}.$EXTENSION_HTML -while IFS=, read -r PROFILE ACCOUNT_NUM REPREGION TITLE_ID RESULT SCORED LEVEL TITLE_TEXT NOTES ASFF_COMPLIANCE_TYPE CHECK_SEVERITY CHECK_SERVICENAME;do +while IFS=, read -r PROFILE ACCOUNT_NUM REGION_FROM_CHECK TITLE_ID RESULT SCORED LEVEL TITLE_TEXT NOTES ASFF_COMPLIANCE_TYPE CHECK_SEVERITY CHECK_SERVICENAME;do if [[ $RESULT == "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 ''$REGION_FROM_CHECK'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML @@ -190,7 +190,7 @@ while IFS=, read -r PROFILE ACCOUNT_NUM REPREGION TITLE_ID RESULT SCORED LEVEL T 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 ''$REGION_FROM_CHECK'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML @@ -204,7 +204,7 @@ while IFS=, read -r PROFILE ACCOUNT_NUM REPREGION TITLE_ID RESULT SCORED LEVEL T 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 ''$REGION_FROM_CHECK'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML @@ -218,7 +218,7 @@ while IFS=, read -r PROFILE ACCOUNT_NUM REPREGION TITLE_ID RESULT SCORED LEVEL T 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 ''$REGION_FROM_CHECK'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$ASFF_COMPLIANCE_TYPE'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$CHECK_SERVICENAME'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML echo ''$TITLE_ID'' >> ${OUTPUT_FILE_NAME}.$EXTENSION_HTML