Commit 96d5701
feat: allow import/reimport to wait for deduplication to complete (#15007)
* feat(importers): add async-wait import execution mode
Introduce a third import/reimport post-processing execution mode and make
it configurable per request and via the user profile.
- async (default): dispatch post-processing to the background, respond immediately
- async_wait (new): dispatch to the background but wait for deduplication to
finish before responding, so scan_added notifications and the returned
statistics reflect the deduplicated state
- sync: run post-processing inline in the web process
Replace the legacy UserContactInfo.block_execution boolean with an
import_execution_mode CharField; a data migration maps block_execution=True
to the 'sync' mode. wants_block_execution() now derives from the sync mode,
preserving global foreground-execution semantics for all async tasks.
Add a request field import_execution_mode (ImportScanSerializer/
ReImportScanSerializer) resolved against the profile (request override wins),
and a deduplication_complete boolean in the import/reimport response. The
post_process_findings_batch task now stores its result (ignore_result=False)
so async_wait can join on it via AsyncResult.get(); the join is bounded by
the new DD_IMPORT_ASYNC_WAIT_TIMEOUT setting.
* feat(importers): make scan_added notifications dedup-aware
notify_scan_added used the importer's in-memory finding lists, whose
duplicate flag is stale because deduplication runs on separately-fetched
instances. Once deduplication has completed (sync or async_wait, signalled by
deduplication_complete), refresh the duplicate flag from the database and split
each action list into "real" and duplicate findings:
- findings_new -> net-new (excludes deduplicated)
- findings_new_duplicate / findings_reactivated_duplicate / findings_untouched_duplicate
- finding_count -> recomputed to exclude new/reactivated duplicates
(so an all-duplicate import sends scan_added_empty)
In plain async mode (dedup not awaited) the lists are left untouched, preserving
historical behavior. Mail and webhook scan_added templates render the new
duplicate sections.
Note: import/reimport response statistics are already dedup-accurate once
awaited — Test.statistics / Test_Import.statistics query Finding and annotate
the duplicate count directly; only the notification used stale in-memory data.
* refactor(importers): await dedup before all import notifications
Move wait_for_post_processing() ahead of the test_added notification dispatch
(not just notify_scan_added) so both notifications sent during a fresh import
are emitted after deduplication has completed in async_wait mode. The reimporter
already orders the await before its single notification.
* refactor: rename import_execution_mode to deduplication_execution_mode
The mode governs whether the request waits for deduplication, so name it for
that. Rename the UserContactInfo field, the request/serializer field, the
ImporterOptions attribute and validator, the resolver, and the
IMPORT_EXECUTION_MODE_* constants to DEDUPLICATION_EXECUTION_MODE_*. Add a
RenameField migration (0271) rather than mutating the add/remove migrations
introduced earlier on this branch.
* refactor: rename DD_IMPORT_ASYNC_WAIT_TIMEOUT to DD_DEDUPLICATION_ASYNC_WAIT_TIMEOUT, default 60s
* refactor: keep block_execution as global switch, deduplication_execution_mode for import dedup
block_execution gates every async task (notifications, jira, grading, deletes,
reindex, ...) via we_want_async, so it must remain. Restore it as the global
"run all async tasks in the foreground" flag and make deduplication_execution_mode
an independent field that only controls how import/reimport deduplication
post-processing is dispatched and awaited.
- Restore the block_execution field; wants_block_execution() reads it again.
- resolve_deduplication_execution_mode() falls back to block_execution -> sync.
- Replace the destructive remove migration (0270) with a seed-only data migration
that maps block_execution=True -> deduplication_execution_mode 'sync'; keep both.
- Restore fixtures, the profile form field, and the user detail view (both fields).
- Revert the test sweep: tests that need global foreground use block_execution=True
again; the dedicated deduplication_execution_mode tests are updated for the split.
* docs: 2.60 upgrade notes for deduplication_execution_mode
* fix(importers): honor profile deduplication_execution_mode for UI import/reimport
The API resolves deduplication_execution_mode in the serializer, but the UI
import/reimport views built their context straight from the form and never
resolved it, so UI imports silently defaulted to async regardless of the user's
profile. Resolve it from the profile in both UI process_form paths.
* test: revert set_block_execution to the block_execution checkbox
Option B keeps block_execution as a checkbox in the profile form, so the
integration helper toggles id_block_execution again instead of the
deduplication_execution_mode select (which no longer existed under that id).
* refactor(migrations): split into schema add + data seed (2 migrations)
Collapse the add/rename pair into a single AddField that creates
deduplication_execution_mode with its final name, and keep the seed as a
separate data migration, per the convention of keeping schema and data
migrations apart. Drops the interim rename migration.
* test: use versioned_fixtures so dedup mode tests pass under V3_FEATURE_LOCATIONS
The CI 'true' matrix variant enables V3_FEATURE_LOCATIONS, where loading
dojo_testdata.json (containing Endpoints) raises NotImplementedError. Decorate
the test classes with @versioned_fixtures so the locations fixture is used in
that mode, matching the other import/reimport test suites.
* test(perf): add async_wait deduplication performance test
Covers the 'async_wait' execution mode: post-processing is dispatched to a
background worker and the request joins on it before responding. The dedup
queries run in the worker (separate connection), not the web request, so the
only web-side delta over the plain async path is the post-dedup notification
refresh SELECT (+1): 109->110 first import, 89->90 second.
Does not use CELERY_TASK_ALWAYS_EAGER (that would run the task inline on the
request connection and wrongly count worker queries). The dedup batch is
dispatched async and the join (AsyncResult.get) is mocked to return instantly,
simulating a finished worker. Adds an optional dedup_mode param to
_deduplication_performance.
* fix(migrations): renumber import-execution-mode migrations onto dev (0270/0271)
dev added 0269_normalize_blank_finding_components, colliding with this branch's
0269. Renumber to 0270_usercontactinfo_deduplication_execution_mode and
0271_seed_deduplication_execution_mode and repoint the dependency chain onto
dev's 0269.
* fix(importers): drop ignore_result=False override on post_process_findings_batch
The async_wait deduplication join works with the global CELERY_TASK_IGNORE_RESULT=True
default (verified live by reviewer against a real broker/worker). Removing the
per-task override avoids writing a celery_taskmeta result row for every batch of
every import (incl. plain async), which were retained for CELERY_RESULT_EXPIRES.
* test(e2e): selenium integration test for async_wait deduplication mode
Adds async_wait coverage to the existing tests/dedupe_test.py DedupeTest suite:
set the user profile to 'async_wait', import a Checkmarx report into two tests of
a dedupe-on-engagement engagement, and assert the duplicates are visible
immediately (check_nb_duplicates_no_wait, no sleep/retry) — proving the request
blocked until the cross-process deduplication finished. This exercises the real
worker->request join that the eager unit tests cannot cover.
Adds a set_deduplication_execution_mode() helper to base_test_class.
* fix(importers): make async_wait actually join via per-dispatch ignore_result
The async_wait join via AsyncResult.get() was a silent no-op: the global
CELERY_TASK_IGNORE_RESULT=True means post_process_findings_batch never
stores a result, so .get() returns immediately instead of blocking. The
import responded with deduplication_complete=true while dedup had not run.
Pass ignore_result=False only on the async_wait dispatch, threaded through
dojo_dispatch_task to apply_async. The task decorator stays plain @app.task,
so async/sync imports do not write useless celery_taskmeta result rows.
* test(dedupe): replace Selenium async_wait test with real-worker API guard
The Selenium async_wait test could not fail when the cross-process join was
broken: with only 2 findings and several page navigations between the import
and the duplicate check, the background dedupe finished regardless of whether
the import actually blocked. It also never read deduplication_complete.
Remove it and add ImportAsyncWaitApiTest: a pure-API test that imports a
400-finding report twice into a dedup-on-engagement engagement against the
real worker, then asserts (no sleep/retry) that async_wait reports
deduplication_complete=true with all 400 marked duplicate at response time,
while plain async returns incomplete with fewer marked. A no-op join makes
async_wait behave like async and fails the test.
* test(dedupe): make async_wait integration test deterministic via gated dedup delay
The giant-report approach was non-deterministic in CI: deduplication is
dispatched per batch during the import, so on a fast worker plain `async`
finishes dedup before the response and marks all findings too. The count
discriminator then couldn't tell async_wait from async (the control failed
`400 != 400`), and a broken async_wait could false-pass.
Add DD_DEDUPLICATION_BATCH_PROCESS_TEST_DELAY (default 0, no-op in production),
set to 3s on the integration-test celeryworker only. Each dedup batch sleeps
before doing work, so async_wait blocks past it (all duplicates marked) while
async returns mid-delay (none marked) — independent of worker speed. Rewrite
ImportAsyncWaitApiTest to a small report and assert async marks exactly 0.
Verified locally: green normally; fails `0 != 25` when the join fix is reverted.
* test(dedupe): scope async_wait dedup delay to the test's findings, widen margin
The first delay attempt broke two ways in CI:
- It delayed *every* dedup batch in the UI suite, breaking the timing-sensitive
close_old_findings_dedupe_test.
- 3s was too short: the async control's import + observation GET took longer
than that on CI, so dedup finished first and the control marked all findings.
Add DD_DEDUPLICATION_BATCH_PROCESS_TEST_DELAY_FILTER (a finding-title prefix,
case-insensitive) so only the async_wait test's own findings are delayed -- other
dedupe tests run at full speed. Bump the delay to 10s for margin over the import
round-trip. The Generic importer title-cases findings, so the match uses
istartswith.
Verified locally against the real worker: test_wait now blocks ~10s and marks
all 25; async marks 0 at response time; reverting the join fix fails 0 != 25.
* test(dedupe): log WARN when the test-only dedup delay fires
Diagnostic for CI: surfaces exactly which post_process_findings_batch
invocations hit the integration-test delay (and the finding count + filter),
to confirm the async_wait test's batches are being delayed as intended.
* test(dedupe): assert only deduplication_complete for the async control
The async control's marked-count assertion was non-deterministic: its
post_process_findings_batch task races the import request's own DB commit, so
on CI the findings were not visible when the delay filter ran (no sleep) yet
were deduplicated moments later -> the control saw all 25 marked and failed.
Drop the count assertion for async and keep only deduplication_complete is
False, which is deterministic and mode-based. The duplicate-count guarantee
stays on the async_wait test, where the worker-side delay (confirmed firing via
the WARN log: 10s/25 findings per batch) makes it robust and a no-op join still
fails 0 != NUM_FINDINGS.
* test(dedupe): note async_wait eager test is not a real guard
Document that test_import_async_wait_returns_statistics cannot fail when the
cross-process join is broken (eager .get() always returns inline), pointing to
the real-worker ImportAsyncWaitApiTest. Kept for endpoint/field plumbing
coverage and future reference.
* docs(dedupe): document import/reimport deduplication execution mode
Extend the deduplication 'Background processing' section with the new
deduplication_execution_mode (async / async_wait / sync), the per-profile and
per-request override, the deduplication_complete response field, the
DD_DEDUPLICATION_ASYNC_WAIT_TIMEOUT bound, and its relationship to the global
block_execution flag. Previously this was only in the 2.60 upgrade note.
* feat(importers): log async_wait deduplication wait duration
Record how long the import request blocked on the deduplication join:
- DEBUG line with elapsed seconds + task count + success on completion.
- the timeout/error WARNING now includes the elapsed time before it gave up.
Timing detail is DEBUG so it does not add noise per import in production; the
timeout case stays at WARNING since it signals a degraded (respond-anyway) join.
* chore: stop tracking local-only files committed by mistake
CLAUDE.local.md, fix-jira-push-eligibility.md, phase10-session-prompt.md,
scripts/run-nuclei-docker.sh and tagoulus-replcement-options.md were
accidentally added in an earlier refactor commit; none belong in this feature.
Remove them from version control (kept on disk locally) and gitignore
CLAUDE.local.md / CLAUDE.md so they cannot be re-added.
* fix(migrations): renumber dedup-execution-mode migrations onto current dev leaf
dev advanced past the previous 0270/0271 numbering (it now ships
0270_finding_visibility_perf_indexes through
0272_reencrypt_tool_config_credentials_aes_gcm), so the PR's
0270/0271 migrations reintroduced a second leaf node off
0269_normalize_blank_finding_components -> 'Conflicting migrations
detected; multiple leaf nodes in the migration graph'.
Renumber onto the current leaf:
- 0270_usercontactinfo_deduplication_execution_mode -> 0273_..., dep 0272_reencrypt_tool_config_credentials_aes_gcm
- 0271_seed_deduplication_execution_mode -> 0274_..., dep 0273_usercontactinfo_deduplication_execution_mode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(perf): bump async_wait dedup query baseline to match measured counts
CI 'Check counts are up to date' measured 94/74 for
test_deduplication_performance_pghistory_async_wait (first/second
import) vs the committed 93/73. The web-side delta over plain async
(92/72) is +2, not +1: the post-dedup notification refresh SELECT plus
one result-backend write from the per-dispatch ignore_result=False that
enables the AsyncResult.get() join. Update the baseline and the
docstring accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(migrations): renumber dedup-execution-mode migrations onto current dev leaf
Rebasing onto dev pulled in 0273_product_upper_name_index,
0274_finding_sla_expiration_index and 0275_usercontactinfo_user_state_details,
colliding with this branch's 0273/0274 and producing two leaf nodes.
Renumber onto the current leaf:
- 0273_usercontactinfo_deduplication_execution_mode -> 0276_..., dep 0275_usercontactinfo_user_state_details
- 0274_seed_deduplication_execution_mode -> 0277_..., dep 0276_usercontactinfo_deduplication_execution_mode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: move deduplication_execution_mode upgrade note to 3.1
The feature now ships in 3.1.0, not 2.60. Revert 2.60.md to its
"no special instructions" state and fold the deduplication execution
mode section into 3.1.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 6851210 commit 96d5701
28 files changed
Lines changed: 987 additions & 22 deletions
File tree
- docs/content
- en/open_source/upgrading
- triage_findings/finding_deduplication
- dojo
- api_v2
- db_migrations
- engagement/ui
- finding
- importers
- settings
- templates_classic
- dojo
- notifications
- mail
- webhooks
- templates/dojo
- test/ui
- user
- ui
- tests
- unittests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
| 156 | + | |
| 157 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
49 | 55 | | |
50 | 56 | | |
51 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
Lines changed: 12 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
111 | 123 | | |
112 | 124 | | |
113 | 125 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| |||
431 | 433 | | |
432 | 434 | | |
433 | 435 | | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
434 | 446 | | |
435 | 447 | | |
436 | 448 | | |
| |||
476 | 488 | | |
477 | 489 | | |
478 | 490 | | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
479 | 499 | | |
480 | 500 | | |
481 | 501 | | |
| |||
534 | 554 | | |
535 | 555 | | |
536 | 556 | | |
| 557 | + | |
537 | 558 | | |
538 | 559 | | |
539 | 560 | | |
| |||
632 | 653 | | |
633 | 654 | | |
634 | 655 | | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
635 | 664 | | |
636 | 665 | | |
637 | 666 | | |
| |||
805 | 834 | | |
806 | 835 | | |
807 | 836 | | |
| 837 | + | |
808 | 838 | | |
809 | 839 | | |
810 | | - | |
811 | | - | |
812 | | - | |
| 840 | + | |
| 841 | + | |
813 | 842 | | |
814 | 843 | | |
815 | 844 | | |
| |||
821 | 850 | | |
822 | 851 | | |
823 | 852 | | |
824 | | - | |
| 853 | + | |
825 | 854 | | |
826 | | - | |
| 855 | + | |
| 856 | + | |
827 | 857 | | |
828 | 858 | | |
829 | 859 | | |
| |||
842 | 872 | | |
843 | 873 | | |
844 | 874 | | |
| 875 | + | |
| 876 | + | |
845 | 877 | | |
846 | 878 | | |
847 | 879 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | | - | |
| 64 | + | |
| 65 | + | |
65 | 66 | | |
66 | | - | |
67 | | - | |
| 67 | + | |
| 68 | + | |
68 | 69 | | |
69 | 70 | | |
70 | 71 | | |
| |||
75 | 76 | | |
76 | 77 | | |
77 | 78 | | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
78 | 84 | | |
79 | 85 | | |
80 | 86 | | |
| |||
83 | 89 | | |
84 | 90 | | |
85 | 91 | | |
86 | | - | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
87 | 96 | | |
88 | 97 | | |
89 | 98 | | |
| |||
Lines changed: 16 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
Lines changed: 30 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
948 | 948 | | |
949 | 949 | | |
950 | 950 | | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
951 | 955 | | |
952 | 956 | | |
953 | 957 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
470 | 470 | | |
471 | 471 | | |
472 | 472 | | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
473 | 487 | | |
474 | 488 | | |
475 | 489 | | |
| |||
0 commit comments