Skip to content

Add per-user finding bookmarks and /me/queue/ API#4

Merged
cmitchell8 merged 4 commits into
mural-mainfrom
bookmarks/findings-and-queue-api
May 13, 2026
Merged

Add per-user finding bookmarks and /me/queue/ API#4
cmitchell8 merged 4 commits into
mural-mainfrom
bookmarks/findings-and-queue-api

Conversation

@cmitchell8
Copy link
Copy Markdown
Collaborator

Summary

Adds the BE primitives the watercolor FE needs for the saved-heart, the Queue screen, and the nav-bar saved-count badge. Implements the PRD at https://github.com/tactivos/security_pipeline/blob/main/docs/agent-work/prd-fe-bookmarks-and-user-queue-api.md.

What ships

  • New FindingBookmark model (user + finding, unique pair, two named indexes; reversible additive migration 0265_findingbookmark).
  • Five DRF endpoints under /api/v2/:
    • GET /bookmarks/ — paged list of the requesting user's saved findings.
    • POST /bookmarks/ — idempotent save. 201 on first save, 200 if it already exists, 403 if the user can't access the finding. IntegrityError on concurrent POSTs is caught and treated as "already exists".
    • DELETE /bookmarks/{finding_id}/ — idempotent un-save (204 either way).
    • GET /bookmarks/count/ — single indexed COUNT(*) for the nav badge.
    • GET /me/queue/ — union of bookmarked + reviewers-of-me, deduped, with order_by in {severity, cvss, age, due_in} and assigned_only=true skipping the bookmark side for the Home "Picked for you" rail.
  • FindingCardSerializer slim card payload: id, title, severity, cve/cwe, cvss_score, created_at, age_days, scanner, sla_pct, due_in, tags, assignees, status, saved.
  • All endpoints reuse get_authorized_findings(Permissions.Finding_View) so stale bookmarks on now-inaccessible findings silently drop out of list/queue/count results without deleting the row.
  • Admin registration on FindingBookmark for support.
  • DRF tests under unittests/test_apiv2_bookmarks.py covering the four PRD verification cases.
  • @extend_schema annotations so drf_spectacular picks up the new endpoints; schema not regenerated (will pick up on next manage.py spectacular).

Deviations from the PRD

  • Finding.assigned_to does not exist on this fork. Upstream DefectDojo models assignment as Finding.reviewers (M2M to user). I implemented /me/queue/ as bookmarked ∪ findings.reviewers__contains=me and exposed assignees as an array of avatar chip objects on the card serializer. The PRD's "Open Questions" section already anticipated multi-assignee, so this is the natural mapping rather than a workaround.
  • Pagination style. The repo's REST default is LimitOffsetPagination; the PRD specifies ?page=&page_size=. I attached a per-viewset PageNumberPagination subclass (BookmarkPagination) with page_size=12, max_page_size=100, and page_size_query_param=page_size so the two new viewsets match the PRD wire format without disturbing the rest of the API.
  • Detail URL kwarg. DELETE /bookmarks/{finding_id}/ uses lookup_field = "finding_id" instead of pk, since the FE only knows the finding id it wants to un-save and bookmark row ids are internal.

Test plan

  • manage.py migrate applies 0265_findingbookmark cleanly on a fresh DB.
  • manage.py migrate dojo 0264 rolls it back cleanly.
  • manage.py test unittests.test_apiv2_bookmarks passes (4 verification cases + count/list/assigned_only).
  • manage.py spectacular --file schema.yml produces a schema that includes /api/v2/bookmarks/, /api/v2/bookmarks/count/, and /api/v2/me/queue/ with response shapes.
  • Manual smoke: create a bookmark, GET count, GET list, DELETE it, GET count again — counts move 0 -> 1 -> 0 and list mirrors them.
  • Cross-product 403 check: as user A, attempt to bookmark a finding in user B's exclusive product; expect 403.
  • Revoke user A's Product_Member row for a product, then GET /me/queue/: bookmarked findings from that product no longer appear; the FindingBookmark row is still in the DB.

Notes for reviewer

  • BookmarkPagination, the helper _SEVERITY_RANK, and the FindingCardAssigneeSerializer chip palette are intentionally local to this feature — the rest of the API still uses limit/offset and the existing FindingSerializer.
  • The card serializer's assignees.color_hex is a deterministic palette pick keyed on username so the FE can render avatar chips without a second API round-trip. If the FE ever wants this driven from user profile data instead, swap the palette for a profile field.

🤖 Generated with Claude Code

Backs the watercolor FE's "saved" heart, Queue screen, and nav badge.

- New `FindingBookmark` model (user + finding, unique pair, two indexes).
- New endpoints under `/api/v2/`:
  - `GET    /bookmarks/`              list user's saved findings
  - `POST   /bookmarks/`              idempotent save (201 / 200)
  - `DELETE /bookmarks/{finding_id}/` idempotent un-save (204)
  - `GET    /bookmarks/count/`        cheap badge count
  - `GET    /me/queue/`               bookmarked ∪ reviewers-of-me, deduped
- `FindingCardSerializer` slim payload (id, title, severity, cvss_score,
  age_days, scanner, sla_pct, due_in, tags, assignees, status, saved).
- Authorization reuses the fork's `get_authorized_findings(Finding_View)`
  so stale bookmarks on now-inaccessible findings silently drop out.
- Migration `0265_findingbookmark` is additive only — no `ALTER` on
  `finding`, no data backfill, reversible.
- Admin registration for support / debugging.
- DRF tests cover the four verification cases from the PRD: idempotent
  create / list / delete, cross-product 403, stale-bookmark drop, and
  union dedup.

PRD: https://github.com/tactivos/security_pipeline/blob/main/docs/agent-work/prd-fe-bookmarks-and-user-queue-api.md

Deviation from PRD: the upstream `Finding` model has no `assigned_to` FK;
assignment is modeled as `Finding.reviewers` (M2M to user). The queue
endpoint unions reviewers-of-me instead, and the card payload exposes
`assignees` as an array. The PRD's "Open Questions" section already
anticipated multi-assignee, so this is the natural mapping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmitchell8 and others added 3 commits May 13, 2026 14:42
Ruff `--fix` autofixes (docstring D2xx formatting, import sorting,
unused noqas) plus seven manual touch-ups:

- dojo/api_v2/serializers.py: replace bare `except: pass` in
  `FindingCardSerializer.get_scanner` with a `logger.debug` (S110).
- dojo/api_v2/views.py: replace ambiguous Unicode `×` and `∪` in
  docstrings / OpenAPI summaries with ASCII (RUF001/RUF002); convert
  truthy-string membership check to a set literal (PLR6201); collapse
  the queue-union if/else to a ternary (SIM108).
- unittests/test_apiv2_bookmarks.py: drop the unused local binding
  before `return` in `_make_finding` (RET504).

`ruff check .` now passes locally on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #3 (dashboard aggregations) landed `0265_categorymapping` first, so
this branch's migration is renumbered `0265_findingbookmark` ->
`0266_findingbookmark` and its `dependencies` now points at
`0265_categorymapping` to keep the Django migration graph linear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cmitchell8 cmitchell8 merged commit 1b517cb into mural-main May 13, 2026
8 checks passed
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