Skip to content

Commit

Permalink
#6620 Create API importer for Bugcrowd (#6621)
Browse files Browse the repository at this point in the history
* Create API importer for Bugcrowd

* Fix linting

* Documentation update

* Implement URI extraction via regex, pagination fetch loop, switch to unique id from tool deduplication alg

* Update api_client.py

* Various fixes

* Fix dateutil parse and auth header

* Fix linting

* Switch to session

* Implement unit testing - WIP

* Bugcrowd api importer unit tests

* Fix flake8

* Simplify parameterization for bugcrowd JSONAPI format

* Fix urlencoding and loop for pagination

* Implement generator api client

* v3 of fetcher with multithreading

* Linting with Black, test data changed for generator function, fix tests

* fix pep8 and add ignore W503 in flake8

* remove json from test

* Use logger for endpoint parsing errors, without breaking parser

* Strip bug url to improve endpoint parsing

* Remove regex usage

* Handle endpoint uri a bit better

* use logger error for endpoint converting

* Improve requests exception handling

* Remove regexes, convert_endpoint function

* Raise exeptions for responses and connection tests

* Do not save broken endpoints, add cleaning in tests

* Align to dev branch

* Named ValidationError exceptions

* Fix conflicts

* Fix conflicts

* Add response text in error message

* Fix liniting

* Update __init__.py

Co-authored-by: Damien Carol <[email protected]>
  • Loading branch information
Gby56 and damiencarol authored Sep 14, 2022
1 parent 83051b4 commit 9bb46ba
Show file tree
Hide file tree
Showing 15 changed files with 1,469 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ ignore =
E128
# line break after binary operator
W504
# Line break occurred before a binary operator (conflicting with black)
W503
# undefined file name excpetion
F821

Expand Down
12 changes: 12 additions & 0 deletions docs/content/en/integrations/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ Import Brakeman Scanner findings in JSON format.

Import Bugcrowd results in CSV format.

### Bugcrowd API

