Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: security exceptions #1486

Merged
merged 31 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0a79b17
feat(backend): add exception object
Mohamed-Hacene Feb 7, 2025
e006166
fix(backend): typos
Mohamed-Hacene Feb 7, 2025
f121950
feat(backend): add severity display in Exception serializer
Mohamed-Hacene Feb 7, 2025
aadf275
feat(frontend): add Exception crud
Mohamed-Hacene Feb 7, 2025
ebd29a6
feat: add exeption field in related objects
Mohamed-Hacene Feb 7, 2025
f320ded
chore: run format
Mohamed-Hacene Feb 7, 2025
5ff9596
fix: exception creation from list view
Mohamed-Hacene Feb 7, 2025
7884c83
feat: add migration
Mohamed-Hacene Feb 7, 2025
c0f8fda
chore: format migration
Mohamed-Hacene Feb 7, 2025
e012f31
fix: add max_length for models.CharField
Mohamed-Hacene Feb 7, 2025
6c5e0ec
feat(locale): add exception fr translations
Mohamed-Hacene Feb 7, 2025
f93e87d
fix(locale): add missing owners translation
Mohamed-Hacene Feb 7, 2025
57b8a42
fix: remove requirement_assessments field from ExceptionReadSerializer
Mohamed-Hacene Feb 7, 2025
0c5b9d3
feat: update Exception icon
Mohamed-Hacene Feb 7, 2025
f630589
chore: add simple quotes
Mohamed-Hacene Feb 7, 2025
e509970
rename exception->security_exception
eric-intuitem Feb 8, 2025
1e5eecf
fix casing
eric-intuitem Feb 8, 2025
aa78743
add default value
eric-intuitem Feb 8, 2025
283792a
Merge branch 'main' into CA-837-implement-security-exceptions
eric-intuitem Feb 8, 2025
2b541c1
rename exceptionsToReview to acceptancesToReview
eric-intuitem Feb 8, 2025
786182a
Update 0052_securityexception_appliedcontrol_security_exceptions_and_…
eric-intuitem Feb 8, 2025
2672584
fix _/- errors
eric-intuitem Feb 8, 2025
de03a66
add boolean to disable double-dash addition
eric-intuitem Feb 8, 2025
240c275
Update SecurityExceptionForm.svelte
eric-intuitem Feb 8, 2025
6693d33
fix -/_ mismatch
eric-intuitem Feb 8, 2025
da07e11
feat: add approver field
Mohamed-Hacene Feb 10, 2025
bd05a85
Merge branch 'main' into CA-837-implement-security-exceptions
Mohamed-Hacene Feb 11, 2025
9743d05
chore: adapt security exceptions AutocompleteSelect forms
Mohamed-Hacene Feb 11, 2025
db5a857
chore: run format
Mohamed-Hacene Feb 11, 2025
c2ccb4e
feat: use crud.ts reverseForeignKeys without add and delete buttons
Mohamed-Hacene Feb 12, 2025
0550811
feat: add security exceptions filters
Mohamed-Hacene Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Generated by Django 5.1.5 on 2025-02-10 09:32

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", "0051_rename_project_perimeter_alter_perimeter_options_and_more"),
("iam", "0010_user_preferences"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="SecurityException",
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"),
),
(
"ref_id",
models.CharField(
blank=True,
max_length=100,
null=True,
verbose_name="Reference ID",
),
),
(
"severity",
models.SmallIntegerField(
choices=[
(-1, "undefined"),
(0, "low"),
(1, "medium"),
(2, "high"),
(3, "critical"),
],
default=-1,
verbose_name="Severity",
),
),
Comment on lines +57 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding severity validation.

The severity field allows an 'undefined' value which might lead to ambiguity in security exception tracking.

Apply this diff to make severity a required field with a more appropriate default:

-                        default=-1,
+                        default=0,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"severity",
models.SmallIntegerField(
choices=[
(-1, "undefined"),
(0, "low"),
(1, "medium"),
(2, "high"),
(3, "critical"),
],
default=-1,
verbose_name="Severity",
),
),
"severity",
models.SmallIntegerField(
choices=[
(-1, "undefined"),
(0, "low"),
(1, "medium"),
(2, "high"),
(3, "critical"),
],
default=0,
verbose_name="Severity",
),
),

