Skip to content
Draft
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
43 changes: 24 additions & 19 deletions dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1508,15 +1508,18 @@ class Meta:


class RiskAcceptanceSerializer(serializers.ModelSerializer):
product = serializers.PrimaryKeyRelatedField(
queryset=Product.objects.all(), required=True,
)
path = serializers.SerializerMethodField()

def create(self, validated_data):
instance = super().create(validated_data)
user = getattr(self.context.get("request", None), "user", None)
ra_helper.add_findings_to_risk_acceptance(user, instance, instance.accepted_findings.all())
ra_helper.add_findings_to_risk_acceptance(user, instance, instance.accepted_findings.all()) # TODO: check this if needed
return instance

def update(self, instance, validated_data):
def update(self, instance, validated_data): # TODO: check what is happening here as well
# 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", [])]
Expand All @@ -1537,33 +1540,34 @@ def update(self, instance, validated_data):

@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.product and 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 get_product(self, obj):
return ProductSerializer(read_only=True).to_representation(
obj.product,
)

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"
super().validate(data)

if self.context["request"].method != "POST":
if "product" in data and data["product"] != self.instance.product:
msg = f"Change of product is not possible. Current: {self.instance.product.pk}, new: {data["product"].pk}."
raise serializers.ValidationError(msg)

def validate_findings_have_same_product(finding_objects: list[Finding]): # TODO: 'clean' might be enought?
products = finding_objects.values_list("test__engagement__product__id", flat=True).distinct().count()
if products > 1:
msg = "You are not permitted to add findings from multiple products"
raise PermissionDenied(msg)

findings = data.get("accepted_findings", [])
Expand All @@ -1574,11 +1578,11 @@ def validate_findings_have_same_engagement(finding_objects: list[Finding]):
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)
validate_findings_have_same_product(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)
validate_findings_have_same_product(existing_and_new_findings)
return data

class Meta:
Expand Down Expand Up @@ -1751,6 +1755,7 @@ def process_risk_acceptance(self, data):
if not isinstance(is_risk_accepted, bool):
return
# Determine how to proceed based on the value of `risk_accepted`
# TODO: this should be more sofisticated: better resnonses
if is_risk_accepted and not self.instance.risk_accepted and self.instance.test.engagement.product.enable_simple_risk_acceptance and not data.get("active", False):
ra_helper.simple_risk_accept(self.context["request"].user, self.instance)
elif not is_risk_accepted and self.instance.risk_accepted: # turning off risk_accepted
Expand Down
6 changes: 3 additions & 3 deletions dojo/api_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,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", "files")
.distinct()
)

Expand Down Expand Up @@ -722,7 +722,7 @@ def get_queryset(self):
return (
get_authorized_risk_acceptances(Permissions.Risk_Acceptance)
.prefetch_related(
"notes", "engagement_set", "owner", "accepted_findings",
"notes", "product", "owner", "accepted_findings",
)
.distinct()
)
Expand Down Expand Up @@ -1735,7 +1735,7 @@ class ProductViewSet(
)

def get_queryset(self):
return get_authorized_products(Permissions.Product_View).distinct()
return get_authorized_products(Permissions.Product_View).prefetch_related("risk_acceptances").distinct() # TODO: Test this, and is it needed?

