Dashboard aggregations + six-category taxonomy for watercolor FE#3
Merged
Conversation
Implements the BE side of the watercolor FE Home screen. Adds a deterministic six-category taxonomy (web/cloud/supply/identity/data/ config), three aggregation endpoints under /api/v2/dashboard/, and a ?category= filter on the existing findings list — all sharing the same resolver so a tile count and the Browse rows behind it never disagree. Model - CategoryMapping in dojo/models.py with (cwe, tag_pattern, scanner) unique_together, choice-constrained category id, and save()-computed specificity (CWE=3 + tag=2 + scanner=1 summed). - Schema migration 0265_categorymapping with a RunPython step that loaddata's the 22-row starter fixture covering the CWEs / tags / scanners called out in PRD §4. Reversible. - dojo/fixtures/category_mappings.json seed file. Resolver - dojo/taxonomy/categorize.py exposes categorize(finding) for a single Finding (iterates the cached mapping list) and categorize_queryset(qs) which annotates `category` in a single SQL pass via CASE WHEN built from the mapping table (no per-row Python iteration on the annotation path). - Tie-break: higher specificity wins; on equal specificity the row with the most recent updated_at wins. Default fallback is "web". API - dojo/api_v2/dashboard/ package with three viewsets routed at /api/v2/dashboard/queue-counters/, /category-counts/, /search-totals/. - All three scope through dojo.finding.queries.get_authorized_findings so product-membership permissions apply. - Per-user 60s cache keyed on a hash of MAX(finding.updated) so the cache invalidates implicitly on any finding mutation. - sla_breach uses Finding.sla_expiration_date which exists on this fork; fixed_this_week uses Finding.mitigated (no status-history table on this fork — documented in the OpenAPI description as an undercount). Filter - ApiFindingFilter gains a `category` filter that defers to categorize_queryset(); repeated ?category= params OR together. Admin - CategoryMappingAdmin with list_filter on category and a "Test this mapping" action that runs categorize() against the 20 most recent findings using only the selected rules and surfaces the resolutions as Django messages. Tests - unittests/test_dashboard_aggregations.py covers every seed-fixture rule, the specificity tie-break, the three endpoints at 0/1/100 fixture sizes, the ?category= filter, and permission isolation between two users on disjoint products. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ruff `--fix` autofixes (docstring D2xx formatting, import sorting, unused imports, ternary simplification, unused noqas, etc.) plus four manual touch-ups: - dojo/models.py: reorder CategoryMapping so `save()` precedes `compute_specificity()` to satisfy DJ012 (Django style guide). - dojo/taxonomy/categorize.py: move `Iterable` import into a `TYPE_CHECKING` block (TC003). - unittests/test_dashboard_aggregations.py: make `_build_finding`'s `active` keyword-only (FBT002); add `# noqa: S106` on the three test-only fixture passwords (the per-file rule only covers S105). `ruff check .` now passes locally on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmitchell8
added a commit
that referenced
this pull request
May 13, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the BE side of
prd-fe-dashboard-aggregations-and-category-taxonomy.mdso the watercolor FE Home screen has live counts and a tile-click filter to Browse against the same mapping.CategoryMappingmodel (cwe,tag_pattern,scanner,category, save()-computedspecificity,updated_at) plus migration0265_categorymappingthat loads the 22-row starter fixture (dojo/fixtures/category_mappings.json).dojo/taxonomy/categorize.py:categorize(finding)returns a category id, iterating the cached mapping list in Python.categorize_queryset(qs)annotatescategoryin a single SQL pass via aCASE WHENderived from the mapping table — no Python iteration on the annotation path.web. Tie-break: higher specificity > more recentupdated_at./api/v2/dashboard/:queue-counters/→{critical, high, sla_breach, fixed_this_week}category-counts/→[{id, count}]always six entries in fixed ordersearch-totals/→{findings, categories}(categories capped at 8)dojo.finding.queries.get_authorized_findings, are cached per-user for 60s, and key onMAX(finding.updated)for implicit invalidation.?category=<id>filter added to the findings list (ApiFindingFilter.filter_category); repeated values OR together. Defers tocategorize_querysetso the Browse rows always match the tile count.CategoryMappingAdminwithlist_filteroncategoryand a "Test this mapping" admin action that runscategorize()against the 20 most recent findings using only the selected rules.unittests/test_dashboard_aggregations.pycovers every seed rule, specificity tie-break, the three endpoints at 0/1/100 findings, the?category=filter, and permission isolation between two users on disjoint products.Deviations from the PRD
sla_breachusesFinding.sla_expiration_date(present on this fork). The view wraps the query in a try/except and returns0as the PRD-documented fallback if the lookup ever errors.fixed_this_weekusesFinding.mitigateddirectly. This fork does not maintain a status-history table covering the full Mitigated/Inactive/False-Positive transition set, so the count is an undercount relative to the PRD's ideal. The OpenAPI description on the endpoint says so explicitly.0XXXX_category_mapping.py; in this fork the next available numeric prefix is0265.dojo/api_v2/urls.pyas the place to register routes; in this fork API routes are registered on thev2_apiDefaultRouter indojo/urls.py, which is what this PR uses.get_authorized_findings(Permissions.Finding_View, user=...)underdojo/finding/queries.py, not underdojo/authorization/.Test plan
python manage.py migrate dojoruns cleanly and loads the 22 starter rows.GET /api/v2/dashboard/queue-counters/returns the four-key payload and thecriticalcount matchesFinding.objects.filter(active=True, severity='Critical')for the calling user.GET /api/v2/dashboard/category-counts/returns exactly six rows in the fixed orderweb, cloud, supply, identity, data, config.GET /api/v2/dashboard/search-totals/returns{findings, categories}withcategories <= 8.GET /api/v2/findings/?category=webreturns only findings whose annotated category isweb; multi-value?category=web&category=cloudORs them.CategoryMappingrow routing CWE-79 todata, hitcategory-counts/after the 60s TTL elapses or with a busted cache key (any finding mutation) — counts shift accordingly. Revert.unittests/test_dashboard_aggregations.pypasses locally.drf-spectacularschema regenerates cleanly with the three new endpoints.🤖 Generated with Claude Code