Skip to content

Commit

Permalink
Merge branch 'main' into #218-3cf
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-intuitem committed May 8, 2024
2 parents dea79c4 + 14d56e5 commit cf7f302
Show file tree
Hide file tree
Showing 20 changed files with 224 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/backend-coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
echo DJANGO_DEBUG='True' >> .env
echo POSTGRES_NAME=postgres >> .env
echo POSTGRES_USER=postgres >> .env
echo POSTGRES_PASSWORD=postgers >> .env
echo POSTGRES_PASSWORD=postgres >> .env
echo DB_HOST=localhost >> .env
echo EMAIL_HOST=localhost >> .env
echo EMAIL_PORT=1025 >> .env
Expand Down
3 changes: 3 additions & 0 deletions backend/app_tests/api/test_api_compliance_assessments.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def test_get_compliance_assessments(self, test):
"framework": {
"id": str(Framework.objects.all()[0].id),
"str": str(Framework.objects.all()[0]),
"implementation_groups_definition": None,
"min_score": 1,
"max_score": 4,
},
Expand Down Expand Up @@ -157,6 +158,7 @@ def test_create_compliance_assessments(self, test):
"framework": {
"id": str(Framework.objects.all()[0].id),
"str": str(Framework.objects.all()[0]),
"implementation_groups_definition": None,
"min_score": Framework.objects.all()[0].min_score,
"max_score": Framework.objects.all()[0].max_score,
},
Expand Down Expand Up @@ -202,6 +204,7 @@ def test_update_compliance_assessments(self, test):
"framework": {
"id": str(Framework.objects.all()[0].id),
"str": str(Framework.objects.all()[0]),
"implementation_groups_definition": None,
"min_score": Framework.objects.all()[0].min_score,
"max_score": Framework.objects.all()[0].max_score,
},
Expand Down
47 changes: 47 additions & 0 deletions backend/core/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections.abc import MutableMapping
from datetime import date, timedelta

from django.db.models import Count
Expand All @@ -9,6 +10,20 @@
from .models import *
from .utils import camel_case


