Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect scan_date at import time for all findings imported #5547

Merged
merged 13 commits into from
Dec 7, 2021
21 changes: 20 additions & 1 deletion docs/content/en/integrations/importing.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,23 @@ A classic way of reimporting a scan is by specifying the ID of the test instead:
"scan_type": 'ZAP Scan',
"test": 123,
}
```
```

## Using the Scan Completion Date (API: `scan_date`) field

DefectDojo offers a plethora of supported scanner reports, but not all of them contain the
information most important to a user. The `scan_date` field is a flexible smart feature that
allows users to set the completion date of the a given scan report, and have it propagate
down to all the findings imported. This field is **not** mandatory, but the default value for
this field is the date of import (whenever the request is processed and a successful response is returned).

Here are the following use cases for using this field:

1. The report **does not** set the date, and `scan_date` is **not** set at import
- Finding date will be the default value of `scan_date`
StefanFl marked this conversation as resolved.
Show resolved Hide resolved
2. The report **sets** the date, and the `scan_date` is **not** set at import
- Finding date will be whatever the report sets
3. The report **does not** set the date, and the `scan_date` is **set** at import
- Finding date will be whatever the user set for `scan_date`
4. The report **sets** the date, and the `scan_date` is **set** at import
- Finding date will be whatever the user set for `scan_date`
9 changes: 6 additions & 3 deletions dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,7 @@ def get_findings_list(self, obj) -> List[int]:


class ImportScanSerializer(serializers.Serializer):
scan_date = serializers.DateField(default=datetime.date.today)
scan_date = serializers.DateField(required=False)