(
"status",
models.CharField(
choices=[
("draft", "draft"),
("in_review", "in review"),
("approved", "approved"),
("resolved", "resolved"),
("expired", "expired"),
("deprecated", "deprecated"),
],
default="draft",
max_length=20,
verbose_name="Status",
),
),
(
"expiration_date",
models.DateField(
help_text="Specify when the security exception will no longer apply",
null=True,
verbose_name="Expiration date",
),
),
(
"approver",
models.ForeignKey(
blank=True,
max_length=200,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="Approver",
),
),
(
"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",
),
),
(
"owners",
models.ManyToManyField(
blank=True,
related_name="security_exceptions",
to=settings.AUTH_USER_MODEL,
verbose_name="Owner",
),
),
],
options={
"ordering": ["name"],
"abstract": False,
},
),
migrations.AddField(
model_name="appliedcontrol",
name="security_exceptions",
field=models.ManyToManyField(
blank=True,
related_name="applied_controls",
to="core.securityexception",
verbose_name="Security exceptions",
),
),
migrations.AddField(
model_name="asset",
name="security_exceptions",
field=models.ManyToManyField(
blank=True,
related_name="assets",
to="core.securityexception",
verbose_name="Security exceptions",
),
),
migrations.AddField(
model_name="requirementassessment",
name="security_exceptions",
field=models.ManyToManyField(
blank=True,
related_name="requirement_assessments",
to="core.securityexception",
verbose_name="Security exceptions",
),
),
migrations.AddField(
model_name="riskscenario",
name="security_exceptions",
field=models.ManyToManyField(
blank=True,
related_name="risk_scenarios",
to="core.securityexception",
verbose_name="Security exceptions",
),
),
migrations.AddField(
model_name="vulnerability",
name="security_exceptions",
field=models.ManyToManyField(
blank=True,
related_name="vulnerabilities",
to="core.securityexception",
verbose_name="Security exceptions",
),
),
]
95 changes: 93 additions & 2 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from pathlib import Path
from typing import Self, Type, Union

from rest_framework.renderers import status
import yaml
from django.apps import apps
from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -1394,6 +1393,68 @@ def __str__(self):
return self.folder.name + "/" + self.name


class SecurityException(NameDescriptionMixin, FolderMixin, PublishInRootFolderMixin):
class Severity(models.IntegerChoices):
UNDEFINED = -1, "undefined"
LOW = 0, "low"
MEDIUM = 1, "medium"
HIGH = 2, "high"
CRITICAL = 3, "critical"

class Status(models.TextChoices):
DRAFT = "draft", "draft"
IN_REVIEW = "in_review", "in review"
APPROVED = "approved", "approved"
RESOLVED = "resolved", "resolved"
EXPIRED = "expired", "expired"
DEPRECATED = "deprecated", "deprecated"

ref_id = models.CharField(
max_length=100, null=True, blank=True, verbose_name=_("Reference ID")
)
severity = models.SmallIntegerField(
verbose_name="Severity", choices=Severity.choices, default=Severity.UNDEFINED
)
status = models.CharField(
verbose_name="Status",
choices=Status.choices,
null=False,
default=Status.DRAFT,
max_length=20,
)
expiration_date = models.DateField(
help_text="Specify when the security exception will no longer apply",
null=True,
verbose_name="Expiration date",
)
owners = models.ManyToManyField(
User,
blank=True,
verbose_name="Owner",
related_name="security_exceptions",
)
approver = models.ForeignKey(
User,
max_length=200,
verbose_name=_("Approver"),
on_delete=models.SET_NULL,
null=True,
blank=True,
)

fields_to_check = ["name"]

def __str__(self):
return self.name

def clean(self):
super().clean()
if self.expiration_date and self.expiration_date < now().date():
raise ValidationError(
{"expiration_date": "Expiration date must be in the future"}
)


class Asset(
NameDescriptionMixin, FolderMixin, PublishInRootFolderMixin, FilteringLabelMixin
):
Expand Down Expand Up @@ -1517,6 +1578,13 @@ class Type(models.TextChoices):
verbose_name=_("Owner"),
related_name="assets",
)
security_exceptions = models.ManyToManyField(
SecurityException,
blank=True,
verbose_name="Security exceptions",
related_name="assets",
)

fields_to_check = ["name"]

class Meta:
Expand Down Expand Up @@ -1855,7 +1923,6 @@ class Status(models.TextChoices):
help_text=_("Cost of the measure (using globally-chosen currency)"),
verbose_name=_("Cost"),
)

