From fa3ce1bf7ae7221f19675bf3622ce8217aa494de Mon Sep 17 00:00:00 2001 From: dogboat Date: Fri, 3 Apr 2026 11:26:25 -0400 Subject: [PATCH 01/27] wip cicd split --- dojo/api_v2/serializers.py | 7 + dojo/api_v2/views.py | 14 ++ .../db_migrations/0264_cicd_infrastructure.py | 131 ++++++++++++++++++ dojo/engagement/views.py | 4 +- dojo/forms.py | 6 +- dojo/models.py | 25 +++- dojo/urls.py | 2 + 7 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 dojo/db_migrations/0264_cicd_infrastructure.py diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index dc15ac6c2dc..32e6bf98ced 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -49,6 +49,7 @@ App_Analysis, BurpRawRequestResponse, Check_List, + CICDInfrastructure, Development_Environment, Dojo_User, DojoMeta, @@ -811,6 +812,12 @@ class Meta: fields = "__all__" +class CICDInfrastructureSerializer(serializers.ModelSerializer): + class Meta: + model = CICDInfrastructure + fields = "__all__" + + class ToolTypeSerializer(serializers.ModelSerializer): class Meta: model = Tool_Type diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 3f8bb0cf169..a33df051385 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -86,6 +86,7 @@ App_Analysis, BurpRawRequestResponse, Check_List, + CICDInfrastructure, Development_Environment, Dojo_User, DojoMeta, @@ -2209,6 +2210,19 @@ def get_queryset(self): # Authorization: configuration +class CICDInfrastructureViewSet( + DojoModelViewSet, +): + serializer_class = serializers.CICDInfrastructureSerializer + queryset = CICDInfrastructure.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_fields = ["id", "name", "infrastructure_type"] + permission_classes = (permissions.UserHasConfigurationPermissionSuperuser,) + + def get_queryset(self): + return CICDInfrastructure.objects.all().order_by("id") + + class ToolTypesViewSet( DojoModelViewSet, ): diff --git a/dojo/db_migrations/0264_cicd_infrastructure.py b/dojo/db_migrations/0264_cicd_infrastructure.py new file mode 100644 index 00000000000..c1df6848fdf --- /dev/null +++ b/dojo/db_migrations/0264_cicd_infrastructure.py @@ -0,0 +1,131 @@ +import logging + +from django.db import migrations, models +import django.db.models.deletion + +logger = logging.getLogger(__name__) + + +def migrate_tool_configs_to_cicd_infrastructure(apps, schema_editor): + """ + For each Tool_Configuration referenced by an engagement's build_server, + source_code_management_server, or orchestration_engine FK, create a + CICDInfrastructure record and point the new engagement FK to it. + """ + Engagement = apps.get_model("dojo", "Engagement") + CICDInfrastructure = apps.get_model("dojo", "CICDInfrastructure") + + field_mappings = [ + ("build_server", "cicd_build_server", "build_server"), + ("source_code_management_server", "cicd_scm_server", "scm_server"), + ("orchestration_engine", "cicd_orchestration_engine", "orchestration"), + ] + + for old_field, new_field, infra_type in field_mappings: + engagements_with_old_fk = Engagement.objects.filter( + **{f"{old_field}__isnull": False}, + ).select_related(old_field) + + for engagement in engagements_with_old_fk: + tc = getattr(engagement, old_field) + if tc is None: + continue + + cicd_infra, created = CICDInfrastructure.objects.get_or_create( + name=tc.name, + infrastructure_type=infra_type, + defaults={ + "description": tc.description or "", + "url": tc.url or "", + }, + ) + if created: + logger.info( + "Created CICDInfrastructure '%s' (type=%s) from Tool_Configuration '%s'", + cicd_infra.name, infra_type, tc.name, + ) + + setattr(engagement, new_field, cicd_infra) + engagement.save(update_fields=[new_field]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("dojo", "0263_language_type_unique_language"), + ] + + operations = [ + # Step 1: Create CICDInfrastructure model + migrations.CreateModel( + name="CICDInfrastructure", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=200)), + ("description", models.CharField(blank=True, max_length=2000, null=True)), + ("url", models.URLField(blank=True, help_text="Public URL of the tool (e.g., https://jenkins.company.com)", max_length=2000, null=True)), + ("infrastructure_type", models.CharField(choices=[("build_server", "Build Server"), ("scm_server", "SCM Server"), ("orchestration", "Orchestration Engine")], max_length=30)), + ], + options={ + "ordering": ["name"], + }, + ), + # Step 2: Add new FK fields to Engagement (before removing old ones) + migrations.AddField( + model_name="engagement", + name="cicd_build_server", + field=models.ForeignKey( + blank=True, null=True, + help_text="Build server used for this CI/CD engagement", + limit_choices_to={"infrastructure_type": "build_server"}, + on_delete=django.db.models.deletion.SET_NULL, + related_name="engagements_as_build_server", + to="dojo.cicdinfrastructure", + verbose_name="Build Server", + ), + ), + migrations.AddField( + model_name="engagement", + name="cicd_scm_server", + field=models.ForeignKey( + blank=True, null=True, + help_text="Source code management server used for this CI/CD engagement", + limit_choices_to={"infrastructure_type": "scm_server"}, + on_delete=django.db.models.deletion.SET_NULL, + related_name="engagements_as_scm_server", + to="dojo.cicdinfrastructure", + verbose_name="SCM Server", + ), + ), + migrations.AddField( + model_name="engagement", + name="cicd_orchestration_engine", + field=models.ForeignKey( + blank=True, null=True, + help_text="Orchestration engine used for this CI/CD engagement", + limit_choices_to={"infrastructure_type": "orchestration"}, + on_delete=django.db.models.deletion.SET_NULL, + related_name="engagements_as_orchestration", + to="dojo.cicdinfrastructure", + verbose_name="Orchestration Engine", + ), + ), + # Step 3: Migrate data from Tool_Configuration to CICDInfrastructure + migrations.RunPython( + migrate_tool_configs_to_cicd_infrastructure, + reverse_code=migrations.RunPython.noop, + ), + # Step 4: Remove old FK fields from Engagement + migrations.RemoveField( + model_name="engagement", + name="build_server", + ), + migrations.RemoveField( + model_name="engagement", + name="source_code_management_server", + ), + migrations.RemoveField( + model_name="engagement", + name="orchestration_engine", + ), + ] diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py index 0154aa5d336..3df05b63057 100644 --- a/dojo/engagement/views.py +++ b/dojo/engagement/views.py @@ -1606,8 +1606,8 @@ def get_excludes(): def get_foreign_keys(): - return ["build_server", "lead", "orchestration_engine", "preset", "product", - "report_type", "requester", "source_code_management_server"] + return ["cicd_build_server", "cicd_orchestration_engine", "cicd_scm_server", + "lead", "preset", "product", "report_type", "requester"] def csv_export(request): diff --git a/dojo/forms.py b/dojo/forms.py index e33d7ef51c4..1f8accb2509 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -1058,10 +1058,10 @@ def __init__(self, *args, **kwargs): del self.fields["build_id"] del self.fields["commit_hash"] del self.fields["branch_tag"] - del self.fields["build_server"] - del self.fields["source_code_management_server"] + del self.fields["cicd_build_server"] + del self.fields["cicd_scm_server"] # del self.fields['source_code_management_uri'] - del self.fields["orchestration_engine"] + del self.fields["cicd_orchestration_engine"] else: del self.fields["test_strategy"] del self.fields["status"] diff --git a/dojo/models.py b/dojo/models.py index a41f5640889..eb47aec9567 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -1337,6 +1337,25 @@ def __str__(self): return self.name +class CICDInfrastructure(models.Model): + INFRASTRUCTURE_TYPE_CHOICES = ( + ("build_server", "Build Server"), + ("scm_server", "SCM Server"), + ("orchestration", "Orchestration Engine"), + ) + + name = models.CharField(max_length=200) + description = models.CharField(max_length=2000, null=True, blank=True) + url = models.URLField(max_length=2000, null=True, blank=True, help_text=_("Public URL of the tool (e.g., https://jenkins.company.com)")) + infrastructure_type = models.CharField(max_length=30, choices=INFRASTRUCTURE_TYPE_CHOICES) + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name + + class Product_API_Scan_Configuration(models.Model): product = models.ForeignKey(Product, null=False, blank=False, on_delete=models.CASCADE) tool_configuration = models.ForeignKey(Tool_Configuration, null=False, blank=False, on_delete=models.CASCADE) @@ -1472,10 +1491,10 @@ class Engagement(BaseModel): null=True, blank=True, help_text=_("Commit hash from repo"), verbose_name=_("Commit Hash")) branch_tag = models.CharField(editable=True, max_length=150, null=True, blank=True, help_text=_("Tag or branch of the product the engagement tested."), verbose_name=_("Branch/Tag")) - build_server = models.ForeignKey(Tool_Configuration, verbose_name=_("Build Server"), help_text=_("Build server responsible for CI/CD test"), null=True, blank=True, related_name="build_server", on_delete=models.CASCADE) - source_code_management_server = models.ForeignKey(Tool_Configuration, null=True, blank=True, verbose_name=_("SCM Server"), help_text=_("Source code server for CI/CD test"), related_name="source_code_management_server", on_delete=models.CASCADE) 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) + cicd_build_server = models.ForeignKey("CICDInfrastructure", null=True, blank=True, related_name="engagements_as_build_server", on_delete=models.SET_NULL, limit_choices_to={"infrastructure_type": "build_server"}, verbose_name=_("Build Server"), help_text=_("Build server used for this CI/CD engagement")) + cicd_scm_server = models.ForeignKey("CICDInfrastructure", null=True, blank=True, related_name="engagements_as_scm_server", on_delete=models.SET_NULL, limit_choices_to={"infrastructure_type": "scm_server"}, verbose_name=_("SCM Server"), help_text=_("Source code management server used for this CI/CD engagement")) + cicd_orchestration_engine = models.ForeignKey("CICDInfrastructure", null=True, blank=True, related_name="engagements_as_orchestration", on_delete=models.SET_NULL, limit_choices_to={"infrastructure_type": "orchestration"}, verbose_name=_("Orchestration Engine"), help_text=_("Orchestration engine used for this CI/CD engagement")) 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.")) 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.")) diff --git a/dojo/urls.py b/dojo/urls.py index 9b9a8d6a399..3342ad3cb52 100644 --- a/dojo/urls.py +++ b/dojo/urls.py @@ -16,6 +16,7 @@ AppAnalysisViewSet, BurpRawRequestResponseViewSet, CeleryViewSet, + CICDInfrastructureViewSet, ConfigurationPermissionViewSet, DevelopmentEnvironmentViewSet, DojoMetaViewSet, @@ -152,6 +153,7 @@ v2_api.register(r"tests", TestsViewSet, basename="test") v2_api.register(r"test_types", TestTypesViewSet, basename="test_type") v2_api.register(r"test_imports", TestImportViewSet, basename="test_imports") +v2_api.register(r"cicd_infrastructure", CICDInfrastructureViewSet, basename="cicd_infrastructure") v2_api.register(r"tool_configurations", ToolConfigurationsViewSet, basename="tool_configuration") v2_api.register(r"tool_product_settings", ToolProductSettingsViewSet, basename="tool_product_settings") v2_api.register(r"tool_types", ToolTypesViewSet, basename="tool_type") From 910d4b91a712d9c33d29ac343533043e29e9108f Mon Sep 17 00:00:00 2001 From: dogboat Date: Wed, 8 Apr 2026 14:26:52 -0400 Subject: [PATCH 02/27] migration updates --- .../db_migrations/0264_cicd_infrastructure.py | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/dojo/db_migrations/0264_cicd_infrastructure.py b/dojo/db_migrations/0264_cicd_infrastructure.py index c1df6848fdf..38794d170f0 100644 --- a/dojo/db_migrations/0264_cicd_infrastructure.py +++ b/dojo/db_migrations/0264_cicd_infrastructure.py @@ -2,6 +2,8 @@ from django.db import migrations, models import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations logger = logging.getLogger(__name__) @@ -115,7 +117,20 @@ class Migration(migrations.Migration): migrate_tool_configs_to_cicd_infrastructure, reverse_code=migrations.RunPython.noop, ), - # Step 4: Remove old FK fields from Engagement + # Step 4: Remove old pgtrigger triggers (they reference old column names) + pgtrigger.migrations.RemoveTrigger( + model_name='engagement', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='engagement', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='engagement', + name='delete_delete', + ), + # Step 5: Remove old FK fields from Engagement migrations.RemoveField( model_name="engagement", name="build_server", @@ -128,4 +143,48 @@ class Migration(migrations.Migration): model_name="engagement", name="orchestration_engine", ), + # Step 6: Update pghistory event table FK fields to point to CICDInfrastructure + migrations.RenameField( + model_name="engagementevent", + old_name="build_server", + new_name="cicd_build_server", + ), + migrations.RenameField( + model_name="engagementevent", + old_name="source_code_management_server", + new_name="cicd_scm_server", + ), + migrations.RenameField( + model_name="engagementevent", + old_name="orchestration_engine", + new_name="cicd_orchestration_engine", + ), + migrations.AlterField( + model_name='engagementevent', + name='cicd_build_server', + field=models.ForeignKey(blank=True, db_constraint=False, db_index=False, help_text='Build server used for this CI/CD engagement', limit_choices_to={'infrastructure_type': 'build_server'}, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='dojo.cicdinfrastructure', verbose_name='Build Server'), + ), + migrations.AlterField( + model_name='engagementevent', + name='cicd_orchestration_engine', + field=models.ForeignKey(blank=True, db_constraint=False, db_index=False, help_text='Orchestration engine used for this CI/CD engagement', limit_choices_to={'infrastructure_type': 'orchestration'}, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='dojo.cicdinfrastructure', verbose_name='Orchestration Engine'), + ), + migrations.AlterField( + model_name='engagementevent', + name='cicd_scm_server', + field=models.ForeignKey(blank=True, db_constraint=False, db_index=False, help_text='Source code management server used for this CI/CD engagement', limit_choices_to={'infrastructure_type': 'scm_server'}, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='dojo.cicdinfrastructure', verbose_name='SCM Server'), + ), + # Step 7: Re-create pgtrigger triggers with new column names + pgtrigger.migrations.AddTrigger( + model_name='engagement', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_engagementevent" ("active", "api_test", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL;', hash='a217ec77b975020749afc350ee463c5867cfea27', operation='INSERT', pgid='pgtrigger_insert_insert_125f1', table='dojo_engagement', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='engagement', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."api_test" IS DISTINCT FROM (NEW."api_test") OR OLD."branch_tag" IS DISTINCT FROM (NEW."branch_tag") OR OLD."build_id" IS DISTINCT FROM (NEW."build_id") OR OLD."check_list" IS DISTINCT FROM (NEW."check_list") OR OLD."cicd_build_server_id" IS DISTINCT FROM (NEW."cicd_build_server_id") OR OLD."cicd_orchestration_engine_id" IS DISTINCT FROM (NEW."cicd_orchestration_engine_id") OR OLD."cicd_scm_server_id" IS DISTINCT FROM (NEW."cicd_scm_server_id") OR OLD."commit_hash" IS DISTINCT FROM (NEW."commit_hash") OR OLD."deduplication_on_engagement" IS DISTINCT FROM (NEW."deduplication_on_engagement") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."done_testing" IS DISTINCT FROM (NEW."done_testing") OR OLD."engagement_type" IS DISTINCT FROM (NEW."engagement_type") OR OLD."first_contacted" IS DISTINCT FROM (NEW."first_contacted") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."lead_id" IS DISTINCT FROM (NEW."lead_id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."pen_test" IS DISTINCT FROM (NEW."pen_test") OR OLD."preset_id" IS DISTINCT FROM (NEW."preset_id") OR OLD."product_id" IS DISTINCT FROM (NEW."product_id") OR OLD."progress" IS DISTINCT FROM (NEW."progress") OR OLD."reason" IS DISTINCT FROM (NEW."reason") OR OLD."report_type_id" IS DISTINCT FROM (NEW."report_type_id") OR OLD."requester_id" IS DISTINCT FROM (NEW."requester_id") OR OLD."source_code_management_uri" IS DISTINCT FROM (NEW."source_code_management_uri") OR OLD."status" IS DISTINCT FROM (NEW."status") OR OLD."target_end" IS DISTINCT FROM (NEW."target_end") OR OLD."target_start" IS DISTINCT FROM (NEW."target_start") OR OLD."test_strategy" IS DISTINCT FROM (NEW."test_strategy") OR OLD."threat_model" IS DISTINCT FROM (NEW."threat_model") OR OLD."tmodel_path" IS DISTINCT FROM (NEW."tmodel_path") OR OLD."tracker" IS DISTINCT FROM (NEW."tracker") OR OLD."version" IS DISTINCT FROM (NEW."version"))', func='INSERT INTO "dojo_engagementevent" ("active", "api_test", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL;', hash='6a9569fa21d5d7ad16eb018bc4e6236e8401bced', operation='UPDATE', pgid='pgtrigger_update_update_65136', table='dojo_engagement', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='engagement', + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_engagementevent" ("active", "api_test", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (OLD."active", OLD."api_test", OLD."branch_tag", OLD."build_id", OLD."check_list", OLD."cicd_build_server_id", OLD."cicd_orchestration_engine_id", OLD."cicd_scm_server_id", OLD."commit_hash", OLD."created", OLD."deduplication_on_engagement", OLD."description", OLD."done_testing", OLD."engagement_type", OLD."first_contacted", OLD."id", OLD."lead_id", OLD."name", OLD."pen_test", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."preset_id", OLD."product_id", OLD."progress", OLD."reason", OLD."report_type_id", OLD."requester_id", OLD."source_code_management_uri", OLD."status", OLD."target_end", OLD."target_start", OLD."test_strategy", OLD."threat_model", OLD."tmodel_path", OLD."tracker", OLD."updated", OLD."version"); RETURN NULL;', hash='de64abfdac94fadfbe7a8cd33212b1dc26ad9600', operation='DELETE', pgid='pgtrigger_delete_delete_9f4df', table='dojo_engagement', when='AFTER')), + ), ] From 696b114f772fa13f11b63e5ea9cfbb44ad4ceb98 Mon Sep 17 00:00:00 2001 From: dogboat Date: Thu, 9 Apr 2026 08:16:05 -0400 Subject: [PATCH 03/27] old ui --- dojo/cicd_infrastructure/__init__.py | 0 dojo/cicd_infrastructure/urls.py | 10 +++ dojo/cicd_infrastructure/views.py | 67 +++++++++++++++ dojo/forms.py | 7 ++ dojo/templates/dojo/cicd_infrastructure.html | 81 +++++++++++++++++++ .../dojo/delete_cicd_infrastructure.html | 13 +++ .../dojo/edit_cicd_infrastructure.html | 13 +++ .../dojo/new_cicd_infrastructure.html | 13 +++ dojo/urls.py | 2 + 9 files changed, 206 insertions(+) create mode 100644 dojo/cicd_infrastructure/__init__.py create mode 100644 dojo/cicd_infrastructure/urls.py create mode 100644 dojo/cicd_infrastructure/views.py create mode 100644 dojo/templates/dojo/cicd_infrastructure.html create mode 100644 dojo/templates/dojo/delete_cicd_infrastructure.html create mode 100644 dojo/templates/dojo/edit_cicd_infrastructure.html create mode 100644 dojo/templates/dojo/new_cicd_infrastructure.html diff --git a/dojo/cicd_infrastructure/__init__.py b/dojo/cicd_infrastructure/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/cicd_infrastructure/urls.py b/dojo/cicd_infrastructure/urls.py new file mode 100644 index 00000000000..26d6873c11f --- /dev/null +++ b/dojo/cicd_infrastructure/urls.py @@ -0,0 +1,10 @@ +from django.urls import re_path + +from . import views + +urlpatterns = [ + re_path(r"^cicd_infrastructure/add$", views.new_cicd_infrastructure, name="add_cicd_infrastructure"), + re_path(r"^cicd_infrastructure/(?P\d+)/edit$", views.edit_cicd_infrastructure, name="edit_cicd_infrastructure"), + re_path(r"^cicd_infrastructure/(?P\d+)/delete$", views.delete_cicd_infrastructure, name="delete_cicd_infrastructure"), + re_path(r"^cicd_infrastructure$", views.cicd_infrastructure, name="cicd_infrastructure"), +] diff --git a/dojo/cicd_infrastructure/views.py b/dojo/cicd_infrastructure/views.py new file mode 100644 index 00000000000..d388d50de52 --- /dev/null +++ b/dojo/cicd_infrastructure/views.py @@ -0,0 +1,67 @@ +import logging + +from django.contrib import messages +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils.translation import gettext as _ + +from dojo.authorization.authorization_decorators import user_is_configuration_authorized +from dojo.forms import CICDInfrastructureForm +from dojo.models import CICDInfrastructure +from dojo.utils import add_breadcrumb + +logger = logging.getLogger(__name__) + + +@user_is_configuration_authorized("dojo.view_cicdinfrastructure") +def cicd_infrastructure(request): + confs = CICDInfrastructure.objects.all().order_by("name") + add_breadcrumb(title=_("CI/CD Infrastructure List"), top_level=not len(request.GET), request=request) + return render(request, "dojo/cicd_infrastructure.html", {"confs": confs}) + + +@user_is_configuration_authorized("dojo.add_cicdinfrastructure") +def new_cicd_infrastructure(request): + if request.method == "POST": + form = CICDInfrastructureForm(request.POST) + if form.is_valid(): + form.save() + messages.add_message(request, messages.SUCCESS, + _("CI/CD Infrastructure successfully created."), + extra_tags="alert-success") + return HttpResponseRedirect(reverse("cicd_infrastructure")) + else: + form = CICDInfrastructureForm() + add_breadcrumb(title=_("New CI/CD Infrastructure"), top_level=False, request=request) + return render(request, "dojo/new_cicd_infrastructure.html", {"form": form}) + + +@user_is_configuration_authorized("dojo.change_cicdinfrastructure") +def edit_cicd_infrastructure(request, ciid): + conf = get_object_or_404(CICDInfrastructure, pk=ciid) + if request.method == "POST": + form = CICDInfrastructureForm(request.POST, instance=conf) + if form.is_valid(): + form.save() + messages.add_message(request, messages.SUCCESS, + _("CI/CD Infrastructure successfully updated."), + extra_tags="alert-success") + return HttpResponseRedirect(reverse("cicd_infrastructure")) + else: + form = CICDInfrastructureForm(instance=conf) + add_breadcrumb(title=_("Edit CI/CD Infrastructure"), top_level=False, request=request) + return render(request, "dojo/edit_cicd_infrastructure.html", {"form": form, "conf": conf}) + + +@user_is_configuration_authorized("dojo.delete_cicdinfrastructure") +def delete_cicd_infrastructure(request, ciid): + conf = get_object_or_404(CICDInfrastructure, pk=ciid) + if request.method == "POST": + conf.delete() + messages.add_message(request, messages.SUCCESS, + _("CI/CD Infrastructure successfully deleted."), + extra_tags="alert-success") + return HttpResponseRedirect(reverse("cicd_infrastructure")) + add_breadcrumb(title=_("Delete CI/CD Infrastructure"), top_level=False, request=request) + return render(request, "dojo/delete_cicd_infrastructure.html", {"conf": conf}) diff --git a/dojo/forms.py b/dojo/forms.py index 1f8accb2509..92d916eb9de 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -65,6 +65,7 @@ Benchmark_Product, Benchmark_Product_Summary, Benchmark_Requirement, + CICDInfrastructure, Check_List, Choice, ChoiceAnswer, @@ -2647,6 +2648,12 @@ def clean(self): return form_data +class CICDInfrastructureForm(forms.ModelForm): + class Meta: + model = CICDInfrastructure + fields = ["name", "description", "url", "infrastructure_type"] + + class SLAConfigForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dojo/templates/dojo/cicd_infrastructure.html b/dojo/templates/dojo/cicd_infrastructure.html new file mode 100644 index 00000000000..2415e2492c4 --- /dev/null +++ b/dojo/templates/dojo/cicd_infrastructure.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} +{% load navigation_tags %} +{% load authorization_tags %} +{% block content %} + {{ block.super }} +
+
+
+
+

