Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Any vulnerabilities which were not contained in the previous import will be adde

If any incoming Findings match Findings that already exist, the incoming Findings will be discarded rather than recorded as Duplicates. These Findings have been recorded already \- no need to add a new Finding object. The Test page will show these Findings as **Left Untouched**.

### Fields fix_available and fix_version

If any incoming Findings match Findings that already exist, the incoming Finding is checked if the fields `fix_available` and `fix_version` differ and are updated if yes. These Findings have been recorded already \- no need to add a new Finding object. The Test page will show these Findings as **Left Untouched**.

### Close Findings

If there are any Findings that already exist in the Test but which are not present in the incoming report, you can choose to automatically set those Findings to Inactive and Mitigated (on the assumption that those vulnerabilities have been resolved since the previous import). The Test page will show these Findings as **Closed**.
Expand Down
18 changes: 18 additions & 0 deletions dojo/db_migrations/0245_finding_fix_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.11 on 2025-08-28 09:16

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dojo', '0244_pghistory_indices'),
]

operations = [
migrations.AddField(
model_name='finding',
name='fix_version',
field=models.CharField(blank=True, help_text='Version of the affected component in which the flaw is fixed.', max_length=100, null=True, verbose_name='Fix version'),
),
]
7 changes: 7 additions & 0 deletions dojo/importers/default_reimporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ def process_matched_mitigated_finding(
to cover circumstances where mitigation timestamps are different, and
decide which one to honor
"""
if existing_finding.fix_available != unsaved_finding.fix_available:
existing_finding.fix_available = unsaved_finding.fix_available
existing_finding.fix_version = unsaved_finding.fix_version

# if the reimported item has a mitigation time, we can compare
if unsaved_finding.is_mitigated:
# The new finding is already mitigated, so nothing to change on the
Expand Down Expand Up @@ -588,6 +592,9 @@ def process_matched_active_finding(
# First check that the existing finding is definitely not mitigated
if not (existing_finding.mitigated and existing_finding.is_mitigated):
logger.debug("Reimported item matches a finding that is currently open.")
if existing_finding.fix_available != unsaved_finding.fix_available:
existing_finding.fix_available = unsaved_finding.fix_available
existing_finding.fix_version = unsaved_finding.fix_version
if unsaved_finding.is_mitigated:
logger.debug("Reimported mitigated item matches a finding that is currently open, closing.")
# TODO: Implement a date comparison for opened defectdojo findings before closing them by reimporting,
Expand Down
5 changes: 5 additions & 0 deletions dojo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2415,6 +2415,11 @@ class Finding(models.Model):
default=None,
verbose_name=_("Fix Available"),
help_text=_("Denotes if there is a fix available for this flaw."))
fix_version = models.CharField(null=True,
blank=True,
max_length=100,
verbose_name=_("Fix version"),
help_text=_("Version of the affected component in which the flaw is fixed."))
impact = models.TextField(verbose_name=_("Impact"),
null=True,
blank=True,
Expand Down
20 changes: 20 additions & 0 deletions dojo/templates/dojo/view_finding.html
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,12 @@ <h3 class="pull-left finding-title">
{% if finding.component_version %}
<th>Component Version</th>
{% endif %}
{% if finding.fix_available %}
<th>Fix Available</th>
{% endif %}
{% if finding.fix_version %}
<th>Fixed Version</th>
{% endif %}
{% if finding.has_jira_configured or finding.jira_issue %}
<th>JIRA</th>
<th>JIRA Change</th>
Expand Down Expand Up @@ -615,6 +621,20 @@ <h3 class="pull-left finding-title">
</span>
</td>
{% endif %}
{% if finding.fix_available %}
<td>
<span>
{{ finding.fix_available }}
</span>
</td>
{% endif %}
{% if finding.fix_version %}
<td>
<span>
{{ finding.fix_version }}
</span>
</td>
{% endif %}
{% if finding.has_jira_configured or finding.has_jira_issue or finding.has_jira_group_issue %}
<td id="jira">
{% if finding.has_jira_group_issue %}
Expand Down
7 changes: 7 additions & 0 deletions dojo/tools/anchore_grype/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,16 @@ def get_findings(self, file, test):
finding_description += f"\n**Package URL:** {artifact_purl}"

finding_mitigation = None
fix_available = False
fix_version = None
if vuln_fix_versions:
fix_available = True
finding_mitigation = "Upgrade to version:"
if len(vuln_fix_versions) == 1:
finding_mitigation += f" {vuln_fix_versions[0]}"
fix_version = vuln_fix_versions[0]
else:
fix_version = ", ".join(vuln_fix_versions)
for fix_version in vuln_fix_versions:
finding_mitigation += f"\n- {fix_version}"

Expand Down Expand Up @@ -200,6 +205,8 @@ def get_findings(self, file, test):
dynamic_finding=False,
nb_occurences=1,
file_path=file_path,
fix_available=fix_available,
fix_version=fix_version,
)
dupes[dupe_key].unsaved_vulnerability_ids = vulnerability_ids

Expand Down
315 changes: 315 additions & 0 deletions unittests/scans/anchore_grype/fix_available.json

Large diffs are not rendered by default.

313 changes: 313 additions & 0 deletions unittests/scans/anchore_grype/fix_not_available.json

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions unittests/test_import_reimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ def __init__(self, *args, **kwargs):
self.scan_type_gitlab_dast = "GitLab DAST Report"

self.anchore_grype_file_name = get_unit_tests_scans_path("anchore_grype") / "check_all_fields.json"
self.anchore_grype_file_name_fix_not_available = get_unit_tests_scans_path("anchore_grype") / "fix_not_available.json"
self.anchore_grype_file_name_fix_available = get_unit_tests_scans_path("anchore_grype") / "fix_available.json"
self.anchore_grype_scan_type = "Anchore Grype"

self.checkmarx_one_open_and_false_positive = get_unit_tests_scans_path("checkmarx_one") / "one-open-one-false-positive.json"
Expand Down Expand Up @@ -1693,6 +1695,30 @@ def test_import_reimport_vulnerability_ids(self):
self.assertEqual("GHSA-v6rh-hp5x-86rv", findings[3].vulnerability_ids[0])
self.assertEqual("CVE-2021-44420", findings[3].vulnerability_ids[1])

def test_import_reimport_fix_available(self):
import0 = self.import_scan_with_params(self.anchore_grype_file_name_fix_not_available, scan_type=self.anchore_grype_scan_type)
test_id = import0["test"]
test = Test.objects.get(id=test_id)
findings = Finding.objects.filter(test=test)
self.assertEqual(1, len(findings))
self.assertEqual(False, findings[0].fix_available)
self.assertEqual(None, findings[0].fix_version)

test_type = Test_Type.objects.get(name=self.anchore_grype_scan_type)
reimport_test = Test(
engagement=test.engagement,
test_type=test_type,
scan_type=self.anchore_grype_scan_type,
target_start=datetime.now(timezone.get_current_timezone()),
target_end=datetime.now(timezone.get_current_timezone()),
)
reimport_test.save()
self.reimport_scan_with_params(reimport_test.id, self.anchore_grype_file_name_fix_available, scan_type=self.anchore_grype_scan_type)
findings = Finding.objects.filter(test=reimport_test)
self.assertEqual(1, len(findings))
self.assertEqual(True, findings[0].fix_available)
self.assertEqual("1.2.3", findings[0].fix_version)

def test_import_history_reactivated_and_untouched_findings_do_not_mix(self):
import0 = self.import_scan_with_params(self.generic_import_1, scan_type=self.scan_type_generic)
test_id = import0["test"]
Expand Down
16 changes: 16 additions & 0 deletions unittests/tools/test_anchore_grype_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,22 @@ def test_grype_issue_9618(self):
findings = parser.get_findings(testfile, Test())
self.assertEqual(35, len(findings))

def test_grype_fix_not_available(self):
with (get_unit_tests_scans_path("anchore_grype") / "fix_not_available.json").open(encoding="utf-8") as testfile:
parser = AnchoreGrypeParser()
findings = parser.get_findings(testfile, Test())
self.assertEqual(1, len(findings))
self.assertEqual(findings[0].fix_available, False)
self.assertEqual(findings[0].fix_version, None)

def test_grype_fix_available(self):
with (get_unit_tests_scans_path("anchore_grype") / "fix_available.json").open(encoding="utf-8") as testfile:
parser = AnchoreGrypeParser()
findings = parser.get_findings(testfile, Test())
self.assertEqual(1, len(findings))
self.assertEqual(findings[0].fix_available, True)
self.assertEqual(findings[0].fix_version, "1.2.3")

def test_grype_issue_9942(self):
with (get_unit_tests_scans_path("anchore_grype") / "issue_9942.json").open(encoding="utf-8") as testfile:
parser = AnchoreGrypeParser()
Expand Down
Loading