From 3a03b02c310c00c4a175e5a13a004d5da59a9d9e Mon Sep 17 00:00:00 2001 From: Abderrahmane Smimite Date: Fri, 7 Nov 2025 18:49:09 +0100 Subject: [PATCH 01/47] migration --- .../core/migrations/0112_asset_is_critical.py | 17 +++++++++++++++++ backend/core/models.py | 2 ++ backend/core/serializers.py | 7 +++++++ 3 files changed, 26 insertions(+) create mode 100644 backend/core/migrations/0112_asset_is_critical.py diff --git a/backend/core/migrations/0112_asset_is_critical.py b/backend/core/migrations/0112_asset_is_critical.py new file mode 100644 index 0000000000..4a61888349 --- /dev/null +++ b/backend/core/migrations/0112_asset_is_critical.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.7 on 2025-11-07 17:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0111_finding_priority"), + ] + + operations = [ + migrations.AddField( + model_name="asset", + name="is_critical", + field=models.BooleanField(default=False, verbose_name="is_critical"), + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index cfebbe23d0..def25bdc27 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -2149,6 +2149,8 @@ class Type(models.TextChoices): is_published = models.BooleanField(_("published"), default=True) observation = models.TextField(null=True, blank=True, verbose_name=_("Observation")) + is_critical = models.BooleanField("is_critical", default=False) + fields_to_check = ["name"] class Meta: diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 62da407c39..fcbcded2f3 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -14,6 +14,7 @@ ) from core.utils import time_state from ebios_rm.models import EbiosRMStudy, Stakeholder +from tprm.models import Solution from global_settings.utils import ff_is_enabled from iam.models import * from django.contrib.auth.models import Permission @@ -390,6 +391,11 @@ class AssetWriteSerializer(BaseModelSerializer): queryset=Asset.objects.all(), required=False, ) + solutions = serializers.PrimaryKeyRelatedField( + many=True, + queryset=Solution.objects.all(), + required=False, + ) class Meta: model = Asset @@ -454,6 +460,7 @@ class AssetReadSerializer(AssetWriteSerializer): personal_data = FieldsRelatedField(many=True) asset_class = FieldsRelatedField(["name"]) overridden_children_capabilities = FieldsRelatedField(many=True) + solutions = FieldsRelatedField(many=True) children_assets = serializers.SerializerMethodField() security_objectives = serializers.SerializerMethodField() From 679a3f3891dd533a495f7da334877e1ac8fe22eb Mon Sep 17 00:00:00 2001 From: Abderrahmane Smimite Date: Fri, 7 Nov 2025 18:49:24 +0100 Subject: [PATCH 02/47] frontend --- frontend/messages/en.json | 2 ++ .../Forms/ModelForm/AssetForm.svelte | 19 +++++++++++++++++++ frontend/src/lib/utils/crud.ts | 5 ++++- frontend/src/lib/utils/schemas.ts | 4 +++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index f07a74ce58..b02606cb15 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -834,6 +834,7 @@ "tpSolutions": "Third-Party solutions", "solution": "Solution", "addSolution": "Add solution", + "solutionsLinkedToAssetHelpText": "Solutions associated with this asset", "providerEntity": "Provider entity", "addProduct": "Add product", "representatives": "Representatives", @@ -1000,6 +1001,7 @@ "tooManyLoginAttempts": "Too many login attempts. Please try again later.", "step": "Step {number}", "critical": "Critical", + "isCritical": "Is critical", "info": "Information", "vulnerabilities": "Vulnerabilities", "severity": "Severity", diff --git a/frontend/src/lib/components/Forms/ModelForm/AssetForm.svelte b/frontend/src/lib/components/Forms/ModelForm/AssetForm.svelte index 0a9d4fc859..938a662497 100644 --- a/frontend/src/lib/components/Forms/ModelForm/AssetForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm/AssetForm.svelte @@ -130,6 +130,13 @@ bind:cachedValue={formDataCache['owner']} label={m.owner()} /> + + {#if initialData.ebios_rm_studies} Date: Fri, 7 Nov 2025 19:13:54 +0100 Subject: [PATCH 03/47] is critical and associated filter for assets --- backend/core/views.py | 1 + frontend/src/lib/utils/crud.ts | 2 +- frontend/src/lib/utils/table.ts | 12 +++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/backend/core/views.py b/backend/core/views.py index 15ed4c78bd..f9b09214a2 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -675,6 +675,7 @@ class Meta: "filtering_labels", "asset_class", "personal_data", + "is_critical", ] diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts index 589e2709b8..3b7dd3611b 100644 --- a/frontend/src/lib/utils/crud.ts +++ b/frontend/src/lib/utils/crud.ts @@ -500,7 +500,7 @@ export const URL_MODEL_MAP: ModelMap = { disableDelete: true }, { field: 'assets', urlModel: 'vulnerabilities' }, - { field: 'assets', urlModel: 'solutions' }, + { field: 'assets', urlModel: 'solutions', disableCreate: true, disableDelete: true }, { field: 'assets', urlModel: 'personal-data', disableCreate: true, disableDelete: true } ], foreignKeyFields: [ diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts index f542e8f311..837e65d61b 100644 --- a/frontend/src/lib/utils/table.ts +++ b/frontend/src/lib/utils/table.ts @@ -834,6 +834,15 @@ const ASSET_CLASS_FILTER: ListViewFilterConfig = { } }; +const ASSET_IS_CRITICAL_FILTER: ListViewFilterConfig = { + component: AutocompleteSelect, + props: { + label: 'is_critical', + options: YES_NO_OPTIONS, + multiple: true + } +}; + const REFERENCE_CONTROL_CATEGORY_FILTER: ListViewFilterConfig = { component: AutocompleteSelect, props: { @@ -1286,7 +1295,8 @@ export const listViewFields = { folder: DOMAIN_FILTER, type: ASSET_TYPE_FILTER, filtering_labels: LABELS_FILTER, - asset_class: ASSET_CLASS_FILTER + asset_class: ASSET_CLASS_FILTER, + is_critical: ASSET_IS_CRITICAL_FILTER } }, 'asset-class': { From 7d3da20e734b46eaeb2dea517ed56fe29e2fb5e2 Mon Sep 17 00:00:00 2001 From: Abderrahmane Smimite Date: Fri, 7 Nov 2025 20:00:06 +0100 Subject: [PATCH 04/47] legal identifiers --- .../0008_entity_legal_identifiers.py | 22 +++ backend/tprm/models.py | 6 + frontend/messages/en.json | 8 + .../Forms/LegalIdentifierField.svelte | 183 ++++++++++++++++++ .../Forms/ModelForm/EntityForm.svelte | 7 + frontend/src/lib/utils/schemas.ts | 3 +- 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 backend/tprm/migrations/0008_entity_legal_identifiers.py create mode 100644 frontend/src/lib/components/Forms/LegalIdentifierField.svelte diff --git a/backend/tprm/migrations/0008_entity_legal_identifiers.py b/backend/tprm/migrations/0008_entity_legal_identifiers.py new file mode 100644 index 0000000000..71cce95add --- /dev/null +++ b/backend/tprm/migrations/0008_entity_legal_identifiers.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.7 on 2025-11-07 18:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tprm", "0007_entity_relationship"), + ] + + operations = [ + migrations.AddField( + model_name="entity", + name="legal_identifiers", + field=models.JSONField( + blank=True, + default=dict, + help_text="Legal identifiers (LEI, EUID, VAT, DUNS, etc.)", + verbose_name="Legal identifiers", + ), + ), + ] diff --git a/backend/tprm/models.py b/backend/tprm/models.py index fe63ade8d0..ca075ca96f 100644 --- a/backend/tprm/models.py +++ b/backend/tprm/models.py @@ -33,6 +33,12 @@ class Entity(NameDescriptionMixin, FolderMixin, PublishInRootFolderMixin): verbose_name=_("Relationship"), help_text=_("Type of relationship with this entity"), ) + legal_identifiers = models.JSONField( + default=dict, + blank=True, + verbose_name=_("Legal identifiers"), + help_text=_("Legal identifiers (LEI, EUID, VAT, DUNS, etc.)"), + ) fields_to_check = ["name"] diff --git a/frontend/messages/en.json b/frontend/messages/en.json index b02606cb15..8933ceb877 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -840,6 +840,14 @@ "representatives": "Representatives", "representative": "Representative", "addRepresentative": "Add representative", + "legalIdentifiers": "Legal identifiers", + "addLegalIdentifier": "Add identifier", + "noLegalIdentifierAdded": "No legal identifiers added yet", + "identifierType": "Identifier type", + "identifierValue": "Identifier value", + "identifierErrorMessage": "Invalid identifier", + "enterIdentifierValue": "Enter identifier value...", + "removeIdentifier": "Remove identifier", "phone": "Phone", "role": "Role", "questions": "Questions", diff --git a/frontend/src/lib/components/Forms/LegalIdentifierField.svelte b/frontend/src/lib/components/Forms/LegalIdentifierField.svelte new file mode 100644 index 0000000000..e7eef6c403 --- /dev/null +++ b/frontend/src/lib/components/Forms/LegalIdentifierField.svelte @@ -0,0 +1,183 @@ + + +
+
+ + +
+ + {#if Object.keys(legalIdentifiers).length === 0} +
+ {m.noLegalIdentifierAdded()} +
+ {/if} + +
+ {#each Object.entries(legalIdentifiers) as [type, identifier], i (i + '-' + type)} +
+
+ {#if $errors && $errors[type]} +
{m.identifierErrorMessage()}
+ {/if} + + +
+ +
+ {#if $errors && $errors[type]} + + {/if} + + updateIdentifierValue(type, e.target.value)} + placeholder={m.enterIdentifierValue()} + class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ +
+ +
+
+ + + + {/each} +
+ + + +
+ + diff --git a/frontend/src/lib/components/Forms/ModelForm/EntityForm.svelte b/frontend/src/lib/components/Forms/ModelForm/EntityForm.svelte index 60f1c07faf..77250959d5 100644 --- a/frontend/src/lib/components/Forms/ModelForm/EntityForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm/EntityForm.svelte @@ -2,6 +2,7 @@ import AutocompleteSelect from '../AutocompleteSelect.svelte'; import TextArea from '$lib/components/Forms/TextArea.svelte'; import TextField from '$lib/components/Forms/TextField.svelte'; + import LegalIdentifierField from '../LegalIdentifierField.svelte'; import type { SuperValidated } from 'sveltekit-superforms'; import type { ModelInfo, CacheLock } from '$lib/utils/types'; import { m } from '$paraglide/messages'; @@ -57,3 +58,9 @@ label={m.relationship()} multiple /> + diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts index 25ff326777..ad75a52b7d 100644 --- a/frontend/src/lib/utils/schemas.ts +++ b/frontend/src/lib/utils/schemas.ts @@ -570,7 +570,8 @@ export const EntitiesSchema = z.object({ message: "Link must be either empty or a valid URL starting with 'http'" }) .optional(), - relationship: z.string().optional().array().optional() + relationship: z.string().optional().array().optional(), + legal_identifiers: z.record(z.string()).optional() }); export const EntityAssessmentSchema = z.object({ From be5d0cbaaef627fe7a5239e262e097811f90a735 Mon Sep 17 00:00:00 2001 From: Abderrahmane Smimite Date: Fri, 7 Nov 2025 20:17:09 +0100 Subject: [PATCH 05/47] fixup --- backend/tprm/serializers.py | 9 +++++++++ .../lib/components/Forms/LegalIdentifierField.svelte | 8 ++------ frontend/src/lib/utils/crud.ts | 10 ++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/backend/tprm/serializers.py b/backend/tprm/serializers.py index fee3cf2047..c695332bc5 100644 --- a/backend/tprm/serializers.py +++ b/backend/tprm/serializers.py @@ -22,6 +22,15 @@ class EntityReadSerializer(BaseModelSerializer): folder = FieldsRelatedField() owned_folders = FieldsRelatedField(many=True) relationship = FieldsRelatedField(many=True) + legal_identifiers = serializers.SerializerMethodField() + + def get_legal_identifiers(self, obj): + """Format legal identifiers as a readable string for display""" + if not obj.legal_identifiers: + return "" + return "\n".join( + [f"{key}: {value}" for key, value in obj.legal_identifiers.items()] + ) class Meta: model = Entity diff --git a/frontend/src/lib/components/Forms/LegalIdentifierField.svelte b/frontend/src/lib/components/Forms/LegalIdentifierField.svelte index e7eef6c403..48e5550431 100644 --- a/frontend/src/lib/components/Forms/LegalIdentifierField.svelte +++ b/frontend/src/lib/components/Forms/LegalIdentifierField.svelte @@ -33,13 +33,9 @@ const identifierTypes = [ { value: 'LEI', label: 'LEI (Legal Entity Identifier)' }, - { value: 'VAT', label: 'VAT Number' }, - { value: 'DUNS', label: 'DUNS Number' }, { value: 'EUID', label: 'EUID (European Unique Identifier)' }, - { value: 'SIRET', label: 'SIRET' }, - { value: 'SIREN', label: 'SIREN' }, - { value: 'TAX_ID', label: 'Tax ID' }, - { value: 'COMPANY_REG', label: 'Company Registration Number' }, + { value: 'DUNS', label: 'DUNS Number' }, + { value: 'VAT', label: 'VAT Number' }, { value: 'OTHER', label: 'Other' } ]; diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts index 3b7dd3611b..666b6eb31c 100644 --- a/frontend/src/lib/utils/crud.ts +++ b/frontend/src/lib/utils/crud.ts @@ -760,6 +760,16 @@ export const URL_MODEL_MAP: ModelMap = { localNamePlural: 'entities', verboseName: 'Entity', verboseNamePlural: 'Entities', + detailViewFields: [ + { field: 'id' }, + { field: 'ref_id' }, + { field: 'name' }, + { field: 'description' }, + { field: 'mission' }, + { field: 'relationship' }, + { field: 'legal_identifiers' }, + { field: 'reference_link' } + ], reverseForeignKeyFields: [ { field: 'entity', urlModel: 'entity-assessments' }, { field: 'entity', urlModel: 'representatives' }, From 56df9de6485fad711e5838637274a2c59a89b6f9 Mon Sep 17 00:00:00 2001 From: Abderrahmane Smimite Date: Fri, 7 Nov 2025 22:35:31 +0100 Subject: [PATCH 06/47] feat: contracts management --- backend/core/startup.py | 13 ++ backend/core/urls.py | 2 + backend/core/views.py | 1 + backend/tprm/migrations/0009_contract.py | 139 ++++++++++++++++++ backend/tprm/models.py | 81 +++++++++- backend/tprm/serializers.py | 22 ++- backend/tprm/views.py | 25 +++- frontend/messages/en.json | 7 + .../src/lib/components/Forms/ModelForm.svelte | 3 + .../Forms/ModelForm/ContractForm.svelte | 113 ++++++++++++++ .../src/lib/components/SideBar/navData.ts | 7 +- frontend/src/lib/utils/crud.ts | 23 ++- frontend/src/lib/utils/schemas.ts | 15 ++ frontend/src/lib/utils/table.ts | 20 +++ frontend/src/lib/utils/types.ts | 1 + 15 files changed, 465 insertions(+), 7 deletions(-) create mode 100644 backend/tprm/migrations/0009_contract.py create mode 100644 frontend/src/lib/components/Forms/ModelForm/ContractForm.svelte diff --git a/backend/core/startup.py b/backend/core/startup.py index 8dde2a6056..37b2c5b97f 100644 --- a/backend/core/startup.py +++ b/backend/core/startup.py @@ -34,6 +34,7 @@ "view_riskmatrix", "view_riskscenario", "view_solution", + "view_contract", "view_storedlibrary", "view_threat", "view_vulnerability", @@ -175,6 +176,7 @@ "add_riskassessment", "add_riskscenario", "add_solution", + "add_contract", "add_threat", "add_vulnerability", "change_appliedcontrol", @@ -193,6 +195,7 @@ "change_riskassessment", "change_riskscenario", "change_solution", + "change_contract", "change_threat", "delete_appliedcontrol", "delete_asset", @@ -209,6 +212,7 @@ "delete_riskassessment", "delete_riskscenario", "delete_solution", + "delete_contract", "delete_threat", "view_appliedcontrol", "view_asset", @@ -233,6 +237,7 @@ "view_riskmatrix", "view_riskscenario", "view_solution", + "view_contract", "view_storedlibrary", "view_threat", "view_user", @@ -414,6 +419,7 @@ "add_riskmatrix", "add_riskscenario", "add_solution", + "add_contract", "add_threat", "change_appliedcontrol", "change_asset", @@ -432,6 +438,7 @@ "change_riskmatrix", "change_riskscenario", "change_solution", + "change_contract", "change_threat", "delete_appliedcontrol", "delete_asset", @@ -453,6 +460,7 @@ "delete_vulnerability", "delete_riskscenario", "delete_solution", + "delete_contract", "delete_threat", "view_appliedcontrol", "view_asset", @@ -476,6 +484,7 @@ "view_riskmatrix", "view_riskscenario", "view_solution", + "view_contract", "view_storedlibrary", "view_threat", "view_user", @@ -770,6 +779,10 @@ "change_solution", "view_solution", "delete_solution", + "add_contract", + "change_contract", + "view_contract", + "delete_contract", "add_entityassessment", "change_entityassessment", "view_entityassessment", diff --git a/backend/core/urls.py b/backend/core/urls.py index d904aa66f8..3fc4adf0cc 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -4,6 +4,7 @@ RepresentativeViewSet, SolutionViewSet, EntityAssessmentViewSet, + ContractViewSet, ) from library.views import StoredLibraryViewSet, LoadedLibraryViewSet import importlib @@ -23,6 +24,7 @@ ) router.register(r"solutions", SolutionViewSet, basename="solutions") router.register(r"representatives", RepresentativeViewSet, basename="representatives") +router.register(r"contracts", ContractViewSet, basename="contracts") router.register(r"perimeters", PerimeterViewSet, basename="perimeters") router.register(r"risk-matrices", RiskMatrixViewSet, basename="risk-matrices") router.register(r"vulnerabilities", VulnerabilityViewSet, basename="vulnerabilities") diff --git a/backend/core/views.py b/backend/core/views.py index f9b09214a2..719d05fca2 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -5797,6 +5797,7 @@ class EvidenceViewSet(BaseModelViewSet): "owner", "status", "expiry_date", + "contracts", ] @action(detail=False, name="Get all evidences owners") diff --git a/backend/tprm/migrations/0009_contract.py b/backend/tprm/migrations/0009_contract.py new file mode 100644 index 0000000000..f6b821eb1d --- /dev/null +++ b/backend/tprm/migrations/0009_contract.py @@ -0,0 +1,139 @@ +# Generated by Django 5.2.7 on 2025-11-07 20:57 + +import django.db.models.deletion +import iam.models +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0112_asset_is_critical"), + ("iam", "0016_folder_filtering_labels"), + ("tprm", "0008_entity_legal_identifiers"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Contract", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="Updated at"), + ), + ( + "is_published", + models.BooleanField(default=False, verbose_name="published"), + ), + ("name", models.CharField(max_length=200, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ( + "status", + models.CharField( + choices=[ + ("draft", "Draft"), + ("active", "Active"), + ("expired", "Expired"), + ("terminated", "Terminated"), + ], + default="draft", + max_length=20, + verbose_name="Status", + ), + ), + ( + "start_date", + models.DateField(blank=True, null=True, verbose_name="Start date"), + ), + ( + "end_date", + models.DateField(blank=True, null=True, verbose_name="End date"), + ), + ( + "ref_id", + models.CharField( + blank=True, + help_text="Contract reference number or identifier", + max_length=255, + verbose_name="Reference ID", + ), + ), + ( + "entities", + models.ManyToManyField( + blank=True, + help_text="Entities involved in this contract", + related_name="contracts", + to="tprm.entity", + verbose_name="Entities", + ), + ), + ( + "evidences", + models.ManyToManyField( + blank=True, + help_text="Supporting evidence for this contract", + related_name="contracts", + to="core.evidence", + verbose_name="Evidences", + ), + ), + ( + "filtering_labels", + models.ManyToManyField( + blank=True, to="core.filteringlabel", verbose_name="Labels" + ), + ), + ( + "folder", + models.ForeignKey( + default=iam.models.Folder.get_root_folder_id, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_folder", + to="iam.folder", + ), + ), + ( + "owner", + models.ManyToManyField( + blank=True, + related_name="contracts", + to=settings.AUTH_USER_MODEL, + verbose_name="Owner", + ), + ), + ( + "solutions", + models.ManyToManyField( + blank=True, + help_text="Solutions covered by this contract", + related_name="contracts", + to="tprm.solution", + verbose_name="Solutions", + ), + ), + ], + options={ + "verbose_name": "Contract", + "verbose_name_plural": "Contracts", + }, + ), + ] diff --git a/backend/tprm/models.py b/backend/tprm/models.py index ca075ca96f..b63f8a960b 100644 --- a/backend/tprm/models.py +++ b/backend/tprm/models.py @@ -1,7 +1,14 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from core.base_models import NameDescriptionMixin, AbstractBaseModel -from core.models import Assessment, ComplianceAssessment, Evidence, Asset, Terminology +from core.models import ( + Assessment, + ComplianceAssessment, + Evidence, + Asset, + Terminology, + FilteringLabelMixin, +) from iam.models import Folder, FolderMixin, PublishInRootFolderMixin from iam.views import User @@ -163,6 +170,74 @@ class Meta: verbose_name_plural = _("Solutions") +class Contract(NameDescriptionMixin, FolderMixin, FilteringLabelMixin): + """ + A contract represents an agreement between multiple entities + """ + + class Status(models.TextChoices): + DRAFT = "draft", _("Draft") + ACTIVE = "active", _("Active") + EXPIRED = "expired", _("Expired") + TERMINATED = "terminated", _("Terminated") + + owner = models.ManyToManyField( + User, + verbose_name=_("Owner"), + related_name="contracts", + blank=True, + ) + entities = models.ManyToManyField( + Entity, + related_name="contracts", + blank=True, + verbose_name=_("Entities"), + help_text=_("Entities involved in this contract"), + ) + evidences = models.ManyToManyField( + Evidence, + blank=True, + related_name="contracts", + verbose_name=_("Evidences"), + help_text=_("Supporting evidence for this contract"), + ) + solutions = models.ManyToManyField( + "tprm.Solution", + blank=True, + related_name="contracts", + verbose_name=_("Solutions"), + help_text=_("Solutions covered by this contract"), + ) + status = models.CharField( + max_length=20, + choices=Status.choices, + default=Status.DRAFT, + verbose_name=_("Status"), + ) + start_date = models.DateField( + blank=True, + null=True, + verbose_name=_("Start date"), + ) + end_date = models.DateField( + blank=True, + null=True, + verbose_name=_("End date"), + ) + ref_id = models.CharField( + max_length=255, + blank=True, + verbose_name=_("Reference ID"), + help_text=_("Contract reference number or identifier"), + ) + + fields_to_check = ["name"] + + class Meta: + verbose_name = _("Contract") + verbose_name_plural = _("Contracts") + + common_exclude = ["created_at", "updated_at"] auditlog.register( @@ -181,3 +256,7 @@ class Meta: Solution, exclude_fields=common_exclude, ) +auditlog.register( + Contract, + exclude_fields=common_exclude, +) diff --git a/backend/tprm/serializers.py b/backend/tprm/serializers.py index c695332bc5..4cbd3f4450 100644 --- a/backend/tprm/serializers.py +++ b/backend/tprm/serializers.py @@ -8,7 +8,7 @@ from core.utils import RoleCodename, UserGroupCodename from iam.models import Folder, Role, RoleAssignment, UserGroup from django.contrib.auth import get_user_model -from tprm.models import Entity, EntityAssessment, Representative, Solution +from tprm.models import Entity, EntityAssessment, Representative, Solution, Contract from django.utils.translation import gettext_lazy as _ import structlog @@ -22,6 +22,7 @@ class EntityReadSerializer(BaseModelSerializer): folder = FieldsRelatedField() owned_folders = FieldsRelatedField(many=True) relationship = FieldsRelatedField(many=True) + contracts = FieldsRelatedField(many=True) legal_identifiers = serializers.SerializerMethodField() def get_legal_identifiers(self, obj): @@ -276,6 +277,7 @@ class SolutionReadSerializer(BaseModelSerializer): provider_entity = FieldsRelatedField() recipient_entity = FieldsRelatedField() assets = FieldsRelatedField(many=True) + contracts = FieldsRelatedField(many=True) class Meta: model = Solution @@ -286,3 +288,21 @@ class SolutionWriteSerializer(BaseModelSerializer): class Meta: model = Solution exclude = ["recipient_entity"] + + +class ContractReadSerializer(BaseModelSerializer): + folder = FieldsRelatedField() + owner = FieldsRelatedField(many=True) + entities = FieldsRelatedField(many=True) + evidences = FieldsRelatedField(many=True) + solutions = FieldsRelatedField(many=True) + + class Meta: + model = Contract + exclude = [] + + +class ContractWriteSerializer(BaseModelSerializer): + class Meta: + model = Contract + exclude = [] diff --git a/backend/tprm/views.py b/backend/tprm/views.py index ee45cd57b3..fb5cfa9981 100644 --- a/backend/tprm/views.py +++ b/backend/tprm/views.py @@ -1,7 +1,7 @@ from rest_framework.response import Response from iam.models import Folder, RoleAssignment, UserGroup from core.views import BaseModelViewSet as AbstractBaseModelViewSet -from tprm.models import Entity, Representative, Solution, EntityAssessment +from tprm.models import Entity, Representative, Solution, EntityAssessment, Contract from rest_framework.decorators import action import structlog @@ -24,7 +24,13 @@ class EntityViewSet(BaseModelViewSet): """ model = Entity - filterset_fields = ["name", "folder", "relationship", "relationship__name"] + filterset_fields = [ + "name", + "folder", + "relationship", + "relationship__name", + "contracts", + ] class EntityAssessmentViewSet(BaseModelViewSet): @@ -144,10 +150,23 @@ class SolutionViewSet(BaseModelViewSet): """ model = Solution - filterset_fields = ["name", "provider_entity", "assets", "criticality"] + filterset_fields = ["name", "provider_entity", "assets", "criticality", "contracts"] def perform_create(self, serializer): serializer.save() solution = serializer.instance solution.recipient_entity = Entity.objects.get(builtin=True) solution.save() + + +class ContractViewSet(BaseModelViewSet): + """ + API endpoint that allows contracts to be viewed or edited. + """ + + model = Contract + filterset_fields = ["name", "folder", "entities", "status", "owner"] + + @action(detail=False, name="Get status choices") + def status(self, request): + return Response(dict(Contract.Status.choices)) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 8933ceb877..6ae7bc3eeb 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -53,6 +53,8 @@ "associatedEntityAssessments": "Associated entity assessments", "associatedRepresentatives": "Associated representatives", "associatedSolutions": "Associated solutions", + "associatedContracts": "Associated contracts", + "associatedEntities": "Associated entities", "associatedTaskTemplates": "Associated tasks", "associatedObjectsCount": "Associated objects", "home": "Home", @@ -207,6 +209,8 @@ "existingControls": "Existing controls", "strengthOfKnowledge": "Strength of knowledge", "dueDate": "Due date", + "startDate": "Start date", + "endDate": "End date", "attachment": "Attachment", "observation": "Observation", "observationHelpText": "Additional notes and comments in markdown format", @@ -837,6 +841,9 @@ "solutionsLinkedToAssetHelpText": "Solutions associated with this asset", "providerEntity": "Provider entity", "addProduct": "Add product", + "contracts": "Contracts", + "contract": "Contract", + "addContract": "Add contract", "representatives": "Representatives", "representative": "Representative", "addRepresentative": "Add representative", diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index 26da9efcc9..7cf81f4e0d 100644 --- a/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm.svelte @@ -23,6 +23,7 @@ import EntitiesForm from './ModelForm/EntityForm.svelte'; import EntityAssessmentForm from './ModelForm/EntityAssessmentForm.svelte'; import SolutionsForm from './ModelForm/SolutionForm.svelte'; + import ContractsForm from './ModelForm/ContractForm.svelte'; import RepresentativesForm from './ModelForm/RepresentativeForm.svelte'; import FrameworksForm from './ModelForm/FrameworkForm.svelte'; import UsersForm from './ModelForm/UserForm.svelte'; @@ -453,6 +454,8 @@ /> {:else if URLModel === 'solutions'} + {:else if URLModel === 'contracts'} + {:else if URLModel === 'representatives'} {:else if URLModel === 'frameworks'} diff --git a/frontend/src/lib/components/Forms/ModelForm/ContractForm.svelte b/frontend/src/lib/components/Forms/ModelForm/ContractForm.svelte new file mode 100644 index 0000000000..11695e6ccc --- /dev/null +++ b/frontend/src/lib/components/Forms/ModelForm/ContractForm.svelte @@ -0,0 +1,113 @@ + + +