+ CI/CD Infrastructure + +

+
+
+ {% if confs %} +
+ + + + + + + + + + + + {% for conf in confs %} + + + + + + + + {% endfor %} + +
NameDescriptionURLTypeActions
+ {% if "dojo.change_cicdinfrastructure"|has_configuration_permission:request %} + {{ conf.name }} + {% else %} + {{ conf.name }} + {% endif %} + + {% if conf.description %}{{ conf.description }}{% endif %} + + {% if conf.url %}{{ conf.url }}{% endif %} + + {{ conf.get_infrastructure_type_display }} + + {% if "dojo.delete_cicdinfrastructure"|has_configuration_permission:request %} + + + + {% endif %} +
+
+ {% else %} +

No CI/CD infrastructure found.

+ {% endif %} +
+
+{% endblock %} diff --git a/dojo/templates/dojo/delete_cicd_infrastructure.html b/dojo/templates/dojo/delete_cicd_infrastructure.html new file mode 100644 index 00000000000..9fa1f219fa3 --- /dev/null +++ b/dojo/templates/dojo/delete_cicd_infrastructure.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} + {{ block.super }} +

Delete CI/CD Infrastructure

+

Are you sure you want to delete {{ conf.name }}?

+

Any engagements referencing this infrastructure will have the reference cleared.

