From eaf7e66238de8ab56de2f2fd870cc41372541bc6 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:11:59 +0200 Subject: [PATCH] Prepare model --- dojo/api_v2/serializers.py | 17 ++++ ...la_configuration_test_sla_configuration.py | 24 +++++ dojo/engagement/views.py | 11 ++- dojo/forms.py | 13 ++- dojo/models.py | 94 ++++++++++++++++++- dojo/product/views.py | 6 +- dojo/sla_config/helpers.py | 2 +- dojo/sla_config/views.py | 10 +- dojo/templates/dojo/view_eng.html | 58 ++++++++++++ dojo/templates/dojo/view_test.html | 40 ++++++++ dojo/test/views.py | 11 ++- ruff.toml | 1 + 12 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 dojo/db_migrations/0219_engagement_sla_configuration_test_sla_configuration.py diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index cdc951fdb8e..097adacc884 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -1055,6 +1055,13 @@ def validate(self, data): if data.get("target_start") > data.get("target_end"): msg = "Your target start date exceeds your target end date" raise serializers.ValidationError(msg) + async_updating = getattr(self.instance.product, "async_updating", None) + if async_updating: # TODO: test + new_sla_config = data.get("sla_configuration", None) + old_sla_config = getattr(self.instance, "sla_configuration", None) + if new_sla_config and old_sla_config and new_sla_config != old_sla_config: + msg = "Finding SLA expiration dates are currently being recalculated. The SLA configuration for this product cannot be changed until the calculation is complete." + raise serializers.ValidationError(msg) return data def build_relational_field(self, field_name, relation_info): @@ -1399,6 +1406,16 @@ class Meta: model = Test exclude = ("inherited_tags",) + def validate(self, data): + async_updating = getattr(self.instance.engagement.product, "async_updating", None) + if async_updating: # TODO: test + new_sla_config = data.get("sla_configuration", None) + old_sla_config = getattr(self.instance, "sla_configuration", None) + if new_sla_config and old_sla_config and new_sla_config != old_sla_config: + msg = "Finding SLA expiration dates are currently being recalculated. The SLA configuration for this product cannot be changed until the calculation is complete." + raise serializers.ValidationError(msg) + return data + def build_relational_field(self, field_name, relation_info): if field_name == "notes": return NoteSerializer, {"many": True, "read_only": True} diff --git a/dojo/db_migrations/0219_engagement_sla_configuration_test_sla_configuration.py b/dojo/db_migrations/0219_engagement_sla_configuration_test_sla_configuration.py new file mode 100644 index 00000000000..1f17f58a3a1 --- /dev/null +++ b/dojo/db_migrations/0219_engagement_sla_configuration_test_sla_configuration.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.3 on 2024-11-20 08:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0218_system_settings_enforce_verified_status_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='engagement', + name='sla_configuration', + field=models.ForeignKey(blank=True, default=None, help_text='If no configuration will be configured, inherited (from product) will be applied.', null=True, on_delete=django.db.models.deletion.RESTRICT, to='dojo.sla_configuration'), + ), + migrations.AddField( + model_name='test', + name='sla_configuration', + field=models.ForeignKey(blank=True, default=None, help_text='If no configuration will be configured, inherited (from engagement) will be applied.', null=True, on_delete=django.db.models.deletion.RESTRICT, to='dojo.sla_configuration'), + ), + ] diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py index 70ff8a7b160..8d9d1cd558d 100644 --- a/dojo/engagement/views.py +++ b/dojo/engagement/views.py @@ -82,6 +82,7 @@ Product, Product_API_Scan_Configuration, Risk_Acceptance, + SLA_Configuration, System_Settings, Test, Test_Import, @@ -277,6 +278,7 @@ def edit_engagement(request, eid): # first save engagement details new_status = form.cleaned_data.get("status") engagement.product = form.cleaned_data.get("product") + initial_sla_config = Engagement.objects.get(pk=form.instance.id).sla_configuration engagement = form.save(commit=False) if (new_status == "Cancelled" or new_status == "Completed"): engagement.active = False @@ -285,10 +287,14 @@ def edit_engagement(request, eid): engagement.save() form.save_m2m() + msg = _("Engagement updated successfully.") + # check if the SLA config was changed, append additional context to message + if initial_sla_config != form.instance.sla_configuration: + msg += _(" All SLA expiration dates for findings within this product will be recalculated asynchronously for the newly assigned SLA configuration.") messages.add_message( request, messages.SUCCESS, - "Engagement updated successfully.", + msg, extra_tags="alert-success") success, jira_project_form = jira_helper.process_jira_project_form(request, instance=jira_project, target="engagement", engagement=engagement, product=engagement.product) @@ -469,6 +475,8 @@ def get(self, request, eid, *args, **kwargs): cred_eng = Cred_Mapping.objects.filter( engagement=eng.id).select_related("cred_id").order_by("cred_id") + sla = SLA_Configuration.objects.filter(id=eng.sla_configuration_id).first() + add_breadcrumb(parent=eng, top_level=False, request=request) title = "" @@ -495,6 +503,7 @@ def get(self, request, eid, *args, **kwargs): "cred_eng": cred_eng, "network": network, "preset_test_type": preset_test_type, + "sla": sla, }) def post(self, request, eid, *args, **kwargs): diff --git a/dojo/forms.py b/dojo/forms.py index 334a958e93f..124dd53d0db 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -983,6 +983,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # if this product has findings being asynchronously updated, disable the sla config field + if self.instance.product.async_updating: + self.fields["sla_configuration"].disabled = True + self.fields["sla_configuration"].widget.attrs["message"] = "Finding SLA expiration dates are currently being recalculated. " + \ + "This field cannot be changed until the calculation is complete." if product: self.fields["preset"] = forms.ModelChoiceField(help_text="Settings and notes for performing this engagement.", required=False, queryset=Engagement_Presets.objects.filter(product=product)) self.fields["lead"].queryset = get_authorized_users_for_product_and_product_type(None, product, Permissions.Product_View).filter(is_active=True) @@ -1058,6 +1063,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # if this product has findings being asynchronously updated, disable the sla config field + if self.instance.engagement.product.async_updating: + self.fields["sla_configuration"].disabled = True + self.fields["sla_configuration"].widget.attrs["message"] = "Finding SLA expiration dates are currently being recalculated. " + \ + "This field cannot be changed until the calculation is complete." + if obj: product = get_product(obj) self.fields["lead"].queryset = get_authorized_users_for_product_and_product_type(None, product, Permissions.Product_View).filter(is_active=True) @@ -1069,7 +1080,7 @@ class Meta: model = Test fields = ["title", "test_type", "target_start", "target_end", "description", "environment", "percent_complete", "tags", "lead", "version", "branch_tag", "build_id", "commit_hash", - "api_scan_configuration"] + "api_scan_configuration", "sla_configuration"] class DeleteTestForm(forms.ModelForm): diff --git a/dojo/models.py b/dojo/models.py index fe48896daa6..9fdcec8f0a2 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -42,6 +42,9 @@ from tagulous.models import TagField from tagulous.models.managers import FakeTagRelatedManager +from dojo.product.helpers import update_sla_expiration_dates_product_async +from dojo.sla_config.helpers import update_sla_expiration_dates_sla_config_async + logger = logging.getLogger(__name__) deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication") @@ -1010,8 +1013,22 @@ def save(self, *args, **kwargs): product.async_updating = True super(Product, product).save() # launch the async task to update all finding sla expiration dates - from dojo.sla_config.helpers import update_sla_expiration_dates_sla_config_async update_sla_expiration_dates_sla_config_async(self, tuple(severities), products) + # set the async updating flag to true for all eng's product using this sla config + engs = Engagement.objects.filter(sla_configuration=self) + for eng in engs: + eng.product.async_updating = True + super(Product, product).save() + # launch the async task to update all finding sla expiration dates + update_sla_expiration_dates_sla_config_async(self, tuple(severities), products) + # set the async updating flag to true for all test's product using this sla config + tests = Test.objects.filter(sla_configuration=self) + for test in tests: + test.engproduct.async_updating = True + super(Product, product).save() + # launch the async task to update all finding sla expiration dates + update_sla_expiration_dates_sla_config_async(self, tuple(severities), products) + # TODO: Merge these products ^ def clean(self): sla_days = [self.critical, self.high, self.medium, self.low] @@ -1172,7 +1189,6 @@ def save(self, *args, **kwargs): sla_config.async_updating = True super(SLA_Configuration, sla_config).save() # launch the async task to update all finding sla expiration dates - from dojo.product.helpers import update_sla_expiration_dates_product_async update_sla_expiration_dates_product_async(self, sla_config) def get_absolute_url(self): @@ -1485,6 +1501,12 @@ class Engagement(models.Model): source_code_management_uri = models.URLField(max_length=600, null=True, blank=True, editable=True, verbose_name=_("Repo"), help_text=_("Resource link to source code")) orchestration_engine = models.ForeignKey(Tool_Configuration, verbose_name=_("Orchestration Engine"), help_text=_("Orchestration service responsible for CI/CD test"), null=True, blank=True, related_name="orchestration", on_delete=models.CASCADE) deduplication_on_engagement = models.BooleanField(default=False, verbose_name=_("Deduplication within this engagement only"), help_text=_("If enabled deduplication will only mark a finding in this engagement as duplicate of another finding if both findings are in this engagement. If disabled, deduplication is on the product level.")) + sla_configuration = models.ForeignKey(SLA_Configuration, + null=True, + blank=True, + default=None, + on_delete=models.RESTRICT, + help_text=_("If no configuration will be configured, inherited (from product) will be applied.")) tags = TagField(blank=True, force_lowercase=True, help_text=_("Add tags that help describe this engagement. Choose from the list or add new tags. Press Enter key to add.")) inherited_tags = TagField(blank=True, force_lowercase=True, help_text=_("Internal use tags sepcifically for maintaining parity with product. This field will be present as a subset in the tags field")) @@ -1500,6 +1522,34 @@ def __str__(self): self.target_start.strftime( "%b %d, %Y")) + def save(self, *args, **kwargs): + # get the engagement's sla config before saving (if this is an existing product) + initial_sla_config = None + if self.pk is not None: + initial_sla_config = getattr(Engagement.objects.get(pk=self.pk), "sla_configuration", None) + # if initial sla config exists and async finding update is already running, revert sla config before saving + if initial_sla_config and self.product.async_updating: + self.sla_configuration = initial_sla_config + + super().save(*args, **kwargs) + + # if the initial sla config exists and async finding update is not running + if initial_sla_config is not None and not self.product.async_updating: + # get the new sla config from the saved engagement + new_sla_config = getattr(self, "sla_configuration", None) + # if the sla config has changed, update finding sla expiration dates within this engagement's product + if new_sla_config and (initial_sla_config != new_sla_config): + # set the async updating flag to true for this engagement's product + self.product.async_updating = True + super(Product, self.product).save(*args, **kwargs) + # set the async updating flag to true for the sla config assigned to this product + sla_config = getattr(self, "sla_configuration", None) + if sla_config: + sla_config.async_updating = True + super(SLA_Configuration, sla_config).save() + # launch the async task to update all finding sla expiration dates + update_sla_expiration_dates_product_async(self.product, sla_config) + def get_absolute_url(self): from django.urls import reverse return reverse("view_engagement", args=[str(self.id)]) @@ -2066,6 +2116,12 @@ class Test(models.Model): branch_tag = models.CharField(editable=True, max_length=150, null=True, blank=True, help_text=_("Tag or branch that was tested, a reimport may update this field."), verbose_name=_("Branch/Tag")) api_scan_configuration = models.ForeignKey(Product_API_Scan_Configuration, null=True, editable=True, blank=True, on_delete=models.CASCADE, verbose_name=_("API Scan Configuration")) + sla_configuration = models.ForeignKey(SLA_Configuration, + null=True, + blank=True, + default=None, + on_delete=models.RESTRICT, + help_text=_("If no configuration will be configured, inherited (from engagement) will be applied.")) class Meta: indexes = [ @@ -2077,6 +2133,34 @@ def __str__(self): return f"{self.title} ({self.test_type})" return str(self.test_type) + def save(self, *args, **kwargs): + # get the test's sla config before saving (if this is an existing product) + initial_sla_config = None + if self.pk is not None: + initial_sla_config = getattr(Test.objects.get(pk=self.pk), "sla_configuration", None) + # if initial sla config exists and async finding update is already running, revert sla config before saving + if initial_sla_config and self.engagement.product.async_updating: + self.sla_configuration = initial_sla_config + + super().save(*args, **kwargs) + + # if the initial sla config exists and async finding update is not running + if initial_sla_config is not None and not self.engagement.product.async_updating: + # get the new sla config from the saved test + new_sla_config = getattr(self, "sla_configuration", None) + # if the sla config has changed, update finding sla expiration dates within this test's product + if new_sla_config and (initial_sla_config != new_sla_config): + # set the async updating flag to true for this test's product + self.product.async_updating = True + super(Product, self.engagement.product).save(*args, **kwargs) + # set the async updating flag to true for the sla config assigned to this test's product + sla_config = getattr(self, "sla_configuration", None) + if sla_config: + sla_config.async_updating = True + super(SLA_Configuration, sla_config).save() + # launch the async task to update all finding sla expiration dates + update_sla_expiration_dates_product_async(self.engagement.product, sla_config) + def get_absolute_url(self): from django.urls import reverse return reverse("view_test", args=[str(self.id)]) @@ -3001,7 +3085,11 @@ def get_sla_start_date(self): return self.date def get_sla_period(self): - sla_configuration = SLA_Configuration.objects.filter(id=self.test.engagement.product.sla_configuration_id).first() + sla_configuration = SLA_Configuration.objects.filter(id=self.test.sla_configuration_id).first() + if not sla_configuration: + sla_configuration = SLA_Configuration.objects.filter(id=self.test.engagement.sla_configuration_id).first() + if not sla_configuration: + sla_configuration = SLA_Configuration.objects.filter(id=self.test.engagement.product.sla_configuration_id).first() sla_period = getattr(sla_configuration, self.severity.lower(), None) enforce_period = getattr(sla_configuration, str("enforce_" + self.severity.lower()), None) return sla_period, enforce_period diff --git a/dojo/product/views.py b/dojo/product/views.py index 8c20b50627a..83699f3f045 100644 --- a/dojo/product/views.py +++ b/dojo/product/views.py @@ -944,13 +944,13 @@ def edit_product(request, pid): if form.is_valid(): initial_sla_config = Product.objects.get(pk=form.instance.id).sla_configuration form.save() - msg = "Product updated successfully." + msg = _("Product updated successfully.") # check if the SLA config was changed, append additional context to message if initial_sla_config != form.instance.sla_configuration: - msg += " All SLA expiration dates for findings within this product will be recalculated asynchronously for the newly assigned SLA configuration." + msg += _(" All SLA expiration dates for findings within this product will be recalculated asynchronously for the newly assigned SLA configuration.") messages.add_message(request, messages.SUCCESS, - _(msg), + msg, extra_tags="alert-success") success, jform = jira_helper.process_jira_project_form(request, instance=jira_project, product=product) diff --git a/dojo/sla_config/helpers.py b/dojo/sla_config/helpers.py index 9d76faaa061..4820be234cb 100644 --- a/dojo/sla_config/helpers.py +++ b/dojo/sla_config/helpers.py @@ -17,7 +17,7 @@ def update_sla_expiration_dates_sla_config_sync(sla_config, severities, products logger.info(f"Updating finding SLA expiration dates within the {sla_config} SLA configuration") # update each finding that is within the SLA configuration that was saved for f in Finding.objects.filter(test__engagement__product__sla_configuration_id=sla_config.id, severity__in=severities): - f.save() + f.save() # TODO: Add for other layers as well # reset the async updating flag to false for all products using this sla config for product in products: product.async_updating = False diff --git a/dojo/sla_config/views.py b/dojo/sla_config/views.py index c07e8dadc2a..c282f8f9e6e 100644 --- a/dojo/sla_config/views.py +++ b/dojo/sla_config/views.py @@ -8,7 +8,7 @@ from dojo.authorization.authorization import user_has_configuration_permission_or_403 from dojo.authorization.authorization_decorators import user_is_configuration_authorized from dojo.forms import SLAConfigForm -from dojo.models import Product, SLA_Configuration, System_Settings +from dojo.models import Engagement, Product, SLA_Configuration, System_Settings, Test from dojo.utils import add_breadcrumb logger = logging.getLogger(__name__) @@ -41,8 +41,12 @@ def edit_sla_config(request, slaid): if request.method == "POST" and request.POST.get("delete"): if sla_config.id != 1: - if Product.objects.filter(sla_configuration=sla_config).count(): - msg = f'The "{sla_config}" SLA configuration could not be deleted, as it is currently in use by one or more products.' + if ( + Product.objects.filter(sla_configuration=sla_config).count() or + Engagement.objects.filter(sla_configuration=sla_config).count() or + Test.objects.filter(sla_configuration=sla_config).count() + ): + msg = f'The "{sla_config}" SLA configuration could not be deleted, as it is currently in use by one or more products, engagements or tests.' messages.add_message(request, messages.ERROR, msg, diff --git a/dojo/templates/dojo/view_eng.html b/dojo/templates/dojo/view_eng.html index 7aa7d631d3e..27b9254a5c9 100644 --- a/dojo/templates/dojo/view_eng.html +++ b/dojo/templates/dojo/view_eng.html @@ -921,6 +921,64 @@