Skip to content

fix(reports): limit the report filter's test choices to viewable tests - #16875

Open
svader0 wants to merge 2 commits into
DefectDojo:devfrom
svader0:harden-report-filter-test-choices
Open

svader0 wants to merge 2 commits into
DefectDojo:devfrom
svader0:harden-report-filter-test-choices

Conversation

@svader0

@svader0 svader0 commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator

The test choices in the report findings filter now come from the tests the current user can view, like the asset, engagement and organization choices in the same filter. Adds a test for the report pages that use the filter. No change for users who can view every test.

The report findings filter now builds its test choices from the tests the
current user can view, like the asset, engagement and organization choices
in the same filter. Adds a test for the report pages that use it.
@svader0 svader0 added this to the 3.5.200 milestone Nov 13, 2026
@svader0

svader0 commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator Author

Adversarial review of this diff. CI is green, and I found nothing that blocks a merge. The scoping change is correct. I found no other field in this filter that needs the same change. My notes are below, most important first.

1. A test id outside the user's scope now drops the test condition

Before this change, ?test=<id> for a test the user cannot view gave an empty result. Now that id fails form validation. django-filter then leaves test out of cleaned_data, so the page shows every finding the user can view.

I checked this in a Django shell on 3.5.102 as a non-superuser:

form valid rows returned
without this PR yes 0 of 1
with this PR no ("Select a valid choice") 1 of 1

The asset, engagement and organization fields in this filter already work this way, so the new behavior matches them. If a user loses access to a test, a saved report widget or bookmark for it now gives a wider report. The rows are still limited to findings the user can view. If we want an empty result instead, ReportFindingFilterHelper.qs can return .none() when the bound form is invalid. That touches every scoped field, so I think it belongs in its own PR.

2. The test_report subtest passes without the dropdown

The test title also shows on that page outside the <select>. I counted one hit outside the select. So assertContains(response, self.TITLE_A) passes even if the dropdown loses test A. Assert on the option instead:

self.assertContains(response, f'<option value="{self.test_a.id}"')

3. No check for a user who can view every test

The PR body says nothing changes for those users, but no subtest asserts it. Add a superuser pass that expects both titles.

4. The choices are not narrowed to the report scope (older than this PR)

On a test, engagement, asset or organization report, the dropdown lists every test the user can view. duplicate_finding in the same __init__ narrows to the report scope to avoid a huge dropdown. For a superuser on a big instance, each report page still renders every test as an option. self.test, self.engagement, self.product and self.prod_type are already set, so the same narrowing works here. This is fine as a follow-up.

5. The class default is still Test.objects.all()

test comes from Meta field generation, and __init__ swaps its queryset. The three sibling fields declare queryset=...objects.none() on the class instead. Declare test the same way:

test = ModelChoiceFilter(queryset=Test.objects.none(), label="Test")

Then the field shows nothing if a later edit skips the __init__ block.

6. The option order is not fixed

The registered get_authorized_tests has no order_by, and Test has no Meta.ordering. So the database picks the option order. The old Test.objects.all() default had the same gap, so this is not a regression. .order_by("id") matches the fallback in dojo/test/queries.py.

7. Small cleanups

  • The loop covers 3 of the 5 report pages that render this filter. The asset and engagement report pages are not in it. They use the same class, so the risk is low.
  • force_login does not need a password. create_user(username=...) alone works and drops the # noqa: S106.
  • DojoTestCase already gives self.client, so Client() and its import can go.
  • if "test" in self.form.fields: is always true in this class, because nothing here deletes test. It matches the sibling checks, so I am fine with it.

What I checked

  • In an earlier local run, the new test failed without the fix (3 of 3 subtests) and passed with it. That run covered V3 Locations on and off.
  • .prefetch_related("test_type") is used by the option labels, and it matches the other get_authorized_tests calls in this file.
  • The cached get_authorized_tests("view") result is not changed in place, because .prefetch_related() returns a copy.

The test report page shows the test title outside the dropdown, so the
old positive check passed with an empty dropdown. The test now looks for
the option element itself. A second test checks that a superuser sees
every test in the choices.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant