Skip to content

Token revocation - #2

Open
svader0 wants to merge 245 commits into
devfrom
token-revocation
Open

Token revocation#2
svader0 wants to merge 245 commits into
devfrom
token-revocation

Conversation

@svader0

@svader0 svader0 commented May 27, 2026

Copy link
Copy Markdown
Owner

⚠️ Pre-Approval check ⚠️

We don't want to waste your time, so if you're unsure whether your hypothetical enhancement meets the criteria for approval, please file an issue to get pre-approval before beginning work on a PR.
Learn more here: https://github.com/DefectDojo/django-DefectDojo/blob/master/readme-docs/CONTRIBUTING.md#submission-pre-approval

Description

Describe the feature / bug fix implemented by this PR.
If this is a new parser, the parser guide may be worth (re)reading.

Test results

Ideally you extend the test suite in tests/ and dojo/unittests to cover the changed in this PR.
Alternatively, describe what you have and haven't tested.

Documentation

Please update any documentation when needed in the documentation folder)

Checklist

This checklist is for your information.

  • Make sure to rebase your PR against the very latest dev.
  • Features/Changes should be submitted against the dev.
  • Bugfixes should be submitted against the bugfix branch.
  • Give a meaningful name to your PR, as it may end up being used in the release notes.
  • Your code is Ruff compliant (see ruff.toml).
  • Your code is python 3.13 compliant.
  • If this is a new feature and not a bug fix, you've included the proper documentation in the docs at https://github.com/DefectDojo/django-DefectDojo/tree/dev/docs as part of this PR.
  • Model changes must include the necessary migrations in the dojo/db_migrations folder.
  • Add applicable tests to the unit tests.
  • Add the proper label to categorize your PR.

Extra information

Please clear everything below when submitting your pull request, it's here purely for your information.

Moderators: Labels currently accepted for PRs:

  • Import Scans (for new scanners/importers)
  • enhancement
  • performance
  • feature
  • bugfix
  • maintenance (a.k.a chores)
  • dependencies
  • New Migration (when the PR introduces a DB migration)
  • settings_changes (when the PR introduces changes or new settings in settings.dist.py)

Contributors: Git Tips

Rebase on dev branch

If the dev branch has changed since you started working on it, please rebase your work after the current dev.

On your working branch mybranch:

git rebase dev mybranch

In case of conflict:

 git mergetool
 git rebase --continue

When everything's fine on your local branch, force push to your myOrigin remote:

git push myOrigin --force-with-lease

To cancel everything:

git rebase --abort

Squashing commits

git rebase -i origin/dev
  • Replace pick by fixup on the commits you want squashed out
  • Replace pick by reword on the first commit if you want to change the commit message
  • Save the file and quit your editor

Force push to your myOrigin remote:

git push myOrigin --force-with-lease

svader0 added 5 commits May 29, 2026 18:56
- Add token_expiry DateTimeField to UserContactInfo (was missing from
  initial commit despite migration referencing it)
- Register ApiTokenViewSet at api-tokens/ in urls.py (was missing from
  initial commit despite ViewSet existing in views.py)
- Add unit tests for list, retrieve, revoke, expiry enforcement, and
  default-expiry-on-reset behaviours
