forked from DefectDojo/django-DefectDojo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add StackHawk HawkScan webhook event parser (DefectDojo#5941)
* Initial addition of the StackHawk custom parser * Adding actual StackHawk Parser, Tests, and a script to aid with unit tests. #Kaakaww * Adding some tests for testing invalid files * Polishing hashcode & updating some in-line comments * Additional polish and documentation tweaks for the StackHawk parser * Tweaking comments * Fixing hashcode key for StackHawk scanner * Fixing Flake8 findings * Document the new unit test script in the running the tests section * Don't set the unique_id_from_tool as the plugin id maps to a vulnerability id from the tool * Polish based on review comments * Handle false positive and risk accepted per endpoint & at the finding level
- Loading branch information
1 parent
36d86c7
commit 2b4a8de
Showing
15 changed files
with
1,722 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
#/bin/env bash | ||
|
||
unset PROFILE | ||
unset TEST_CASE | ||
|
||
usage() { | ||
echo | ||
echo "This script helps with running unit tests." | ||
echo | ||
echo "Options:" | ||
echo " --profile -p {DOCKER_PROFILE_NAME}" | ||
echo " --test-case -t {YOUR_FULLY_QUALIFIED_TEST_CASE}" | ||
echo | ||
echo " --help -h - prints this dialogue." | ||
echo | ||
echo "Environment Variables:" | ||
echo " DD_PROFILE={DOCKER_PROFILE_NAME}" | ||
echo | ||
echo "You must specify a test case (arg) and profile (arg or env var)!" | ||
echo | ||
echo "Example command:" | ||
echo "./dc-unittest.sh --profile mysql-rabbitmq --test-case unittests.tools.test_stackhawk_parser.TestStackHawkParser" | ||
} | ||
|
||
while [[ $# -gt 0 ]]; do | ||
case $1 in | ||
-p|--profile) | ||
PROFILE="$2" | ||
shift # past argument | ||
shift # past value | ||
;; | ||
-t|--test-case) | ||
TEST_CASE="$2" | ||
shift # past argument | ||
shift # past value | ||
;; | ||
-h|--help) | ||
usage | ||
exit 0 | ||
;; | ||
-*|--*) | ||
echo "Unknown option $1" | ||
usage | ||
exit 1 | ||
;; | ||
*) | ||
POSITIONAL_ARGS+=("$1") # save positional arg | ||
shift # past argument | ||
;; | ||
esac | ||
done | ||
|
||
if [ -z $PROFILE ] | ||
then | ||
if [ -z $DD_PROFILE ] | ||
then | ||
echo "No profile supplied." | ||
usage | ||
exit 1 | ||
else | ||
PROFILE=$DD_PROFILE | ||
fi | ||
fi | ||
|
||
if [ -z $TEST_CASE ] | ||
then | ||
echo "No test case supplied." | ||
usage | ||
exit 1 | ||
fi | ||
|
||
echo "Running docker compose unit tests with profile $PROFILE and test case $TEST_CASE ..." | ||
docker-compose --profile $PROFILE --env-file ./docker/environments/$PROFILE.env exec uwsgi bash -c "python manage.py test $TEST_CASE -v2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import json | ||
|
||
from dojo.models import Endpoint, Finding | ||
from django.utils.dateparse import parse_datetime | ||
|
||
|
||
class StackHawkScanMetadata: | ||
def __init__(self, completed_scan): | ||
self.date = completed_scan['scan']['startedTimestamp'] | ||
self.component_name = completed_scan['scan']['application'] | ||
self.component_version = completed_scan['scan']['env'] | ||
self.static_finding = False | ||
self.dynamic_finding = True | ||
self.service = completed_scan['scan']['application'] | ||
|
||
|
||
class StackHawkParser(object): | ||
""" | ||
DAST findings from StackHawk | ||
""" | ||
|
||
def get_scan_types(self): | ||
return ["StackHawk HawkScan"] | ||
|
||
def get_label_for_scan_types(self, scan_type): | ||
return "StackHawk HawkScan" | ||
|
||
def get_description_for_scan_types(self, scan_type): | ||
return "StackHawk webhook event can be imported in JSON format." | ||
|
||
def get_findings(self, json_output, test): | ||
completed_scan = self.__parse_json(json_output) | ||
|
||
metadata = StackHawkScanMetadata(completed_scan) | ||
findings = self.__extract_findings(completed_scan, metadata, test) | ||
|
||
return findings | ||
|
||
def __extract_findings(self, completed_scan, metadata: StackHawkScanMetadata, test): | ||
findings = {} | ||
|
||
if 'findings' in completed_scan: | ||
raw_findings = completed_scan['findings'] | ||
|
||
for raw_finding in raw_findings: | ||
key = raw_finding['pluginId'] | ||
if key not in findings: | ||
finding = self.__extract_finding(raw_finding, metadata, test) | ||
findings[key] = finding | ||
|
||
# Update the test description these scan results are linked to. | ||
test.description = 'View scan details here: ' + self.__hyperlink(completed_scan['scan']['scanURL']) | ||
|
||
return list(findings.values()) | ||
|
||
def __extract_finding(self, raw_finding, metadata: StackHawkScanMetadata, test) -> Finding: | ||
|
||
steps_to_reproduce = "Use a specific message link and click 'Validate' to see the cURL!\n\n" | ||
|
||
host = raw_finding['host'] | ||
endpoints = [] | ||
|
||
paths = raw_finding['paths'] | ||
for path in paths: | ||
steps_to_reproduce += '**' + path['path'] + '**' +\ | ||
self.__endpoint_status(path['status']) +\ | ||
'\n' + self.__hyperlink(path['pathURL']) + '\n' | ||
endpoint = Endpoint.from_uri(host + path['path']) | ||
endpoints.append(endpoint) | ||
|
||
are_all_endpoints_risk_accepted = self.__are_all_endpoints_in_status(paths, 'RISK_ACCEPTED') | ||
are_all_endpoints_false_positive = self.__are_all_endpoints_in_status(paths, 'FALSE_POSITIVE') | ||
|
||
finding = Finding( | ||
test=test, | ||
title=raw_finding['pluginName'], | ||
date=parse_datetime(metadata.date), | ||
severity=raw_finding['severity'], | ||
description="View this finding in the StackHawk platform at:\n" + | ||
self.__hyperlink(raw_finding['findingURL']), | ||
steps_to_reproduce=steps_to_reproduce, | ||
component_name=metadata.component_name, | ||
component_version=metadata.component_version, | ||
static_finding=metadata.static_finding, | ||
dynamic_finding=metadata.dynamic_finding, | ||
vuln_id_from_tool=raw_finding['pluginId'], | ||
nb_occurences=raw_finding['totalCount'], | ||
service=metadata.service, | ||
false_p=are_all_endpoints_false_positive, | ||
risk_accepted=are_all_endpoints_risk_accepted | ||
) | ||
|
||
finding.unsaved_endpoints.extend(endpoints) | ||
return finding | ||
|
||
@staticmethod | ||
def __parse_json(json_output): | ||
report = json.load(json_output) | ||
|
||
if 'scanCompleted' not in report or 'service' not in report or report['service'] != 'StackHawk': | ||
# By verifying the json data, we can now make certain assumptions. | ||
# Specifically, that the attributes accessed when parsing the finding will always exist. | ||
# See our documentation for more details on this data: | ||
# https://docs.stackhawk.com/workflow-integrations/webhook.html#scan-completed | ||
raise ValueError(" Unexpected JSON format provided. " | ||
"Need help? " | ||
"Check out the StackHawk Docs at " | ||
"https://docs.stackhawk.com/workflow-integrations/defect-dojo.html" | ||
) | ||
|
||
return report['scanCompleted'] | ||
|
||
@staticmethod | ||
def __hyperlink(link: str) -> str: | ||
return '[' + link + '](' + link + ')' | ||
|
||
@staticmethod | ||
def __endpoint_status(status: str) -> str: | ||
if status == 'NEW': | ||
return '** - New**' | ||
elif status == 'RISK_ACCEPTED': | ||
return '** - Marked "Risk Accepted"**' | ||
elif status == 'FALSE_POSITIVE': | ||
return '** - Marked "False Positive"**' | ||
else: | ||
return "" | ||
|
||
@staticmethod | ||
def __are_all_endpoints_in_status(paths, check_status: str) -> bool: | ||
return all(item['status'] == check_status for item in paths) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{} |
4 changes: 4 additions & 0 deletions
4
unittests/scans/stackhawk/oddly_familiar_json_that_isnt_us.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"service": "Not StackHawk", | ||
"scanCompleted": {} | ||
} |
Oops, something went wrong.