def destroy(self, request, *args, **kwargs):
instance = self.get_object()
Expand Down
9 changes: 9 additions & 0 deletions dojo/asset/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
views.view_product_components,
name="view_product_components",
),
re_path(
r"^asset/(?P<pid>\d+)/risk_acceptance$",
views.view_product_risk_acceptances,
name="view_product_risk_acceptances",
),
re_path(
r"^asset/(?P<pid>\d+)/engagements$",
views.view_engagements,
Expand Down Expand Up @@ -177,6 +182,7 @@
re_path(r"^product$", redirect_view("product")),
re_path(r"^product/(?P<pid>\d+)$", redirect_view("view_product")),
re_path(r"^product/(?P<pid>\d+)/components$", redirect_view("view_product_components")),
re_path(r"^product/(?P<pid>\d+)/risk_acceptance$", redirect_view("view_product_risk_acceptances")),
re_path(r"^product/(?P<pid>\d+)/engagements$", redirect_view("view_engagements")),
re_path(r"^product/(?P<product_id>\d+)/import_scan_results$", redirect_view("import_scan_results_prod")),
re_path(r"^product/(?P<pid>\d+)/metrics$", redirect_view("view_product_metrics")),
Expand Down Expand Up @@ -214,6 +220,8 @@
name="view_product"),
re_path(r"^product/(?P<pid>\d+)/components$", views.view_product_components,
name="view_product_components"),
re_path(r"^product/(?P<pid>\d+)/risk_acceptance$", views.view_product_risk_acceptances,
name="view_product_risk_acceptances"),
re_path(r"^product/(?P<pid>\d+)/engagements$", views.view_engagements,
name="view_engagements"),
re_path(
Expand Down Expand Up @@ -283,6 +291,7 @@
re_path(r"^asset$", redirect_view("product")),
re_path(r"^asset/(?P<pid>\d+)$", redirect_view("view_product")),
re_path(r"^asset/(?P<pid>\d+)/components$", redirect_view("view_product_components")),
re_path(r"^asset/(?P<pid>\d+)/risk_acceptance$", redirect_view("view_product_risk_acceptances")),
re_path(r"^asset/(?P<pid>\d+)/engagements$", redirect_view("view_engagements")),
re_path(r"^asset/(?P<product_id>\d+)/import_scan_results$", redirect_view("import_scan_results_prod")),
re_path(r"^asset/(?P<pid>\d+)/metrics$", redirect_view("view_product_metrics")),
Expand Down
50 changes: 50 additions & 0 deletions dojo/db_migrations/0249_risk_acceptance_add_product_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated migration - Step 1: Add product field to Risk_Acceptance

from django.db import migrations, models
import django.db.models.deletion
import pgtrigger


class Migration(migrations.Migration):

dependencies = [
('dojo', '0248_alter_general_survey_expiration'),
]

operations = [
# Add product field (nullable initially so we can populate it)
migrations.AddField(
model_name='risk_acceptance',
name='product',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='risk_acceptances', to='dojo.product'),
),
migrations.AddField(
model_name='risk_acceptanceevent',
name='product',
field=models.ForeignKey(db_constraint=False, db_index=False, editable=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='dojo.product'),
),
pgtrigger.migrations.RemoveTrigger(
model_name='risk_acceptance',
name='insert_insert',
),
pgtrigger.migrations.RemoveTrigger(
model_name='risk_acceptance',
name='update_update',
),
pgtrigger.migrations.RemoveTrigger(
model_name='risk_acceptance',
name='delete_delete',
),
pgtrigger.migrations.AddTrigger(
model_name='risk_acceptance',
trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_risk_acceptanceevent" ("accepted_by", "created", "decision", "decision_details", "expiration_date", "expiration_date_handled", "expiration_date_warned", "id", "name", "owner_id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "product_id", "reactivate_expired", "recommendation", "recommendation_details", "restart_sla_expired", "updated") VALUES (NEW."accepted_by", NEW."created", NEW."decision", NEW."decision_details", NEW."expiration_date", NEW."expiration_date_handled", NEW."expiration_date_warned", NEW."id", NEW."name", NEW."owner_id", NEW."path", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."product_id", NEW."reactivate_expired", NEW."recommendation", NEW."recommendation_details", NEW."restart_sla_expired", NEW."updated"); RETURN NULL;', hash='83d5189fd3362f9e91757621240964180e09bf95', operation='INSERT', pgid='pgtrigger_insert_insert_d29bd', table='dojo_risk_acceptance', when='AFTER')),
),
pgtrigger.migrations.AddTrigger(
model_name='risk_acceptance',
trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."accepted_by" IS DISTINCT FROM (NEW."accepted_by") OR OLD."decision" IS DISTINCT FROM (NEW."decision") OR OLD."decision_details" IS DISTINCT FROM (NEW."decision_details") OR OLD."expiration_date" IS DISTINCT FROM (NEW."expiration_date") OR OLD."expiration_date_handled" IS DISTINCT FROM (NEW."expiration_date_handled") OR OLD."expiration_date_warned" IS DISTINCT FROM (NEW."expiration_date_warned") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."owner_id" IS DISTINCT FROM (NEW."owner_id") OR OLD."path" IS DISTINCT FROM (NEW."path") OR OLD."product_id" IS DISTINCT FROM (NEW."product_id") OR OLD."reactivate_expired" IS DISTINCT FROM (NEW."reactivate_expired") OR OLD."recommendation" IS DISTINCT FROM (NEW."recommendation") OR OLD."recommendation_details" IS DISTINCT FROM (NEW."recommendation_details") OR OLD."restart_sla_expired" IS DISTINCT FROM (NEW."restart_sla_expired"))', func='INSERT INTO "dojo_risk_acceptanceevent" ("accepted_by", "created", "decision", "decision_details", "expiration_date", "expiration_date_handled", "expiration_date_warned", "id", "name", "owner_id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "product_id", "reactivate_expired", "recommendation", "recommendation_details", "restart_sla_expired", "updated") VALUES (NEW."accepted_by", NEW."created", NEW."decision", NEW."decision_details", NEW."expiration_date", NEW."expiration_date_handled", NEW."expiration_date_warned", NEW."id", NEW."name", NEW."owner_id", NEW."path", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."product_id", NEW."reactivate_expired", NEW."recommendation", NEW."recommendation_details", NEW."restart_sla_expired", NEW."updated"); RETURN NULL;', hash='6e5515509e5c952f582b91b5ac3aa7f5bed0f727', operation='UPDATE', pgid='pgtrigger_update_update_55e64', table='dojo_risk_acceptance', when='AFTER')),
),
pgtrigger.migrations.AddTrigger(
model_name='risk_acceptance',
trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_risk_acceptanceevent" ("accepted_by", "created", "decision", "decision_details", "expiration_date", "expiration_date_handled", "expiration_date_warned", "id", "name", "owner_id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "product_id", "reactivate_expired", "recommendation", "recommendation_details", "restart_sla_expired", "updated") VALUES (OLD."accepted_by", OLD."created", OLD."decision", OLD."decision_details", OLD."expiration_date", OLD."expiration_date_handled", OLD."expiration_date_warned", OLD."id", OLD."name", OLD."owner_id", OLD."path", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."product_id", OLD."reactivate_expired", OLD."recommendation", OLD."recommendation_details", OLD."restart_sla_expired", OLD."updated"); RETURN NULL;', hash='68cfbb774b18823b974228d517729985c0087130', operation='DELETE', pgid='pgtrigger_delete_delete_7d103', table='dojo_risk_acceptance', when='AFTER')),
),
]
79 changes: 79 additions & 0 deletions dojo/db_migrations/0250_risk_acceptance_migrate_to_product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Generated migration - Step 2: Migrate Risk_Acceptance data from Engagement to Product