@svader0
svader0 force-pushed the token-revocation branch from 814c47d to 071d035 Compare May 29, 2026 23:56
renovate Bot and others added 21 commits August 6, 2026 23:09
…/workflows/validate_docs_build.yml) (DefectDojo#15526)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…v44 (.github/workflows/renovate.yaml) (DefectDojo#15473)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…fectdojo/chart.yaml) (DefectDojo#15532)

* chore(deps): update valkey docker tag from 0.24.6 to v0.25.0 (helm/defectdojo/chart.yaml)

* update Helm documentation

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Bumps [redis](https://github.com/redis/redis-py) from 8.0.1 to 8.1.0.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](redis/redis-py@v8.0.1...v8.1.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…o#15538)

Bumps [django-polymorphic](https://github.com/django-commons/django-polymorphic) from 4.11.6 to 4.11.7.
- [Release notes](https://github.com/django-commons/django-polymorphic/releases)
- [Commits](django-commons/django-polymorphic@v4.11.6...v4.11.7)

---
updated-dependencies:
- dependency-name: django-polymorphic
  dependency-version: 4.11.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…1.6.0 (requirements-dev.txt) (DefectDojo#15544)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…ile.nginx-alpine) (DefectDojo#15546)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…lassified and versioned (DefectDojo#15556)

Replaces the create-only lifecycle section. Editing was blocked because a mapping
edit can change what a finding's identity is built from -- true of some edits and
not others -- so the page now documents the distinction the API makes, the
acknowledgement an identity-relevant edit requires, the impact endpoint for
checking before committing, the transition window and its two limits, and the
revision history. Rolling forward to a new parser stays documented, as the right
answer when a report format changes enough to yield a different set of findings.

Also flags the Django admin as the one path that bypasses versioning.

Co-authored-by: DefectDojo Agent <greg-agent-4@defectdojo.com>
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.10.2 to 3.10.3.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](Python-Markdown/markdown@3.10.2...3.10.3)

---
updated-dependencies:
- dependency-name: markdown
  dependency-version: 3.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.16.0...0.16.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [pyopenssl](https://github.com/pyca/pyopenssl) from 26.3.0 to 26.4.0.
- [Changelog](https://github.com/pyca/pyopenssl/blob/main/CHANGELOG.rst)
- [Commits](pyca/pyopenssl@26.3.0...26.4.0)

---
updated-dependencies:
- dependency-name: pyopenssl
  dependency-version: 26.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…efectDojo#15542)

Bumps [drf-spectacular-sidecar](https://github.com/tfranzel/drf-spectacular-sidecar) from 2026.7.1 to 2026.8.1.
- [Commits](tfranzel/drf-spectacular-sidecar@2026.7.1...2026.8.1)

---
updated-dependencies:
- dependency-name: drf-spectacular-sidecar
  dependency-version: 2026.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…/workflows/validate_docs_build.yml) (DefectDojo#15527)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Bumps [django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms) from 2.6 to 2.7.
- [Release notes](https://github.com/django-crispy-forms/django-crispy-forms/releases)
- [Changelog](https://github.com/django-crispy-forms/django-crispy-forms/blob/main/CHANGELOG.md)
- [Commits](django-crispy-forms/django-crispy-forms@2.6...2.7)

---
updated-dependencies:
- dependency-name: django-crispy-forms
  dependency-version: '2.7'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… UI (DefectDojo#15567)

* docs(notifications): note where notification settings live in the Pro UI

The Pro UI now serves notification settings as three pages -- Personal, System and
Template -- instead of the classic single page with a Scope drop-down, and the webhook
page moved with them.

Added as a Pro callout on the two configuration pages rather than a rewrite: the
Scope drop-down is still exactly how it works in open source, so the existing
instructions stay correct for that audience.

Pairs with the DefectDojo Pro change on the same release line.

* docs(notifications): use the site's Pro-note style, not an undefined shortcode

The Pro callout used `{{% alert %}}`, a Docsy shortcode this site does not define, so
`hugo` failed the docs build outright:

    failed to extract shortcode: template for shortcode "alert" not found

Those two were the only `{{% alert %}}` uses in the whole content tree, which was the
tell. Replaced with the highlighted-span convention the docs already use for Pro-only
notes. Verified with a local `hugo` build rather than another CI round-trip.
…tDojo#15472)

An engagement whose `status` or `engagement_type` column holds NULL (or an
empty string) could not be saved at all. Both fields are declared `null=True`
but neither was given `blank=True`, and Django counts `None` among a field's
empty values, so `full_clean()` -- which Engagement runs on every save --
rejected the row with `{'status': ['This field cannot be blank.']}`: the model
refusing a value its own column permits.

Every import and reimport writes its engagement back at the end of the run
(`save_without_resurrecting(self.test.engagement)`), so one such row turned
every subsequent scan ingest into that engagement into a hard failure. Through
the API the Django ValidationError is translated to HTTP 400, so callers got a
rejected import rather than any findings.

Neither column offers an empty choice and both declare a default, so an empty
value carries no meaning the rest of the codebase can read -- filters, reports
and the UI all assume one of the listed choices. `Engagement.pre_save_logic`
now fills an empty value in from the field's own default, which keeps a value
outside the choice list from becoming valid (as widening to `blank=True` would)
and lets each affected row heal the next time anything saves it. No data
migration: `pre_save_logic` runs before `full_clean`, so the very save that
used to fail is the one that repairs the row, and affected engagements recover
on their next (re)import rather than needing a backfill on deploy.

Tests cover both fields on the model save path, the import and reimport
write-backs, and that a populated value is never overwritten.


Claude-Session: https://claude.ai/code/session_01HzJ7XLk2RsabJbYyJJgxvg

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bumps [cryptography](https://github.com/pyca/cryptography) from 49.0.0 to 50.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…lk (DefectDojo#15571)

Documents the DISA STIG Checklist scan type: the two STIG Viewer formats it
reads, how the four checklist statuses become finding states, how DISA
categories and assessor severity overrides map to severity, what each finding
carries, and how findings are identified.

Two points get more than a passing mention because they change how someone
organizes their imports. Findings are identified by V-number *on the assessed
asset*, so the same rule failing on two hosts stays two findings and a STIG
release upgrade keeps history. And closing on re-import is driven by an item's
absence from the report, so a test holding several assets' checklists needs
Close Old Findings turned off.

On the compliance side, STIG rules cite CCIs rather than naming controls, so
control coverage gains a section on the CCI crosswalk, the precedence between
mapping sources, and the fact that the backfill command now runs both passes.
The compliance profile's configuration-test-types note gains the STIG case,
including why it is not switched on for you.

Co-authored-by: devGregA <greg-agent-2@defectdojo.com>
* refactor(ui): remove the classic Bootstrap UI

The deprecation banner announced that the classic UI is retired and the
redesigned UI becomes the default in 3.3.0. This removes the classic tree
and the machinery that chose between the two.

Removed:
- dojo/templates_classic/ (the Bootstrap 3 / SB Admin 2 tree)
- dojo/template_loaders.py: UIPreferenceLoader picked a tree per request
- UserContactInfo.ui_use_tailwind and the opt-in banner it gated
- dojo/static/dojo/{css,js}/classic/ and the vendored Bootswatch build
- 16 npm dependencies that only the classic UI used

TEMPLATES is now a plain filesystem + app-dirs chain over dojo/templates,
wrapped in the cached loader outside debug mode. The per-tree caching that
UIPreferenceLoader did internally is preserved that way.

Three templates were shadowed rather than superseded: UIPreferenceLoader
searched both trees ahead of the filesystem loader, so the classic copy won
for every user and the consolidated app-dir copies underneath had gone
stale. Deleting the tree naively would have regressed them, so the live
versions are ported across:

- notifications/mail/scan_added.tpl and webhooks/scan_added.tpl kept the
  duplicate-findings sections added in DefectDojo#15007
- dojo/action_history.html kept has_active_filters from DefectDojo#15082

Two exceptions keep the app-dir copy deliberately: alert/other.tpl, whose
|safe removal (344c913) the classic copy never received, so escaping in
alert notifications is restored; and the GitHub form templates, which the
Tailwind rebuild edited on purpose.

DataTables no longer loads the Bootstrap styling integrations; markup is
styled against DataTables' own dt-* classes, which datatables-dd.css was
already written for. Two visual regressions this surfaced are fixed here:
the control row is inlined again (the integration supplied form-inline),
and report_base.html states its heading scale and opts out of the app's
uppercase h6 label styling, both of which the vendored Bootstrap build
used to provide.

Verified against a running instance, before and after, plus 163 unit tests
covering report rendering, search, notifications, and audit log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEcRPkdyRP6wtEnqVkSB9a

* chore(deps): drop three unused npm packages and fix report icons

Follow-up audit of the Dockerfiles and package.json.

The Dockerfiles referenced none of the removed paths, so they needed no
change. collectstatic copies all of components/node_modules into the nginx
image, so an unused dependency is shipped, not just installed:

- alpinejs and htmx.org: the app loads committed vendored copies from
  dojo/static/dojo/js/vendor/, and nothing references the npm paths
- font-awesome: only report_base.html loaded it, and it was the v4 build

Dropping font-awesome means reports use the v6 build that the rest of the
app already uses, which ships with the fontawesomefree pip package rather
than npm. That also explains a comment repeated across seven report
templates -- "for some reason the font-awesome icons don't work with the
report template". Two causes, both fixed here: the templates use v6 class
names (fa-solid) that the v4 build does not define, and report_base.html
sets font-family on `*` with !important, which overrode the icon font and
rendered every icon as a missing glyph.

Verified in the browser: icons resolve to Font Awesome 6 Free and render
at full width instead of a fallback box, with body font and heading scale
unchanged. Dependencies are down to 19, from 37 before this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEcRPkdyRP6wtEnqVkSB9a

* fix(ui): restore the handles the UI suite navigates by, and the jQuery dropdown API

With the classic tree gone the admin user renders the redesigned templates, and
the Selenium suite lost three things it had been navigating by. None of the
failures were assertion failures -- every one was a NoSuchElementException.

The sidebar repeats every product tab label. "Engagements", "Findings",
"Endpoints", "Components" and "Metrics" all appear in the sidebar, which renders
~60 anchors before the product tab bar, so By.LINK_TEXT / By.PARTIAL_LINK_TEXT
resolved to the sidebar entry. Those entries are bound to Alpine with
@click.prevent, so the click never navigated -- it expanded a submenu, left the
tab's dropdown closed, and the failure surfaced one step later on a menu item
that was in the DOM but not rendered, so link text could not match it. The tab
bar now carries data-testid handles and tests go through a new
BaseTestCase.open_product_tab() helper that documents the trap.

The classic tree carried ids the redesigned tree never picked up:
simple_search_submit on the search button, product_component_view and
id_user_menu and menu_configuration in the sidebar. They are restored on the
equivalent elements, under the same permission gates. The Configuration section
expands on click rather than hover, so the one test that hovered it now clicks.

$.fn.dropdown was missing. index.js replaces bootstrap.min.js by delegating
clicks, but never registered the jQuery plugin, so the inline onclick handlers in
view_test, findings_list_snippet, finding_related_actions and view_objects died
with "$(...).dropdown is not a function". It is now backed by the same open/close
logic, resolving the toggle, the .dropdown container and the .dropdown-menu alike.
This one was user-visible, not just a test problem.

The eight reported failures were only the first failure in each of the nine CI
groups: the runner uses failfast=True and the entrypoint stops a group at its
first failing file, so most of the suite never ran. Fixing only those eight would
have surfaced the next one a CI cycle later, so the same breakage is fixed at all
~30 affected call sites, and all 41 files in the UI matrix were run locally.

Also adds data-testid="report-link" to the engagement, test and organization
report menu items, which By.PARTIAL_LINK_TEXT "Report" could no longer reach past
the sidebar's "Reports" section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): stop the footer eating submit clicks on the long forms

The engagement and add-test forms run long enough that their button row sits
next to the page footer, and the footer wins the click. Selenium sometimes says
so (ElementClickInterceptedException naming #footer-wrapper) and sometimes the
click simply lands on the footer: the form is never submitted, the page never
changes, and the test fails much later asserting a success banner that was never
going to appear. A page-source dump taken at one such failure shows the browser
still sitting on a pristine "New Interactive Engagement" form -- no success
alert, no error alert, no errorlist.

click_submit() centres the button in the viewport before clicking it, which
takes it out from under the footer. Measured on threat_model_test.py, the file
that flaked most readily: 4 failures in 12 runs before, 0 in 12 after.

is_success_message_present() now waits for the banner instead of sampling once.
It only renders after the redirect that follows a POST, so the assertion was
riding on the 1s implicit wait -- fine locally, not always enough on a loaded
runner. Every caller asserts the banner is present and nothing asserts its
absence, so the wait costs nothing on the happy path and only delays a genuine
failure.

Both are applied to the two forms where interception was actually observed
rather than to every submit in the suite.

Two things deliberately NOT done. Targeting the "Done" button instead of the
first input.btn.btn-primary looks like the obvious tightening -- new_eng.html
renders "Import Scan Results", "Add Tests" then "Done", so the bare selector
submits the first one while the comment says Done -- but "Done" is the
bottom-right button, nearest the footer, and switching to it made
threat_model_test and calendar_test fail outright. That ordering is also
byte-identical in the template the classic UI used, so it is long-standing and
not a regression. And the footer overlap itself is left alone: #footer-wrapper
is position:static in normal flow and does not overlap the buttons at rest, so
this is a layout shift during load, not a stylesheet bug with an obvious fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): route the settings-form saves through click_submit too

The previous commit hardened the engagement and add-test forms against the
footer swallowing a submit click, and CI then failed in the one place it had
not covered:

    enable_false_positive_history -> change_system_setting
    AssertionError: False is not true   # assertTrue(is_enabled) after the save

Same mechanism, on the system settings form -- also long enough to put its save
button next to the footer. This one matters more than most: set_suite_settings()
calls change_system_setting() at the head of nearly every file in the suite, so
a click that lands on the footer there takes the whole file down before its
first real test. set_block_execution() and set_deduplication_execution_mode()
submit the same way on the profile form and get the same treatment.

The notification helpers submit that form too and are deliberately left alone.
notifications_test.py fails roughly one run in three locally with or without the
change -- on a different test each time -- so there is no signal to act on, and
it has passed every CI run so far. Patching it would have been guesswork.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): stop toggling false positive history while deduplication is on

The settings view refuses this combination outright:

    "Settings cannot be saved: Deduplicate findings and False positive history
     can not be set at the same time."

test_toggle_deduplication runs immediately before test_toggle_false_positive_history
and deliberately ends with deduplication ON, so the very next test asks for a
save the view will not perform, and change_system_setting()'s assertion fails.

This has been latent, not new: the save was already being refused, but the click
that submitted it was landing on the footer often enough that deduplication
never actually persisted, so the next test found it off and passed. Fixing the
swallowed click made the earlier test's effect stick, which is what surfaced
this. Confirmed against a running instance -- setting deduplication on and then
enabling false positive history through the UI produces exactly that warning and
no save.

So turn deduplication off around the false-positive-history toggle and restore
it afterwards, which keeps both contracts: the file still leaves deduplication
enabled for dedupe_test.py, and false positive history disabled as its default.

Verified with deduplication pre-set to on, the state CI reaches: all six files
in this group pass, and the settings land at deduplication on / false positive
history off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): centre the checkbox clicks too, not just the submits

A lost click on a checkbox is worse than a lost click on a submit button, and
until now only the submits were protected. When the footer eats a submit, the
form is simply never sent. When it eats a checkbox, the submit still happens and
the form saves with the checkbox in its old state, so the setting silently does
not change and the failure lands somewhere that looks unrelated.

That is what CI was showing. Once the submits stopped being lost, the failures
moved onto the checkboxes further down the same long form:

    enable_false_positive_history         -> assertTrue(is_enabled)   (earlier)
    enable_retroactive_false_positive_history -> assertTrue(is_enabled)

The settings view re-renders the bound form after a POST, so the checkbox in the
response reflects what was submitted, not what was saved. change_system_setting()
asserting False can therefore only mean the checkbox click never registered --
the save itself would have re-rendered it checked either way.

click_submit() is now a thin wrapper over a general click_centered(), used for
the setting toggles in change_system_setting() and set_block_execution() as well.

notifications_test.py gets the same treatment, and this reverses an earlier call.
I had left it alone after a single run suggested the change made things worse,
but that run was against a database dirtied by dozens of earlier files. On a
clean database -- what CI actually gives each group -- the unpatched file fails
at test_enable_personal_notification with the mail setting simply not applied,
which is the lost-click signature exactly. Patched, it passes 22/22.

Verified on a fresh database: all of groups 04 and 05 pass, plus product_test,
dedupe_test, close_old_findings_test and threat_model_test as the heaviest users
of the changed helper -- 290 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

* test(ui): route every form submit through click_submit()

Fixing these one at a time has cost a CI cycle per site. Each run surfaced the
next lost click a little further along -- the engagement submit, then the
settings save, then the false positive history toggle, then the retroactive
toggle, then the CI/CD engagement's Done button -- always the same cause, the
footer taking a click on a long form, only ever a different button.

So convert the whole class at once: all 89 form-submit clicks across 29 files.
The change is mechanical and semantically identical -- same selector, same
element, centred in the viewport before the click. The one time centring made
things worse was when it was paired with changing WHICH button was clicked
(targeting Done instead of the first primary submit, which moved the click
nearer the footer rather than away from it); that is not what this does.

The last CI run left group 02 failing on exactly this, in engagement_test.py,
where the submit is input[value='Done'] -- the bottom-right button and so the
most footer-exposed of the three.

Verified over the first 34 files of the UI matrix on a clean database, 0
failures, covering every group that has failed at any point (01, 02, 04, 05)
including engagement_test.py itself. The remaining files were still running when
this went out; they are all the same transformation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rpv46XWjWXCWTQSkawKSHp

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(psirt): new PSIRT section — advisory feeds, terms acceptance, clearance states

Documents the platform-native PSIRT advisory feeds shipping in Pro
(beta, behind the PSIRT feature flag + license entitlement): the
customer-self-fetch posture, the 20-source shipped catalog with its
cleared / pending-clearance states, the recorded terms-acceptance
transaction (append-only ledger, encrypted credentials), custom feeds,
feed health, and the attribution rendered with advisory content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(psirt): Import SBOM and Feed Findings pages

Documents the two surfaces that answer "am I vulnerable to this advisory?" — the
global SBOM upload that gets inventory in without visiting each product, and the
triage queue that gives each advisory its answer.

The Feed Findings page spends its length on the part users will otherwise get
wrong: the answer is three-valued, and "not affected" and "unknown" are different
claims. Reporting a component with no recorded version as "not affected" would
tell someone they are safe on the strength of missing data, so the docs say what
each state means and what the reader can do about an unknown. Same reasoning for
verified vs unverified matches, and for why a confirmed match is never retracted
automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(psirt): Matching Rules and Cases/SLA pages

Two surfaces that were shipping without documentation, and both need it more
than the pages already here — each has a behaviour a user will otherwise read
as a bug.

**Matching Rules** explains why a rule can be refused. A rule graded weak
cannot be enabled, and someone who hits that without knowing the rule exists
will conclude the feature is broken rather than that their rule matches every
advisory in the store. The page leads with the grading, states the two-sided
requirement with a worked example, and covers group scope, opt-out precedence,
templates, and why deleting a group switches its rules off.

**Cases and SLA** documents the grouping suggestions and the clock. The parts
that need saying out loud:

* a breach never clears — work finished late stops as breached, not met;
* the triage clock cannot be paused at all, and why;
* a paused clock's deadline moves, so paused time is not charged to the team;
* the tier is fixed when the clock starts and can only ever move up;
* mitigation priority is not the SLA tier, and downgrading one does not move a
  deadline;
* closing a case does not stop a clock, because the obligation belongs to the
  advisories inside it;
* you do not need a case at all to get a finding into DefectDojo.

Also reordered the section so the sidebar matches the order the entries appear
in the app menu — they disagreed, which would have had a reader following two
different sequences for the same feature — and added a "how it fits together"
list to the section index, including that the workflow pieces are optional.

* docs(psirt): advisory publishing and the PSIRT dashboard

Two pages for the halves of PSIRT that were undocumented, plus the index entries
that place them.

`advisories.md` covers the direction nothing else in PSIRT does: publishing your
own advisories rather than consuming other people's. The parts a reader needs
before they use it, not after — that editing approved content costs you the
approvals (and why that is not a setting), that a rejection needs a reason, that
preflight reports un-built checks as pending rather than passing, that exclusions
beat both derived and added recipients, and that `skipped_not_configured` and
`not_implemented` are neither success nor failure.

`dashboard.md` leads with the four-way exposure split and says plainly that
"not affected" and "no signal" are not the same claim, because that is the
misreading the whole design exists to prevent. Also why feed health and triage SLA
sit side by side — an SLA that looks healthy while a feed has silently stopped
polling is the worst combination, and only visible together — and why feed health
declines to say "all clear".

The index now reads as seven steps with an explicit note that steps 1-5 are about
what others published and step 6 is the other direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(psirt): components, SLA policies, settings, and the relevance surfaces

Three new pages for surfaces that previously had no documentation, and
which until now could only be configured through the Django admin:

* Components — the SBOM inventory as PSIRT reads it, one row per
  (component, asset), and the per-pair judgement you add: a PCRSS
  rating, an authoritative CPE (the strongest correlation axis, which a
  purl-keyed SBOM row cannot carry), and tags. Says plainly that a
  versionless row can only ever be answered "unknown".
* SLA Policies — tuning the tier ladder. Separates the clock definition
  (the obligation, decided in code) from the tiers (the calibration you
  own), and explains why editing a tier never moves a running clock and
  why deleting one that live clocks use is refused.
* PSIRT Settings — case-worthiness weights and bands, the "new" item
  window, and the material-change policy, including why KEV-add is on by
  default and KEV-remove is not.

Matching Rules gains the four capabilities from the relevance pipeline:
building rules from an inventory, previewing a rule or an unsaved
condition set, per-rule effectiveness with the dead vs insufficient-data
distinction, and per-asset coverage with the inventory tempering that
stops "covered" being earned by one precise rule beside a thousand
unwatched components.

Index updated and the chapter renumbered so the reading order matches
the workflow.

---------

Co-authored-by: Greg Anderson <greg@Gregs-MacBook-Pro.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The branch was ~607 commits behind and its files had been reorganized out from
under it (user API/UI/model code moved into dojo/user/*), so the feature is
re-applied against the current structure rather than merged line by line.

Relocations:
- RevokeApiTokenView moves to dojo/user/api/views.py, with its input modelled by
  a real RevokeApiTokenSerializer instead of reading request.data by hand.
- token_expiry moves to the UserContactInfo definition in dojo/user/models.py.
- Migration renumbered 0269 -> 0290 behind 0289_remove_usercontactinfo_ui_use_tailwind.
- The classic api_v2_key template was deleted upstream; that copy is dropped.

Two defects in the original approach are fixed:

1. The instance-wide default was bypassable. It stamped an expiry onto the user
   during reset_token_for_user, but tokens are also minted by the lazy create on
   the UI key page and by DRF's api-token-auth endpoint. A token obtained either
   of those ways got no expiry at all, so the control could be sidestepped by
   choosing a different path. Expiry is now derived at authentication time from
   the token's own created timestamp, which covers every path including any added
   later. An explicit per-user token_expiry still overrides it.

2. token_expiry would have been user-editable. UserContactInfoForm is the
   self-service profile form and its Meta only excludes user/slack_user_id, so a
   new model field arrives on it as an editable input; a user could have cleared
   or extended their own expiry. It is now disabled there, alongside the existing
   timestamp fields, leaving superusers the only writers via user_contact_infos.

Also: revoking clears the stale per-user override so the owner's next token is
not dead on arrival, notifies the owner that their token was revoked and logs the
actor, and the key page now shows the effective expiry instead of always claiming
the token is permanent.

Tests cover both regressions above, expiry precedence, the api-token-auth path,
revoke authz (anonymous/non-superuser/superuser), and input validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Conflicts have been resolved. A maintainer will review the pull request shortly.

devGregA and others added 2 commits August 8, 2026 08:22
The serializer imported Token and token_expires_at inside get_token_expiry to
sidestep a circular import that does not exist: dojo.user.authentication reaches
only authorization, models and notifications.helper, none of which import back
into dojo.user.api.serializer, and dojo/user/api/views.py already imports both
modules at module scope. Resolves PLC0415 (import-outside-top-level), which was
gating the whole unit-test suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, drop a Docsy shortcode

The six failures were all in the new test file; the rest of the suite (6491 tests)
was unaffected.

Five of them failed in setup with 403 "API token has expired." rather than at
their assertion: dojo_testdata's admin token has an old created timestamp, so any
test switching on a default lifetime expired the very credential used to build
its fixtures. The admin token is now stamped fresh in setUp.

That collision is worth stating rather than papering over, so
test_enabling_the_default_expires_pre_existing_tokens now pins it directly: a
200-day-old token works, and the moment a 90-day default is switched on the same
token is rejected. Evaluating expiry at use is what makes the control impossible
to sidestep, and retroactivity is the price. The docs now carry that warning,
along with the recovery path (the UI uses session auth and can issue a new token).

The sixth was a real test defect: a disabled form field falls back to its initial
value through a round trip that drops microseconds, so comparing exact datetimes
failed on precision while the property under test held. It now asserts the posted
value was ignored, to the second.

Also replaces the {{% alert %}} block added with the earlier docs edit. That is a
Docsy shortcode; this site runs Thulite Doks, where {{< highlight >}} is the only
shortcode in use across docs/content, so it would not have rendered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.