wbcompliance 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wbcompliance/__init__.py +1 -0
- wbcompliance/admin/__init__.py +16 -0
- wbcompliance/admin/compliance_form.py +56 -0
- wbcompliance/admin/compliance_task.py +135 -0
- wbcompliance/admin/compliance_type.py +8 -0
- wbcompliance/admin/risk_management/__init__.py +3 -0
- wbcompliance/admin/risk_management/checks.py +7 -0
- wbcompliance/admin/risk_management/incidents.py +50 -0
- wbcompliance/admin/risk_management/rules.py +63 -0
- wbcompliance/admin/utils.py +46 -0
- wbcompliance/apps.py +14 -0
- wbcompliance/factories/__init__.py +21 -0
- wbcompliance/factories/compliance.py +246 -0
- wbcompliance/factories/risk_management/__init__.py +12 -0
- wbcompliance/factories/risk_management/backends.py +42 -0
- wbcompliance/factories/risk_management/checks.py +12 -0
- wbcompliance/factories/risk_management/incidents.py +84 -0
- wbcompliance/factories/risk_management/rules.py +100 -0
- wbcompliance/filters/__init__.py +2 -0
- wbcompliance/filters/compliances.py +189 -0
- wbcompliance/filters/risk_management/__init__.py +3 -0
- wbcompliance/filters/risk_management/checks.py +22 -0
- wbcompliance/filters/risk_management/incidents.py +113 -0
- wbcompliance/filters/risk_management/rules.py +110 -0
- wbcompliance/filters/risk_management/tables.py +112 -0
- wbcompliance/filters/risk_management/utils.py +3 -0
- wbcompliance/management/__init__.py +10 -0
- wbcompliance/migrations/0001_initial_squashed_squashed_0010_alter_checkedobjectincidentrelationship_resolved_by_and_more.py +1744 -0
- wbcompliance/migrations/0011_alter_riskrule_parameters.py +21 -0
- wbcompliance/migrations/0012_alter_compliancetype_options.py +20 -0
- wbcompliance/migrations/0013_alter_riskrule_unique_together.py +16 -0
- wbcompliance/migrations/0014_alter_reviewcompliancetask_year.py +27 -0
- wbcompliance/migrations/0015_auto_20240103_0957.py +43 -0
- wbcompliance/migrations/0016_checkedobjectincidentrelationship_report_details_and_more.py +37 -0
- wbcompliance/migrations/0017_alter_rulebackend_incident_report_template.py +20 -0
- wbcompliance/migrations/0018_alter_rulecheckedobjectrelationship_unique_together.py +39 -0
- wbcompliance/migrations/0019_rulegroup_riskrule_activation_date_and_more.py +60 -0
- wbcompliance/migrations/__init__.py +0 -0
- wbcompliance/models/__init__.py +20 -0
- wbcompliance/models/compliance_form.py +626 -0
- wbcompliance/models/compliance_task.py +800 -0
- wbcompliance/models/compliance_type.py +133 -0
- wbcompliance/models/enums.py +13 -0
- wbcompliance/models/risk_management/__init__.py +4 -0
- wbcompliance/models/risk_management/backend.py +139 -0
- wbcompliance/models/risk_management/checks.py +194 -0
- wbcompliance/models/risk_management/dispatch.py +41 -0
- wbcompliance/models/risk_management/incidents.py +619 -0
- wbcompliance/models/risk_management/mixins.py +115 -0
- wbcompliance/models/risk_management/rules.py +654 -0
- wbcompliance/permissions.py +32 -0
- wbcompliance/serializers/__init__.py +30 -0
- wbcompliance/serializers/compliance_form.py +320 -0
- wbcompliance/serializers/compliance_task.py +463 -0
- wbcompliance/serializers/compliance_type.py +26 -0
- wbcompliance/serializers/risk_management/__init__.py +19 -0
- wbcompliance/serializers/risk_management/checks.py +53 -0
- wbcompliance/serializers/risk_management/incidents.py +227 -0
- wbcompliance/serializers/risk_management/rules.py +158 -0
- wbcompliance/tasks.py +112 -0
- wbcompliance/tests/__init__.py +0 -0
- wbcompliance/tests/conftest.py +63 -0
- wbcompliance/tests/disable_signals.py +82 -0
- wbcompliance/tests/mixins.py +17 -0
- wbcompliance/tests/risk_management/__init__.py +0 -0
- wbcompliance/tests/risk_management/models/__init__.py +0 -0
- wbcompliance/tests/risk_management/models/test_backends.py +0 -0
- wbcompliance/tests/risk_management/models/test_checks.py +55 -0
- wbcompliance/tests/risk_management/models/test_incidents.py +327 -0
- wbcompliance/tests/risk_management/models/test_rules.py +255 -0
- wbcompliance/tests/signals.py +89 -0
- wbcompliance/tests/test_filters.py +23 -0
- wbcompliance/tests/test_models.py +57 -0
- wbcompliance/tests/test_serializers.py +48 -0
- wbcompliance/tests/test_views.py +377 -0
- wbcompliance/tests/tests.py +21 -0
- wbcompliance/urls.py +238 -0
- wbcompliance/viewsets/__init__.py +40 -0
- wbcompliance/viewsets/buttons/__init__.py +9 -0
- wbcompliance/viewsets/buttons/compliance_form.py +78 -0
- wbcompliance/viewsets/buttons/compliance_task.py +149 -0
- wbcompliance/viewsets/buttons/risk_managment/__init__.py +3 -0
- wbcompliance/viewsets/buttons/risk_managment/checks.py +11 -0
- wbcompliance/viewsets/buttons/risk_managment/incidents.py +51 -0
- wbcompliance/viewsets/buttons/risk_managment/rules.py +35 -0
- wbcompliance/viewsets/compliance_form.py +425 -0
- wbcompliance/viewsets/compliance_task.py +513 -0
- wbcompliance/viewsets/compliance_type.py +38 -0
- wbcompliance/viewsets/display/__init__.py +22 -0
- wbcompliance/viewsets/display/compliance_form.py +317 -0
- wbcompliance/viewsets/display/compliance_task.py +453 -0
- wbcompliance/viewsets/display/compliance_type.py +22 -0
- wbcompliance/viewsets/display/risk_managment/__init__.py +11 -0
- wbcompliance/viewsets/display/risk_managment/checks.py +46 -0
- wbcompliance/viewsets/display/risk_managment/incidents.py +155 -0
- wbcompliance/viewsets/display/risk_managment/rules.py +146 -0
- wbcompliance/viewsets/display/risk_managment/tables.py +51 -0
- wbcompliance/viewsets/endpoints/__init__.py +27 -0
- wbcompliance/viewsets/endpoints/compliance_form.py +207 -0
- wbcompliance/viewsets/endpoints/compliance_task.py +193 -0
- wbcompliance/viewsets/endpoints/compliance_type.py +9 -0
- wbcompliance/viewsets/endpoints/risk_managment/__init__.py +12 -0
- wbcompliance/viewsets/endpoints/risk_managment/checks.py +16 -0
- wbcompliance/viewsets/endpoints/risk_managment/incidents.py +36 -0
- wbcompliance/viewsets/endpoints/risk_managment/rules.py +32 -0
- wbcompliance/viewsets/endpoints/risk_managment/tables.py +14 -0
- wbcompliance/viewsets/menu/__init__.py +17 -0
- wbcompliance/viewsets/menu/compliance_form.py +49 -0
- wbcompliance/viewsets/menu/compliance_task.py +130 -0
- wbcompliance/viewsets/menu/compliance_type.py +17 -0
- wbcompliance/viewsets/menu/risk_management.py +56 -0
- wbcompliance/viewsets/risk_management/__init__.py +21 -0
- wbcompliance/viewsets/risk_management/checks.py +49 -0
- wbcompliance/viewsets/risk_management/incidents.py +204 -0
- wbcompliance/viewsets/risk_management/mixins.py +52 -0
- wbcompliance/viewsets/risk_management/rules.py +179 -0
- wbcompliance/viewsets/risk_management/tables.py +96 -0
- wbcompliance/viewsets/titles/__init__.py +17 -0
- wbcompliance/viewsets/titles/compliance_form.py +101 -0
- wbcompliance/viewsets/titles/compliance_task.py +60 -0
- wbcompliance/viewsets/titles/compliance_type.py +13 -0
- wbcompliance/viewsets/titles/risk_managment/__init__.py +1 -0
- wbcompliance/viewsets/titles/risk_managment/checks.py +0 -0
- wbcompliance/viewsets/titles/risk_managment/incidents.py +0 -0
- wbcompliance/viewsets/titles/risk_managment/rules.py +0 -0
- wbcompliance/viewsets/titles/risk_managment/tables.py +7 -0
- wbcompliance-2.2.1.dist-info/METADATA +7 -0
- wbcompliance-2.2.1.dist-info/RECORD +129 -0
- wbcompliance-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from celery import shared_task
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.contrib.auth.models import Group
|
|
7
|
+
from django.contrib.contenttypes.models import ContentType
|
|
8
|
+
from django.core.files.base import ContentFile
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.db.models import Q
|
|
11
|
+
from django.utils.translation import gettext_lazy as _
|
|
12
|
+
from slugify import slugify
|
|
13
|
+
from wbcore.contrib.documents.models import Document, DocumentType
|
|
14
|
+
from wbcore.contrib.documents.models.mixins import DocumentMixin
|
|
15
|
+
from wbcore.models import WBModel
|
|
16
|
+
|
|
17
|
+
User = get_user_model()
|
|
18
|
+
SelfComplianceType = TypeVar("SelfComplianceType", bound="ComplianceType")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def can_active_request(instance, user: "User") -> bool:
|
|
22
|
+
if instance.changer:
|
|
23
|
+
current_profile = instance.changer
|
|
24
|
+
else:
|
|
25
|
+
current_profile = instance.creator
|
|
26
|
+
|
|
27
|
+
return user.is_superuser or (
|
|
28
|
+
user.profile != current_profile and user.has_perm("wbcompliance.administrate_compliance")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ComplianceType(WBModel):
|
|
33
|
+
class Meta:
|
|
34
|
+
verbose_name = "Compliance Type"
|
|
35
|
+
verbose_name_plural = "Compliance Types"
|
|
36
|
+
permissions = [("administrate_compliance", "Can Administrate Compliance")]
|
|
37
|
+
|
|
38
|
+
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
|
|
39
|
+
description = models.TextField(default="", blank=True, verbose_name=_("Description"))
|
|
40
|
+
in_charge = models.ManyToManyField(
|
|
41
|
+
Group,
|
|
42
|
+
related_name="compliance_types",
|
|
43
|
+
blank=True,
|
|
44
|
+
verbose_name=_("Group of administrators"),
|
|
45
|
+
help_text=_("groups responsible for managing this type of compliance"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __str__(self) -> str:
|
|
49
|
+
return "{}".format(self.name)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_endpoint_basename(cls) -> str:
|
|
53
|
+
return "wbcompliance:compliancetype"
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_representation_endpoint(cls) -> str:
|
|
57
|
+
return "wbcompliance:compliancetyperepresentation-list"
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_representation_value_key(cls) -> str:
|
|
61
|
+
return "id"
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_representation_label_key(cls) -> str:
|
|
65
|
+
return "{{name}}"
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_administrators(cls, type: SelfComplianceType | None = None) -> models.QuerySet["User"]:
|
|
69
|
+
administrators = (
|
|
70
|
+
get_user_model()
|
|
71
|
+
.objects.filter(
|
|
72
|
+
Q(groups__permissions__codename="administrate_compliance")
|
|
73
|
+
| Q(user_permissions__codename="administrate_compliance")
|
|
74
|
+
)
|
|
75
|
+
.distinct()
|
|
76
|
+
)
|
|
77
|
+
if type:
|
|
78
|
+
administrators = administrators.filter(groups__in=type.in_charge.all())
|
|
79
|
+
return administrators
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def is_administrator(cls, user) -> bool:
|
|
83
|
+
return user.has_perm("wbcompliance.administrate_compliance")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@shared_task
|
|
87
|
+
def update_or_create_compliance_document(user_id: int, content_type_id: int, object_id: int, send_email: bool = True):
|
|
88
|
+
user = get_user_model().objects.get(id=user_id)
|
|
89
|
+
content_type = ContentType.objects.get(id=content_type_id)
|
|
90
|
+
content_object = content_type.model_class().objects.get(id=object_id)
|
|
91
|
+
|
|
92
|
+
filename = "{}.pdf".format(slugify(str(content_object)))
|
|
93
|
+
logging.getLogger("weasyprint").setLevel(logging.CRITICAL)
|
|
94
|
+
pdf_content = content_object.generate_pdf()
|
|
95
|
+
logging.getLogger("weasyprint").setLevel(logging.INFO)
|
|
96
|
+
content_file = ContentFile(pdf_content, name=filename)
|
|
97
|
+
|
|
98
|
+
document_type, _ = DocumentType.objects.get_or_create(name="Compliance")
|
|
99
|
+
document, _ = Document.objects.update_or_create(
|
|
100
|
+
document_type=document_type,
|
|
101
|
+
system_created=True,
|
|
102
|
+
system_key="{}-{}-{}".format(content_type.model, content_object.id, filename),
|
|
103
|
+
defaults={
|
|
104
|
+
"file": content_file,
|
|
105
|
+
"name": filename,
|
|
106
|
+
"permission_type": Document.PermissionType.PRIVATE,
|
|
107
|
+
"creator": user,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
document.link(content_object)
|
|
111
|
+
if send_email:
|
|
112
|
+
document.send_email(
|
|
113
|
+
to_emails=user.email,
|
|
114
|
+
as_link=True,
|
|
115
|
+
subject="Compliance PDF - {}".format(str(content_object)),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ComplianceDocumentMixin(DocumentMixin):
|
|
120
|
+
def get_permissions_for_user_and_document(self, user, view, created):
|
|
121
|
+
"""
|
|
122
|
+
allows user to access the document associated with this object
|
|
123
|
+
:return: The tuple list permission for the corresponding user
|
|
124
|
+
|
|
125
|
+
:note: Core implements a signal that automatically calls this function when instantiating the view
|
|
126
|
+
"""
|
|
127
|
+
if ComplianceType.is_administrator(user):
|
|
128
|
+
return [
|
|
129
|
+
("document.view_document", False),
|
|
130
|
+
("document.change_document", False),
|
|
131
|
+
("document.delete_document", False),
|
|
132
|
+
]
|
|
133
|
+
return []
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from wbcore.contrib.color.enums import WBColor
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class IncidentSeverity(models.TextChoices):
|
|
6
|
+
LOW = "LOW", "Low"
|
|
7
|
+
MEDIUM = "MEDIUM", "Medium"
|
|
8
|
+
HIGH = "HIGH", "High"
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def get_color_map(cls) -> list:
|
|
12
|
+
colors = [WBColor.GREEN_LIGHT.value, WBColor.YELLOW_LIGHT.value, WBColor.RED_LIGHT.value]
|
|
13
|
+
return [choice for choice in zip(cls, colors)]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, Optional
|
|
4
|
+
|
|
5
|
+
from django.contrib.contenttypes.models import ContentType
|
|
6
|
+
from django.db import models
|
|
7
|
+
from wbcore import serializers as wb_serializers
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from wbcompliance.models.risk_management.incidents import RiskIncidentType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class IncidentResult:
|
|
15
|
+
breached_object: models.Model | None
|
|
16
|
+
breached_object_repr: str
|
|
17
|
+
breached_value: str | None
|
|
18
|
+
report_details: dict[str, Any]
|
|
19
|
+
severity: "RiskIncidentType"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AbstractRuleBackend:
|
|
23
|
+
OBJECT_FIELD_NAME = "evaluated_object"
|
|
24
|
+
evaluated_object: Any
|
|
25
|
+
evaluation_date: date
|
|
26
|
+
parameters: dict[str, Any]
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
evaluation_date: date,
|
|
31
|
+
evaluated_object: Any,
|
|
32
|
+
json_parameters: Optional[Dict[str, Any]] = None,
|
|
33
|
+
thresholds: Optional[
|
|
34
|
+
Iterable
|
|
35
|
+
] = None, # TODO refactor threshold to be a dataclass (DTO) to remove indirect dependency to the module
|
|
36
|
+
**kwargs
|
|
37
|
+
):
|
|
38
|
+
if not json_parameters:
|
|
39
|
+
json_parameters = {}
|
|
40
|
+
self.evaluation_date = evaluation_date
|
|
41
|
+
setattr(self, self.OBJECT_FIELD_NAME, evaluated_object)
|
|
42
|
+
self.thresholds = thresholds if thresholds else []
|
|
43
|
+
self.parameters = self._deserialize_parameters(json_parameters)
|
|
44
|
+
for k, v in self.parameters.items():
|
|
45
|
+
setattr(self, k, v)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_all_active_relationships(cls) -> Iterable:
|
|
49
|
+
raise NotImplementedError()
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
53
|
+
"""
|
|
54
|
+
Return the serializer to deserialize the parameters given as json
|
|
55
|
+
Returns:
|
|
56
|
+
A serializer class
|
|
57
|
+
Raises:
|
|
58
|
+
NotImplementedError
|
|
59
|
+
"""
|
|
60
|
+
raise NotImplementedError()
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def _deserialize_parameters(cls, json_parameters) -> Dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
The parameters are stored in the rule as a json field.
|
|
66
|
+
|
|
67
|
+
E.g. As such, model are stored with their id.
|
|
68
|
+
|
|
69
|
+
This allows for the implementing backend to override the default deserialization behavior
|
|
70
|
+
Args:
|
|
71
|
+
json_parameters: The serialized dictionary
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The deserializer dictionary. Default to returning the json parameters untouched
|
|
75
|
+
"""
|
|
76
|
+
# If parameters are not empty, we expect the backend to define `get_serializer_class. This will crash otherwise
|
|
77
|
+
serializer = cls.get_serializer_class()(data=json_parameters)
|
|
78
|
+
if serializer.is_valid():
|
|
79
|
+
return serializer.validated_data
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(serializer.errors)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def get_allowed_content_type(cls) -> "ContentType":
|
|
85
|
+
"""
|
|
86
|
+
This function is called upon backend registration in order to determine the allowed checked_object content type
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The allowed content type
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
NotImplementedError
|
|
93
|
+
"""
|
|
94
|
+
raise NotImplementedError()
|
|
95
|
+
|
|
96
|
+
def _build_dto_args(self):
|
|
97
|
+
"""
|
|
98
|
+
Can be overrided to define what the default DataTransferObject is going to be
|
|
99
|
+
It defaults to calling _build_dto on the evaluated object
|
|
100
|
+
Returns:
|
|
101
|
+
An object holding the DTO representation
|
|
102
|
+
"""
|
|
103
|
+
if hasattr(self.evaluated_object, "_build_dto"):
|
|
104
|
+
return self.evaluated_object._build_dto(self.evaluation_date)
|
|
105
|
+
return tuple()
|
|
106
|
+
|
|
107
|
+
def _process_dto(self, *dtos) -> Generator[IncidentResult, None, None]:
|
|
108
|
+
"""
|
|
109
|
+
Check the rule against the instantiated backend given the DTO
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
yield the breach object, its string representation, the incident report and the severity as IncidentResult
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
NotImplementedError
|
|
116
|
+
"""
|
|
117
|
+
raise NotImplementedError()
|
|
118
|
+
|
|
119
|
+
def check_rule(self, *dto_args, **kwargs) -> Generator[IncidentResult, None, None]:
|
|
120
|
+
"""
|
|
121
|
+
Build and Check the rule against the instantiated backend given its attributes
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
yield the breach object, its string representation, the incident report and the severity as IncidentResult
|
|
125
|
+
"""
|
|
126
|
+
if not dto_args:
|
|
127
|
+
# We build the data transfer object if it is not provided.
|
|
128
|
+
dto_args = self._build_dto_args()
|
|
129
|
+
if any(dto_args):
|
|
130
|
+
yield from self._process_dto(*dto_args, **kwargs)
|
|
131
|
+
|
|
132
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Determine if the instantiated backend can be evaluated given its attributes
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if the backend rule can be evaluated
|
|
138
|
+
"""
|
|
139
|
+
return True
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from types import DynamicClassAttribute
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from celery import shared_task
|
|
6
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
7
|
+
from django.contrib.contenttypes.models import ContentType
|
|
8
|
+
from django.db import models
|
|
9
|
+
from django.template import Context, Template
|
|
10
|
+
from django.utils.translation import gettext_lazy as _
|
|
11
|
+
from wbcore.contrib.color.enums import WBColor
|
|
12
|
+
from wbcore.contrib.icons import WBIcon
|
|
13
|
+
from wbcore.models import WBModel
|
|
14
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
15
|
+
|
|
16
|
+
from .incidents import CheckedObjectIncidentRelationship
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RiskCheck(ComplexToStringMixin, WBModel):
|
|
20
|
+
class CheckStatus(models.TextChoices):
|
|
21
|
+
PENDING = "PENDING", "Pending"
|
|
22
|
+
RUNNING = "RUNNING", "Running"
|
|
23
|
+
FAILED = "FAILED", "Failed"
|
|
24
|
+
SUCCESS = "SUCCESS", "Success"
|
|
25
|
+
WARNING = "WARNING", "Warning"
|
|
26
|
+
|
|
27
|
+
@DynamicClassAttribute
|
|
28
|
+
def icon(self):
|
|
29
|
+
return {
|
|
30
|
+
"PENDING": WBIcon.PENDING.icon,
|
|
31
|
+
"RUNNING": WBIcon.RUNNING.icon,
|
|
32
|
+
"FAILED": WBIcon.REJECT.icon,
|
|
33
|
+
"SUCCESS": WBIcon.CONFIRM.icon,
|
|
34
|
+
"WARNING": WBIcon.WARNING.icon,
|
|
35
|
+
}[self.value]
|
|
36
|
+
|
|
37
|
+
@DynamicClassAttribute
|
|
38
|
+
def color(self):
|
|
39
|
+
return {
|
|
40
|
+
"PENDING": WBColor.YELLOW_LIGHT.value,
|
|
41
|
+
"RUNNING": WBColor.BLUE_LIGHT.value,
|
|
42
|
+
"FAILED": WBColor.RED_LIGHT.value,
|
|
43
|
+
"SUCCESS": WBColor.GREEN_LIGHT.value,
|
|
44
|
+
"WARNING": WBColor.YELLOW_DARK.value,
|
|
45
|
+
}[self.value]
|
|
46
|
+
|
|
47
|
+
rule_checked_object_relationship = models.ForeignKey(
|
|
48
|
+
"wbcompliance.RuleCheckedObjectRelationship",
|
|
49
|
+
on_delete=models.CASCADE,
|
|
50
|
+
verbose_name=_("Rule-Checked Object Relationship"),
|
|
51
|
+
related_name="checks",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
creation_datetime = models.DateTimeField(
|
|
55
|
+
auto_now_add=True,
|
|
56
|
+
verbose_name=_("Creation Date"),
|
|
57
|
+
help_text=_("Time at which the check was created/triggered"),
|
|
58
|
+
)
|
|
59
|
+
evaluation_date = models.DateField(
|
|
60
|
+
verbose_name=_("Evaluation Date"), help_text=_("The date at which the rule was evaluated")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
trigger_content_type = models.ForeignKey(
|
|
64
|
+
ContentType, on_delete=models.CASCADE, related_name="triggered_checks", blank=True, null=True
|
|
65
|
+
)
|
|
66
|
+
trigger_id = models.PositiveIntegerField(blank=True, null=True)
|
|
67
|
+
trigger = GenericForeignKey("trigger_content_type", "trigger_id")
|
|
68
|
+
|
|
69
|
+
status = models.CharField(
|
|
70
|
+
max_length=32, default=CheckStatus.PENDING, choices=CheckStatus.choices, verbose_name=_("Status")
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def is_active(self) -> bool:
|
|
75
|
+
return self.trigger is not None
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def rule(self):
|
|
79
|
+
return self.rule_checked_object_relationship.rule
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def checked_object(self) -> models.Model:
|
|
83
|
+
"""
|
|
84
|
+
Return the object from which the rule needs to be check against
|
|
85
|
+
"""
|
|
86
|
+
return self.rule_checked_object_relationship.checked_object
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def previous_check(self) -> Self | None:
|
|
90
|
+
"""
|
|
91
|
+
Property holding the last valid check
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The last valid check
|
|
95
|
+
"""
|
|
96
|
+
with suppress(RiskCheck.DoesNotExist):
|
|
97
|
+
return (
|
|
98
|
+
RiskCheck.objects.filter(
|
|
99
|
+
evaluation_date__lt=self.evaluation_date,
|
|
100
|
+
rule_checked_object_relationship=self.rule_checked_object_relationship,
|
|
101
|
+
)
|
|
102
|
+
.order_by("-evaluation_date", "-creation_datetime")
|
|
103
|
+
.first()
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def compute_str(self) -> str:
|
|
107
|
+
return _("{} - {}").format(
|
|
108
|
+
self.rule_checked_object_relationship.checked_object,
|
|
109
|
+
self.evaluation_date,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def evaluate(
|
|
113
|
+
self, *explicit_dto, override_incident: bool = False, ignore_informational_threshold: bool = False
|
|
114
|
+
) -> list[models.Model]:
|
|
115
|
+
"""
|
|
116
|
+
Evaluate the check and returns tuple of incidents information
|
|
117
|
+
Args:
|
|
118
|
+
override_incident: True if the existing incident needs to be overriden
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
self.status = self.CheckStatus.RUNNING
|
|
124
|
+
self.save()
|
|
125
|
+
rule_backend = self.rule.rule_backend.backend(
|
|
126
|
+
self.evaluation_date, self.checked_object, self.rule.parameters, self.rule.thresholds.all()
|
|
127
|
+
)
|
|
128
|
+
self.status = self.CheckStatus.SUCCESS
|
|
129
|
+
incidents = []
|
|
130
|
+
report_template = Template(self.rule.rule_backend.incident_report_template)
|
|
131
|
+
for incident_result in rule_backend.check_rule(*explicit_dto):
|
|
132
|
+
if (
|
|
133
|
+
ignore_informational_threshold
|
|
134
|
+
and incident_result.severity.is_ignorable
|
|
135
|
+
and incident_result.severity.is_informational
|
|
136
|
+
):
|
|
137
|
+
self.status = self.CheckStatus.WARNING
|
|
138
|
+
else:
|
|
139
|
+
self.status = self.CheckStatus.FAILED
|
|
140
|
+
# If the check is passive, we aggregated incident per breached object and return it for further processing
|
|
141
|
+
report = report_template.render(Context({"report_details": incident_result.report_details}))
|
|
142
|
+
if not self.is_active:
|
|
143
|
+
incident, created = self.rule.get_or_create_incident(
|
|
144
|
+
self.evaluation_date,
|
|
145
|
+
incident_result.severity,
|
|
146
|
+
incident_result.breached_object,
|
|
147
|
+
incident_result.breached_object_repr,
|
|
148
|
+
)
|
|
149
|
+
incident.update_or_create_relationship(
|
|
150
|
+
self,
|
|
151
|
+
report,
|
|
152
|
+
incident_result.report_details,
|
|
153
|
+
incident_result.breached_value,
|
|
154
|
+
incident_result.severity,
|
|
155
|
+
override_incident=override_incident or created,
|
|
156
|
+
)
|
|
157
|
+
incidents.append(incident)
|
|
158
|
+
else:
|
|
159
|
+
# If the check is active, the only thing that matter is whether the check led to incident or not
|
|
160
|
+
CheckedObjectIncidentRelationship.objects.create(
|
|
161
|
+
rule_check=self,
|
|
162
|
+
report=report,
|
|
163
|
+
report_details=incident_result.report_details,
|
|
164
|
+
breached_value=incident_result.breached_value,
|
|
165
|
+
severity=incident_result.severity,
|
|
166
|
+
)
|
|
167
|
+
self.save()
|
|
168
|
+
return incidents
|
|
169
|
+
|
|
170
|
+
class Meta:
|
|
171
|
+
verbose_name = "Risk Check"
|
|
172
|
+
verbose_name_plural = "Risk Checks"
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def get_endpoint_basename(cls) -> str:
|
|
176
|
+
return "wbcompliance:riskcheck"
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def get_representation_endpoint(cls) -> str:
|
|
180
|
+
return "wbcompliance:riskcheckrepresentation-list"
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def get_representation_value_key(cls) -> str:
|
|
184
|
+
return "id"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@shared_task
|
|
188
|
+
def evaluate_as_task(
|
|
189
|
+
check_id: int, *dto, override_incident: bool = False, ignore_informational_threshold: bool = False
|
|
190
|
+
):
|
|
191
|
+
check = RiskCheck.objects.get(id=check_id)
|
|
192
|
+
check.evaluate(
|
|
193
|
+
*dto, override_incident=override_incident, ignore_informational_threshold=ignore_informational_threshold
|
|
194
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import MultipleObjectsReturned
|
|
4
|
+
from django.db.utils import ProgrammingError
|
|
5
|
+
|
|
6
|
+
from .rules import RuleBackend, RuleGroup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register(backend_name: str | None, incident_report_template: str | None = None, rule_group_key: str | None = None):
|
|
10
|
+
"""
|
|
11
|
+
Decorator to include when a backend need automatic registration
|
|
12
|
+
Args:
|
|
13
|
+
backend_name:
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
if not backend_name:
|
|
19
|
+
raise ValueError("At least one name must be passed to register.")
|
|
20
|
+
|
|
21
|
+
def _decorator(backend_class):
|
|
22
|
+
with suppress(RuntimeError, MultipleObjectsReturned, ProgrammingError):
|
|
23
|
+
defaults = {
|
|
24
|
+
"name": backend_name,
|
|
25
|
+
"allowed_checked_object_content_type": backend_class.get_allowed_content_type(),
|
|
26
|
+
}
|
|
27
|
+
if incident_report_template:
|
|
28
|
+
defaults["incident_report_template"] = incident_report_template
|
|
29
|
+
if rule_group_key:
|
|
30
|
+
defaults["rule_group"] = RuleGroup.objects.get_or_create(
|
|
31
|
+
key=rule_group_key, defaults={"name": rule_group_key.title()}
|
|
32
|
+
)[0]
|
|
33
|
+
|
|
34
|
+
RuleBackend.objects.update_or_create(
|
|
35
|
+
backend_class_path=backend_class.__module__,
|
|
36
|
+
backend_class_name=backend_class.__name__,
|
|
37
|
+
defaults=defaults,
|
|
38
|
+
)
|
|
39
|
+
return backend_class
|
|
40
|
+
|
|
41
|
+
return _decorator
|