from django.db import migrations

import logging

logger = logging.getLogger(__name__)

def migrate_risk_acceptance_to_product(apps, schema_editor):
"""
Migrate existing risk acceptances from engagement level to product level.
For each risk acceptance, find its engagement and set the product field.
"""
Risk_Acceptance = apps.get_model('dojo', 'Risk_Acceptance')
Engagement = apps.get_model('dojo', 'Engagement')

# Get all risk acceptances that don't have a product set
risk_acceptances_updated = 0
risk_acceptances_orphaned = 0

for risk_acceptance in Risk_Acceptance.objects.filter(product__isnull=True):
# Find the engagement that has this risk acceptance
engagement = Engagement.objects.filter(risk_acceptance=risk_acceptance).first()
if engagement:
# Set the product from the engagement
risk_acceptance.product = engagement.product
risk_acceptance.save()
risk_acceptances_updated += 1
else:
# This shouldn't happen in normal cases, but if a risk acceptance has no engagement,
# we need to handle it. We should delete.
risk_acceptance.delete()
risk_acceptances_orphaned += 1
logger.warning(f"Risk Acceptance {risk_acceptance.id} '{risk_acceptance.name}' has no associated engagement so it can be removed.")

logger.debug(f"Migration complete: {risk_acceptances_updated} risk acceptances migrated to product level")
if risk_acceptances_orphaned > 0:
logger.warning(f"{risk_acceptances_orphaned} orphaned risk acceptances found (no associated engagement)")