Import Bugcrowd submissions directly from the API using the API token.
Set your API key directly in the format `username:password` in the API Token input, it will be added to the header `'Authorization': 'Token {}'.format(self.api_token),`
For each product, you can configure 2 things:
- Service key 1: the bugcrowd program code (it's the slug name in the url for the program, url safe)
- Service key 2: the bugcrowd target name (the full name, it will be url-encoded, you can find it in https://tracker.bugcrowd.com/<YOURPROGRAM>/settings/scope/target_groups)
- It can be left empty so that all program submissions are imported

That way, per product, you can use the same program but separate by target, which is a fairly common way of filtering/grouping Bugcrowd.
Adding support for a 3rd filtering would be possible with Service Key 3, feel free to make a PR.

### Bundler-Audit

Import the text output generated with bundle-audit check
Expand Down
3 changes: 3 additions & 0 deletions dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,7 @@ def saml2_attrib_map_format(dict):
'Solar Appscreener Scan': ['title', 'file_path', 'line', 'severity'],
'pip-audit Scan': ['vuln_id_from_tool', 'component_name', 'component_version'],
'Edgescan Scan': ['unique_id_from_tool'],
'Bugcrowd API': ['unique_id_from_tool'],
'Rubocop Scan': ['vuln_id_from_tool', 'file_path', 'line'],
'JFrog Xray Scan': ['title', 'description', 'component_name', 'component_version'],
'CycloneDX Scan': ['vuln_id_from_tool', 'component_name', 'component_version'],
Expand Down Expand Up @@ -1234,6 +1235,7 @@ def saml2_attrib_map_format(dict):
'Semgrep JSON Report': True,
'Generic Findings Import': True,
'Edgescan Scan': True,
'Bugcrowd API': True,
'Veracode SourceClear Scan': True,
'Twistlock Image Scan': True
}
Expand Down Expand Up @@ -1340,6 +1342,7 @@ def saml2_attrib_map_format(dict):
'Gitleaks Scan': DEDUPE_ALGO_HASH_CODE,
'pip-audit Scan': DEDUPE_ALGO_HASH_CODE,
'Edgescan Scan': DEDUPE_ALGO_HASH_CODE,
'Bugcrowd API': DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL,
'Rubocop Scan': DEDUPE_ALGO_HASH_CODE,
'JFrog Xray Scan': DEDUPE_ALGO_HASH_CODE,
'CycloneDX Scan': DEDUPE_ALGO_HASH_CODE,
Expand Down
2 changes: 2 additions & 0 deletions dojo/templates/dojo/add_product_api_scan_configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ <h3> Add {{ product.name }} API Scan Configuration</h3>
API Scan Configurations are supported for the test types SonarQube API, Cobalt.io API and Edgescan API.
<ul>
<li>For <b>SonarQube API</b> the field <b>Service key 1</b> has to be set with the SonarQube project key.</li>
<li>For <b>Bugcrowd API</b> the field <b>Service key 1</b> has to be set with the Bugcrowd program code.
<b>Service key 2</b> can be set with the target in the Bugcrowd program (will be url encoded for the api call), if not supplied, will fetch all submissions in the program</li>
<li>For <b>Cobalt.io API</b> the field <b>Service key 1</b> has to be set with the Cobalt.io asset id.
<b>Service key 2</b> will be populated with the asset name while saving the configuration.</li>
<li>For <b>Edgescan API</b> the field <b>Service key 1</b> has to be set with the Edgescan asset id.</li>
Expand Down
2 changes: 2 additions & 0 deletions dojo/templates/dojo/edit_product_api_scan_configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ <h3>Edit API Scan Configuration</h3>
API Scan Configurations are supported for the test types SonarQube API, Cobalt.io API and Edgescan API.
<ul>
<li>For <b>SonarQube API</b> the field <b>Service key 1</b> has to be set with the SonarQube project key.</li>
<li>For <b>Bugcrowd API</b> the field <b>Service key 1</b> has to be set with the Bugcrowd program code.
<b>Service key 2</b> can be set with the target in the Bugcrowd program (will be url encoded for the api call), if not supplied, will fetch all submissions in the program</li>
<li>For <b>Cobalt.io API</b> the field <b>Service key 1</b> has to be set with the Cobalt.io asset id.
<b>Service key 2</b> will be populated with the asset name while saving the configuration.</li>
<li>For <b>Edgescan API</b> the field <b>Service key 1</b> has to be set with the Edgescan asset id.</li>
Expand Down
5 changes: 4 additions & 1 deletion dojo/tool_config/factory.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from dojo.tools.sonarqube_api.api_client import SonarQubeAPI
from dojo.tools.cobalt_api.api_client import CobaltAPI
from dojo.tools.edgescan.api_client import EdgescanAPI
from dojo.tools.bugcrowd_api.api_client import BugcrowdAPI

SCAN_APIS = {'SonarQube': SonarQubeAPI,
'Cobalt.io': CobaltAPI,
'Edgescan API': EdgescanAPI}
'Edgescan API': EdgescanAPI,
'Bugcrowd API': BugcrowdAPI
}


def create_API(tool_configuration):
Expand Down
Empty file.
137 changes: 137 additions & 0 deletions dojo/tools/bugcrowd_api/api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import requests
from urllib.parse import urlencode
from dojo.models import Tool_Type


class BugcrowdAPI:
"""
A simple client for the bugcrowd.io API
"""

bugcrowd_api_url = "https://api.bugcrowd.com"
default_headers = {
"Accept": "application/vnd.bugcrowd+json",
"User-Agent": "DefectDojo",
"Bugcrowd-Version": "2021-10-28",
}

def __init__(self, tool_config):
Tool_Type.objects.get_or_create(name="Bugcrowd API")

self.session = requests.Session()
if tool_config.authentication_type == "API":
self.api_token = tool_config.api_key
self.session.headers.update(
{"Authorization": "Token {}".format(self.api_token)}
)
self.session.headers.update(self.default_headers)
else:
raise Exception(
"bugcrowd Authentication type {} not supported".format(
tool_config.authentication_type
)
)

def get_findings(self, program, target):
"""
Returns the findings in a paginated iterator for a given bugcrowd program and target, if target is *, everything is returned
:param program:
:param target:
:return:
"""
params_default = {
"filter[duplicate]": "false",
"filter[program]": program,
"page[limit]": 100,
"page[offset]": 0,
"include": "monetary_rewards,target",
"sort": "submitted-desc",
}

if target:
params = params_default
params["filter[target]"] = target
params_encoded = urlencode(params)
else:
params_encoded = urlencode(params_default)

