Skip to content

feat(vuln-id): normalize Vulnerability_Id into a Vulnerability entity + ordered references (entity-only cutover)#15331

Draft
Maffooch wants to merge 18 commits into
devfrom
vuln-id-entity
Draft

feat(vuln-id): normalize Vulnerability_Id into a Vulnerability entity + ordered references (entity-only cutover)#15331
Maffooch wants to merge 18 commits into
devfrom
vuln-id-entity

Conversation

@Maffooch

@Maffooch Maffooch commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Shortcut: sc-13887

Normalize Vulnerability_Id into a Vulnerability entity + ordered through-table

Replaces the per-finding Vulnerability_Id rows with a normalized Vulnerability registry
entity plus an ordered FindingVulnerabilityReference through-table. This is an entity-only
cutover
: after the migration the entity/reference store is the single source of truth for all
reads and writes. There is no dual-write, no read feature flag, and no reconciliation command —
those were part of an earlier reversible design that has been dropped in favor of a clean conversion.

The legacy Vulnerability_Id model and its dojo_vulnerability_id table are retained (unused) for
a transition period
— nothing reads or writes them after the cutover, but they are kept as a
frozen pre-cutover snapshot and removed together in a future release, once the entity store has
been validated in production. The destructive DROP TABLE is the one irreversible step here, so it
is deliberately not taken in the same release that introduces the entity.

