Adds 'json-asff' and 'securityhub' output modes

json-asff mode outputs JSON, similar to the standard 'json' mode with one check per line, but in AWS Security Finding Format - used by AWS Security Hub
Currently uses a generic Type, Resources and ProductArn value, but sets the Id to a unique value that includes the details of the message, in order to separate out checks that run against multiple resources and output one result per resource per check. This ensures that findings can be updated, should the resource move in or out of compliance

securityhub mode generates the ASFF JSON and then passes it to an 'aws securityhub batch-import-findings' call, once per resource per check. Output to the screen is similar to the standard mode, but prints whether or not the finding was submitted successfully

Fixes #524
This commit is contained in:
Marc Jay
2020-04-07 16:08:07 +01:00
parent b5e1c9002a
commit 92e1f17a80
5 changed files with 127 additions and 101 deletions

View File

@@ -11,15 +11,15 @@
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
if [[ $MODE != "mono" && $MODE != "text" && $MODE != "csv" && $MODE != "json" ]]; then
if [[ "$MODE" != "mono" && "$MODE" != "text" && "$MODE" != "csv" && "$MODE" != "json" && "$MODE" != "json-asff" && "$MODE" != "securityhub" ]]; then
echo ""
echo "$OPTRED ERROR!$OPTNORMAL Invalid output mode. Choose text, mono, or csv."
echo "$OPTRED ERROR!$OPTNORMAL Invalid output mode. Choose text, mono, csv, json, json-asff or securityhub."
usage
EXITCODE=1
exit $EXITCODE
fi
if [[ "$MODE" == "mono" || "$MODE" == "csv" || "$MODE" == "json" ]]; then
if [[ "$MODE" == "mono" || "$MODE" == "csv" || "$MODE" == "json" || "$MODE" == "json-asff" ]]; then
MONOCHROME=1
fi

View File

