Skip to content

Commit

Permalink
Merge pull request #434 from intuitem/feat/audit-action-plan
Browse files Browse the repository at this point in the history
Feat/audit action plan
  • Loading branch information
eric-intuitem authored May 16, 2024
2 parents 8884db5 + 0231d4d commit 6a398d9
Show file tree
Hide file tree
Showing 20 changed files with 514 additions and 49 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/functional-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ jobs:
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Install dependencies
working-directory: ${{ env.working-directory }}
run: npm ci
run: |
npm install
npm ci
- name: Install Playwright browser ${{ matrix.playwright-browser }}
working-directory: ${{ env.working-directory }}
run: npx playwright install --with-deps ${{ matrix.playwright-browser }}
Expand Down
3 changes: 3 additions & 0 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,9 @@ def get_selected_implementation_groups(self):
if group.get("ref_id") in self.selected_implementation_groups
]

def get_requirement_assessments(self):
return RequirementAssessment.objects.filter(compliance_assessment=self)

def get_requirements_status_count(self):
requirements_status_count = []
for st in RequirementAssessment.Status:
Expand Down
68 changes: 68 additions & 0 deletions backend/core/templates/core/action_plan_pdf.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{% extends 'core/base_pdf.html' %}
{% block content %}
{% load i18n core_extras %}

<style>
table, th, td {
border: 1px solid black;
}
</style>

<main class="p-2 m-2 main">
<h1 class="flex justify-center">Action plan</h1>
<div class="bg-white p-2 m-2 space-x-3 flex flex-row justify-center">
<p class="font-semibold text-lg"> {% trans "Domain" %}: {{ compliance_assessment.project.folder }}</p>
<p class="font-semibold text-lg">/</p>
<p class="font-semibold text-lg"> {% trans "Project" %}: {{ compliance_assessment.project.name }}</p>
<p class="font-semibold text-lg">/</p>
<p class="font-semibold text-lg"> {% trans "Audit" %}: {{ compliance_assessment.name }} - {{ compliance_assessment.version }}</p>
<p class="font-semibold text-lg">/</p>
<p class="font-semibold text-lg"> {% trans "Framework" %}: {{ compliance_assessment.framework }}</p>
</div>
<p class="p-2 m-2 text-lg font-semibold"> {% trans "Associated applied controls" %}: </p>
<p class="m-2 p-2" style="color: #374151">{% trans "Separated by status and sorted by eta" %}</p>
{% for status, applied_controls in context.items%}
{% for status_color, color in color_map.items %}
{% if status_color == status %}
<p class="flex p-2 m-2 text-lg font-semibold justify-center" style="background-color: {{color}}">{% trans status|title %}:</p>
{% endif %}
{% endfor %}
<div class="bg-white p-2 m-2 shadow overflow-hidden rounded-lg flex flex-row">
<div class="flex w-full">
<table class="w-full p-2 mt-2">
<thead>
<tr class="uppercase">
<th class="text-md p-2 text-center">{% trans "Name" %}</th>
<th class="text-md p-2 text-center">{% trans "Description" %}</th>
<th class="text-md p-2 text-center">{% trans "Category" %}</th>
<th class="text-md p-2 text-center">{% trans "ETA" %}</th>
<th class="text-md p-2 text-center">{% trans "Expiry date" %}</th>
<th class="text-md p-2 text-center">{% trans "Effort" %}</th>
<th class="text-md p-2 text-center">{% trans "Matching requirements" %}</th>
</tr>
</thead>
<tbody>
{% for applied_control in applied_controls %}
<tr>
<td class="text-md p-2 text-center">{{ applied_control.name }}</td>
<td class="text-md p-2 text-center">{{ applied_control.description }}</td>
<td class="text-md p-2 text-center">{{ applied_control.category }}</td>
<td class="text-md p-2 text-center">{{ applied_control.eta }}</td>
<td class="text-md p-2 text-center">{{ applied_control.expiry_date }}</td>
<td class="text-md p-2 text-center">{{ applied_control.effort }}</td>
<td class="text-md p-2 text-center">{% get_requirements_count applied_control compliance_assessment %}</td>
</tr>
{% empty %}
<tr>
<td class="text-md p-2 text-center" colspan="7">
<p>{% trans "No entries found" %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</main>
{% endblock %}
12 changes: 12 additions & 0 deletions backend/core/templatetags/core_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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 ""
Expand Down
86 changes: 86 additions & 0 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
13 changes: 10 additions & 3 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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é"
}
9 changes: 8 additions & 1 deletion frontend/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
11 changes: 9 additions & 2 deletions frontend/src/lib/components/ModelTable/ModelTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
<span class={cssClasses}>
{localItems(languageTag())[text]}
Expand Down Expand Up @@ -245,7 +246,13 @@
{value.str ?? '-'}
{/if}
{:else if value && value.hexcolor}
<p class="flex w-fit min-w-24 justify-center px-2 py-1 rounded-md ml-2 whitespace-nowrap" style="background-color: {value.hexcolor}">{value.name ?? value.str ?? '-'}</p>
<p class="flex w-fit min-w-24 justify-center px-2 py-1 rounded-md ml-2 whitespace-nowrap" style="background-color: {value.hexcolor}">
{#if localItems(languageTag())[toCamelCase(value.name ?? value.str ?? '-')]}
{localItems(languageTag())[toCamelCase(value.name ?? value.str ?? '-')]}
{:else}
{value.name ?? value.str ?? '-'}
{/if}
</p>
{:else if ISO_8601_REGEX.test(value)}
{formatDateOrDateTime(value, languageTag())}
{:else}
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/lib/utils/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,13 @@ export function localItems(languageTag: string): LocalItems {
}),
attemptToRemoveOnlyAdminUserGroup: m.attemptToRemoveOnlyAdminUserGroup({
languageTag: languageTag
})
}),
actionPlan: m.actionPlan({ languageTag: languageTag }),
matchingRequirements: m.matchingRequirements({ languageTag: languageTag }),
remediationPlan: m.remediationPlan({ languageTag: languageTag }),
incoming: m.incoming({ languageTag: languageTag }),
today: m.today({ languageTag: languageTag }),
outdated: m.outdated({ languageTag: languageTag })
};
return LOCAL_ITEMS;
}
6 changes: 3 additions & 3 deletions frontend/src/routes/(app)/analyses-registry/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<ModelTable source={data.table} URLModel={data.URLModel}>
<span slot="actions" let:meta class="space-x-2 whitespace-nowrap">
<a
href="/risk-assessments/{meta.id}/plan"
href="/risk-assessments/{meta.id}/remediation-plan"
class="unstyled cursor-pointer text-xl text-slate-500 hover:text-indigo-700"
on:click={stopPropagation}><i class="fa-solid fa-heart-pulse" /></a
>
Expand All @@ -49,12 +49,12 @@
>
<p class="block px-4 py-2 text-sm text-gray-800">Treatment plan</p>
<a
href="/risk-assessments/{meta.id}/plan/export/pdf"
href="/risk-assessments/{meta.id}/remediation-plan/export/pdf"
class="block px-4 py-2 text-sm text-gray-800 hover:bg-gray-200"
on:click={stopPropagation}>... as PDF</a
>
<a
href="/risk-assessments/{meta.id}/plan/export/csv"
href="/risk-assessments/{meta.id}/remediation-plan/export/csv"
class="block px-4 py-2 text-sm text-gray-800 border-b hover:bg-gray-200"
on:click={stopPropagation}>... as csv</a
>
Expand Down
Loading

0 comments on commit 6a398d9

Please sign in to comment.