Dynamic groups#13001
Conversation
🔴 Risk threshold exceeded.This pull request contains multiple security findings, including potential Denial of Service vulnerabilities related to Redis cache rebuilding and in-memory sorting, an authorization bypass issue in dynamic finding group filters, and several sensitive file edits that require review and configuration in
🔴 Configured Codepaths Edit in
|
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/finding/helper.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/finding_group/redis.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/finding_group/urls.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/finding_group/views_dynamic.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/models.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/templates/base.html
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_group_dynamic_findings.html
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_group_dynamic_findings_list_snippet.html
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_groups_dynamic_list.html
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_groups_dynamic_list_snippet.html
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/filters.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/finding/helper.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/models.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/filters.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/finding_group/redis.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/finding_group/views_dynamic.py
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_group_dynamic_findings_list_snippet.html
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_groups_dynamic_list_snippet.html
| Vulnerability | Configured Codepaths Edit |
|---|---|
| Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml. |
Potential Denial of Service (DoS) via Resource Exhaustion in dojo/models.py
| Vulnerability | Potential Denial of Service (DoS) via Resource Exhaustion |
|---|---|
| Description | The patch introduces a critical performance bottleneck and potential Denial of Service (DoS) vulnerability. Every time a finding is created, updated, or deleted, a blocking Redis SET operation is performed to update a timestamp (LAST_FINDING_CHANGE). This high volume of synchronous writes, especially during bulk operations like scanner imports, can overwhelm the Redis server and/or the application's connection pool. This timestamp also acts as a cache invalidation mechanism for dynamic finding groups. If LAST_FINDING_CHANGE is newer than LAST_FINDING_UPDATE, the entire dynamic finding group cache is rebuilt by iterating over all findings in the database. This resource-intensive full cache rebuild will be triggered frequently, leading to degraded performance and potential Denial of Service for users accessing dynamic finding groups. |
django-DefectDojo/dojo/models.py
Lines 2825 to 2833 in 8ff0705
Denial of Service via Unbounded Cache Rebuild in dojo/finding_group/redis.py
| Vulnerability | Denial of Service via Unbounded Cache Rebuild |
|---|---|
| Description | The load_or_rebuild_finding_groups function iterates over all Finding objects in the database to rebuild the Redis cache. This rebuild is triggered whenever a finding or vulnerability ID is saved or deleted, causing a timestamp mismatch. In a large system, this operation is extremely resource-intensive (CPU, memory, database, Redis I/O). The absence of a locking mechanism allows multiple concurrent rebuilds, leading to a Denial of Service. |
django-DefectDojo/dojo/finding_group/redis.py
Lines 1 to 226 in 8ff0705
Authorization Bypass (IDOR) in dojo/filters.py
| Vulnerability | Authorization Bypass (IDOR) |
|---|---|
| Description | The DynamicFindingGroupsFilter and DynamicFindingGroupsFindingsFilter in dojo/filters.py are instantiated with request.GET, allowing a user to supply a pid (product ID) via a GET parameter. When pid is present, the set_related_object_fields method directly uses this pid in Engagement.objects.filter(product_id=self.pid) without performing an authorization check to ensure the user has permission to access the specified product. This bypasses the intended authorization flow, which is only applied when pid is not provided. |
django-DefectDojo/dojo/filters.py
Lines 2043 to 2120 in 8ff0705
Denial of Service via In-Memory Sorting in dojo/finding_group/views_dynamic.py
| Vulnerability | Denial of Service via In-Memory Sorting |
|---|---|
| Description | The ListDynamicFindingGroups and its subclasses load all relevant dynamic finding groups into memory, filter them, and then sort the entire collection in memory using Python's sorted() function. Pagination is applied only after this in-memory sorting is complete. For instances with a large number of findings and dynamic groups, especially when accessed by users with broad permissions (e.g., superusers), this can lead to significant memory consumption and high CPU usage, potentially causing a Denial of Service. |
django-DefectDojo/dojo/finding_group/views_dynamic.py
Lines 1 to 282 in 8ff0705
We've notified @mtesauro.
All finding details can be found in the DryRun Security Dashboard.
934806b to
89cc47a
Compare
* dynamic-group * own mode for each user
89cc47a to
f8bc92c
Compare
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
|
Conflicts have been resolved. A maintainer will review the pull request shortly. |
| ], | ||
| label="Severity", | ||
| ) | ||
| script_id = CharFilter(lookup_expr="icontains", label="Script ID") |
There was a problem hiding this comment.
Can we rename this to vuln_id_from_tool to avoid confusion as the script_id field doesn't exist in the Finding model?
| ) | ||
| script_id = CharFilter(lookup_expr="icontains", label="Script ID") | ||
| reporter = ModelMultipleChoiceFilter(queryset=Dojo_User.objects.none(), label="Reporter") | ||
| status = ChoiceFilter(choices=[("Yes", "Yes"), ("No", "No")], label="Active") |
There was a problem hiding this comment.
Can we rename this to active or is_active to avoid confusion as the status field doesn't exist in the Finding model?
| "Info": 1, | ||
| } | ||
| USER_MODES_KEY = "finding_groups_user_modes" | ||
| SYSTEM_CHANGE = "finding_groups_last_finding_change" |
There was a problem hiding this comment.
Coudl this have a more clear name like LAST_FINDING_CHANGE or LAST_FINDING_UPDATE
| return None | ||
|
|
||
| def update_sev_sla(self, finding: Finding) -> None: | ||
| if SEVERITY_ORDER[finding.severity] > SEVERITY_ORDER[self.severity]: |
There was a problem hiding this comment.
Could this also work without another mapping by using the numerical_severity as we do elsewhere?
There was a problem hiding this comment.
I will change to use Finding.get_number_severity
| def get_days_remaining(self) -> str: | ||
| if self.sla_finding_id: | ||
| finding = Finding.objects.filter(id=self.sla_finding_id).first() | ||
| days_remaining = finding.sla_days_remaining() | ||
| severity = finding.severity | ||
| sla_start_date = finding.get_sla_start_date().strftime("%b %d, %Y") | ||
| status = "age-green" | ||
| status_text = f"Remediation for {severity.lower()} findings due in {days_remaining} days or less (started {sla_start_date})" | ||
| if days_remaining and days_remaining < 0: | ||
| status = "age-red" | ||
| status_text = f"Overdue: Remediation for {severity.lower()} findings overdue {days_remaining} days (started {sla_start_date})" | ||
| days_remaining = abs(days_remaining) | ||
| elif any( | ||
| Finding.objects.filter( | ||
| id__in=self.finding_ids, | ||
| active=True, | ||
| ), | ||
| ): | ||
| status = "severity-Info" | ||
| status_text = "No SLA set, but at least one finding is active" | ||
| days_remaining = "No SLA" | ||
| else: | ||
| status = "age-blue" | ||
| status_text = "No active finding" | ||
| days_remaining = "Concluded" | ||
| title = ( | ||
| f'<a class="has-popover" data-toggle="tooltip" data-placement="bottom" title="" href="#" data-content="{status_text}">' | ||
| f'<span class="label severity {status}">{days_remaining}</span></a>' | ||
| ) | ||
| return mark_safe(title) |
There was a problem hiding this comment.
I wonder if this could be made more clean by maybe putting it in a display tag to avoid mixing logic and display too much? I also wonder about the overlap there is with the Finding_Group model which also has logic to determine SLA data (and severity?) for a list of findings.
| def get_redis_client() -> redis.Redis: | ||
| host = getattr(settings, "REDIS_HOST", "redis") | ||
| port = getattr(settings, "REDIS_PORT", 6379) | ||
| return redis.Redis(host=host, port=port, decode_responses=True) |
There was a problem hiding this comment.
I think this needs to reuse existing config in settings.dist.py?
There was a problem hiding this comment.
I didn’t find anything definitive in the settings that confirms the default host and port for Redis. Because of that, I decided to handle it directly in the code. This way, if someone wants to change the host or port, they just need to define those two variables in the project settings. It works, but I’m not sure this is the best practice. Would you recommend handling it differently?
| "Low": 2, | ||
| "Info": 1, | ||
| } | ||
| USER_MODES_KEY = "finding_groups_user_modes" |
There was a problem hiding this comment.
I was confused by the word "USER" in this name. If I look at the code it seems to be grouping mode of the group. Is there are more clear name for this constant? Or is it that the grouping mode is stored per user?
There was a problem hiding this comment.
It's per user. Once a user selects a mode, the next time they access the tab it will default to their previously chosen mode, without affecting other users who may have a different mode set.
| def get_finding_groups(self, request: HttpRequest, products=None): | ||
| user_fids, active_fids = self.get_findings(products) | ||
| list_finding_group = [] | ||
| for finding_group in self.finding_groups_map.values(): | ||
| finding_group.finding_ids = set(finding_group.finding_ids) & user_fids | ||
| if self.filter_finding_group(finding_group, request): | ||
| if finding_group.finding_ids & active_fids: | ||
| list_finding_group.append(finding_group) | ||
| return self.order_field(request, list_finding_group) | ||
|
|
There was a problem hiding this comment.
Can you explain what's happening here?
There was a problem hiding this comment.
-
It retrieves two sets of finding IDs:
user_fids: all findings the user is allowed to see (possibly restricted by products).
active_fids: the subset of findings that are currently active. -
It iterates through all finding groups in self.finding_groups_map.
-
For each group, it intersects (
&) the group’s finding IDs with user_fids, so the group only keeps findings the user has access to. -
It applies additional filters through
filter_finding_group, based on the request. -
If the group still has at least one active finding (
finding_group.finding_ids & active_fids), it is added to the result list to show inListOpenDynamicFindingGroups. -
Finally, it orders the groups using
order_fieldand returns them.
There was a problem hiding this comment.
Can you add some comments or an overall explanation of how the grouping and caching works?
Are you doing the filtering/grouping/ordering in python code?
Are you caching the results per user?
There was a problem hiding this comment.
I'm doing it in Python code, and I'm not caching the results per user. I use the finding_group created for the title mode, for example, stored in Redis, which is used by everyone who chose this mode, and I filter the findings and groups that the user can't see or wants to filter.
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| DD_TEST = os.getenv("DD_TEST", "False").lower() == "true" |
There was a problem hiding this comment.
It feels weird to have this flag/variable. Can we avoid it or at least use the same pattern as our other env variables via settings.dist.py?
There was a problem hiding this comment.
I created this variable in docker-compose.override.unit_tests_cicd.yml to avoid using Redis during CI, because Redis is not created and there is no variable to indicate when we are running tests in CI. Do you recommend something to avoid this variable?
valentijnscholten
left a comment
There was a problem hiding this comment.
Thanks for the PR, as said before I like the idea of being able to group findings in the UI.
I am still thinking if this is the best way to do it, especially since there's quite a lot of code just to be able to put the groups into Redis.
Before diving into it in detail, I've left some comments to take a look at.
I also tried out the feature locally, but I run into an error when grouping on vuln_id_from_tool:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/views/generic/base.py", line 105, in view
return self.dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/django/views/generic/base.py", line 144, in dispatch
return handler(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/dojo/finding_group/views_dynamic.py", line 115, in get
self.finding_groups_map = load_or_rebuild_finding_groups(mode=mode) if mode else {}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/dojo/finding_group/redis.py", line 243, in load_or_rebuild_finding_groups
DynamicFindingGroups.add_finding(finding, mode)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/dojo/finding_group/redis.py", line 129, in add_finding
finding_group_id = base64.b64encode(finding_group_name.encode()).decode()
^^^^^^^^^^^^^^^^^^^^^^^^^
Exception Type: AttributeError at /dynamic_finding_group/open
Exception Value: 'NoneType' object has no attribute 'encode'
When grouping on title it just keeps spinning, maybe it's just very slow. This might make sense as title is a free form text field which will be hard to group by efficiently.
Similar for CVE which should be easier to group by, but also this requests doesn't seem to complete just spinning.
To be honest I couldn't follow all of the code and there seems to be alot of processing inside python instead of the database. Maybe that's why it's not performing at the moment?
My instance has ~170K findings.
|
I understand why this happened: I didn’t handle the case where vuln_id_from_tool is None. does not filter it out. i will fix that, sorry. |
|
When I look at your screenshots, the list of Dynamic Finding Groups looks a lot like pages in Defect Dojo where we display lists of entities with some statistics. For example the View Engagement page lists all the Tests in the Engagement and the number of findings, etc. This basically a way to view at findings that are grouped by a certain field, in this case This PR wants to group findings by other fields such as For example if I do an SQL query to group findings by This gives me similar results to the screenshots in this PR:
And that is including some joines to list the affected products, which would be a nice feature. I might be forgetting something, but my initial approach would be to implement the dynamic grouping in the same way. What are your thoughts on this @LeoOMaia Also did another test today to look at the static finding group pages, but running into this error: |
|
I think your idea would be much better and would make it possible to remove the Redis storage. Regarding the message you received, was it an error or just a warning? I checked, and indeed I didn’t define a default ordering for when the user doesn’t request any sorting. I’m opening a PR to add a default ordering. You’re right that grouping by title and vuln_id_from_tool would work correctly with the query you shared, and I agree this is a good approach for those fields. For CVE, however, the logic would need to be different. A single finding can have multiple CVEs, so the idea was to create groups for each CVE, which means the same finding could belong to multiple groups. Because of that, the query would have to be adapted, but I believe it’s still possible to achieve with a modified version of your example. I’m currently looking into how I can adjust my implementation to better align with the query-based approach you suggested. |
|
Thank you for your comments! I’ll convert this PR into a draft so I can carefully review my code and remove Redis following your suggestion. It might take a little time, but once I’m done, I’ll reopen it. |
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |

This is acording to #12684 and #12814.
I developed a new tab to group findings in accord in the mode (Vuln ID from tool/Title/CVE) the user wants to group dynamically.
Advantages of using dynamic grouping instead of static groups:
Disadvantages:
Below is how it will be shown for the user to choose the group:

The mode selected by the user (default is empty and displays nothing):

Below is an example with the static and dynamic groups tab:
And another example where a user has 1 and 2 products, and another has only 2 products where all the findings are mitigated:
Below is the findings page in a dynamic group:
