Skip to content

Commit b54652e

Browse files
bugfix: reimport: close_old_findings must respect service field (#12782)
* reimport: close_old_findings must respect service field * reimport: close_old_findings must respect service field * close_old_findings: update docs and help texts * typo * reimport docs tweak * reimport: assert that reopen respect service field
1 parent b5f3286 commit b54652e

8 files changed

Lines changed: 115 additions & 24 deletions

File tree

docs/content/en/about_defectdojo/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ There are two different methods to import a report from a security tool into Def
4848
- **Import** handles the report as a single point-in-time record. Importing a report creates a Test within DefectDojo that holds the Findings rendered from that report.
4949
- **Reimport** is used to extend an existing Test. If you have a more open-ended approach to your testing process, you continuously Reimport the latest version of your report to an existing Test. DefectDojo will compare the results of the incoming report to your existing data, record any changes, and then adjust the Findings in the Test so that they match the latest report.
5050

51-
Both methods also use **Deduplication** differently: while two discrete Imported Tests in the same Product will identify and label duplicate Findings, Reimport will discard duplicate Findings altogether.
51+
Both methods also use **Deduplication** differently: while two discrete Imported Tests in the same Product will identify and label duplicate Findings, Reimport will skip duplicates in uploaded reports as theses Findings already exist in Defect Dojo.
5252

5353
Generally speaking - if a point-in-time report is what you need, Import is the best method to use. If you are continuously running and ingesting reports from a tool, Reimport is the better method for keeping things organized.
5454

docs/content/en/open_source/archived_docs/usage/features.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -386,15 +386,12 @@ details about the deduplication process : switch
386386

387387
### Deduplication - APIv2 parameters
388388

389-
- `close_old_findings` : if true, findings that are not
390-
duplicates and that were in the previous scan of the same type
391-
(example ZAP) for the same engagement (or product in case of
392-
\"close_old_findings_product_scope\") and that are not present in the new
393-
scan are closed (Inactive, Verified, Mitigated).
394-
- `close_old_findings_product_scope` : if true, close_old_findings applies
395-
to all findings of the same type in the product. Note that
396-
\"Deduplication on engagement\" is no longer used to determine the
397-
scope of close_old_findings.
389+
| Parameter | Import behaviour | Reimport Behaviour |
390+
|-----------|------------------|-------------------|
391+
| `close_old_findings` | if `true`, findings that are not duplicates and that were in the previous scan of the same type (example ZAP) for the same **engagement** (or product in case of `close_old_findings_product_scope`) and that are not present in the new scan are closed (`Inactive`, `Verified`, `Mitigated`). | if `true`, findings that that are in the same **test** and that are not present in the new scan are closed (`Inactive`, `Verified`, `Mitigated`) |
392+
| `close_old_findings_product_scope` | if true, `close_old_findings` applies to all findings of the same type in the whole **product**. Note that "Deduplication on engagement" is no longer used to determine the scope of `close_old_findings` | has no effect |
393+
394+
The `close_old_findings` feature will respect the value of the `service` field to only close findings with an identical `service` value.
398395

399396
### Deduplication / Similar findings
400397

dojo/api_v2/serializers.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2302,14 +2302,17 @@ class ImportScanSerializer(CommonImportScanSerializer):
23022302
close_old_findings = serializers.BooleanField(
23032303
required=False,
23042304
default=False,
2305-
help_text="Select if old findings no longer present in the report get closed as mitigated when importing. "
2306-
"If service has been set, only the findings for this service will be closed.",
2305+
help_text="Old findings no longer present in the new report get closed as mitigated when importing. "
2306+
"If service has been set, only the findings for this service will be closed. "
2307+
"This only affects findings within the same engagement.",
23072308
)
23082309
close_old_findings_product_scope = serializers.BooleanField(
23092310
required=False,
23102311
default=False,
2311-
help_text="Select if close_old_findings applies to all findings of the same type in the product. "
2312-
"By default, it is false meaning that only old findings of the same type in the engagement are in scope.",
2312+
help_text="Old findings no longer present in the new report get closed as mitigated when importing. "
2313+
"If service has been set, only the findings for this service will be closed. "
2314+
"This only affects findings within the same product."
2315+
"By default, it is false meaning that only old findings of the same type in the engagement are in scope.",
23132316
)
23142317
version = serializers.CharField(
23152318
required=False, help_text="Version that was scanned.",
@@ -2380,15 +2383,15 @@ class ReImportScanSerializer(CommonImportScanSerializer):
23802383
# also for ReImport.
23812384
close_old_findings = serializers.BooleanField(
23822385
required=False,
2383-
default=True,
2384-
help_text="Select if old findings no longer present in the report get closed as mitigated when importing.",
2386+
default=False,
2387+
help_text="Old findings no longer present in the new report get closed as mitigated when importing. "
2388+
"If service has been set, only the findings for this service will be closed. "
2389+
"This only affects findings within the same test.",
23852390
)
23862391
close_old_findings_product_scope = serializers.BooleanField(
23872392
required=False,
23882393
default=False,
2389-
help_text="Select if close_old_findings applies to all findings of the same type in the product. "
2390-
"By default, it is false meaning that only old findings of the same type in the engagement are in scope. "
2391-
"Note that this only applies on the first call to reimport-scan.",
2394+
help_text="This has no effect on reimport",
23922395
)
23932396
version = serializers.CharField(
23942397
required=False,

dojo/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ class ReImportScanForm(forms.Form):
651651
label="Choose report file",
652652
allow_empty_file=True,
653653
required=False)
654-
close_old_findings = forms.BooleanField(help_text="Select if old findings no longer present in the report get closed as mitigated when importing.",
654+
close_old_findings = forms.BooleanField(help_text="Select if old findings in the same test that are no longer present in the report get closed as mitigated when importing.",
655655
required=False, initial=True)
656656
version = forms.CharField(max_length=100, required=False, help_text="Version that will be set on existing Test object. Leave empty to leave existing value in place.")
657657
branch_tag = forms.CharField(max_length=100, required=False, help_text="Branch or Tag that was scanned.")

dojo/importers/default_reimporter.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,15 @@ def process_findings(
162162
at import time
163163
"""
164164
self.deduplication_algorithm = self.determine_deduplication_algorithm()
165-
self.original_items = list(self.test.finding_set.all())
165+
# Only process findings with the same service value (or None)
166+
# Even though the service values is used in the hash_code calculation,
167+
# we need to make sure there are no side effects such as closing findings
168+
# for findings with a different service value
169+
# https://github.com/DefectDojo/django-DefectDojo/issues/12754
170+
original_findings = self.test.finding_set.all().filter(service=self.service)
171+
logger.debug(f"original_findings_qyer: {original_findings.query}")
172+
self.original_items = list(original_findings)
173+
logger.debug(f"original_items: {[(item.id, item.hash_code) for item in self.original_items]}")
166174
self.new_items = []
167175
self.reactivated_items = []
168176
self.unchanged_items = []

dojo/importers/endpoint_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def add_endpoints_to_unsaved_finding(
5252
finding=finding,
5353
endpoint=ep,
5454
defaults={"date": finding.date})
55-
logger.debug(f"IMPORT_SCAN: {len(endpoints)} imported")
55+
logger.debug(f"IMPORT_SCAN: {len(endpoints)} endpoints imported")
5656
return
5757

5858
@dojo_async_task

unittests/dojo_test_case.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ def import_scan_with_params(self, filename, scan_type="ZAP Scan", engagement=1,
594594
return self.import_scan(payload, expected_http_status_code)
595595

596596
def reimport_scan_with_params(self, test_id, filename, scan_type="ZAP Scan", engagement=1, minimum_severity="Low", *, active=True, verified=False, push_to_jira=None,
597-
tags=None, close_old_findings=True, group_by=None, engagement_name=None, scan_date=None,
597+
tags=None, close_old_findings=True, group_by=None, engagement_name=None, scan_date=None, service=None,
598598
product_name=None, product_type_name=None, auto_create_context=None, expected_http_status_code=201, test_title=None):
599599
with Path(filename).open(encoding="utf-8") as testfile:
600600
payload = {
@@ -640,6 +640,9 @@ def reimport_scan_with_params(self, test_id, filename, scan_type="ZAP Scan", eng
640640
if scan_date is not None:
641641
payload["scan_date"] = scan_date
642642

643+
if service is not None:
644+
payload["service"] = service
645+
643646
return self.reimport_scan(payload, expected_http_status_code=expected_http_status_code)
644647

645648
def endpoint_meta_import_scan_with_params(self, filename, product=1, product_name=None, *,

unittests/test_import_reimport.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1335,6 +1335,83 @@ def test_import_param_close_old_findings_with_and_without_service_2(self):
13351335
engagement_findings_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=True, is_mitigated=False).count()
13361336
self.assertEqual(engagement_findings_count, 4)
13371337

1338+
# close_old_findings functionality: second import to different engagement with different service should not close findings from the first engagement
1339+
def test_reimport_close_old_findings_different_engagements_different_services(self):
1340+
logger.debug("importing clair report with service A into engagement 1")
1341+
with assertTestImportModelsCreated(self, imports=1, affected_findings=4, created=4):
1342+
import1 = self.import_scan_with_params(self.clair_few_findings, scan_type=self.scan_type_clair, engagement=1, close_old_findings=True, service="service_A")
1343+
1344+
test_id = import1["test"]
1345+
test = self.get_test(test_id)
1346+
findings = self.get_test_findings_api(test_id)
1347+
self.log_finding_summary_json_api(findings)
1348+
# imported count must match count in the report
1349+
self.assert_finding_count_json(4, findings)
1350+
1351+
# imported findings should be active in engagement 1
1352+
engagement1_findings = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=True, is_mitigated=False)
1353+
self.assertEqual(engagement1_findings.count(), 4)
1354+
1355+
# reimporting the same report into the same test with a different service should not close any findings and create 4 new findings
1356+
self.reimport_scan_with_params(test_id, self.clair_few_findings, scan_type=self.scan_type_clair, close_old_findings=True, service="service_B")
1357+
1358+
engagement1_active_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=True, is_mitigated=False)
1359+
self.assertEqual(engagement1_active_finding_count.count(), 8)
1360+
engagement1_mitigated_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=False, is_mitigated=True)
1361+
self.assertEqual(engagement1_mitigated_finding_count.count(), 0)
1362+
# verify findings from engagement 1 are still the same (not mitigated/closed)
1363+
for finding in engagement1_active_finding_count:
1364+
self.assertTrue(finding.active)
1365+
self.assertFalse(finding.is_mitigated)
1366+
1367+
# reimporting an empty report with service A should close all findings from the first import, but not the reimported ones with service B
1368+
self.reimport_scan_with_params(test_id, self.clair_empty, scan_type=self.scan_type_clair, close_old_findings=True, service="service_A")
1369+
1370+
engagement1_active_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=True, is_mitigated=False)
1371+
self.assertEqual(engagement1_active_finding_count.count(), 4)
1372+
engagement1_mitigated_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=False, is_mitigated=True)
1373+
self.assertEqual(engagement1_mitigated_finding_count.count(), 4)
1374+
1375+
for finding in engagement1_active_finding_count:
1376+
self.assertTrue(finding.active)
1377+
self.assertFalse(finding.is_mitigated)
1378+
self.assertEqual(finding.service, "service_B")
1379+
1380+
for finding in engagement1_mitigated_finding_count:
1381+
self.assertFalse(finding.active)
1382+
self.assertTrue(finding.is_mitigated)
1383+
self.assertEqual(finding.service, "service_A")
1384+
1385+
# reimporting an empty report with service B should close all findings from the second import, and not reopen any findings
1386+
self.reimport_scan_with_params(test_id, self.clair_empty, scan_type=self.scan_type_clair, close_old_findings=True, service="service_B")
1387+
1388+
engagement1_active_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=True, is_mitigated=False)
1389+
self.assertEqual(engagement1_active_finding_count.count(), 0)
1390+
engagement1_mitigated_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=False, is_mitigated=True)
1391+
self.assertEqual(engagement1_mitigated_finding_count.count(), 8)
1392+
1393+
for finding in engagement1_mitigated_finding_count:
1394+
self.assertFalse(finding.active)
1395+
self.assertTrue(finding.is_mitigated)
1396+
1397+
# reimporting a report with findings and service A should reopen the 4findings with service_A but leave the findings with service_B closed.
1398+
self.reimport_scan_with_params(test_id, self.clair_few_findings, scan_type=self.scan_type_clair, close_old_findings=True, service="service_A")
1399+
1400+
engagement1_active_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=True, is_mitigated=False)
1401+
self.assertEqual(engagement1_active_finding_count.count(), 4)
1402+
engagement1_mitigated_finding_count = Finding.objects.filter(test__engagement_id=1, test__test_type=test.test_type, active=False, is_mitigated=True)
1403+
self.assertEqual(engagement1_mitigated_finding_count.count(), 4)
1404+
1405+
for finding in engagement1_active_finding_count:
1406+
self.assertTrue(finding.active)
1407+
self.assertFalse(finding.is_mitigated)
1408+
self.assertEqual(finding.service, "service_A")
1409+
1410+
for finding in engagement1_mitigated_finding_count:
1411+
self.assertFalse(finding.active)
1412+
self.assertTrue(finding.is_mitigated)
1413+
self.assertEqual(finding.service, "service_B")
1414+
13381415
def test_import_reimport_generic(self):
13391416
"""
13401417
This test do a basic import and re-import of a generic JSON report
@@ -1873,7 +1950,7 @@ def import_scan_with_params_ui(self, filename, scan_type="ZAP Scan", engagement=
18731950

18741951
return self.import_scan_ui(engagement, payload)
18751952

1876-
def reimport_scan_with_params_ui(self, test_id, filename, scan_type="ZAP Scan", minimum_severity="Low", *, active=True, verified=False, push_to_jira=None, tags=None, close_old_findings=True, scan_date=None):
1953+
def reimport_scan_with_params_ui(self, test_id, filename, scan_type="ZAP Scan", minimum_severity="Low", *, active=True, verified=False, push_to_jira=None, tags=None, close_old_findings=True, scan_date=None, service=None):
18771954
# Mimic old functionality for active/verified to avoid breaking tests
18781955
activePayload = "force_to_true"
18791956
if not active:
@@ -1902,6 +1979,9 @@ def reimport_scan_with_params_ui(self, test_id, filename, scan_type="ZAP Scan",
19021979
if scan_date is not None:
19031980
payload["scan_date"] = scan_date
19041981

1982+
if service is not None:
1983+
payload["service"] = service
1984+
19051985
return self.reimport_scan_ui(test_id, payload)
19061986

19071987
# Observations:

0 commit comments

Comments
 (0)