diff --git a/docker-compose.yml b/docker-compose.yml index 20dcaf35b88..13f9be7a4ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -127,6 +127,7 @@ services: POSTGRES_PASSWORD: ${DD_DATABASE_PASSWORD:-defectdojo} volumes: - defectdojo_postgres:/var/lib/postgresql/data + - ./psql_bck:/psql_bck redis: # Pinning to this version due to licensing constraints image: redis:7.2.9-alpine@sha256:fce236b99c58ef7196c4e243e43f533b404d5c17239cae4e6e262b729a1952b3 diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index e53b0b35475..bcc63110851 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -31,7 +31,6 @@ save_vulnerability_ids, save_vulnerability_ids_template, ) -from dojo.finding.queries import get_authorized_findings from dojo.group.utils import get_auth_group_name from dojo.importers.auto_create_context import AutoCreateContextManager from dojo.importers.base_importer import BaseImporter @@ -117,6 +116,7 @@ LargeScanSizeProductAnnouncement, ScanTypeProductAnnouncement, ) +from dojo.risk_acceptance.helper import validate_findings_engagement from dojo.tools.factory import ( get_choices_sorted, requires_file, @@ -1535,68 +1535,68 @@ def create(self, validated_data): return instance def update(self, instance, validated_data): - # Determine findings to risk accept, and findings to unaccept risk - existing_findings = Finding.objects.filter(risk_acceptance=self.instance.id) - new_findings_ids = [x.id for x in validated_data.get("accepted_findings", [])] - new_findings = Finding.objects.filter(id__in=new_findings_ids) - findings_to_add = set(new_findings) - set(existing_findings) - findings_to_remove = set(existing_findings) - set(new_findings) - findings_to_add = Finding.objects.filter(id__in=[x.id for x in findings_to_add]) - findings_to_remove = Finding.objects.filter(id__in=[x.id for x in findings_to_remove]) + if "accepted_findings" in validated_data: + # Determine findings to risk accept, and findings to unaccept risk + existing_findings = Finding.objects.filter(risk_acceptance=self.instance.id) + new_findings_ids = [x.id for x in validated_data.get("accepted_findings", [])] + new_findings = Finding.objects.filter(id__in=new_findings_ids) + findings_to_add = set(new_findings) - set(existing_findings) + findings_to_remove = set(existing_findings) - set(new_findings) + findings_to_add = Finding.objects.filter(id__in=[x.id for x in findings_to_add]) + findings_to_remove = Finding.objects.filter(id__in=[x.id for x in findings_to_remove]) + else: + findings_to_remove = findings_to_add = [] + # Make the update in the database instance = super().update(instance, validated_data) - user = getattr(self.context.get("request", None), "user", None) - # Add the new findings - ra_helper.add_findings_to_risk_acceptance(user, instance, findings_to_add) - # Remove the ones that were not present in the payload - for finding in findings_to_remove: - ra_helper.remove_finding_from_risk_acceptance(user, instance, finding) + + if findings_to_add or findings_to_remove: + user = getattr(self.context.get("request", None), "user", None) + # Add the new findings + ra_helper.add_findings_to_risk_acceptance(user, instance, findings_to_add) + # Remove the ones that were not present in the payload + for finding in findings_to_remove: + ra_helper.remove_finding_from_risk_acceptance(user, instance, finding) return instance @extend_schema_field(serializers.CharField()) def get_path(self, obj): - engagement = Engagement.objects.filter( - risk_acceptance__id__in=[obj.id], - ).first() path = "No proof has been supplied" - if engagement and obj.filename() is not None: + if obj.filename() is not None: path = reverse( - "download_risk_acceptance", args=(engagement.id, obj.id), + "download_risk_acceptance", args=(obj.id, ), ) request = self.context.get("request") if request: path = request.build_absolute_uri(path) return path - @extend_schema_field(serializers.IntegerField()) - def get_engagement(self, obj): - engagement = Engagement.objects.filter( - risk_acceptance__id__in=[obj.id], - ).first() - return EngagementSerializer(read_only=True).to_representation( - engagement, - ) - def validate(self, data): - def validate_findings_have_same_engagement(finding_objects: list[Finding]): - engagements = finding_objects.values_list("test__engagement__id", flat=True).distinct().count() - if engagements > 1: - msg = "You are not permitted to add findings from multiple engagements" - raise PermissionDenied(msg) - - findings = data.get("accepted_findings", []) - findings_ids = [x.id for x in findings] - finding_objects = Finding.objects.filter(id__in=findings_ids) - authed_findings = get_authorized_findings(Permissions.Finding_Edit).filter(id__in=findings_ids) - if len(findings) != len(authed_findings): - msg = "You are not permitted to add one or more selected findings to this risk acceptance" - raise PermissionDenied(msg) - if self.context["request"].method == "POST": - validate_findings_have_same_engagement(finding_objects) - elif self.context["request"].method in {"PATCH", "PUT"}: - existing_findings = Finding.objects.filter(risk_acceptance=self.instance.id) - existing_and_new_findings = existing_findings | finding_objects - validate_findings_have_same_engagement(existing_and_new_findings) + + findings = data.get("accepted_findings", self.instance.accepted_findings.all() if self.instance else None) + engagement = data.get("engagement", self.instance.engagement if self.instance else None) + validate_findings_engagement(engagement, findings) + return data + + # def validate_findings_have_same_engagement(finding_objects: list[Finding]): # TODO: check + # engagements = finding_objects.values_list("test__engagement__id", flat=True).distinct().count() + # if engagements > 1: + # msg = "You are not permitted to add findings from multiple engagements" # TODO: same is missing for UI + # raise PermissionDenied(msg) + + # findings = data.get("accepted_findings", []) + # findings_ids = [x.id for x in findings] + # finding_objects = Finding.objects.filter(id__in=findings_ids) + # authed_findings = get_authorized_findings(Permissions.Finding_Edit).filter(id__in=findings_ids) + # if len(findings) != len(authed_findings): + # msg = "You are not permitted to add one or more selected findings to this risk acceptance" + # raise PermissionDenied(msg) + # if self.context["request"].method == "POST": + # validate_findings_have_same_engagement(finding_objects) + # elif self.context["request"].method in {"PATCH", "PUT"}: + # existing_findings = Finding.objects.filter(risk_acceptance=self.instance.id) + # existing_and_new_findings = existing_findings | finding_objects + # validate_findings_have_same_engagement(existing_and_new_findings) return data class Meta: diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 28c59befe08..dc260f03bc2 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -420,7 +420,7 @@ def destroy(self, request, *args, **kwargs): def get_queryset(self): return ( get_authorized_engagements(Permissions.Engagement_View) - .prefetch_related("notes", "risk_acceptance", "files") + .prefetch_related("notes", "risk_acceptance_set", "files") .distinct() ) @@ -704,7 +704,7 @@ def get_queryset(self): return ( get_authorized_risk_acceptances(Permissions.Risk_Acceptance) .prefetch_related( - "notes", "engagement_set", "owner", "accepted_findings", + "notes", "engagement", "owner", "accepted_findings", ) .distinct() ) diff --git a/dojo/authorization/authorization.py b/dojo/authorization/authorization.py index c536f1b0086..212ffd6c755 100644 --- a/dojo/authorization/authorization.py +++ b/dojo/authorization/authorization.py @@ -23,6 +23,7 @@ Product_Type, Product_Type_Group, Product_Type_Member, + Risk_Acceptance, Stub_Finding, Test, ) @@ -92,6 +93,11 @@ def user_has_permission(user, obj, permission): and permission in Permissions.get_engagement_permissions() ): return user_has_permission(user, obj.product, permission) + if ( + isinstance(obj, Risk_Acceptance) + and permission in Permissions.get_engagement_permissions() + ): + return user_has_permission(user, obj.engagement, permission) if ( isinstance(obj, Test) and permission in Permissions.get_test_permissions() diff --git a/dojo/db_migrations/0231_add_engagement_risk_acceptance.py b/dojo/db_migrations/0231_add_engagement_risk_acceptance.py new file mode 100644 index 00000000000..fecabd8b597 --- /dev/null +++ b/dojo/db_migrations/0231_add_engagement_risk_acceptance.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.8 on 2025-05-01 12:54 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0230_alter_jira_instance_accepted_mapping_resolution_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='risk_acceptance', + name='engagement', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='dojo.engagement'), + ), + ] diff --git a/dojo/db_migrations/0232_set_risk_acceptance_engagement.py b/dojo/db_migrations/0232_set_risk_acceptance_engagement.py new file mode 100644 index 00000000000..cb5152f99d2 --- /dev/null +++ b/dojo/db_migrations/0232_set_risk_acceptance_engagement.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.8 on 2025-05-01 12:59 + +import django.db.models.deletion +from django.db import migrations, models +import logging + +logger = logging.getLogger(__name__) + +def set_engagement_based_on_findings(apps, schema_editor): + Engagement = apps.get_model('dojo', 'Engagement') + RiskAcceptance = apps.get_model('dojo', 'Risk_Acceptance') + through_model = Engagement.risk_acceptance.through + + for rel in through_model.objects.all(): + ra = RiskAcceptance.objects.get(pk=rel.risk_acceptance_id) + ra.engagement_id = rel.engagement_id + ra.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0231_add_engagement_risk_acceptance'), + ] + + operations = [ + migrations.RunPython(set_engagement_based_on_findings, migrations.RunPython.noop), + ] diff --git a/dojo/db_migrations/0233_alter_risk_acceptance_engagement.py b/dojo/db_migrations/0233_alter_risk_acceptance_engagement.py new file mode 100644 index 00000000000..8ebca9eb667 --- /dev/null +++ b/dojo/db_migrations/0233_alter_risk_acceptance_engagement.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.8 on 2025-05-01 12:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0232_set_risk_acceptance_engagement'), + ] + + operations = [ + migrations.AlterField( + model_name='risk_acceptance', + name='engagement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dojo.engagement'), + ), + migrations.RemoveField( + model_name='engagement', + name='risk_acceptance', + ), + ] diff --git a/dojo/engagement/urls.py b/dojo/engagement/urls.py index c70bb56a95e..43100b3da5d 100644 --- a/dojo/engagement/urls.py +++ b/dojo/engagement/urls.py @@ -36,17 +36,17 @@ views.add_risk_acceptance, name="add_risk_acceptance"), re_path(r"^engagement/(?P\d+)/risk_acceptance/add/(?P\d+)$", views.add_risk_acceptance, name="add_risk_acceptance"), - re_path(r"^engagement/(?P\d+)/risk_acceptance/(?P\d+)$", + re_path(r"^engagement/risk_acceptance/(?P\d+)$", views.view_risk_acceptance, name="view_risk_acceptance"), - re_path(r"^engagement/(?P\d+)/risk_acceptance/(?P\d+)/edit$", + re_path(r"^engagement/risk_acceptance/(?P\d+)/edit$", views.edit_risk_acceptance, name="edit_risk_acceptance"), - re_path(r"^engagement/(?P\d+)/risk_acceptance/(?P\d+)/expire$", + re_path(r"^engagement/risk_acceptance/(?P\d+)/expire$", views.expire_risk_acceptance, name="expire_risk_acceptance"), - re_path(r"^engagement/(?P\d+)/risk_acceptance/(?P\d+)/reinstate$", + re_path(r"^engagement/risk_acceptance/(?P\d+)/reinstate$", views.reinstate_risk_acceptance, name="reinstate_risk_acceptance"), - re_path(r"^engagement/(?P\d+)/risk_acceptance/(?P\d+)/delete$", + re_path(r"^engagement/risk_acceptance/(?P\d+)/delete$", views.delete_risk_acceptance, name="delete_risk_acceptance"), - re_path(r"^engagement/(?P\d+)/risk_acceptance/(?P\d+)/download$", + re_path(r"^engagement/risk_acceptance/(?P\d+)/download$", views.download_risk_acceptance, name="download_risk_acceptance"), re_path(r"^engagement/(?P\d+)/threatmodel$", views.view_threatmodel, name="view_threatmodel"), diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py index 3dd4a2cadcd..7098b928431 100644 --- a/dojo/engagement/views.py +++ b/dojo/engagement/views.py @@ -425,7 +425,7 @@ def get_template(self): return "dojo/view_eng.html" def get_risks_accepted(self, eng): - return eng.risk_acceptance.all().select_related("owner").annotate(accepted_findings_count=Count("accepted_findings__id")) + return eng.risk_acceptance_set.all().select_related("owner").annotate(accepted_findings_count=Count("accepted_findings__id")) def get_filtered_tests( self, @@ -1226,8 +1226,6 @@ def add_risk_acceptance(request, eid, fid=None): if notes: risk_acceptance.notes.add(notes) - eng.risk_acceptance.add(risk_acceptance) - findings = form.cleaned_data["accepted_findings"] risk_acceptance = ra_helper.add_findings_to_risk_acceptance(request.user, risk_acceptance, findings) @@ -1241,13 +1239,16 @@ def add_risk_acceptance(request, eid, fid=None): return redirect_to_return_url_or_else(request, reverse("view_engagement", args=(eid, ))) else: risk_acceptance_title_suggestion = f"Accept: {finding}" - form = RiskAcceptanceForm(initial={"owner": request.user, "name": risk_acceptance_title_suggestion}) + form = RiskAcceptanceForm(initial={"owner": request.user, "name": risk_acceptance_title_suggestion, "engagement": eng.id}) finding_choices = Finding.objects.filter(duplicate=False, test__engagement=eng).filter(NOT_ACCEPTED_FINDINGS_QUERY).order_by("title") form.fields["accepted_findings"].queryset = finding_choices if fid: form.fields["accepted_findings"].initial = {fid} + field = form.fields["engagement"] + field.widget = field.hidden_widget() + product_tab = Product_Tab(eng.product, title="Risk Acceptance", tab="engagements") product_tab.setEngagement(eng) @@ -1258,20 +1259,20 @@ def add_risk_acceptance(request, eid, fid=None): }) -@user_is_authorized(Engagement, Permissions.Engagement_View, "eid") -def view_risk_acceptance(request, eid, raid): - return view_edit_risk_acceptance(request, eid=eid, raid=raid, edit_mode=False) +@user_is_authorized(Risk_Acceptance, Permissions.Engagement_View, "raid") +def view_risk_acceptance(request, raid): + return view_edit_risk_acceptance(request, raid=raid, edit_mode=False) -@user_is_authorized(Engagement, Permissions.Risk_Acceptance, "eid") -def edit_risk_acceptance(request, eid, raid): - return view_edit_risk_acceptance(request, eid=eid, raid=raid, edit_mode=True) +@user_is_authorized(Risk_Acceptance, Permissions.Risk_Acceptance, "raid") +def edit_risk_acceptance(request, raid): + return view_edit_risk_acceptance(request, raid=raid, edit_mode=True) # will only be called by view_risk_acceptance and edit_risk_acceptance -def view_edit_risk_acceptance(request, eid, raid, *, edit_mode=False): +def view_edit_risk_acceptance(request, raid, *, edit_mode=False): risk_acceptance = get_object_or_404(Risk_Acceptance, pk=raid) - eng = get_object_or_404(Engagement, pk=eid) + eng = risk_acceptance.engagement if edit_mode and not eng.product.enable_full_risk_acceptance: raise PermissionDenied @@ -1380,12 +1381,16 @@ def view_edit_risk_acceptance(request, eid, raid, *, edit_mode=False): extra_tags="alert-success") if not errors: logger.debug("redirecting to return_url") - return redirect_to_return_url_or_else(request, reverse("view_risk_acceptance", args=(eid, raid))) + return redirect_to_return_url_or_else(request, reverse("view_risk_acceptance", args=(raid, ))) logger.error("errors found") elif edit_mode: risk_acceptance_form = EditRiskAcceptanceForm(instance=risk_acceptance) + if risk_acceptance_form: + field = risk_acceptance_form.fields["engagement"] + field.widget = field.hidden_widget() + note_form = NoteForm() replace_form = ReplaceRiskAcceptanceProofForm(instance=risk_acceptance) add_findings_form = AddFindingsRiskAcceptanceForm(instance=risk_acceptance) @@ -1428,34 +1433,32 @@ def view_edit_risk_acceptance(request, eid, raid, *, edit_mode=False): }) -@user_is_authorized(Engagement, Permissions.Risk_Acceptance, "eid") -def expire_risk_acceptance(request, eid, raid): +@user_is_authorized(Risk_Acceptance, Permissions.Risk_Acceptance, "raid") +def expire_risk_acceptance(request, raid): risk_acceptance = get_object_or_404(prefetch_for_expiration(Risk_Acceptance.objects.all()), pk=raid) - # Validate the engagement ID exists before moving forward - get_object_or_404(Engagement, pk=eid) ra_helper.expire_now(risk_acceptance) - return redirect_to_return_url_or_else(request, reverse("view_risk_acceptance", args=(eid, raid))) + return redirect_to_return_url_or_else(request, reverse("view_risk_acceptance", args=(raid, ))) -@user_is_authorized(Engagement, Permissions.Risk_Acceptance, "eid") -def reinstate_risk_acceptance(request, eid, raid): +@user_is_authorized(Risk_Acceptance, Permissions.Risk_Acceptance, "raid") +def reinstate_risk_acceptance(request, raid): risk_acceptance = get_object_or_404(prefetch_for_expiration(Risk_Acceptance.objects.all()), pk=raid) - eng = get_object_or_404(Engagement, pk=eid) + eng = risk_acceptance.engagement if not eng.product.enable_full_risk_acceptance: raise PermissionDenied ra_helper.reinstate(risk_acceptance, risk_acceptance.expiration_date) - return redirect_to_return_url_or_else(request, reverse("view_risk_acceptance", args=(eid, raid))) + return redirect_to_return_url_or_else(request, reverse("view_risk_acceptance", args=(raid, ))) -@user_is_authorized(Engagement, Permissions.Risk_Acceptance, "eid") -def delete_risk_acceptance(request, eid, raid): +@user_is_authorized(Risk_Acceptance, Permissions.Risk_Acceptance, "raid") +def delete_risk_acceptance(request, raid): risk_acceptance = get_object_or_404(Risk_Acceptance, pk=raid) - eng = get_object_or_404(Engagement, pk=eid) + eng = risk_acceptance.engagement ra_helper.delete(eng, risk_acceptance) @@ -1467,13 +1470,10 @@ def delete_risk_acceptance(request, eid, raid): return HttpResponseRedirect(reverse("view_engagement", args=(eng.id, ))) -@user_is_authorized(Engagement, Permissions.Engagement_View, "eid") -def download_risk_acceptance(request, eid, raid): +@user_is_authorized(Risk_Acceptance, Permissions.Risk_Acceptance, "raid") +def download_risk_acceptance(request, raid): mimetypes.init() risk_acceptance = get_object_or_404(Risk_Acceptance, pk=raid) - # Ensure the risk acceptance is under the supplied engagement - if not Engagement.objects.filter(risk_acceptance=risk_acceptance, id=eid).exists(): - raise PermissionDenied response = StreamingHttpResponse( FileIterWrapper( (Path(settings.MEDIA_ROOT) / "risk_acceptance.path.name").open(mode="rb"))) diff --git a/dojo/filters.py b/dojo/filters.py index efcd3fe2fdc..fe3794276ab 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -2800,7 +2800,7 @@ class Meta: "name", "accepted_findings", "recommendation", "recommendation_details", "decision", "decision_details", "accepted_by", "owner", "expiration_date", "expiration_date_warned", "expiration_date_handled", "reactivate_expired", - "restart_sla_expired", "notes", + "restart_sla_expired", "notes", "engagement", ] diff --git a/dojo/fixtures/defect_dojo_sample_data.json b/dojo/fixtures/defect_dojo_sample_data.json index 85c7311bf4a..c30e320084a 100644 --- a/dojo/fixtures/defect_dojo_sample_data.json +++ b/dojo/fixtures/defect_dojo_sample_data.json @@ -2784,7 +2784,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -2831,7 +2830,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -2878,7 +2876,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -2925,7 +2922,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -2972,7 +2968,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [ "pci" ], @@ -3019,7 +3014,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -3066,7 +3060,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -3113,7 +3106,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [ "pci" ], @@ -3162,7 +3154,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -3209,7 +3200,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -3254,7 +3244,6 @@ "deduplication_on_engagement": false, "notes": [], "files": [], - "risk_acceptance": [], "tags": [], "inherited_tags": [] } @@ -33296,6 +33285,7 @@ "restart_sla_expired": false, "created": "2024-01-29T15:35:18.089Z", "updated": "2024-01-29T15:35:18.089Z", + "engagement": 1, "accepted_findings": [ 2 ], diff --git a/dojo/fixtures/dojo_testdata.json b/dojo/fixtures/dojo_testdata.json index b35d570eaab..709bc04b994 100644 --- a/dojo/fixtures/dojo_testdata.json +++ b/dojo/fixtures/dojo_testdata.json @@ -532,7 +532,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 2, "version": null, "progress": "threat_model", @@ -562,9 +561,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [ - 1 - ], "lead": 1, "version": null, "progress": "threat_model", @@ -594,7 +590,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 2, "version": null, "progress": "threat_model", @@ -627,7 +622,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 1, "version": null, "progress": "threat_model", @@ -657,7 +651,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 1, "version": null, "progress": "threat_model", @@ -697,6 +690,7 @@ "restart_sla_expired": false, "created": "2023-03-01T22:12:43.829Z", "updated": "2023-03-01T22:12:43.891Z", + "engagement": 2, "accepted_findings": [ 226 ], @@ -712,7 +706,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 1, "version": null, "progress": "threat_model", diff --git a/dojo/fixtures/questionnaire_testdata.json b/dojo/fixtures/questionnaire_testdata.json index c95278c83ac..82ea5830479 100644 --- a/dojo/fixtures/questionnaire_testdata.json +++ b/dojo/fixtures/questionnaire_testdata.json @@ -156,7 +156,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 2, "version": null, "progress": "threat_model", @@ -186,7 +185,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 1, "version": null, "progress": "threat_model", diff --git a/dojo/fixtures/unit_limit_reqresp.json b/dojo/fixtures/unit_limit_reqresp.json index 360156f533b..d89b176c4a9 100644 --- a/dojo/fixtures/unit_limit_reqresp.json +++ b/dojo/fixtures/unit_limit_reqresp.json @@ -82,7 +82,6 @@ "report_type": null, "first_contacted": null, "tmodel_path": "none", - "risk_acceptance": [], "lead": 1, "version": null, "progress": "threat_model", diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 7e7ac9eab1e..ec423f831fe 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1689,8 +1689,8 @@ def process_resolution_from_jira(finding, resolution_id, resolution_name, assign accepted_by=assignee_name, owner=finding.reporter, decision_details=f"Risk Acceptance automatically created from JIRA issue {jira_issue.jira_key} with resolution {resolution_name}", + engagement=finding.test.engagement, ) - finding.test.engagement.risk_acceptance.add(ra) ra_helper.add_findings_to_risk_acceptance(User.objects.get_or_create(username="JIRA")[0], ra, [finding]) status_changed = True elif jira_instance and resolution_name in jira_instance.false_positive_resolutions: diff --git a/dojo/metrics/utils.py b/dojo/metrics/utils.py index 9647f84c7f9..c751f839949 100644 --- a/dojo/metrics/utils.py +++ b/dojo/metrics/utils.py @@ -54,7 +54,7 @@ def finding_queries( "test__engagement__product__prod_type", ).prefetch_related( "risk_acceptance_set", - "test__engagement__risk_acceptance", + "test__engagement__risk_acceptance_set", "test__test_type", ) @@ -148,7 +148,7 @@ def endpoint_queries( ).prefetch_related( "finding__test__engagement__product", "finding__test__engagement__product__prod_type", - "finding__test__engagement__risk_acceptance", + "finding__test__engagement__risk_acceptance_set", "finding__risk_acceptance_set", "finding__reporter", ) diff --git a/dojo/metrics/views.py b/dojo/metrics/views.py index f56430a84c2..be1e10c0fce 100644 --- a/dojo/metrics/views.py +++ b/dojo/metrics/views.py @@ -347,7 +347,7 @@ def product_type_counts(request): "Critical", "High", "Medium", "Low")).prefetch_related( "test__engagement__product", "test__engagement__product__prod_type", - "test__engagement__risk_acceptance", + "test__engagement__risk_acceptance_set", "reporter").order_by( "numerical_severity") @@ -392,7 +392,7 @@ def product_type_counts(request): "Critical", "High", "Medium", "Low")).prefetch_related( "test__engagement__product", "test__engagement__product__prod_type", - "test__engagement__risk_acceptance", + "test__engagement__risk_acceptance_set", "reporter").order_by( "numerical_severity") @@ -549,7 +549,7 @@ def product_tag_counts(request): "Critical", "High", "Medium", "Low")).prefetch_related( "test__engagement__product", "test__engagement__product__prod_type", - "test__engagement__risk_acceptance", + "test__engagement__risk_acceptance_set", "reporter").order_by( "numerical_severity") @@ -597,7 +597,7 @@ def product_tag_counts(request): "Critical", "High", "Medium", "Low")).prefetch_related( "test__engagement__product", "test__engagement__product__prod_type", - "test__engagement__risk_acceptance", + "test__engagement__risk_acceptance_set", "reporter").order_by( "numerical_severity") diff --git a/dojo/models.py b/dojo/models.py index 9e5cd5137f2..ba8b5422db4 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -1505,10 +1505,6 @@ class Engagement(models.Model): default="threat_model", editable=False) tmodel_path = models.CharField(max_length=1000, default="none", editable=False, blank=True, null=True) - risk_acceptance = models.ManyToManyField("Risk_Acceptance", - default=None, - editable=False, - blank=True) done_testing = models.BooleanField(default=False, editable=False) engagement_type = models.CharField(editable=True, max_length=30, default="Interactive", null=True, @@ -1550,7 +1546,7 @@ def copy(self): old_notes = list(self.notes.all()) old_files = list(self.files.all()) old_tags = list(self.tags.all()) - old_risk_acceptances = list(self.risk_acceptance.all()) + old_risk_acceptances = list(self.risk_acceptance_set.all()) old_tests = list(Test.objects.filter(engagement=self)) # Save the object before setting any ManyToMany relationships copy.save() @@ -1565,7 +1561,7 @@ def copy(self): test.copy(engagement=copy) # Copy the risk_acceptances for risk_acceptance in old_risk_acceptances: - copy.risk_acceptance.add(risk_acceptance.copy(engagement=copy)) + risk_acceptance.copy(engagement=copy) # Assign any tags copy.tags.set(old_tags) @@ -1595,9 +1591,6 @@ def unaccepted_open_findings(self): return findings - def accept_risks(self, accepted_risks): - self.risk_acceptance.add(*accepted_risks) - @property def has_jira_issue(self): import dojo.jira_link.helper as jira_helper @@ -2163,9 +2156,6 @@ def unaccepted_open_findings(self): return findings - def accept_risks(self, accepted_risks): - self.engagement.risk_acceptance.add(*accepted_risks) - @property def deduplication_algorithm(self): deduplicationAlgorithm = settings.DEDUPE_ALGO_LEGACY @@ -3663,6 +3653,8 @@ class Risk_Acceptance(models.Model): name = models.CharField(max_length=300, null=False, blank=False, help_text=_("Descriptive name which in the future may also be used to group risk acceptances together across engagements and products")) + engagement = models.ForeignKey(Engagement, blank=False, null=False, on_delete=models.CASCADE) + accepted_findings = models.ManyToManyField(Finding) recommendation = models.CharField(choices=TREATMENT_CHOICES, max_length=2, null=False, default=TREATMENT_FIX, help_text=_("Recommendation from the security team."), verbose_name=_("Security Recommendation")) @@ -3693,6 +3685,16 @@ class Risk_Acceptance(models.Model): def __str__(self): return str(self.name) + def clean(self): + super().clean() + if self.pk: + # Get all findings that do NOT belong to this engagement + problematic_findings = self.accepted_findings.exclude(test__engagement=self.engagement) + if problematic_findings.exists(): + problematic_ids = list(problematic_findings.values_list("id", flat=True)) + msg = f"Findings with IDs {problematic_ids} do not belong to engagement {self.engagement_id}." + raise ValidationError(msg) + def filename(self): # logger.debug('path: "%s"', self.path) if not self.path: @@ -3704,39 +3706,30 @@ def name_and_expiration_info(self): return str(self.name) + (" (expired " if self.is_expired else " (expires ") + (timezone.localtime(self.expiration_date).strftime("%b %d, %Y") if self.expiration_date else "Never") + ")" def get_breadcrumbs(self): - bc = self.engagement_set.first().get_breadcrumbs() + bc = self.engagement.get_breadcrumbs() bc += [{"title": str(self), "url": reverse("view_risk_acceptance", args=( - self.engagement_set.first().product.id, self.id))}] + self.engagement.product.id, self.id))}] return bc @property def is_expired(self): return self.expiration_date_handled is not None - # relationship is many to many, but we use it as one-to-many - @property - def engagement(self): - engs = self.engagement_set.all() - if engs: - return engs[0] - - return None - - def copy(self, engagement=None): + def copy(self, engagement): copy = _copy_model_util(self) # Save the necessary ManyToMany relationships old_notes = list(self.notes.all()) old_accepted_findings_hash_codes = [finding.hash_code for finding in self.accepted_findings.all()] + copy.engagement = engagement # Save the object before setting any ManyToMany relationships copy.save() + # Assign any accepted findings + new_accepted_findings = Finding.objects.filter(test__engagement=engagement, hash_code__in=old_accepted_findings_hash_codes, risk_accepted=True).distinct() + copy.accepted_findings.set(new_accepted_findings) # Copy the notes for notes in old_notes: copy.notes.add(notes.copy()) - # Assign any accepted findings - if engagement: - new_accepted_findings = Finding.objects.filter(test__engagement=engagement, hash_code__in=old_accepted_findings_hash_codes, risk_accepted=True).distinct() - copy.accepted_findings.set(new_accepted_findings) return copy diff --git a/dojo/product/views.py b/dojo/product/views.py index 736b7b5ec7e..57547aba54f 100644 --- a/dojo/product/views.py +++ b/dojo/product/views.py @@ -370,7 +370,7 @@ def finding_querys(request, prod): # prefetch only what's needed to avoid lots of repeated queries findings_query = findings_query.prefetch_related( # 'test__engagement', - # 'test__engagement__risk_acceptance', + # 'test__engagement__risk_acceptance_set', # 'found_by', # 'test', # 'test__test_type', @@ -439,7 +439,7 @@ def endpoint_querys(request, prod): finding__severity__in=( "Critical", "High", "Medium", "Low", "Info")).prefetch_related( "finding__test__engagement", - "finding__test__engagement__risk_acceptance", + "finding__test__engagement__risk_acceptance_set", "finding__risk_acceptance_set", "finding__reporter").annotate(severity=F("finding__severity")) filter_string_matching = get_system_setting("filter_string_matching", False) diff --git a/dojo/risk_acceptance/api.py b/dojo/risk_acceptance/api.py index 2fdaadf0afb..1774488d63c 100644 --- a/dojo/risk_acceptance/api.py +++ b/dojo/risk_acceptance/api.py @@ -52,7 +52,6 @@ def accept_risks(self, request, pk=None): base_findings = model.unaccepted_open_findings owner = request.user accepted = _accept_risks(accepted_risks, base_findings, owner) - model.accept_risks(accepted) result = RiskAcceptanceSerializer(instance=accepted, many=True) return Response(status=status.HTTP_201_CREATED, data=result.data) @@ -75,7 +74,6 @@ def accept_risks(self, request): for engagement in get_authorized_engagements(Permissions.Engagement_View): base_findings = engagement.unaccepted_open_findings accepted = _accept_risks(accepted_risks, base_findings, owner) - engagement.accept_risks(accepted) accepted_result.extend(accepted) result = RiskAcceptanceSerializer(instance=accepted_result, many=True) return Response(status=201, data=result.data) @@ -95,7 +93,8 @@ def _accept_risks(accepted_risks: list[AcceptedRisk], base_findings: QuerySet, o acceptance = Risk_Acceptance.objects.create(owner=owner, name=name[:100], decision=Risk_Acceptance.TREATMENT_ACCEPT, decision_details=risk.justification, - accepted_by=risk.accepted_by[:200]) + accepted_by=risk.accepted_by[:200], + engagement=findings[0].test.engagement) # TODO: Add validator that all findings are from this Eng to Model acceptance.accepted_findings.set(findings) findings.update(risk_accepted=True, active=False) acceptance.save() diff --git a/dojo/risk_acceptance/helper.py b/dojo/risk_acceptance/helper.py index 708892d8c31..07111a7d064 100644 --- a/dojo/risk_acceptance/helper.py +++ b/dojo/risk_acceptance/helper.py @@ -2,7 +2,7 @@ from contextlib import suppress from dateutil.relativedelta import relativedelta -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.urls import reverse from django.utils import timezone @@ -50,7 +50,7 @@ def expire_now(risk_acceptance): create_notification(event="risk_acceptance_expiration", title=title, risk_acceptance=risk_acceptance, accepted_findings=accepted_findings, reactivated_findings=reactivated_findings, engagement=risk_acceptance.engagement, product=risk_acceptance.engagement.product, - url=reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id))) + url=reverse("view_risk_acceptance", args=(risk_acceptance.id, ))) def reinstate(risk_acceptance, old_expiration_date): @@ -94,7 +94,6 @@ def delete(eng, risk_acceptance): post_jira_comments(risk_acceptance, findings, unaccepted_message_creator) risk_acceptance.accepted_findings.clear() - eng.risk_acceptance.remove(risk_acceptance) eng.save() risk_acceptance.path.delete() @@ -185,7 +184,7 @@ def expiration_handler(*args, **kwargs): create_notification(event="risk_acceptance_expiration", title=notification_title, risk_acceptance=risk_acceptance, accepted_findings=risk_acceptance.accepted_findings.all(), engagement=risk_acceptance.engagement, product=risk_acceptance.engagement.product, - url=reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id))) + url=reverse("view_risk_acceptance", args=(risk_acceptance.id, ))) post_jira_comments(risk_acceptance, risk_acceptance.accepted_findings.all(), expiration_warning_message_creator, heads_up_days) @@ -198,7 +197,7 @@ def get_view_risk_acceptance(risk_acceptance: Risk_Acceptance) -> str: # Suppressing this error because it does not happen under most circumstances that a risk acceptance does not have engagement with suppress(AttributeError): get_full_url( - reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id)), + reverse("view_risk_acceptance", args=(risk_acceptance.id, )), ) return "" @@ -206,21 +205,21 @@ def get_view_risk_acceptance(risk_acceptance: Risk_Acceptance) -> str: def expiration_message_creator(risk_acceptance, heads_up_days=0): return "Risk acceptance [({})|{}] with {} findings has expired".format( escape_for_jira(risk_acceptance.name), - get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id))), + get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.id,))), len(risk_acceptance.accepted_findings.all())) def expiration_warning_message_creator(risk_acceptance, heads_up_days=0): return "Risk acceptance [({})|{}] with {} findings will expire in {} days".format( escape_for_jira(risk_acceptance.name), - get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id))), + get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.id, ))), len(risk_acceptance.accepted_findings.all()), heads_up_days) def reinstation_message_creator(risk_acceptance, heads_up_days=0): return "Risk acceptance [({})|{}] with {} findings has been reinstated (expires on {})".format( escape_for_jira(risk_acceptance.name), - get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id))), + get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.id, ))), len(risk_acceptance.accepted_findings.all()), timezone.localtime(risk_acceptance.expiration_date).strftime("%b %d, %Y")) @@ -228,7 +227,7 @@ def accepted_message_creator(risk_acceptance, heads_up_days=0): if risk_acceptance: return "Finding has been added to risk acceptance [({})|{}] with {} findings (expires on {})".format( escape_for_jira(risk_acceptance.name), - get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id))), + get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.id, ))), len(risk_acceptance.accepted_findings.all()), timezone.localtime(risk_acceptance.expiration_date).strftime("%b %d, %Y")) return "Finding has been risk accepted" @@ -236,7 +235,7 @@ def accepted_message_creator(risk_acceptance, heads_up_days=0): def unaccepted_message_creator(risk_acceptance, heads_up_days=0): if risk_acceptance: return "finding was unaccepted/deleted from risk acceptance [({})|{}]".format(escape_for_jira(risk_acceptance.name), - get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.engagement.id, risk_acceptance.id)))) + get_full_url(reverse("view_risk_acceptance", args=(risk_acceptance.id, )))) return "Finding is no longer risk accepted" @@ -294,7 +293,7 @@ def get_almost_expired_risk_acceptances_to_handle(heads_up_days): def prefetch_for_expiration(risk_acceptances): return risk_acceptances.prefetch_related("accepted_findings", "accepted_findings__jira_issue", - "engagement_set", + "engagement", "engagement__jira_project", "engagement__jira_project__jira_instance", ) @@ -371,3 +370,10 @@ def update_endpoint_statuses(finding: Finding, *, accept_risk: bool) -> None: status.risk_accepted = False status.last_modified = timezone.now() status.save() + + +def validate_findings_engagement(engagement, findings): + invalid = [f.id for f in findings if f.test.engagement.id != engagement.id] + if invalid: + msg = f"Findings with IDs {invalid} do not belong to engagement {engagement.id}." + raise ValidationError(msg) diff --git a/dojo/templates/dojo/custom_html_report_endpoint_list.html b/dojo/templates/dojo/custom_html_report_endpoint_list.html index aca9cd3bef9..cd7bb826877 100644 --- a/dojo/templates/dojo/custom_html_report_endpoint_list.html +++ b/dojo/templates/dojo/custom_html_report_endpoint_list.html @@ -86,7 +86,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/custom_html_report_finding_list.html b/dojo/templates/dojo/custom_html_report_finding_list.html index 13f33d03dca..921f840c706 100644 --- a/dojo/templates/dojo/custom_html_report_finding_list.html +++ b/dojo/templates/dojo/custom_html_report_finding_list.html @@ -75,7 +75,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/endpoint_pdf_report.html b/dojo/templates/dojo/endpoint_pdf_report.html index 637527d39ca..240f5b22daf 100644 --- a/dojo/templates/dojo/endpoint_pdf_report.html +++ b/dojo/templates/dojo/endpoint_pdf_report.html @@ -148,7 +148,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/engagement_pdf_report.html b/dojo/templates/dojo/engagement_pdf_report.html index e1333b8b48a..94300f888eb 100644 --- a/dojo/templates/dojo/engagement_pdf_report.html +++ b/dojo/templates/dojo/engagement_pdf_report.html @@ -283,7 +283,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/finding_pdf_report.html b/dojo/templates/dojo/finding_pdf_report.html index 79235b1e07f..51472074192 100644 --- a/dojo/templates/dojo/finding_pdf_report.html +++ b/dojo/templates/dojo/finding_pdf_report.html @@ -127,7 +127,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/product_endpoint_pdf_report.html b/dojo/templates/dojo/product_endpoint_pdf_report.html index bb26f835628..3e58b1391a3 100644 --- a/dojo/templates/dojo/product_endpoint_pdf_report.html +++ b/dojo/templates/dojo/product_endpoint_pdf_report.html @@ -196,7 +196,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/product_pdf_report.html b/dojo/templates/dojo/product_pdf_report.html index 4fec57dee5a..efc924c4dcb 100644 --- a/dojo/templates/dojo/product_pdf_report.html +++ b/dojo/templates/dojo/product_pdf_report.html @@ -253,7 +253,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/product_type_pdf_report.html b/dojo/templates/dojo/product_type_pdf_report.html index fba8bf63e28..15db58c930c 100644 --- a/dojo/templates/dojo/product_type_pdf_report.html +++ b/dojo/templates/dojo/product_type_pdf_report.html @@ -182,7 +182,7 @@
{% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/snippets/risk_acceptance_actions_snippet.html b/dojo/templates/dojo/snippets/risk_acceptance_actions_snippet.html index a756818d4bf..440ed930f4a 100644 --- a/dojo/templates/dojo/snippets/risk_acceptance_actions_snippet.html +++ b/dojo/templates/dojo/snippets/risk_acceptance_actions_snippet.html @@ -3,7 +3,7 @@ {% if include_view %}
  • - + View Risk Acceptance
  • @@ -12,20 +12,20 @@ {% if engagement.product.enable_full_risk_acceptance %} {% if engagement|has_object_permission:"Risk_Acceptance" %}
  • - + Edit Risk Acceptance
  • {% if risk_acceptance.is_expired %}
  • - + Reinstate
  • {% else %}
  • - + Expire Now
  • @@ -44,7 +44,7 @@ Delete Risk Acceptance -
    + {% csrf_token %}
    diff --git a/dojo/templates/dojo/test_pdf_report.html b/dojo/templates/dojo/test_pdf_report.html index 274ca467671..9cbc162f9a3 100644 --- a/dojo/templates/dojo/test_pdf_report.html +++ b/dojo/templates/dojo/test_pdf_report.html @@ -295,7 +295,7 @@
    {% comment %} for some reason the font-awesome icons don't work with the report template{% endcomment %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} - acceptance  {% endfor %} diff --git a/dojo/templates/dojo/view_eng.html b/dojo/templates/dojo/view_eng.html index 265586e4ef4..f621c26b753 100644 --- a/dojo/templates/dojo/view_eng.html +++ b/dojo/templates/dojo/view_eng.html @@ -419,9 +419,9 @@

    Risk Acceptance - {{ risk_acceptance.created|date }} + {{ risk_acceptance.created|date }} {{ risk_acceptance.accepted_by }} - {{ risk_acceptance.name }} + {{ risk_acceptance.name }} {{ risk_acceptance.get_decision_display|default_if_none:"" }} {% if risk_acceptance.decision_details %} diff --git a/dojo/templates/dojo/view_finding.html b/dojo/templates/dojo/view_finding.html index ba973b1ba34..28233cdcaa2 100755 --- a/dojo/templates/dojo/view_finding.html +++ b/dojo/templates/dojo/view_finding.html @@ -330,7 +330,7 @@

    {% if finding.risk_acceptance_set.all %} {% for ra in finding.risk_acceptance_set.all|slice:":5" %} -
    -
    + {% csrf_token %} {% if return_url %} @@ -346,7 +346,7 @@

    Notes

    // keyboard shortcuts document.addEventListener('keydown', function(e) { if (e.key == 'e') { - window.location.assign('{% url 'edit_risk_acceptance' eng.id risk_acceptance.id %}') + window.location.assign('{% url 'edit_risk_acceptance' risk_acceptance.id %}') } }); diff --git a/dojo/templates/notifications/mail/risk_acceptance_expiration.tpl b/dojo/templates/notifications/mail/risk_acceptance_expiration.tpl index ce76a2d1b5b..db2964443fd 100644 --- a/dojo/templates/notifications/mail/risk_acceptance_expiration.tpl +++ b/dojo/templates/notifications/mail/risk_acceptance_expiration.tpl @@ -1,7 +1,7 @@ {% load i18n %} {% load navigation_tags %} {% load display_tags %} -{% url 'view_risk_acceptance' risk_acceptance.engagement.id risk_acceptance.id as risk_acceptance_url %} +{% url 'view_risk_acceptance' risk_acceptance.id as risk_acceptance_url %} {% url 'view_product' risk_acceptance.engagement.product.id as product_url %} {% url 'view_engagement' risk_acceptance.engagement.id as engagement_url %} diff --git a/dojo/templatetags/display_tags.py b/dojo/templatetags/display_tags.py index af6e3dc0e1f..1fcaf023b4b 100644 --- a/dojo/templatetags/display_tags.py +++ b/dojo/templatetags/display_tags.py @@ -732,7 +732,7 @@ def finding_display_status(finding): if "Risk Accepted" in display_status: ra = finding.risk_acceptance if ra: - url = reverse("view_risk_acceptance", args=(finding.test.engagement.id, ra.id)) + url = reverse("view_risk_acceptance", args=(ra.id, )) info = ra.name_and_expiration_info link = '
    Risk Accepted' display_status = display_status.replace("Risk Accepted", link) diff --git a/dojo/tools/api_sonarqube/updater_from_source.py b/dojo/tools/api_sonarqube/updater_from_source.py index f8c450001a1..690cf73cce0 100644 --- a/dojo/tools/api_sonarqube/updater_from_source.py +++ b/dojo/tools/api_sonarqube/updater_from_source.py @@ -101,6 +101,7 @@ def update_finding_status(finding, sonarqube_status): finding.is_mitigated = False Risk_Acceptance.objects.create( owner=finding.reporter, + engagement=finding.test.engagement, ).accepted_findings.set([finding]) elif sonarqube_status == "FALSE-POSITIVE": diff --git a/unittests/test_bulk_risk_acceptance_api.py b/unittests/test_bulk_risk_acceptance_api.py index 05bbe10e7a8..7c413d28259 100644 --- a/unittests/test_bulk_risk_acceptance_api.py +++ b/unittests/test_bulk_risk_acceptance_api.py @@ -91,7 +91,7 @@ def test_test_accept_risks(self): self.assertEqual(self.test_c.unaccepted_open_findings.count(), 33) self.assertEqual(self.test_d.unaccepted_open_findings.count(), 34) - self.assertEqual(self.engagement_2a.risk_acceptance.count(), 0) + self.assertEqual(self.engagement_2a.risk_acceptance_set.count(), 0) def test_engagement_accept_risks(self): accepted_risks = [{"vulnerability_id": f"CVE-1999-{i}", "justification": "Demonstration purposes", @@ -101,7 +101,7 @@ def test_engagement_accept_risks(self): self.assertEqual(len(result.json()), 50) self.assertEqual(self.engagement.unaccepted_open_findings.count(), 50) - self.assertEqual(self.engagement_2a.risk_acceptance.count(), 0) + self.assertEqual(self.engagement_2a.risk_acceptance_set.count(), 0) self.assertEqual(self.engagement_2a.unaccepted_open_findings.count(), 34) def test_finding_accept_risks(self): @@ -111,9 +111,9 @@ def test_finding_accept_risks(self): self.assertEqual(len(result.json()), 106) self.assertEqual(Finding.unaccepted_open_findings().count(), 62) - self.assertEqual(self.engagement_2a.risk_acceptance.count(), 0) + self.assertEqual(self.engagement_2a.risk_acceptance_set.count(), 0) self.assertEqual(self.engagement_2a.unaccepted_open_findings.count(), 34) - for ra in self.engagement_2b.risk_acceptance.all(): + for ra in self.engagement_2b.risk_acceptance_set.all(): for finding in ra.accepted_findings.all(): self.assertEqual(self.engagement_2a.product, finding.test.engagement.product) diff --git a/unittests/test_dashboard.py b/unittests/test_dashboard.py index 853dbe1a07c..85942c4f345 100644 --- a/unittests/test_dashboard.py +++ b/unittests/test_dashboard.py @@ -49,7 +49,7 @@ def accept(when: datetime, product_id: int, title: str): with patch("django.db.models.fields.timezone.now") as mock_now: mock_now.return_value = when findings = Finding.objects.filter(test__engagement__product_id=product_id, title=title) - ra = Risk_Acceptance.objects.create(name="My Risk Acceptance", owner_id=1) + ra = Risk_Acceptance.objects.create(name="My Risk Acceptance", owner_id=1, engagement=findings[0].test.engagement) ra.accepted_findings.add(*findings) findings.update(risk_accepted=True) diff --git a/unittests/test_jira_import_and_pushing_api.py b/unittests/test_jira_import_and_pushing_api.py index 6c0bcb0d26a..14e066fff30 100644 --- a/unittests/test_jira_import_and_pushing_api.py +++ b/unittests/test_jira_import_and_pushing_api.py @@ -2,7 +2,6 @@ import logging from crum import impersonate -from django.urls import reverse from rest_framework.authtoken.models import Token from rest_framework.test import APIClient from vcr import VCR @@ -17,6 +16,7 @@ get_unit_tests_scans_path, toggle_system_setting_boolean, ) +from .test_risk_acceptance import RiskAcceptanceTestUI logger = logging.getLogger(__name__) @@ -288,12 +288,6 @@ def test_import_twice_push_to_jira(self): self.assert_jira_issue_count_in_test(test_id1, 0) self.assert_jira_group_issue_count_in_test(test_id, 0) - def add_risk_acceptance(self, eid, data_risk_accceptance, fid=None): - args = (eid, fid) if fid else (eid,) - response = self.client.post(reverse("add_risk_acceptance", args=args), data_risk_accceptance) - self.assertEqual(302, response.status_code, response.content[:1000]) - return response - def test_import_grouped_reopen_expired_sla(self): # steps # import scan, make sure they are in grouped JIRA @@ -314,6 +308,7 @@ def test_import_grouped_reopen_expired_sla(self): "decision": "A", "decision_details": "it has been decided!", "accepted_by": "pointy haired boss", + "engagement": Finding.objects.get(pk=finding_id).test.engagement.pk, "owner": 1, "expiration_date": "2024-12-31", "reactivate_expired": True, @@ -324,7 +319,7 @@ def test_import_grouped_reopen_expired_sla(self): pre_jira_status = self.get_jira_issue_status(finding_id) - response = self.add_risk_acceptance(1, data_risk_accceptance=ra_data) + response = RiskAcceptanceTestUI.add_risk_acceptance(self, eid=1, data_risk_accceptance=ra_data) self.assertEqual("/engagement/1", response.url) # We do this to update the JIRA diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 0f04e7b7799..8ad46fbf4ee 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -1024,6 +1024,7 @@ def __init__(self, *args, **kwargs): "restart_sla_expired": True, "created": "2020-11-09T23:13:08.520000Z", "updated": "2023-09-15T17:17:39.462854Z", + "engagement": 2, "owner": 1, "accepted_findings": [ 226, @@ -1036,7 +1037,7 @@ def __init__(self, *args, **kwargs): self.permission_create = Permissions.Risk_Acceptance self.permission_update = Permissions.Risk_Acceptance self.permission_delete = Permissions.Risk_Acceptance - self.deleted_objects = 3 + self.deleted_objects = 2 BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) def test_create_object_not_authorized(self): @@ -1061,6 +1062,7 @@ def test_update_forbidden_engagement(self): "restart_sla_expired": True, "created": "2020-11-09T23:13:08.520000Z", "updated": "2023-09-15T17:17:39.462854Z", + "engagement": 1, "owner": 1, "accepted_findings": [ 4, diff --git a/unittests/test_risk_acceptance.py b/unittests/test_risk_acceptance.py index b31389bc432..fed031757dd 100644 --- a/unittests/test_risk_acceptance.py +++ b/unittests/test_risk_acceptance.py @@ -36,6 +36,7 @@ class RiskAcceptanceTestUI(DojoTestCase): "owner": 1, "expiration_date": "2021-07-15", "reactivate_expired": True, + "engagement": 2, } data_remove_finding_from_ra = { @@ -94,13 +95,13 @@ def test_add_findings_to_risk_acceptance_findings_accepted(self): data_add_findings_to_ra = { "add_findings": "Add Selected Findings", - "accepted_findings": [4, 5], + "accepted_findings": [226, 227], } - response = self.client.post(reverse("view_risk_acceptance", args=(1, ra.id)), + response = self.client.post(reverse("view_risk_acceptance", args=(ra.id, )), urlencode(MultiValueDict(data_add_findings_to_ra), doseq=True), content_type="application/x-www-form-urlencoded") - + logger.error(response.content) self.assertEqual(302, response.status_code, response.content[:1000]) self.assert_all_inactive_risk_accepted(Finding.objects.filter(id__in=[2, 3, 4, 5])) @@ -111,7 +112,7 @@ def test_remove_findings_from_risk_acceptance_findings_active(self): data = copy.copy(self.data_remove_finding_from_ra) data["remove_finding_id"] = 2 ra = Risk_Acceptance.objects.last() - response = self.client.post(reverse("view_risk_acceptance", args=(1, ra.id)), data) + response = self.client.post(reverse("view_risk_acceptance", args=(ra.id, )), data) self.assertEqual(302, response.status_code, response.content[:1000]) self.assert_all_active_not_risk_accepted(Finding.objects.filter(id=2)) self.assert_all_inactive_risk_accepted(Finding.objects.filter(id=3)) @@ -124,7 +125,7 @@ def test_remove_risk_acceptance_findings_active(self): data = {"id": ra.id} - self.client.post(reverse("delete_risk_acceptance", args=(1, ra.id)), data) + self.client.post(reverse("delete_risk_acceptance", args=(ra.id, )), data) self.assert_all_active_not_risk_accepted(findings) self.assert_all_active_not_risk_accepted(Finding.objects.filter(test__engagement=1)) @@ -139,7 +140,7 @@ def test_expire_risk_acceptance_findings_active(self): data = {"id": ra.id} - self.client.post(reverse("expire_risk_acceptance", args=(1, ra.id)), data) + self.client.post(reverse("expire_risk_acceptance", args=(ra.id, )), data) ra.refresh_from_db() self.assert_all_active_not_risk_accepted(findings) @@ -161,7 +162,7 @@ def test_expire_risk_acceptance_findings_not_active(self): data = {"id": ra.id} - self.client.post(reverse("expire_risk_acceptance", args=(1, ra.id)), data) + self.client.post(reverse("expire_risk_acceptance", args=(ra.id, )), data) ra.refresh_from_db() # no reactivation on expiry @@ -184,7 +185,7 @@ def test_expire_risk_acceptance_sla_not_reset(self): data = {"id": ra.id} - self.client.post(reverse("expire_risk_acceptance", args=(1, ra.id)), data) + self.client.post(reverse("expire_risk_acceptance", args=(ra.id, )), data) ra.refresh_from_db() @@ -200,7 +201,7 @@ def test_expire_risk_acceptance_sla_reset(self): data = {"id": ra.id} - self.client.post(reverse("expire_risk_acceptance", args=(1, ra.id)), data) + self.client.post(reverse("expire_risk_acceptance", args=(ra.id, )), data) ra.refresh_from_db() @@ -215,7 +216,7 @@ def test_reinstate_risk_acceptance_findings_accepted(self): data = {"id": ra.id} - self.client.post(reverse("reinstate_risk_acceptance", args=(1, ra.id)), data) + self.client.post(reverse("reinstate_risk_acceptance", args=(ra.id, )), data) ra.refresh_from_db() expiration_delta_days = get_system_setting("risk_acceptance_form_default_days", 90)