def reverse_migrate_risk_acceptance_to_engagement(apps, schema_editor):
"""
Reverse migration: restore engagement associations based on the product field.
For each risk acceptance with a product, find an engagement in that product
and associate the risk acceptance with it.
"""
Risk_Acceptance = apps.get_model('dojo', 'Risk_Acceptance')
Engagement = apps.get_model('dojo', 'Engagement')

risk_acceptances_restored = 0

# For each risk acceptance with a product, find an engagement in that product
# and associate the risk acceptance with it
for risk_acceptance in Risk_Acceptance.objects.filter(product__isnull=False):
# Find the first engagement in this product
engagement = Engagement.objects.filter(product=risk_acceptance.product).first()
if engagement:
# Add the risk acceptance to the engagement
engagement.risk_acceptance.add(risk_acceptance)
risk_acceptances_restored += 1
else:
logger.warning(f"Could not find engagement for Risk Acceptance {risk_acceptance.id} in product {risk_acceptance.product.name}")

logger.debug(f"Reverse migration complete: {risk_acceptances_restored} risk acceptances restored to engagement level")


class Migration(migrations.Migration):

dependencies = [
('dojo', '0249_risk_acceptance_add_product_field'),
]

operations = [
# Populate the product field from engagement relationships
migrations.RunPython(
migrate_risk_acceptance_to_product,
reverse_migrate_risk_acceptance_to_engagement
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated migration - Step 3: Finalize Risk_Acceptance move to Product

from django.db import migrations, models
import django.db.models.deletion
import pgtrigger


class Migration(migrations.Migration):

dependencies = [
('dojo', '0250_risk_acceptance_migrate_to_product'),
]

operations = [
migrations.AlterField(
model_name='risk_acceptance',
name='product',
field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='risk_acceptances', to='dojo.product'),
),
migrations.RemoveField(
model_name='engagement',
name='risk_acceptance',
),
]
16 changes: 0 additions & 16 deletions dojo/engagement/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,6 @@
name="engagement_unlink_jira"),
re_path(r"^engagement/(?P<eid>\d+)/complete_checklist$",
views.complete_checklist, name="complete_checklist"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/add$",
views.add_risk_acceptance, name="add_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/add/(?P<fid>\d+)$",
views.add_risk_acceptance, name="add_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/(?P<raid>\d+)$",
views.view_risk_acceptance, name="view_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/(?P<raid>\d+)/edit$",
views.edit_risk_acceptance, name="edit_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/(?P<raid>\d+)/expire$",
views.expire_risk_acceptance, name="expire_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/(?P<raid>\d+)/reinstate$",
views.reinstate_risk_acceptance, name="reinstate_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/(?P<raid>\d+)/delete$",
views.delete_risk_acceptance, name="delete_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/risk_acceptance/(?P<raid>\d+)/download$",
views.download_risk_acceptance, name="download_risk_acceptance"),
re_path(r"^engagement/(?P<eid>\d+)/threatmodel$", views.view_threatmodel,
name="view_threatmodel"),
re_path(r"^engagement/(?P<eid>\d+)/threatmodel/upload$",
Expand Down
Loading
Loading