progress_field = models.IntegerField(
default=0,
verbose_name=_("Progress Field"),
Expand All @@ -1864,6 +1931,12 @@ class Status(models.TextChoices):
MaxValueValidator(100, message="Progress cannot be more than 100"),
],
)
security_exceptions = models.ManyToManyField(
SecurityException,
blank=True,
verbose_name="Security exceptions",
related_name="applied_controls",
)

fields_to_check = ["name"]

Expand Down Expand Up @@ -2009,6 +2082,12 @@ class Status(models.TextChoices):
verbose_name=_("Applied controls"),
related_name="vulnerabilities",
)
security_exceptions = models.ManyToManyField(
SecurityException,
blank=True,
verbose_name="Security exceptions",
related_name="vulnerabilities",
)

fields_to_check = ["name"]

Expand Down Expand Up @@ -2593,6 +2672,12 @@ class RiskScenario(NameDescriptionMixin):
justification = models.CharField(
max_length=500, blank=True, null=True, verbose_name=_("Justification")
)
security_exceptions = models.ManyToManyField(
SecurityException,
blank=True,
verbose_name="Security exceptions",
related_name="risk_scenarios",
)

fields_to_check = ["name"]

Expand Down Expand Up @@ -3369,6 +3454,12 @@ class Result(models.TextChoices):
null=True,
verbose_name=_("Answer"),
)
security_exceptions = models.ManyToManyField(
SecurityException,
blank=True,
verbose_name="Security exceptions",
related_name="requirement_assessments",
)

def __str__(self) -> str:
return self.requirement.display_short
Expand Down
26 changes: 26 additions & 0 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class VulnerabilityReadSerializer(BaseModelSerializer):
folder = FieldsRelatedField()
applied_controls = FieldsRelatedField(many=True)
filtering_labels = FieldsRelatedField(["folder"], many=True)
security_exceptions = FieldsRelatedField(many=True)

class Meta:
model = Vulnerability
Expand Down Expand Up @@ -329,6 +330,7 @@ class AssetReadSerializer(AssetWriteSerializer):
)
filtering_labels = FieldsRelatedField(["folder"], many=True)
type = serializers.CharField(source="get_type_display")
security_exceptions = FieldsRelatedField(many=True)


class AssetImportExportSerializer(BaseModelSerializer):
Expand Down Expand Up @@ -481,6 +483,7 @@ class RiskScenarioReadSerializer(RiskScenarioWriteSerializer):
existing_applied_controls = FieldsRelatedField(many=True)

owner = FieldsRelatedField(many=True)
security_exceptions = FieldsRelatedField(many=True)


class RiskScenarioImportExportSerializer(BaseModelSerializer):
Expand Down Expand Up @@ -541,6 +544,7 @@ class AppliedControlReadSerializer(AppliedControlWriteSerializer):

ranking_score = serializers.IntegerField(source="get_ranking_score")
owner = FieldsRelatedField(many=True)
security_exceptions = FieldsRelatedField(many=True)
# These properties shouldn't be displayed in the frontend detail view as they are simple derivations from fields already displayed in the detail view.
# has_evidences = serializers.BooleanField()
# eta_missed = serializers.BooleanField()
Expand Down Expand Up @@ -962,6 +966,7 @@ class Meta:
folder = FieldsRelatedField()
assessable = serializers.BooleanField(source="requirement.assessable")
requirement = FilteredNodeSerializer()
security_exceptions = FieldsRelatedField(many=True)

class Meta:
model = RequirementAssessment
Expand Down Expand Up @@ -1082,3 +1087,24 @@ class Meta:

class QualificationWriteSerializer(QualificationReadSerializer):
pass


class SecurityExceptionWriteSerializer(BaseModelSerializer):
requirement_assessments = serializers.PrimaryKeyRelatedField(
many=True, queryset=RequirementAssessment.objects.all(), required=False
)

class Meta:
model = SecurityException
fields = "__all__"


class SecurityExceptionReadSerializer(BaseModelSerializer):
folder = FieldsRelatedField()
owners = FieldsRelatedField(many=True)
approver = FieldsRelatedField()
severity = serializers.CharField(source="get_severity_display")

class Meta:
model = SecurityException
fields = "__all__"
Loading
Loading