@@ -18,41 +18,22 @@ textPass(){
fi
PASS_COUNTER=$((PASS_COUNTER+1))
if [[ "$MODE" == "csv" ]]; then
if [[ "$MODE" == "csv" || "$MODE" == "json" || "$MODE" == "json-asff" || "$MODE" == "securityhub" ]]; then
if [[ $2 ]]; then
REPREGION=$2
else
REPREGION=$REGION
fi
echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}PASS${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1"
elif [[ "$MODE" == "json" ]]; then
if [[ $2 ]]; then
REPREGION=$2
else
REPREGION=$REGION
if [[ "$MODE" == "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"
elif [[ "$MODE" == "json" ]]; then
generateJsonOutput "$1" "Pass"
elif [[ "$MODE" == "json-asff" ]]; then
generateJsonAsffOutput "$1" "PASSED" "INFORMATIONAL"
elif [[ "$MODE" == "securityhub" ]]; then
printf " $OK PASS!$NORMAL %s... " "$1"
aws securityhub batch-import-findings --findings "$(generateJsonAsffOutput "$1" "PASSED" "INFORMATIONAL")" | jq -M -r 'if .SuccessCount == 1 then "Successfully submitted finding" else "Failed to upload finding" end'
fi
jq -M -c \
--arg PROFILE "$PROFILE" \
--arg ACCOUNT_NUM "$ACCOUNT_NUM" \
--arg TITLE_TEXT "$TITLE_TEXT" \
--arg MESSAGE "$(echo -e "${1}" | sed -e 's/^[[:space:]]*//')" \
--arg SCORED "$ITEM_SCORED" \
--arg ITEM_LEVEL "$ITEM_LEVEL" \
--arg TITLE_ID "$TITLE_ID" \
--arg REPREGION "$REPREGION" \
--arg TIMESTAMP $(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-n '{
"Profile": $PROFILE,
"Account Number": $ACCOUNT_NUM,
"Control": $TITLE_TEXT,
"Message": $MESSAGE,
"Status": "Pass",
"Scored": $SCORED,
"Level": $ITEM_LEVEL,
"Control ID": $TITLE_ID,
"Region": $REPREGION,
"Timestamp": $TIMESTAMP,
}'
else
echo " $OK PASS!$NORMAL $1"
fi
@@ -63,41 +44,22 @@ textInfo(){
return
fi
if [[ "$MODE" == "csv" ]]; then
if [[ "$MODE" == "csv" || "$MODE" == "json" || "$MODE" == "json-asff" ]]; then
if [[ $2 ]]; then
REPREGION=$2
else
REPREGION=$REGION
fi
echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}INFO${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1"
elif [[ "$MODE" == "json" ]]; then
if [[ $2 ]]; then
REPREGION=$2
else
REPREGION=$REGION
if [[ "$MODE" == "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"
elif [[ "$MODE" == "json" ]]; then
generateJsonOutput "$1" "Info"
elif [[ "$MODE" == "json-asff" ]]; then
generateJsonAsffOutput "$1" "NOT_AVAILABLE" "LOW"
elif [[ "$MODE" == "securityhub" ]]; then
printf " $NOTICE INFO! %s... $NORMAL" "$1"
aws securityhub batch-import-findings --findings "$(generateJsonAsffOutput "$1" "NOT_AVAILABLE" "LOW")" | jq -M -r 'if .SuccessCount == 1 then "Successfully submitted finding" else "Failed to upload finding" end'
fi
jq -M -c \
--arg PROFILE "$PROFILE" \
--arg ACCOUNT_NUM "$ACCOUNT_NUM" \
--arg TITLE_TEXT "$TITLE_TEXT" \
--arg MESSAGE "$(echo -e "${1}" | sed -e 's/^[[:space:]]*//')" \
--arg SCORED "$ITEM_SCORED" \
--arg ITEM_LEVEL "$ITEM_LEVEL" \
--arg TITLE_ID "$TITLE_ID" \
--arg REPREGION "$REPREGION" \
--arg TIMESTAMP $(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-n '{
"Profile": $PROFILE,
"Account Number": $ACCOUNT_NUM,
"Control": $TITLE_TEXT,
"Message": $MESSAGE,
"Status": "Info",
"Scored": $SCORED,
"Level": $ITEM_LEVEL,
"Control ID": $TITLE_ID,
"Region": $REPREGION,
"Timestamp": $TIMESTAMP,
}'
else
echo " $NOTICE INFO! $1 $NORMAL"
fi
@@ -106,41 +68,22 @@ textInfo(){
textFail(){
FAIL_COUNTER=$((FAIL_COUNTER+1))
EXITCODE=3
if [[ "$MODE" == "csv" ]]; then
if [[ "$MODE" == "csv" || "$MODE" == "json" || "$MODE" == "json-asff" || "$MODE" == "securityhub" ]]; then
if [[ $2 ]]; then
REPREGION=$2
else
REPREGION=$REGION
fi
echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}FAIL${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1"
elif [[ "$MODE" == "json" ]]; then
if [[ $2 ]]; then
REPREGION=$2
else
REPREGION=$REGION
if [[ "$MODE" == "csv" ]]; then
echo "$PROFILE${SEP}$ACCOUNT_NUM${SEP}$REPREGION${SEP}$TITLE_ID${SEP}FAIL${SEP}$ITEM_SCORED${SEP}$ITEM_LEVEL${SEP}$TITLE_TEXT${SEP}$1"
elif [[ "$MODE" == "json" ]]; then
generateJsonOutput "$1" "Fail"
elif [[ "$MODE" == "json-asff" ]]; then
generateJsonAsffOutput "$1" "FAILED" "HIGH"
elif [[ "$MODE" == "securityhub" ]]; then
printf " $BAD FAIL! %s... $NORMAL" "$1"
aws securityhub batch-import-findings --findings "$(generateJsonAsffOutput "$1" "FAILED" "HIGH")" | jq -M -r 'if .SuccessCount == 1 then "Successfully submitted finding" else "Failed to upload finding" end'
fi
jq -M -c \
--arg PROFILE "$PROFILE" \
--arg ACCOUNT_NUM "$ACCOUNT_NUM" \
--arg TITLE_TEXT "$TITLE_TEXT" \
--arg MESSAGE "$(echo -e "${1}" | sed -e 's/^[[:space:]]*//')" \
--arg SCORED "$ITEM_SCORED" \
--arg ITEM_LEVEL "$ITEM_LEVEL" \
--arg TITLE_ID "$TITLE_ID" \
--arg REPREGION "$REPREGION" \
--arg TIMESTAMP $(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-n '{
"Profile": $PROFILE,
"Account Number": $ACCOUNT_NUM,
"Control": $TITLE_TEXT,
"Message": $MESSAGE,
"Status": "Fail",
"Scored": $SCORED,
"Level": $ITEM_LEVEL,
"Control ID": $TITLE_ID,
"Region": $REPREGION,
"Timestamp": $TIMESTAMP,
}'
else
echo " $BAD FAIL! $1 $NORMAL"
fi
@@ -179,7 +122,7 @@ textTitle(){
if [[ "$MODE" == "csv" ]]; then
>&2 echo "$TITLE_ID $TITLE_TEXT"
elif [[ "$MODE" == "json" ]]; then
elif [[ "$MODE" == "json" || "$MODE" == "json-asff" ]]; then
:
else
if [[ "$ITEM_SCORED" == "Scored" ]]; then
@@ -189,3 +132,86 @@ textTitle(){
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 $(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-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
local severity=$3
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 UNIQUE_ID "$(LC_ALL=C echo -e "${message}" | tr -cs '[:alnum:]._~-\n' '_')" \
--arg STATUS "$status" \
--arg SEVERITY "$severity" \
--arg SCORED "$ITEM_SCORED" \
--arg ITEM_LEVEL "$ITEM_LEVEL" \
--arg TITLE_ID "$TITLE_ID" \
--arg REPREGION "$REPREGION" \
--arg TIMESTAMP $(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--arg PROWLER_VERSION "$PROWLER_VERSION" \
-n '{
"SchemaVersion": "2018-10-08",
"Id": "prowler-\($TITLE_ID)-\($ACCOUNT_NUM)-\($REPREGION)-\($UNIQUE_ID)",
"ProductArn": "arn:aws:securityhub:\($REPREGION):\($ACCOUNT_NUM):product/\($ACCOUNT_NUM)/default",
"ProductFields": {
"ProviderName": "Prowler",
"ProviderVersion": $PROWLER_VERSION
},
"GeneratorId": "prowler-\($PROWLER_VERSION)",
"AwsAccountId": $ACCOUNT_NUM,
"Types": [
"Software and Configuration Checks"
],
"FirstObservedAt": $TIMESTAMP,
"UpdatedAt": $TIMESTAMP,
"CreatedAt": $TIMESTAMP,
"Severity": {
"Label": $SEVERITY
},
"Title": $TITLE_TEXT,
"Description": $MESSAGE,
"Resources": [
{
"Type": "AwsAccount",
"Id": "AWS: : : :Account:\($ACCOUNT_NUM)",
"Partition": "aws",
"Region": $REPREGION
}
],
"Compliance": {
"Status": $STATUS
}
}'
}

View File

@@ -28,7 +28,7 @@ getWhoami(){
printCsvHeader
textTitle "0.0" "Show report generation info" "NOT_SCORED" "SUPPORT"
textInfo "ARN: $CALLER_ARN TIMESTAMP: $SCRIPT_START_TIME"
elif [[ "$MODE" == "json" ]]; then
elif [[ "$MODE" == "json" || "$MODE" == "json-asff" ]]; then
:
else
echo ""