def flatten_dict(
d: MutableMapping, parent_key: str = "", sep: str = "."
) -> MutableMapping:
items = []
for k, v in d.items():
new_key = parent_key + sep + k if parent_key else k
if isinstance(v, MutableMapping):
items.extend(flatten_dict(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
return dict(items)


STATUS_COLOR_MAP = { # TODO: Move these kinds of color maps to frontend
"undefined": "#CCC",
"planned": "#BFDBFE",
Expand Down Expand Up @@ -240,6 +255,9 @@ def get_sorted_requirement_nodes_rec(
"parent_urn": node.parent_urn,
"ref_id": node.ref_id,
"name": node.name,
"implementation_groups": node.implementation_groups
if node.implementation_groups
else None,
"ra_id": str(req_as.id) if requirements_assessed else None,
"status": req_as.status if requirements_assessed else None,
"is_scored": req_as.is_scored if requirements_assessed else None,
Expand Down Expand Up @@ -275,6 +293,9 @@ def get_sorted_requirement_nodes_rec(
{
"urn": req.urn,
"ref_id": req.ref_id,
"implementation_groups": req.implementation_groups
if req.implementation_groups
else None,
"name": req.name,
"description": req.description,
"ra_id": str(req_as.id),
Expand Down Expand Up @@ -322,6 +343,32 @@ def get_sorted_requirement_nodes_rec(
return tree


def filter_graph_by_implementation_groups(
graph: dict[str, dict], implementation_groups: set[str] | None
) -> dict[str, dict]:
if not implementation_groups:
return graph

def should_include_node(node: dict) -> bool:
node_groups = node.get("implementation_groups")
if node_groups:
return any(group in node_groups for group in implementation_groups)

# Nodes without implementation groups but with children are included
return bool(node.get("children"))

filtered_graph = {}
for key, value in graph.items():
if value.get("children"):
value["children"] = filter_graph_by_implementation_groups(
value["children"], implementation_groups
)
if should_include_node(value):
filtered_graph[key] = value

return filtered_graph


def get_parsed_matrices(user: User, risk_assessments: list | None = None):
(
object_ids_view,
Expand Down
78 changes: 57 additions & 21 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1290,10 +1290,31 @@ def get_global_score(self):
.exclude(status=RequirementAssessment.Status.NOT_APPLICABLE)
.exclude(is_scored=False)
)
score = requirement_assessments_scored.aggregate(models.Avg("score"))
if score["score__avg"] is not None:
return round(score["score__avg"], 1)
return -1
ig = (
set(self.selected_implementation_groups)
if self.selected_implementation_groups
else None
)
score = 0
n = 0
for ras in requirement_assessments_scored:
if not (ig) or (ig & set(ras.requirement.implementation_groups)):
score += ras.score
n += 1
if n > 0:
return round(score / n, 1)
else:
return -1

def get_selected_implementation_groups(self):
framework = self.framework
if not framework.implementation_groups_definition:
return []
return [
group.get("name")
for group in framework.implementation_groups_definition
if group.get("ref_id") in self.selected_implementation_groups
]

def get_requirements_status_count(self):
requirements_status_count = []
Expand Down Expand Up @@ -1332,7 +1353,13 @@ def get_measures_status_count(self):
return measures_status_count

def donut_render(self) -> dict:
compliance_assessments_status = {"values": [], "labels": []}
def union_queries(base_query, groups, field_name):
queries = [
base_query.filter(**{f"{field_name}__icontains": group}).distinct()
for group in groups
]
return queries[0].union(*queries[1:]) if queries else base_query.none()

color_map = {
"in_progress": "#3b82f6",
"non_compliant": "#f87171",
Expand All @@ -1341,24 +1368,33 @@ def donut_render(self) -> dict:
"not_applicable": "#000000",
"compliant": "#86efac",
}
for st in RequirementAssessment.Status:
count = (
RequirementAssessment.objects.filter(status=st)
.filter(compliance_assessment=self)
.filter(requirement__assessable=True)
.count()
)
total = RequirementAssessment.objects.filter(
compliance_assessment=self
).count()
v = {
"name": st,
"localName": camel_case(st.value),

compliance_assessments_status = {"values": [], "labels": []}
for status in RequirementAssessment.Status:
base_query = RequirementAssessment.objects.filter(
status=status, compliance_assessment=self, requirement__assessable=True
).distinct()

if self.selected_implementation_groups:
union_query = union_queries(
base_query,
self.selected_implementation_groups,
"requirement__implementation_groups",
)
else:
union_query = base_query

count = union_query.count()
value_entry = {
"name": status,
"localName": camel_case(status.value),
"value": count,
"itemStyle": {"color": color_map[st]},
"itemStyle": {"color": color_map[status]},
}
compliance_assessments_status["values"].append(v)
compliance_assessments_status["labels"].append(st.label)

compliance_assessments_status["values"].append(value_entry)
compliance_assessments_status["labels"].append(status.label)

return compliance_assessments_status

def quality_check(self) -> dict:
Expand Down
7 changes: 6 additions & 1 deletion backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,12 @@ class Meta:


class ComplianceAssessmentReadSerializer(AssessmentReadSerializer):
framework = FieldsRelatedField(["id", "min_score", "max_score"])
framework = FieldsRelatedField(
["id", "min_score", "max_score", "implementation_groups_definition"]
)
selected_implementation_groups = serializers.ReadOnlyField(
source="get_selected_implementation_groups"
)

class Meta:
model = ComplianceAssessment
Expand Down
45 changes: 37 additions & 8 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,17 @@ class ComplianceAssessmentViewSet(BaseModelViewSet):
def status(self, request):
return Response(dict(ComplianceAssessment.Status.choices))

@action(detail=True, name="Get implementation group choices")
def selected_implementation_groups(self, request, pk):
compliance_assessment = self.get_object()
_framework = compliance_assessment.framework
implementation_groups_definiition = _framework.implementation_groups_definition
implementation_group_choices = {
group["ref_id"]: group["name"]
for group in implementation_groups_definiition
}
return Response(implementation_group_choices)

def perform_create(self, serializer):
"""
Create RequirementAssessment objects for the newly created ComplianceAssessment
Expand Down Expand Up @@ -1240,13 +1251,15 @@ def quality_check_detail(self, request, pk):
@action(detail=True, methods=["get"])
def tree(self, request, pk):
_framework = self.get_object().framework
tree = get_sorted_requirement_nodes(
RequirementNode.objects.filter(framework=_framework).all(),
RequirementAssessment.objects.filter(
compliance_assessment=self.get_object()
).all(),
)
implementation_groups = self.get_object().selected_implementation_groups
return Response(
get_sorted_requirement_nodes(
RequirementNode.objects.filter(framework=_framework).all(),
RequirementAssessment.objects.filter(
compliance_assessment=self.get_object()
).all(),
)
filter_graph_by_implementation_groups(tree, implementation_groups)
)

@action(detail=True)
Expand Down Expand Up @@ -1407,6 +1420,14 @@ def generate_html(
compliance_assessment=compliance_assessment,
).all()

implementation_groups = compliance_assessment.selected_implementation_groups
graph = get_sorted_requirement_nodes(list(requirement_nodes), list(assessments))
graph = filter_graph_by_implementation_groups(graph, implementation_groups)
flattened_graph = flatten_dict(graph)

requirement_nodes = requirement_nodes.filter(urn__in=flattened_graph.values())
assessments = assessments.filter(requirement__urn__in=flattened_graph.values())

node_per_urn = {r.urn: r for r in requirement_nodes}
ancestors = {}
for a in assessments:
Expand Down Expand Up @@ -1487,8 +1508,16 @@ def bar_graph(node: RequirementNode):
content += "<div>"
content += "<p class='font-semibold'>Reviewers</p>"
content += "<ul>"
for reviewer in compliance_assessment.reviewers.all():
content += f"<li>{reviewer}</li>"
for group in compliance_assessment.reviewers.all():
content += f"<li>{group}</li>"
content += "</ul>"
content += "</div>"

content += "<div>"
content += "<p class='font-semibold'>Selected implementation groups</p>"
content += "<ul>"
for group in compliance_assessment.get_selected_implementation_groups():
content += f"<li>{group}</li>"
content += "</ul>"
content += "</div>"

Expand Down
4 changes: 3 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -510,5 +510,7 @@
"setTemporaryPassword2": "Please use a strong one and make sure to inform the user to change it as soon as possible",
"youCanSetNewPassword": "You can set a new password here",
"userWillBeDisconnected": "The user will be disconnected and will need to log in again",
"scoresDefinition": "Scores definition"
"scoresDefinition": "Scores definition",
"selectedImplementationGroups": "Selected implementation groups",
"implementationGroupsDefinition": "Implementation groups definition"
}
14 changes: 8 additions & 6 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"complianceAssessment": "Audit",
"evidences": "Preuves",
"evidence": "Preuve",
"frameworks": "Cadres de référence",
"frameworks": "Référentiels",
"domains": "Domaines",
"projects": "Projets",
"users": "Utilisateurs",
Expand Down Expand Up @@ -97,7 +97,7 @@
"folder": "Domaine",
"riskAssessment": "Évaluation de risque",
"threat": "Menace",
"framework": "Cadre de référence",
"framework": "Référentiel",
"file": "Fichier",
"language": "Langue",
"builtin": "Intégré",
Expand Down Expand Up @@ -150,7 +150,7 @@
"attachment": "Pièce jointe",
"observation": "Observation",
"importMatrices": "Importer des matrices",
"importFrameworks": "Importer des cadres de référence",
"importFrameworks": "Importer des référentiels",
"summary": "Synthèse",
"composer": "Compositeur",
"statistics": "Statistiques",
Expand All @@ -166,7 +166,7 @@
"active": "Active",
"inactive": "Inactive",
"watchlist": "Liste de surveillance",
"watchlistDescription": "Objets expirés ou ayant une ETA proche",
"watchlistDescription": "Objets expirés ou expirant bientôt",
"measuresToReview": "Mesures à revoir",
"exceptionsToReview": "Exceptions à revoir",
"expired": "Expiré",
Expand Down Expand Up @@ -481,7 +481,7 @@
"threatsCovered": "Menaces couvertes",
"noFileDetected": "Erreur: aucun fichier détecté",
"usedRiskMatrices": "Matrices de risque utilisées",
"usedFrameworks": "Cadres de référence utilisés",
"usedFrameworks": "Référentiels utilisés",
"riskAssessmentsStatus": "Statut des évaluations de risque",
"complianceAssessmentsStatus": "Statut des audits",
"noDescription": "Pas de description",
Expand Down Expand Up @@ -510,5 +510,7 @@
"setTemporaryPassword2": "Veuillez en utiliser un solide et assurez-vous d'informer l'utilisateur de le modifier dès que possible.",
"youCanSetNewPassword": "Vous pouvez définir un nouveau mot de passe ici",
"userWillBeDisconnected": "L'utilisateur sera déconnecté et devra se reconnecter",
"scoresDefinition": "Définition des scores"
"scoresDefinition": "Définition des scores",
"selectedImplementationGroups": "Groupes d'implémentation sélectionnés",
"implementationGroupsDefinition": "Définition des groupes d'implémentation"
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
const { value, errors, constraints } = formFieldProxy(form, field);
export let options: { label: string; value: string; suggested?: boolean }[];
export let options: { label: string; value: string; suggested?: boolean }[] = [];
import MultiSelect from 'svelte-multiselect';
import { createEventDispatcher } from 'svelte';
Expand Down
Loading

0 comments on commit cf7f302

Please sign in to comment.