minimum_severity = serializers.ChoiceField(
choices=SEVERITY_CHOICES,
Expand Down Expand Up @@ -1258,7 +1258,7 @@ def save(self, push_to_jira=False):
verified = data['verified']
minimum_severity = data['minimum_severity']
endpoint_to_add = data['endpoint_to_add']
scan_date = data['scan_date']
scan_date = data.get('scan_date', None)
# Will save in the provided environment or in the `Development` one if absent
version = data.get('version', None)
build_id = data.get('build_id', None)
Expand Down Expand Up @@ -1326,7 +1326,10 @@ def validate(self, data):
return data

def validate_scan_data(self, value):
if value.date() > datetime.today().date():
# scan_date is no longer deafulted to "today" at import time, so set it here if necessary
if not value.date:
return None
if value.date() > timezone.localtime(timezone.now()).date():
raise serializers.ValidationError(
'The date cannot be in the future!')
return value
Expand Down
9 changes: 5 additions & 4 deletions dojo/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout
from django.db.models import Count, Q

from dateutil.relativedelta import relativedelta
from django import forms
from django.contrib.auth.password_validation import validate_password
Expand Down Expand Up @@ -373,10 +372,9 @@ class Meta:

class ImportScanForm(forms.Form):
scan_date = forms.DateTimeField(
required=True,
required=False,
label="Scan Completion Date",
help_text="Scan completion date will be used on all findings.",
initial=datetime.now().strftime("%Y-%m-%d"),
widget=forms.TextInput(attrs={'class': 'datepicker'}))
minimum_severity = forms.ChoiceField(help_text='Minimum severity level to be imported',
required=True,
Expand Down Expand Up @@ -445,6 +443,9 @@ def clean(self):
# date can only be today or in the past, not the future
def clean_scan_date(self):
date = self.cleaned_data['scan_date']
# scan_date is no longer deafulted to "today" at import time, so set it here if necessary
if not date:
return None
if date.date() > datetime.today().date():
raise forms.ValidationError("The date cannot be in the future!")
return date
Expand Down Expand Up @@ -514,7 +515,7 @@ def clean(self):
# date can only be today or in the past, not the future
def clean_scan_date(self):
date = self.cleaned_data['scan_date']
if date.date() > datetime.today().date():
if date.date() > timezone.localtime(timezone.now()).date():
raise forms.ValidationError("The date cannot be in the future!")
return date

Expand Down
18 changes: 15 additions & 3 deletions dojo/importers/importer/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def create_test(self, scan_type, test_type_name, engagement, lead, environment,
@dojo_async_task
@app.task(ignore_result=False)
def process_parsed_findings(self, test, parsed_findings, scan_type, user, active, verified, minimum_severity=None,
endpoints_to_add=None, push_to_jira=None, group_by=None, now=timezone.now(), service=None, **kwargs):
endpoints_to_add=None, push_to_jira=None, group_by=None, now=timezone.now(), service=None, scan_date=None, **kwargs):
logger.debug('endpoints_to_add: %s', endpoints_to_add)
new_findings = []
items = parsed_findings
Expand Down Expand Up @@ -100,6 +100,13 @@ def process_parsed_findings(self, test, parsed_findings, scan_type, user, active
item.active = active
if item.verified:
item.verified = verified
# Set the date if the parser does not set it
if not item.date:
item.date = scan_date

# Indicates the scan_date is not the default, overwrite everything
if (scan_date.date() if isinstance(scan_date, datetime.datetime) else scan_date) != now.date():
item.date = scan_date

item.created = now
item.updated = now
Expand Down Expand Up @@ -256,6 +263,11 @@ def import_scan(self, scan, scan_type, engagement, lead, environment, active, ve
user = user or get_current_user()

now = timezone.now()
# scan_date is no longer deafulted to "today" at import time, so set it here if necessary
finding_scan_date = scan_date
if not scan_date:
scan_date = now
finding_scan_date = now
# retain weird existing logic to use current time for provided scan date
scan_date_time = datetime.datetime.combine(scan_date, timezone.now().time())
if settings.USE_TZ:
Expand Down Expand Up @@ -328,7 +340,7 @@ def import_scan(self, scan, scan_type, engagement, lead, environment, active, ve
result = self.process_parsed_findings(test, findings_list, scan_type, user, active,
verified, minimum_severity=minimum_severity,
endpoints_to_add=endpoints_to_add, push_to_jira=push_to_jira,
group_by=group_by, now=now, service=service, sync=False)
group_by=group_by, now=now, service=service, scan_date=finding_scan_date, sync=False)
# Since I dont want to wait until the task is done right now, save the id
# So I can check on the task later
results_list += [result]
Expand All @@ -346,7 +358,7 @@ def import_scan(self, scan, scan_type, engagement, lead, environment, active, ve
new_findings = self.process_parsed_findings(test, parsed_findings, scan_type, user, active,
verified, minimum_severity=minimum_severity,
endpoints_to_add=endpoints_to_add, push_to_jira=push_to_jira,
group_by=group_by, now=now, service=service, sync=True)
group_by=group_by, now=now, service=service, scan_date=finding_scan_date, sync=True)

closed_findings = []
if close_old_findings:
Expand Down
6 changes: 4 additions & 2 deletions unittests/dojo_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,8 @@ def get_test_api(self, test_id):

def import_scan_with_params(self, filename, scan_type='ZAP Scan', engagement=1, minimum_severity='Low', active=True, verified=True,
push_to_jira=None, endpoint_to_add=None, tags=None, close_old_findings=False, group_by=None, engagement_name=None,
product_name=None, product=None, product_type_name=None, auto_create_context=None, expected_http_status_code=201, test_title=None):
product_name=None, product=None, product_type_name=None, auto_create_context=None, expected_http_status_code=201, test_title=None, scan_date=None):
payload = {
"scan_date": '2020-06-04',
"minimum_severity": minimum_severity,
"active": active,
"verified": verified,
Expand Down Expand Up @@ -469,6 +468,9 @@ def import_scan_with_params(self, filename, scan_type='ZAP Scan', engagement=1,
if test_title is not None:
payload['test_title'] = test_title

if scan_date is not None:
payload['scan_date'] = scan_date

return self.import_scan(payload, expected_http_status_code)

def reimport_scan_with_params(self, test_id, filename, scan_type='ZAP Scan', engagement=1, minimum_severity='Low', active=True, verified=True, push_to_jira=None,
Expand Down
82 changes: 80 additions & 2 deletions unittests/test_import_reimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient
from django.test.client import Client
from django.utils import timezone
from .dojo_test_case import DojoAPITestCase, get_unit_tests_path
from .test_utils import assertTestImportModelsCreated
from django.test import override_settings
Expand Down Expand Up @@ -55,6 +56,9 @@ def __init__(self, *args, **kwargs):
self.anchore_file_name = self.scans_path + 'anchore/one_vuln_many_files.json'
self.scan_type_anchore = 'Anchore Engine Scan'

self.acunetix_file_name = self.scans_path + 'acunetix/one_finding.xml'
self.scan_type_acunetix = 'Acunetix Scan'

self.gitlab_dep_scan_components_filename = self.scans_path + 'gitlab_dep_scan/gl-dependency-scanning-report-many-vuln.json'
self.scan_type_gtlab_dep_scan = 'GitLab Dependency Scanning Report'

Expand Down Expand Up @@ -156,6 +160,78 @@ def test_zap_scan_base_not_active_not_verified(self):

return test_id

# import zap scan, testing:
# - import
# - deafult scan_date (today) overrides date not set by parser
def test_import_default_scan_date_parser_not_sets_date(self):
logger.debug('importing zap xml report with date set by parser')
with assertTestImportModelsCreated(self, imports=1, affected_findings=4, created=4):
import0 = self.import_scan_with_params(self.zap_sample0_filename, active=False, verified=False)

test_id = import0['test']
findings = self.get_test_findings_api(test_id, active=False, verified=False)
self.log_finding_summary_json_api(findings)

# Get the date
date = findings['results'][0]['date']
self.assertEqual(date, str(timezone.localtime(timezone.now()).date()))

return test_id

# import acunetix scan, testing:
# - import
# - deafult scan_date (today) does not overrides date set by parser
def test_import_default_scan_date_parser_sets_date(self):
logger.debug('importing original acunetix xml report')
with assertTestImportModelsCreated(self, imports=1, affected_findings=1, created=1):
import0 = self.import_scan_with_params(self.acunetix_file_name, scan_type=self.scan_type_acunetix, active=False, verified=False)

test_id = import0['test']
findings = self.get_test_findings_api(test_id, active=False, verified=False)
self.log_finding_summary_json_api(findings)

# Get the date
date = findings['results'][0]['date']
self.assertEqual(date, '2018-09-24')

return test_id

# import acunetix scan, testing:
# - import
# - set scan_date overrides date not set by parser
def test_import_set_scan_date_parser_not_sets_date(self):
logger.debug('importing original zap xml report')
with assertTestImportModelsCreated(self, imports=1, affected_findings=4, created=4):
import0 = self.import_scan_with_params(self.zap_sample0_filename, active=False, verified=False, scan_date='2006-12-26')

test_id = import0['test']
findings = self.get_test_findings_api(test_id, active=False, verified=False)
self.log_finding_summary_json_api(findings)

# Get the date
date = findings['results'][0]['date']
self.assertEqual(date, '2006-12-26')

return test_id

# import acunetix scan, testing:
# - import
# - set scan_date overrides date set by parser
def test_import_set_scan_date_parser_sets_date(self):
logger.debug('importing acunetix xml report with date set by parser')
with assertTestImportModelsCreated(self, imports=1, affected_findings=1, created=1):
import0 = self.import_scan_with_params(self.acunetix_file_name, scan_type=self.scan_type_acunetix, active=False, verified=False, scan_date='2006-12-26')

test_id = import0['test']
findings = self.get_test_findings_api(test_id, active=False, verified=False)
self.log_finding_summary_json_api(findings)

# Get the date
date = findings['results'][0]['date']
self.assertEqual(date, '2006-12-26')

return test_id

# import checkmarx scan. ZAP parser will never create a finding with active/verified false
# checkmarx will (for false positive for example)
# the goal of this test is to verify the final active/verified status depending on the parser status vs the options choosen during import
Expand Down Expand Up @@ -1135,9 +1211,8 @@ def reimport_scan_ui(self, test, payload):
test = Test.objects.get(id=response.url.split('/')[-1])
return {'test': test.id}

def import_scan_with_params_ui(self, filename, scan_type='ZAP Scan', engagement=1, minimum_severity='Low', active=True, verified=True, push_to_jira=None, endpoint_to_add=None, tags=None, close_old_findings=False):
def import_scan_with_params_ui(self, filename, scan_type='ZAP Scan', engagement=1, minimum_severity='Low', active=True, verified=True, push_to_jira=None, endpoint_to_add=None, tags=None, close_old_findings=False, scan_date=None):
payload = {
"scan_date": '2020-06-04',
"minimum_severity": minimum_severity,
"active": active,
"verified": verified,
Expand All @@ -1157,6 +1232,9 @@ def import_scan_with_params_ui(self, filename, scan_type='ZAP Scan', engagement=
if tags is not None:
payload['tags'] = tags

if scan_date is not None:
payload['scan_date'] = scan_date

return self.import_scan_ui(engagement, payload)

def reimport_scan_with_params_ui(self, test_id, filename, scan_type='ZAP Scan', minimum_severity='Low', active=True, verified=True, push_to_jira=None, tags=None, close_old_findings=True):
Expand Down
2 changes: 2 additions & 0 deletions unittests/test_importers_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def test_parse_findings(self):
minimum_severity = "Info"
active = True
verified = True
scan_date = timezone.localtime(timezone.now()).date()
new_findings = importer.process_parsed_findings(
test,
parsed_findings,
Expand All @@ -86,6 +87,7 @@ def test_parse_findings(self):
active,
verified,
minimum_severity=minimum_severity,
scan_date=scan_date,
sync=True
)

Expand Down