+
{% csrf_token %} +
+ Cancel + +
+
+{% endblock %} diff --git a/dojo/templates/dojo/edit_cicd_infrastructure.html b/dojo/templates/dojo/edit_cicd_infrastructure.html new file mode 100644 index 00000000000..4e7be802e5b --- /dev/null +++ b/dojo/templates/dojo/edit_cicd_infrastructure.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} + {{ block.super }} +

Edit CI/CD Infrastructure

+
{% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} +
+
+ +
+
+
+{% endblock %} diff --git a/dojo/templates/dojo/new_cicd_infrastructure.html b/dojo/templates/dojo/new_cicd_infrastructure.html new file mode 100644 index 00000000000..6c34d1edf47 --- /dev/null +++ b/dojo/templates/dojo/new_cicd_infrastructure.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} + {{ block.super }} +

Add CI/CD Infrastructure

+
{% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} +
+
+ +
+
+
+{% endblock %} diff --git a/dojo/urls.py b/dojo/urls.py index 3342ad3cb52..0d718f1f414 100644 --- a/dojo/urls.py +++ b/dojo/urls.py @@ -86,6 +86,7 @@ from dojo.search.urls import urlpatterns as search_urls from dojo.sla_config.urls import urlpatterns as sla_urls from dojo.survey.urls import urlpatterns as survey_urls +from dojo.cicd_infrastructure.urls import urlpatterns as cicd_infrastructure_urls from dojo.system_settings.urls import urlpatterns as system_settings_urls from dojo.test.urls import urlpatterns as test_urls from dojo.test_type.urls import urlpatterns as test_type_urls @@ -193,6 +194,7 @@ ur += tool_type_urls ur += tool_config_urls ur += tool_product_urls +ur += cicd_infrastructure_urls ur += sla_urls ur += system_settings_urls ur += notifications_urls From e3c28a5f5604567fca0efa9e3d1c35006a77a540 Mon Sep 17 00:00:00 2001 From: dogboat Date: Thu, 9 Apr 2026 08:19:29 -0400 Subject: [PATCH 04/27] display under ci/cd eng details --- dojo/templates/dojo/view_eng.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dojo/templates/dojo/view_eng.html b/dojo/templates/dojo/view_eng.html index a9d2f228d71..df4c4c25a3c 100644 --- a/dojo/templates/dojo/view_eng.html +++ b/dojo/templates/dojo/view_eng.html @@ -892,30 +892,30 @@