Models

  • Vulnerability — global registry of identifier strings (CVE-…, GHSA-…), stored EXACTLY
    as supplied (never case-normalized — the string is the natural key, the display form, and the
    hash input, so normalizing would churn hash_codes; case-insensitivity is an index concern via an
    Upper() index). Carries nullable EPSS/KEV columns for future enrichment (NULL = "not
    enriched"). Rows are never deleted by finding write paths — orphan entities are the (future)
    enrichment registry.
  • FindingVulnerabilityReference — ordered Finding → Vulnerability link. order == 0 is the
    primary id (== Finding.cve), replacing the implicit cve == vulnerability_ids[0] coupling. Two
    UniqueConstraints: (finding, vulnerability) and (finding, order) — the latter is a
    structural guarantee of a single primary id per finding (never two order-0 rows), compatible
    with the delete-then-insert write path.

Vocabulary added in the model docstrings (Dependency-Track/OSV/Snyk convention): a Finding is
the occurrence in your environment; a Vulnerability is the definition it references. Porting
trap documented in place: legacy code filtering the reverse relation with strings via
vulnerability_id__in=<strings> must now go through vulnerability__vulnerability_id__in (a bare
vulnerability_id__in on a reference is now "FK pk in").

Write seam (dojo/vulnerability/manager.py)

Every writer persists ids through one module. Single-finding writes and the importer batch path both
delete-then-insert a finding's references inside a transaction, assigning order 0..n-1 from the
cleaned id list. finding.cve sync stays with the callers exactly as before. Entity rows are
get_or_created race-safely (bulk_create(ignore_conflicts=True) + refetch) — shared global id
strings collide routinely across concurrent imports and every caller converges on the surviving PK.

Hash invariant (unchanged, by construction)

Finding.get_vulnerability_ids() returns "".join(sorted(...)), so the new order column is
irrelevant to the hash, and the entity stores strings byte-identically to the legacy rows. New
imports still hash from the in-memory unsaved_vulnerability_ids (upstream of either store), so
import-time dedupe is unaffected. vulnerability_id_type is never part of the hash. No hash_code
recalculation is required by this change.

Migrations

  • 0286 creates both tables EMPTY, so all indexes (including the two GIN indexes) build on zero
    rows — effectively instant, no AddIndexConcurrently. (A >50M-row install can strip the GIN
    indexes here and add them CONCURRENTLY after 0287; documented in the migration, not the default.)
  • 0287 (atomic = False) runs the convergent backfill in
    dojo/vulnerability/backfill.py: entity inserts are insert-only with ON CONFLICT DO NOTHING; the
    reference build windows by finding_id and, per window, DELETEs that window's references then
    re-inserts them from the current legacy rows, inside one transaction per window. This is what makes
    a pre-run + deploy-time catch-up safe: ids that changed between the pre-run and the deploy are
    reconciled (stale references removed, not left to drift), and there is no unique_finding_order
    collision at occupied order slots. Each window commits independently, so a crash resumes cleanly
    and any re-run converges to a no-op. The legacy source tables are never rewritten, locked, or
    deleted.
  • No drop migration in this PR. The legacy Vulnerability_Id model + dojo_vulnerability_id
    table are retained. The physical DROP TABLE is deferred to a future release and must be guarded
    there with a one-query safety invariant: abort the drop if dojo_vulnerability_id still has
    rows while the reference table is empty (i.e. the backfill never ran / never completed) — the only
    moment such a check can still prevent data loss.

PostgreSQL only

DefectDojo is a PostgreSQL-only application (the models rely on GIN / full-text indexes). The backfill
SQL therefore runs unconditionally and fails loudly on any other backend — the previous silent
vendor-skip is gone, so a backend that can't run the backfill fails at 0287 rather than silently
leaving the entity store empty.

Upgrade note (draft)

  • Big installs may pre-run manage.py migrate_vulnerability_ids on the current release; 0287 is then
    a convergent catch-up during the deploy that also reconciles anything that changed in between.
  • No manual reconciliation/verify step is needed post-cutover: parity is structural (the reference set
    mirrors the legacy per-finding row set), and the legacy table is retained as a fallback snapshot
    until the future drop (which carries the last-line invariant guard).

cve-only findings

A finding with a cve but zero legacy Vulnerability_Id rows (pre-entity drift, e.g. a directly
edited cve) gets no reference — the reference set mirrors the legacy per-finding row set exactly,
so get_vulnerability_ids() (and thus the hash) is byte-identical to what the legacy read produced.
The cve string still lands in the registry as an orphan Vulnerability entity; the cve field is
untouched.

Test evidence (unittests/test_vulnerability_id.py)

Entity read/write correctness, order/uniqueness constraints, dedup-identity (canonical, order-
independent hash input) across scanner-corpus and odd-string shapes, cve-only-has-no-reference, full
entity-only lifecycle, and copy-preserves-ids. Perf counts re-baselined on both sides (not
suppressed). Consumers on the Pro side move in the paired PR.

Not in scope (deferred)

  • Finding.cve retirement — the field stays; order == 0 == cve keeps them in lockstep. Removal
    is its own follow-up (it is a public v2-API field and touches ~100s of call sites + 107 parsers).
  • Enrichment writing to the entity's EPSS/KEV columns — columns exist, populated later; per-finding
    Finding.epss_score remains the current source until then.
  • Explorer grouping by the entity FK instead of the id string — deferred perf refinement.
  • Watson search index / classic server-rendered search — the classic search view already returns 410
    upstream, so the vuln-id lane there is inert.

Merge order: the Pro consumer PR DefectDojo-Inc/dojo-pro#2002 reads the entity
unconditionally (via a runtime import of the entity models). Merge this OSS PR first.

🤖 Generated with Claude Code

@github-actions github-actions Bot added New Migration Adding a new migration file. Take care when merging. settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR apiv2 unittests labels Jul 22, 2026
@Maffooch Maffooch added this to the 3.2.0 milestone Jul 22, 2026
Maffooch added a commit that referenced this pull request Jul 24, 2026
…s, add cutover-safety tests

Review response (#15331) plus prep for the one-way legacy retirement.

Rename (per review): VulnerabilityId -> Vulnerability; module dojo/vulnerability_id/ -> dojo/vulnerability/;
table dojo_vulnerabilityid -> dojo_vulnerability (migration 0285 amended in place). String column stays
vulnerability_id; reference FK stays `vulnerability`. Added vocabulary + porting-trap docstrings.

Must-fixes:
- MF4: UniqueConstraint(finding, order) on FindingVulnerabilityReference (model + migration 0285).
- MF2: verify_vulnerability_ids downgrades order-0 != cve to a non-fatal warning when cve is not among
  the finding's ids (unreachable without breaking hash parity); stays a hard gate for genuine ordering drift.

Bug fixes surfaced by the new tests:
- cve-only hash-parity break: backfill step 4 fabricated a cve-only reference the live dual-write never
  creates, so entity reads diverged from legacy (dedup churn on deploy). Removed; the reference set now
  mirrors the legacy row set exactly. cve stays an orphan registry entity.
- reimporter N+1: reconcile read the non-prefetched legacy relation under flag-on. Now reads through the
  seam (matches the prefetch). Perf re-baselined: -5 queries per reimport across 24 assertions.

Reader ports toward the cutover (route through the flag seam, reversible): Finding.copy, reimporter reconcile.

Tests (fixture-free, both flag states): legacy-independence (poison), dedup differential, real-scanner
corpus (8 parsers), enumerable shapes (cve-only/null-cve/unicode/quotes/copy), MF2 warning, MF4 constraint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot removed the settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR label Jul 24, 2026
Maffooch and others added 12 commits July 23, 2026 21:21
… models

Introduce the OSS vulnerability_id module (schema only, no behavior change):

- dojo/vulnerability_id/models.py: VulnerabilityId (global registry of id
  strings, EPSS/KEV columns nullable, FTS/trigram/Upper indexes) and
  FindingVulnerabilityReference (ordered Finding->VulnerabilityId link,
  order==0 is primary). Plain models.Model with explicit created/updated:
  BaseModel's manager would force order_by("id") and override the
  Meta.ordering=["order"] the reference contract depends on, and its
  full_clean-on-save must stay out of bulk paths.
- dojo/vulnerability_id/admin.py + __init__ admin import (mirrors
  dojo/finding, dojo/location; autodiscover only scans top-level dojo.admin).
- dojo/vulnerability_id/queries.py: get_auth_filter-delegating seam accessors.
- dojo/models.py: facade re-export (string FKs => no circular import).
- settings.dist.py: DD_V3_FEATURE_VULNERABILITY_IDS=(bool, True) + assignment.
- prefetch/registrations.py + authorization/query_registrations.py: register
  both models (unregistered => dropped from prefetch) with RBAC filters under
  vulnerability_id.get_authorized_entities / .get_authorized_references.

makemigrations --check reports exactly these two models pending; manage.py
check clean; ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 0285_vulnerability_id_entity_tables: CreateModel x2 (empty tables => index DDL
  instant; >50M-row GIN-strip fallback documented in the header).
- 0286_backfill_vulnerability_id_entities: atomic=False, Postgres-guarded,
  RunPython forward -> dojo.vulnerability_id.backfill.run_backfill (noop reverse).
- backfill.run_backfill: idempotent, insert-only, set-based SQL shared with the
  WP5 migrate_vulnerability_ids command — (1) entities from legacy table
  (MAX type carry), (2) cve-only entities + Python type-stamp, (3) references
  windowed by finding_id (ROW_NUMBER cve-first then legacy-PK; order 0-based),
  (4) order-0 refs for cve-only findings. ON CONFLICT DO NOTHING everywhere.
- test_migrations.TestVulnerabilityIdBackfill: seeds ordered / cve-drift /
  case-variant / cve-only / null-cve findings, asserts entity extraction, type,
  per-finding order (cve at 0), pair parity, and idempotent re-run.
  Uses a plain TestCase against run_backfill rather than MigratorTestCase:
  django_test_migrations replays the full squashed chain from a wiped schema in
  setUp and collides on legacy tables (dojo_cred_user) — the same reason the only
  other MigratorTestCase in this file is skipped. Migration application itself is
  covered by the create-db and dev-DB migrate runs.

Verified: 0285/0286 apply cleanly on a fresh create-db build and on the dev DB;
backfill test green; re-running run_backfill is a no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every vulnerability-id writer now persists the legacy Vulnerability_Id rows
byte-for-byte AND the new VulnerabilityId entity + FindingVulnerabilityReference
rows, through a single seam. The flag gates reads only (WP4), so the two stores
never drift. Legacy-write blocks are marked "TODO: Delete after Vulnerability_Id
retirement".

- dojo/vulnerability_id/manager.py: bulk_get_or_create_entities (race-safe
  bulk_create(ignore_conflicts)+refetch), persist_for_finding (single-finding
  core; does NOT touch finding.cve — caller keeps the cve sync), and
  VulnerabilityIdManager (importer accumulate/flush; record / record_reconcile /
  flush in one transaction). order == enumerate position => order 0 is the cve.
- base_importer: store_vulnerability_ids -> manager.record; flush_vulnerability_ids
  -> manager.flush() then the unchanged Finding_CWE flush (CWE buffers are NOT
  owned by the manager and keep riding this flush boundary).
- default_reimporter.reconcile_vulnerability_ids: keep the prefetched set-equality
  early-exit (legacy store authoritative) + cve sync; on change -> record_reconcile.
- finding/helper.save_vulnerability_ids: dedupe/sanitize + cve sync unchanged;
  storage delegates to persist_for_finding. (migrate_cve stays legacy-only.)

Tests: test_importers_importer updated to assert the manager's buffers (46 green);
new fixture-free test_vulnerability_id asserts the entity-side dual-write, order,
reconcile, orphan retention, and empty-clears for persist_for_finding, the manager,
and save_vulnerability_ids; test_finding_helper.test_save_vulnerability_ids now
asserts delegation to persist_for_finding + cve sync.

Verified locally: test_importers_importer + test_vulnerability_id +
TestSaveVulnerabilityIds green; manual import/reimport smoke on the dev DB shows
both stores mirrored with correct order and orphan entities retained. The
fixture-based suites (import_reimport / rest_framework / deduplication_logic /
finding_helper API) run in OSS CI (dojo_testdata.json can't load in the Pro
container: watson.searchentry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reads switch to the VulnerabilityId entity/reference store when
V3_FEATURE_VULNERABILITY_IDS is on (default True); off keeps today's legacy-table
reads. The flag is consulted ONLY in dojo/vulnerability_id/queries.py and the two
Finding methods. Writes stay unconditionally dual (WP3), so both stores hold
identical data and the flip is reversible with no drift.

queries.py seam: use_entity_reads, finding_ids_with_vulnerability_ids (in/exact),
vulnerability_id_prefetch (with nested prefix), first_vulnerability_id_subquery
(order==0), finding_vulnerability_id_strings.

Re-pointed (identical output flag-off; entity read flag-on):
- Finding.vulnerability_ids property + get_vulnerability_ids() (cve prepend /
  sorted-join kept byte-for-byte).
- dojo/filters.py both filter fns; risk_acceptance exact-match mixin.
- The 7 prefetch sites (deduplication, commands/dedupe, finding/ui + test/ui
  views, finding/queries x2) -> vulnerability_id_prefetch().
- v2 serializer: VulnerabilityIdsField reads [{"vulnerability_id": s}] from the
  flag-appropriate store (schema pinned via extend_schema_field); create/update
  pop `parsed_vulnerability_ids` and still funnel writes to save_vulnerability_ids.
- Finding.copy() now dual-writes copied ids through persist_for_finding (was a
  legacy-only .objects.create — would have been invisible flag-on).

Deliberately NOT re-pointed this PR (kept on the dual-written legacy table, which
stays correct in both flag states; entity migration deferred to the legacy-drop
phase): watson registration + the classic OSS search view. Rationale: watson is
disabled in the Pro runtime (untestable locally), the classic search view/template
are coupled to watson indexing Vulnerability_Id, and the classic UI is being
retired. This also removes the buildwatson-on-flip requirement. The Pro Vue global
search IS re-pointed to the entity in the Pro PR.

Tests (fixture-free, run locally + CI): test_vulnerability_id.TestReadSeamFlagParity
asserts the v2 wire shape, hash input (get_vulnerability_ids), the property, and the
filter seam are byte-identical flag-off vs flag-on; test_importers_importer reconcile
seeds via the dual-write seam so entity reads resolve. Verified green in BOTH flag
states locally; the fixture-based suites (rest_framework schema, import_reimport,
deduplication_logic, finding_helper API, watson trio) run in OSS CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- migrate_vulnerability_ids: wraps dojo.vulnerability_id.backfill.run_backfill
  (the same routine 0286 runs); --window-size; PostgreSQL-guarded; prints per-step
  row counts; idempotent/resumable so big tenants pre-run it before the deploy.
- verify_vulnerability_ids: read-only reconciliation, nonzero exit on any drift so
  it can gate a ring flip. Per sampled finding (--sample, default 1000): every
  legacy (finding, id) pair has a reference; extra references allowed only when
  cve-only-derived (string == finding.cve); the order-0 reference == cve. Plus
  legacy-row / entity / reference / orphan-entity counts, both table names printed
  with roles. --json for machine output.

Tests (fixture-free): migrate backfills legacy-only findings to entities/refs with
order 0 == cve and is a no-op on re-run; verify passes on consistent data and
raises SystemExit on a legacy row with no matching reference.

Verified on the dev DB: migrate_vulnerability_ids runs clean and idempotent;
verify_vulnerability_ids exits 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… backfill -> flag parity)

TestReadSeamFlagParity seeds via the dual-write seam, so both stores are trivially in
sync. These new tests cover the actual UPGRADE scenario a deployment hits: legacy
Vulnerability_Id rows written the old way (no entity refs), then backfilled by
migrate_vulnerability_ids, then read flag-off (legacy) vs flag-on (entity).

- test_backfilled_normal_data_reads_identically: for seam-shaped data (cve == first/
  lowest-PK row) get_vulnerability_ids, the vulnerability_ids property, and the raw v2
  wire shape are byte-identical across the flag.
- test_backfilled_cve_not_first_row_preserves_hash_and_property: anomalous data where cve
  is not the first-created legacy row keeps hash + property byte-identical (both are
  order-insensitive by construction); only the RAW v2 wire ORDER differs (legacy PK-asc
  vs entity cve-first, same set) — asserted so the bounded divergence is deliberate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unconditional dual-write of VulnerabilityId entities + FindingVulnerabilityReference
rows adds a small constant per-import query delta (batched, not per-finding). Counts
updated to match a pure-OSS CI run: dedup first_import +6 / second_import +4; reimport
import1 +6 / reimport1 +12 / reimport2 +5, across TestDojoImporterPerformanceSmall and
...SmallLocations. Async-task counts unchanged (dual-write is synchronous). reimport3
(queries4) unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three test-only fixes surfaced by V3_FEATURE_VULNERABILITY_IDS defaulting True in OSS
(reads hit the entity store):

- test_bulk_risk_acceptance_api + test_pdf_report_rendering seeded vuln ids via the
  legacy Vulnerability_Id table directly, so entity reads returned empty flag-on. Seed
  via save_vulnerability_ids (unconditional dual-write) instead — production code is
  unchanged and correct. bulk_risk verified locally (3 tests green flag-on); the pdf
  report test loads dojo_testdata (can't run in the Pro container) and relies on OSS CI.
- test_tag_inheritance_perf: re-baseline the 6 EXPECTED_ZAP_* assertNumQueries constants
  for the dual-write delta (+2 import / +12 reimport-no-change / +6 reimport-with-new),
  from a pure-OSS CI run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rest-framework CI matrix runs a V3_FEATURE_LOCATIONS=True leg where base save()
calls full_clean(). The new vuln-id test classes created Product/Test without the
required description / scan_type / environment, so all 7 setUpClass hooks errored with
ValidationError under that leg (they passed the False leg). Provide the required fields
(Finding creation already skips validation via skip_validation=True, so it's unaffected).
Reproduced locally with V3_FEATURE_LOCATIONS=True: 14 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s, add cutover-safety tests

Review response (#15331) plus prep for the one-way legacy retirement.

Rename (per review): VulnerabilityId -> Vulnerability; module dojo/vulnerability_id/ -> dojo/vulnerability/;
table dojo_vulnerabilityid -> dojo_vulnerability (migration 0285 amended in place). String column stays
vulnerability_id; reference FK stays `vulnerability`. Added vocabulary + porting-trap docstrings.

Must-fixes:
- MF4: UniqueConstraint(finding, order) on FindingVulnerabilityReference (model + migration 0285).
- MF2: verify_vulnerability_ids downgrades order-0 != cve to a non-fatal warning when cve is not among
  the finding's ids (unreachable without breaking hash parity); stays a hard gate for genuine ordering drift.

Bug fixes surfaced by the new tests:
- cve-only hash-parity break: backfill step 4 fabricated a cve-only reference the live dual-write never
  creates, so entity reads diverged from legacy (dedup churn on deploy). Removed; the reference set now
  mirrors the legacy row set exactly. cve stays an orphan registry entity.
- reimporter N+1: reconcile read the non-prefetched legacy relation under flag-on. Now reads through the
  seam (matches the prefetch). Perf re-baselined: -5 queries per reimport across 24 assertions.

Reader ports toward the cutover (route through the flag seam, reversible): Finding.copy, reimporter reconcile.

Tests (fixture-free, both flag states): legacy-independence (poison), dedup differential, real-scanner
corpus (8 parsers), enumerable shapes (cve-only/null-cve/unicode/quotes/copy), MF2 warning, MF4 constraint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read vulnerability ids exclusively from the Vulnerability entity +
FindingVulnerabilityReference through-table. Remove the read flag and all
flag-off branches, delete the legacy Vulnerability_Id model, and add migration
0287 dropping dojo_vulnerability_id (0286 backfills the entities first).

- MF2: backfill no longer inserts cve-only references (preserves hash parity)
- MF4: UniqueConstraint(finding, order) on the reference table
- Reimporter reads finding_vulnerability_id_strings (fixes a latent N+1)
- Repoint tests to the entity store; remove legacy-model coverage; delete the
  migrate_cve / verify_vulnerability_ids commands

NOTE: test_tag_inheritance_perf EXPECTED_* query counts need recalibration
under watson-enabled CI after the dual-write removal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dev added 0285_cicd_infrastructure, which collided with the vuln-id 0285.
Renumber the three vuln-id migrations to chain linearly after it:
  0285_vulnerability_id_entity_tables     -> 0286 (dep: 0285_cicd_infrastructure)
  0286_backfill_vulnerability_id_entities -> 0287
  0287_delete_vulnerability_id            -> 0288
makemigrations --check clean; single leaf (0288).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Maffooch and others added 4 commits July 23, 2026 21:59
…for CI

The entity-only cutover dropped the legacy Vulnerability_Id dual-write, which
shifts watson-on query counts and exposed a hand-built-instance read in two
reconcile tests (green locally with watson off; red in CI with watson on):

- test_importers_performance: reimport steps -5 queries (no legacy delete+insert)
- test_tag_inheritance_perf: reimport-no-change -12, reimport-with-new -6
- test_importers_importer reconcile tests: reload findings from the DB before
  reconcile so existing ids read from committed rows. Production loads reimport
  candidates via a prefetch query; a hand-built instance's empty reverse-relation
  cache made unchanged findings look changed.

Counts set from OSS CI actuals (watson enabled; not measurable in the Pro env).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n-id lane

Second CI pass after the rebase onto newer dev:

- test_importers_performance: rebased dev shifted the baseline again (-1 on
  import/dedup steps, -2 more on reimport1). Reset all 22 affected counts to the
  current CI actuals; the warm-up primes FindingVulnerabilityReference.
- dojo/search/views.py: the classic /simple_search vuln-id lane queried the
  deleted Vulnerability_Id via watson. An earlier stub (authorized_vulnerability_ids
  = []) 500'd on watson.filter/.prefetch_related ('list' has no attribute ...),
  breaking search_test and any UI test that visits /simple_search. Remove the lane
  entirely (model is gone and unregistered from watson); Vue global search covers
  vuln ids. The dev DD_WATSON_SEARCH_ENABLED toggle still 410s when watson is off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The committed scanner corpus yields 49 distinct real ids; the >=50 bound was an
arbitrary over-tight threshold. findings_checked>=20 already guards against a
vacuous run, and 40 keeps a strong diversity guard while being robust to the
corpus size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review of the entity-only cutover (rollout machinery):

- backfill.py: the reference build is now convergent — per finding_id window it
  DELETEs that window's references and re-inserts from the current legacy rows,
  inside one transaction per window. A pre-run + deploy-time catch-up where a
  finding's ids changed no longer (a) leaves stale references behind, nor (b)
  raises on the unique_finding_order constraint by re-inserting at occupied
  order slots. Still idempotent and resumable. Window bounds span both the legacy
  and reference tables so stale refs past the legacy max are still reconciled.
- Remove the silent non-PostgreSQL vendor guard from the backfill migration (0287)
  and the migrate_vulnerability_ids command. DefectDojo is PostgreSQL-only, so the
  backfill runs unconditionally and fails loudly rather than skipping and letting
  the unconditional 0288 drop destroy un-migrated data.
- 0288: add a one-query safety invariant before DeleteModel — abort the drop if
  dojo_vulnerability_id still has rows but the reference table is empty (the useful
  residue of the removed verify command).
- Fix stale "frozen until dropped in a follow-up migration" docs in manager.py /
  queries.py — 0288 in this same change drops the legacy table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Maffooch Maffooch changed the title feat(vuln-id): normalize Vulnerability_Id into a VulnerabilityId entity + ordered references feat(vuln-id): normalize Vulnerability_Id into a Vulnerability entity + ordered references (entity-only cutover) Jul 24, 2026
Maffooch and others added 2 commits July 24, 2026 10:14
The compat fields and reconcile/copy paths still described the removed
read-flag + dual-write architecture. Reword to the entity-only reality:
VulnerabilityIdsField reads the entity references; copy()/reimport read
through the entity helper and write through the entity seam; the
unique_finding_order comment says "write path" not "dual-write".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ition

Defer the destructive drop. Instead of the entity-only cutover dropping
dojo_vulnerability_id in the same release, keep the legacy Vulnerability_Id
model and table for a transition period and remove them together in a future
release, once the entity store has proven itself in production.

- Restore the Vulnerability_Id model (unused: no code path reads or writes it;
  entity references are the source of truth) + its dojo.models re-export.
- Delete migration 0288 (no drop, no SeparateDatabaseAndState state/DB split,
  which complicated other tooling). The model stays in Django state, matching
  code — makemigrations --check + manage.py check are clean.
- The physical DROP TABLE moves to a future release; guard it there with the
  invariant (abort if legacy has rows while the reference table is empty).

Validated: fresh migrate from zero through 0287 keeps both table sets; merge,
search, and vulnerability-id suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apiv2 New Migration Adding a new migration file. Take care when merging. unittests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant