Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions dojo/db_migrations/0269_findingexclusion_engagements_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 5.1.14 on 2025-12-08 22:40

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dojo', '0267_risk_acceptance_long_term_acceptance'),
]

operations = [
migrations.AddField(
model_name='findingexclusion',
name='engagements',
field=models.ManyToManyField(blank=True, to='dojo.engagement'),
),
migrations.AddField(
model_name='findingexclusion',
name='product',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dojo.product'),
),
migrations.AddField(
model_name='findingexclusion',
name='product_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dojo.product_type'),
),
migrations.CreateModel(
name='FindingExclusionLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('previous_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Pending', 'Pending'), ('Reviewed', 'Reviewed'), ('Rejected', 'Rejected'), ('Expired', 'Expired')], default='Pending', max_length=8)),
('current_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Pending', 'Pending'), ('Reviewed', 'Reviewed'), ('Rejected', 'Rejected'), ('Expired', 'Expired')], default='Pending', max_length=8)),
('changed_at', models.DateTimeField(auto_now_add=True)),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dojo.dojo_user')),
('finding_exclusion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='dojo.findingexclusion')),
],
options={
'db_table': 'dojo_finding_exclusion_log',
},
),
]
64 changes: 62 additions & 2 deletions dojo/engine_tools/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django import forms
from dojo.templatetags.authorization_tags import is_in_reviewer_group
from dojo.engine_tools.models import FindingExclusion, FindingExclusionDiscussion
from dojo.models import Product, Product_Type, Engagement
from dojo.engine_tools.helpers import Constants


Expand All @@ -19,16 +21,74 @@ class CreateFindingExclusionForm(forms.ModelForm):
label="Practice Origin Exclusion",
help_text="practice where exclusion originates",)

scope = forms.ChoiceField(
choices=[('all', 'All Engagements'), ('specific', 'Specific Engagements')],
widget=forms.RadioSelect,
initial='all',
label="Scope"
)

product_type = forms.ModelChoiceField(
queryset=Product_Type.objects.all().order_by('name'),
required=False,
label="Product Type"
)

product = forms.ModelChoiceField(
queryset=Product.objects.none(),
required=False,
label="Product"
)

engagements = forms.ModelMultipleChoiceField(
queryset=Engagement.objects.none(),
required=False,
label="Engagements"
)

class Meta:
model = FindingExclusion
fields = ["type", "unique_id_from_tool", "reason", "practice"]
fields = ["type", "unique_id_from_tool", "reason", "practice", "scope", "product_type", "product", "engagements"]

def __init__(self, *args, **kwargs):
def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)

if self.initial.get("practice"):
self.fields.pop("practice")

if 'product_type' in self.data:
Copy link
Collaborator

Choose a reason for hiding this comment

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

please review if data is limited 25 rows

try:
product_type_id = int(self.data.get('product_type'))
self.fields['product'].queryset = Product.objects.filter(prod_type_id=product_type_id).order_by('name')
except (ValueError, TypeError):
pass
elif self.instance.pk and self.instance.product_type:
self.fields['product'].queryset = self.instance.product_type.product_set.order_by('name')

if 'product' in self.data:
try:
product_id = int(self.data.get('product'))
self.fields['engagements'].queryset = Engagement.objects.filter(product_id=product_id).order_by('name')
except (ValueError, TypeError):
pass
elif self.instance.pk and self.instance.product:
self.fields['engagements'].queryset = self.instance.product.engagement_set.order_by('name')

def clean(self):
cleaned_data = super().clean()
scope = cleaned_data.get("scope")

if scope == 'specific':
Copy link
Collaborator

Choose a reason for hiding this comment

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

enable scope only when the user is_in_reviewer_group

if not is_in_reviewer_group(self.user):
raise forms.ValidationError("You do not have permission to create a specific engagement exclusion.")
if not cleaned_data.get("product_type"):
self.add_error('product_type', "This field is required when 'Specific Engagements' is selected.")
if not cleaned_data.get("product"):
self.add_error('product', "This field is required when 'Specific Engagements' is selected.")

return cleaned_data


class EditFindingExclusionForm(forms.ModelForm):

Expand Down
50 changes: 47 additions & 3 deletions dojo/engine_tools/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# Dojo
from dojo.models import Finding, Dojo_Group, Notes, Vulnerability_Id
from dojo.group.queries import get_group_members_for_group
from dojo.engine_tools.models import FindingExclusion, FindingExclusionDiscussion
from dojo.engine_tools.models import FindingExclusion, FindingExclusionDiscussion, FindingExclusionLog
from dojo.engine_tools.queries import tag_filter, priority_tag_filter
from dojo.celery import app
from dojo.user.queries import get_user
Expand Down Expand Up @@ -79,7 +79,7 @@ def has_valid_comments(finding_exclusion, user) -> bool:


@app.task
def add_findings_to_whitelist(unique_id_from_tool, relative_url):
def add_findings_to_whitelist(unique_id_from_tool, relative_url, engagement_ids=[], product_ids=[]):
findings_to_update = (
Finding.objects.filter(
Q(cve=unique_id_from_tool) | Q(vuln_id_from_tool=unique_id_from_tool),
Expand All @@ -89,6 +89,12 @@ def add_findings_to_whitelist(unique_id_from_tool, relative_url):
.filter(tag_filter)
)

if product_ids and not engagement_ids:
findings_to_update = findings_to_update.filter(test__engagement__product__in=product_ids)

if engagement_ids:
findings_to_update = findings_to_update.filter(test__engagement__in=engagement_ids)

if findings_to_update.exists():
finding_exclusion_url = get_full_url(relative_url)
system_user = get_user(settings.SYSTEM_USER)
Expand Down Expand Up @@ -119,13 +125,25 @@ def accept_finding_exclusion_inmediately(finding_exclusion: FindingExclusion) ->
finding_exclusion.save()

relative_url = reverse("finding_exclusion", args=[str(finding_exclusion.pk)])
engagement_ids = finding_exclusion.engagements.values_list('id', flat=True)
add_findings_to_whitelist.apply_async(
args=(
finding_exclusion.unique_id_from_tool,
str(relative_url),
list(engagement_ids),
[finding_exclusion.product.id] if finding_exclusion.product else [],
)
)

system_user = get_user(settings.SYSTEM_USER)

FindingExclusionLog.objects.create(
finding_exclusion=finding_exclusion,
changed_by=system_user,
previous_status=finding_exclusion.status,
current_status="Accepted"
)

# Send notification to the developer owner
create_notification(
event="finding_exclusion_approved",
Expand Down Expand Up @@ -255,6 +273,16 @@ def expire_finding_exclusion(expired_fex_id: str) -> None:
risk_status=risk_status,
).prefetch_related("tags", "notes")

engagement_ids = expired_fex.engagements.values_list("id", flat=True)

if expired_fex.product and not engagement_ids:
findings = findings.filter(test__engagement__product=expired_fex.product)

if engagement_ids:
findings = findings.filter(
test__engagement__in=list(engagement_ids)
)

findings_to_update = []

for finding in findings:
Expand All @@ -266,6 +294,13 @@ def expire_finding_exclusion(expired_fex_id: str) -> None:
findings_to_update, ["active", "risk_status"], 1000
)

FindingExclusionLog.objects.create(
finding_exclusion=expired_fex,
changed_by=system_user,
previous_status=expired_fex.status,
current_status="Expired"
)

maintainers = get_reviewers_members()
approvers = get_approvers_members()

Expand Down Expand Up @@ -305,10 +340,13 @@ def check_new_findings_to_exclusion_list():
for finding_exclusion in finding_exclusions:
relative_url = reverse("finding_exclusion", args=[str(finding_exclusion.pk)])
if finding_exclusion.type == "white_list":
engagement_ids = finding_exclusion.engagements.values_list('id', flat=True)
add_findings_to_whitelist.apply_async(
args=(
finding_exclusion.unique_id_from_tool,
relative_url,
list(engagement_ids),
[finding_exclusion.product.id] if finding_exclusion.product else [],
)
)
else:
Expand Down Expand Up @@ -677,7 +715,7 @@ def check_priorization():

@app.task
def remove_findings_from_deleted_finding_exclusions(
unique_id_from_tool: str, fx_type: str
unique_id_from_tool: str, fx_type: str, engagement_ids: list = [], product_ids: list = []
) -> None:
try:
with transaction.atomic():
Expand All @@ -700,6 +738,12 @@ def remove_findings_from_deleted_finding_exclusions(
risk_status=risk_status,
).prefetch_related("tags", "notes")

if product_ids and not engagement_ids:
findings = findings.filter(test__engagement__product__in=product_ids)

if engagement_ids:
findings = findings.filter(test__engagement__in=engagement_ids)

findings_to_update = []

for finding in findings:
Expand Down
27 changes: 26 additions & 1 deletion dojo/engine_tools/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ class FindingExclusion(models.Model):
on_delete=models.CASCADE,
related_name="dojo_user_rejected")
practice = models.CharField(max_length=50, null=True, blank=True)
product_type = models.ForeignKey("Product_Type",
null=True,
blank=True,
on_delete=models.CASCADE)
product = models.ForeignKey("Product",
null=True,
blank=True,
on_delete=models.CASCADE)
engagements = models.ManyToManyField("Engagement",
blank=True)

class Meta:
db_table = "dojo_finding_exlusion"
Expand All @@ -72,7 +82,22 @@ def __str__(self):

class Meta:
db_table = "dojo_finding_exclusion_discussion"



class FindingExclusionLog(models.Model):
finding_exclusion = models.ForeignKey("FindingExclusion", on_delete=models.CASCADE, related_name='logs')
changed_by = models.ForeignKey("Dojo_User", on_delete=models.CASCADE)
previous_status = models.CharField(max_length=8, choices=FindingExclusion.STATUS_CHOICES, blank=True, default="Pending")
current_status = models.CharField(max_length=8, choices=FindingExclusion.STATUS_CHOICES, blank=True, default="Pending")
changed_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f"Log for {self.finding_exclusion.uuid} by {self.changed_by.username} on {self.changed_at}"

class Meta:
db_table = "dojo_finding_exclusion_log"


admin.site.register(FindingExclusionLog)
admin.site.register(FindingExclusion)
admin.site.register(FindingExclusionDiscussion)
3 changes: 3 additions & 0 deletions dojo/engine_tools/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
re_path(r"^engine_tools/finding_exclusions/(?P<fxid>[\w-]+)/delete/$",
views.delete_finding_exclusion,
name="delete_finding_exclusion"),
re_path(r"^engine_tools/finding_exclusions/(?P<fxid>[\w-]+)/reopen/$",
views.reopen_finding_exclusion_request,
name="reopen_finding_exclusion"),
re_path(r"^engine_tools/orphans-reclassification$",
views.orphans_reclassification,
name="orphans_reclassification"),
Expand Down
Loading
Loading