Skip to content

Commit

Permalink
Add StackHawk HawkScan webhook event parser (DefectDojo#5941)
Browse files Browse the repository at this point in the history
* 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
Bwvolleyball authored Feb 28, 2022
1 parent 36d86c7 commit 2b4a8de
Show file tree
Hide file tree
Showing 15 changed files with 1,722 additions and 0 deletions.
73 changes: 73 additions & 0 deletions dc-unittest.sh
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"
5 changes: 5 additions & 0 deletions docs/content/en/integrations/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,11 @@ XML report of SSLyze version 2 scan

JSON report of SSLyze version 3 scan

### StackHawk HawkScan

Import the JSON webhook event from StackHawk.
For more information, check out our [docs on hooking up StackHawk to Defect Dojo](https://docs.stackhawk.com/workflow-integrations/defect-dojo.html)

### Testssl Scan

Import CSV output of testssl scan report.
Expand Down
1 change: 1 addition & 0 deletions dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,7 @@ def saml2_attrib_map_format(dict):
'SSLyze Scan (JSON)': ['title', 'description'],
'Harbor Vulnerability Scan': ['title'],
'Rusty Hog Scan': ['title', 'description'],
'StackHawk HawkScan': ['vuln_id_from_tool', 'component_name', 'component_version'],
}

# This tells if we should accept cwe=0 when computing hash_code with a configurable list of fields from HASHCODE_FIELDS_PER_SCANNER (this setting doesn't apply to legacy algorithm)
Expand Down
Empty file.
130 changes: 130 additions & 0 deletions dojo/tools/stackhawk/parser.py
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)
8 changes: 8 additions & 0 deletions readme-docs/DOCKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The environment variables needed for the different profiles are prepared in file
- `./dc-up-d.sh` - Start the docker containers in the background, it needs one of the profile names as a parameter
- `./dc-stop.sh` - Stop the docker containers, it can take one additional parameter to be used in the stop process.
- `./dc-down.sh` - Stop and remove the docker containers, it can take one additional parameter to be used in the stop and remove process.
- `./dc-unittest.sh` - Utility script to aid in running a specific unit test class. Requires a profile and test case as args.

A default profile can be set with the environment variable `DD_PROFILE`. If this environment variable is set when starting the containers, the parameter for the profile needs not to be given for the start scripts .

Expand Down Expand Up @@ -355,6 +356,13 @@ Run a single test. Example:
python manage.py test unittests.tools.test_dependency_check_parser.TestDependencyCheckParser.test_parse_file_with_no_vulnerabilities_has_no_findings --keepdb
```

For docker compose stack, there is a convenience script (`dc-unittest.sh`) capable of running a single test class.
You will need to provide a docker compose profile (`--profile`), and a test case (`--test-case`). Example:

```
./dc-unittest.sh --profile mysql-rabbitmq --test-case unittests.tools.test_stackhawk_parser.TestStackHawkParser
```

## Running the integration tests
This will run all integration-tests and leave the containers up:

Expand Down
1 change: 1 addition & 0 deletions unittests/scans/stackhawk/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"service": "Not StackHawk",
"scanCompleted": {}
}
Loading

0 comments on commit 2b4a8de

Please sign in to comment.