+ Action plan
+
+
{% trans "Domain" %}: {{ compliance_assessment.project.folder }}
+
/
+
{% trans "Project" %}: {{ compliance_assessment.project.name }}
+
/
+
{% trans "Audit" %}: {{ compliance_assessment.name }} - {{ compliance_assessment.version }}
+
/
+
{% trans "Framework" %}: {{ compliance_assessment.framework }}
+
+ {% trans "Associated applied controls" %}:
+ {% trans "Separated by status and sorted by eta" %}
+ {% for status, applied_controls in context.items%}
+ {% for status_color, color in color_map.items %}
+ {% if status_color == status %}
+ {% trans status|title %}:
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+ {% trans "Name" %} |
+ {% trans "Description" %} |
+ {% trans "Category" %} |
+ {% trans "ETA" %} |
+ {% trans "Expiry date" %} |
+ {% trans "Effort" %} |
+ {% trans "Matching requirements" %} |
+
+
+
+ {% for applied_control in applied_controls %}
+
+ {{ applied_control.name }} |
+ {{ applied_control.description }} |
+ {{ applied_control.category }} |
+ {{ applied_control.eta }} |
+ {{ applied_control.expiry_date }} |
+ {{ applied_control.effort }} |
+ {% get_requirements_count applied_control compliance_assessment %} |
+
+ {% empty %}
+
+
+ {% trans "No entries found" %}
+ |
+
+ {% endfor %}
+
+
+
+
+ {% endfor %}
+
+{% endblock %}
diff --git a/backend/core/templatetags/core_extras.py b/backend/core/templatetags/core_extras.py
index 0aebc1e5b..8fa6edbc5 100644
--- a/backend/core/templatetags/core_extras.py
+++ b/backend/core/templatetags/core_extras.py
@@ -2,6 +2,7 @@
from ciso_assistant.settings import VERSION, BUILD, DEBUG
from core.utils import COUNTRY_FLAGS, LANGUAGES
+from core.models import RequirementAssessment
register = template.Library()
@@ -16,6 +17,17 @@ def app_build():
return f"{BUILD} (dev)" if DEBUG else BUILD
+@register.simple_tag()
+def get_requirements_count(applied_control, compliance_assessment):
+ return (
+ RequirementAssessment.objects.filter(
+ compliance_assessment=compliance_assessment
+ )
+ .filter(applied_controls=applied_control)
+ .count()
+ )
+
+
@register.filter("class")
def _class(obj):
return obj.__class__.__name__ if obj else ""
diff --git a/backend/core/views.py b/backend/core/views.py
index 7db5bebe6..ed2c22b18 100644
--- a/backend/core/views.py
+++ b/backend/core/views.py
@@ -1208,6 +1208,92 @@ def selected_implementation_groups(self, request, pk):
}
return Response(implementation_group_choices)
+ @action(detail=True, methods=["get"], name="Get action plan data")
+ def action_plan(self, request, pk):
+ (viewable_objects, _, _) = RoleAssignment.get_accessible_object_ids(
+ folder=Folder.get_root_folder(),
+ user=request.user,
+ object_type=ComplianceAssessment,
+ )
+ if UUID(pk) in viewable_objects:
+ response = {
+ "planned": list(),
+ "active": list(),
+ "inactive": list(),
+ "none": list(),
+ }
+ compliance_assessment_object = self.get_object()
+ requirement_assessments_objects = (
+ compliance_assessment_object.get_requirement_assessments()
+ )
+ applied_controls = AppliedControlReadSerializer(
+ AppliedControl.objects.filter(
+ requirement_assessments__in=requirement_assessments_objects
+ ).distinct(),
+ many=True,
+ ).data
+ for applied_control in applied_controls:
+ applied_control["requirements_count"] = (
+ RequirementAssessment.objects.filter(
+ compliance_assessment=compliance_assessment_object
+ )
+ .filter(applied_controls=applied_control["id"])
+ .count()
+ )
+ response[applied_control["status"].lower()].append(
+ applied_control
+ ) if applied_control["status"] else response["none"].append(
+ applied_control
+ )
+ return Response(response)
+
+ @action(detail=True, name="Get action plan PDF")
+ def action_plan_pdf(self, request, pk):
+ (object_ids_view, _, _) = RoleAssignment.get_accessible_object_ids(
+ Folder.get_root_folder(), request.user, ComplianceAssessment
+ )
+ if UUID(pk) in object_ids_view:
+ context = {
+ "planned": list(),
+ "active": list(),
+ "inactive": list(),
+ "no status": list(),
+ }
+ color_map = {
+ "planned": "#93c5fd",
+ "active": "#86efac",
+ "inactive": "#fca5a5",
+ "no status": "#e5e7eb",
+ }
+ compliance_assessment_object = self.get_object()
+ requirement_assessments_objects = (
+ compliance_assessment_object.get_requirement_assessments()
+ )
+ applied_controls = (
+ AppliedControl.objects.filter(
+ requirement_assessments__in=requirement_assessments_objects
+ )
+ .distinct()
+ .order_by("eta")
+ )
+ for applied_control in applied_controls:
+ context[applied_control.status].append(
+ applied_control
+ ) if applied_control.status else context["no status"].append(
+ applied_control
+ )
+ data = {
+ "color_map": color_map,
+ "context": context,
+ "compliance_assessment": compliance_assessment_object,
+ }
+ html = render_to_string("core/action_plan_pdf.html", data)
+ pdf_file = HTML(string=html).write_pdf()
+ response = HttpResponse(pdf_file, content_type="application/pdf")
+ return response
+ else:
+ return Response({"error": "Permission denied"})
+
def perform_create(self, serializer):
"""
Create RequirementAssessment objects for the newly created ComplianceAssessment
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index beaba8953..494babc2f 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -517,5 +517,12 @@
"selectedImplementationGroups": "Selected implementation groups",
"implementationGroupsDefinition": "Implementation groups definition",
"threatRadarChart": "Threat radar",
- "noThreatsMapped": "No threats mapped. Consider attaching threats to your risk scenarios for a better overview."
+ "noThreatsMapped": "No threats mapped. Consider attaching threats to your risk scenarios for a better overview.",
+ "actionPlan": "Action plan",
+ "noStatus": "No status",
+ "actionPlanHelpText": "Separated by status and sorted by eta",
+ "matchingRequirements": "Matching requirements",
+ "asZIP": "as ZIP",
+ "incoming": "Incoming",
+ "outdated": "Outdated"
}
diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json
index 0ce84cf96..761dcf9df 100644
--- a/frontend/messages/fr.json
+++ b/frontend/messages/fr.json
@@ -164,8 +164,8 @@
"currentRisk": "Risque courant",
"residualRisk": "Risque résiduel",
"planned": "Planifié",
- "active": "Active",
- "inactive": "Inactive",
+ "active": "Actif",
+ "inactive": "Inactif",
"watchlist": "Liste de surveillance",
"watchlistDescription": "Objets qui ont expiré ou expireront dans les 30 prochains jours",
"measuresToReview": "Mesures à revoir",
@@ -517,5 +517,12 @@
"selectedImplementationGroups": "Groupes d'implémentation sélectionnés",
"implementationGroupsDefinition": "Définition des groupes d'implémentation",
"threatRadarChart": "Radar des menaces",
- "noThreatsMapped": "Aucune menace n'a été attachée. Pensez à lier les menaces à vos scénarios de risque pour une meilleure visibilité."
+ "noThreatsMapped": "Aucune menace n'a été attachée. Pensez à lier les menaces à vos scénarios de risque pour une meilleure visibilité.",
+ "actionPlan": "Plan d'action",
+ "noStatus": "Pas de statut",
+ "actionPlanHelpText": "Séparé par statut et trié par eta",
+ "matchingRequirements": "Exigences associées",
+ "asZIP": "en ZIP",
+ "incoming": "En approche",
+ "outdated": "Dépassé"
}
diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json
index 504bb51e5..d3fae1ab3 100644
--- a/frontend/messages/pt.json
+++ b/frontend/messages/pt.json
@@ -517,5 +517,12 @@
"selectedImplementationGroups": "Grupos de implementação selecionados",
"implementationGroupsDefinition": "Definição de grupos de implementação",
"threatRadarChart": "Radar de ameaças",
- "noThreatsMapped": "Nenhuma ameaça mapeada. Considere anexar ameaças aos seus cenários de risco para ter uma visão geral melhor."
+ "noThreatsMapped": "Nenhuma ameaça mapeada. Considere anexar ameaças aos seus cenários de risco para ter uma visão geral melhor.",
+ "actionPlan": "Plano de ação",
+ "noStatus": "Sem status",
+ "actionPlanHelpText": "Separados por status e classificados por eta",
+ "matchingRequirements": "Requisitos correspondentes",
+ "asZIP": "em ZIP",
+ "incoming": "aproximação",
+ "outdated": "Desatualizado"
}
diff --git a/frontend/src/lib/components/ModelTable/ModelTable.svelte b/frontend/src/lib/components/ModelTable/ModelTable.svelte
index 64eba0549..db711583b 100644
--- a/frontend/src/lib/components/ModelTable/ModelTable.svelte
+++ b/frontend/src/lib/components/ModelTable/ModelTable.svelte
@@ -36,6 +36,7 @@
export let pagination = true;
export let numberRowsPerPage = 10;
export let thFiler = false;
+ export let tags = true;
export let orderBy: { identifier: string; direction: 'asc' | 'desc' } | undefined = undefined;
@@ -206,7 +207,7 @@
{@const tagList = Array.isArray(_tagList) ? _tagList : [_tagList]}
{#each tagList as tag}
{@const tagData = tag.values[meta[tag.key]]}
- {#if tagData}
+ {#if tagData && tags}
{@const {text, cssClasses} = tagData}