next = "{}/submissions?{}".format(self.bugcrowd_api_url, params_encoded)
while next != "":
response = self.session.get(url=next)
response.raise_for_status()
if response.ok:
data = response.json()
if len(data["data"]) != 0:
yield data["data"]

# When we hit the end of the submissions, break out
if len(data["data"]) == 0:
next = ""
break

# Otherwise, keep updating next link
next = "{}{}".format(self.bugcrowd_api_url, data["links"]["next"])
else:
next = "over"

def test_connection(self):
# Request programs
response_programs = self.session.get(
url="{}/programs".format(self.bugcrowd_api_url)
)
response_programs.raise_for_status()

# Request submissions to validate the org token
response_subs = self.session.get(
url="{}/submissions".format(self.bugcrowd_api_url)
)
response_subs.raise_for_status()
if response_programs.ok and response_subs.ok:
data = response_programs.json().get("data")
data_subs = response_subs.json().get("meta")
total_subs = str(data_subs["total_hits"])

progs = list(filter(lambda prog: prog["type"] == "program", data))
program_names = ", ".join(
list(map(lambda p: p["attributes"]["code"], progs))
)
# Request targets to validate the org token
response_targets = self.session.get(
url="{}/targets".format(self.bugcrowd_api_url)
)
response_targets.raise_for_status()
if response_targets.ok:
data_targets = response_targets.json().get("data")
targets = list(
filter(lambda prog: prog["type"] == "target", data_targets)
)
target_names = ", ".join(
list(map(lambda p: p["attributes"]["name"], targets))
)
return f'With {total_subs} submissions, you have access to the "{ program_names }" programs, \
you can use these as Service key 1 for filtering submissions \
You also have targets "{ target_names }" that can be used in Service key 2'
else:
raise Exception(
"Bugcrowd API test not successful, no targets were defined in Bugcrowd which is used for filtering, check your configuration, HTTP response was: {}".format(
response_targets.text
)
)
else:
raise Exception(
"Bugcrowd API test not successful, could not retrieve the programs or submissions, check your configuration, HTTP response for programs was: {}, HTTP response for submissions was: {}".format(
response_programs.text, response_subs.text
)
)

def test_product_connection(self, api_scan_configuration):
submissions = []
submission_gen = self.get_findings(
api_scan_configuration.service_key_1, api_scan_configuration.service_key_2
)
for page in submission_gen:
submissions = submissions + page
submission_number = len(submissions)
return f'You have access to "{submission_number}" submissions (no duplicates)\
in Bugcrowd in the Program code "{api_scan_configuration.service_key_1}" \
and Target "{api_scan_configuration.service_key_2}" (leave service key 2 empty to get all submissions in program)'
63 changes: 63 additions & 0 deletions dojo/tools/bugcrowd_api/importer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import logging
from django.core.exceptions import ValidationError
from dojo.models import Product_API_Scan_Configuration
from dojo.tools.bugcrowd_api.api_client import BugcrowdAPI

logger = logging.getLogger(__name__)


class BugcrowdApiImporter(object):
"""
Import from Bugcrowd API
"""

def get_findings(self, test):
client, config = self.prepare_client(test)
logger.debug(
"Fetching submissions program {} and target {}".format(
str(config.service_key_1), str(config.service_key_2)
)
)

submissions_paged = client.get_findings(
config.service_key_1,
config.service_key_2,
)

submissions = []
counter = 0
for page in submissions_paged:
submissions += page
counter += 1
logger.debug("{} Bugcrowd submissions pages fetched".format(counter))

return submissions

def prepare_client(self, test):
product = test.engagement.product
if test.api_scan_configuration:
config = test.api_scan_configuration
# Double check of config
if config.product != product:
raise ValidationError(
"API Scan Configuration for Bugcrowd API and Product do not match."
)
else:
configs = Product_API_Scan_Configuration.objects.filter(
product=product, tool_configuration__tool_type__name="Bugcrowd API"
)
if configs.count() == 1:
config = configs.first()
elif configs.count() > 1:
raise ValidationError(
"More than one Product API Scan Configuration has been configured, but none of them has been chosen.\
Please specify at Test which one should be used."
)
else:
raise ValidationError(
"There are no API Scan Configurations for this Product. \
Please add at least one API Scan Configuration for bugcrowd to this Product."
)

tool_config = config.tool_configuration
return BugcrowdAPI(tool_config), config
Loading

0 comments on commit 9bb46ba

Please sign in to comment.