fix(dedupe): order locations/endpoints inside hash_code, and catch set-order leaks in parsers - #15513
Open
valentijnscholten wants to merge 3 commits into
Open
Conversation
… order from reaching findings - sort the endpoints ingredient of hash_code on the unsaved (legacy) path: it is the value stored on import, so the scanner's emission order became finding identity and disagreed with every later recomputation - normalize locations/endpoints in the importer before hashing them, as reimport already does, so both compute the hash over the same canonical strings - read the stored Location.location_value on the saved locations path: it called get_location_value() on a Location row, so recomputing the hash of a saved finding with URL locations raised AttributeError (manage.py dedupe) - sort where parsers let a set's iteration order decide what they emit: Blackduck (file_path + finding order), Blackduck Binary Analysis (finding order), SARIF (tags), Legitify (references), Dependency Track (primary vulnerability id + alias order) - add a unit test that scans every parser for that class of defect, so it is caught before merge instead of one report at a time
…rs set up get_locations() feeds the endpoints hash ingredient for the 13 scan types whose HASHCODE_FIELDS_PER_SCANNER includes it, and every caller of the hash paths prefetches the locations relation for exactly that reason -- the batch dedupe loader, build_candidate_scope_queryset and manage.py dedupe among them. None of them got any benefit from it. The saved branch read the relation with finding.locations.filter(...), and .filter() on a related manager clones the queryset and drops _result_cache, so it bypassed the prefetch every time. On top of that the debug line called .count() inside string concatenation, so that query ran regardless of the log level, and location_ref.location took another query per reference. Reading with .all() and narrowing in Python instead makes the prefetch effective. For a finding with three URL locations and its relation prefetched, computing the locations ingredient goes from 5 queries to 0 -- it was 2 + one per location, on every hash computation, so on every import, reimport and rehash. A finding has few locations, so filtering them in Python costs nothing. The debug line now formats lazily off the computed set, so it costs no query at all. unittests/test_hash_code_location_queries.py counts queries rather than timing anything. It pins the prefetched read at zero queries, records that prefetching only the reference level still costs one per location (which is why callers want locations__location, two hops), and checks the ingredient and the stored hash_code are unchanged by narrowing in Python. Verified it catches the original defect: restoring the .filter() read fails it at 5 queries against 0. The existing perf suites are unaffected -- their fixtures use scan types that do not hash endpoints, so get_locations() is never reached there. Confirmed by running test_importers_performance and test_tag_inheritance_perf before and after: unchanged.
valentijnscholten
force-pushed
the
fix/hash-code-endpoint-ordering
branch
from
August 5, 2026 07:51
4e6d29b to
026fb9b
Compare
valentijnscholten
marked this pull request as ready for review
August 5, 2026 17:42
valentijnscholten
requested review from
Maffooch and
blakeaowens
as code owners
August 5, 2026 17:42
|
This pull request contains a critical finding where the sensitive file 'dojo/finding/models.py' was modified by an author not included in the allowed authors list.
🔴 Configured Sensitive Codepath Modified by Non-Allowed Author in
|
| Vulnerability | Configured Sensitive Codepath Modified by Non-Allowed Author |
|---|---|
| Description | File 'dojo/finding/models.py' matches configured sensitive codepath pattern 'dojo/finding/*.py' and was modified by '' (commit 026fb9b) who is not in the allowed authors list. |
We've notified @mtesauro.
Comment to provide feedback on these findings.
Report false positive: @dryrunsecurity fp [FINDING ID] [FEEDBACK]
Report low-impact: @dryrunsecurity nit [FINDING ID] [FEEDBACK]
Example: @dryrunsecurity fp drs_90eda195 This code is not user-facing
All finding details can be found in the DryRun Security Dashboard.
devGregA
self-requested a review
August 8, 2026 15:39
devGregA
approved these changes
Aug 8, 2026
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
#15481 and #15483 each fixed one parser that joined an unordered
setinto a finding. This PR attacks the same class from the other side: it orders the ingredient insidehash_codeitself, fixes the one place where that ordering was missing, and adds a check that finds the parser-side variant automatically instead of one bug report at a time.Ordering the locations does not replace those two PRs — their instability travelled through
description, which the hash has to consume verbatim. But it is why several parsers that build vulnerability ids or locations from a set never needed a fix at all (dojo/tools/dependency_track/parser.pyis one), and there was a real gap.hash_code: the locations ingredient is now ordered on both code pathsThe locations/endpoints ingredient is computed from two different branches of
Finding.get_locations(): from the parser's output before the finding is saved — this is the value stored on import (default_importer.py) — and from the saved rows on every later recomputation (manage.py dedupe, false positive history, reimport). The saved branch sorted, the legacy unsaved branch did not, even though its comment claimed it did:So for scan types that hash
endpoints(Qualys Scan, ffuf, Dirsearch, httpx, Nettacker, Legitify, …) the scanner's emission order became part of the finding's identity, and the storedhash_codedisagreed with whatever a recomputation produced. Both branches sort now.The importer also hashed locations before cleaning them, while reimport cleans first. Cleaning rewrites the canonical string (leading
/off the path, leading?off the query, port coerced toint), so the two could hash the same report differently. The importer now cleans first, like reimport.Reach:
DD_V3_FEATURE_LOCATIONS=Falseonly — the locations path already sorted on both sides.manage.py dedupecrashed for saved findings with locationsThe saved-locations branch called
get_location_value()on aLocationrow, which has no such method (it is defined onAbstractLocation/URL; the row stores the same string inlocation_value). Recomputing thehash_codeof any already-saved finding with URL locations therefore raisedAttributeError, with the defaultDD_V3_FEATURE_LOCATIONS=True. It now reads the storedlocation_value, which is written fromget_location_value()at creation, so the recomputed value matches what import computes. This was only reachable from a recomputation, which is why nothing caught it — import and reimport both hash unsaved findings.Parsers where a set's order still reached the finding
Sorted at the point the set is consumed:
blackduck— file paths joined intofile_path, and the order the findings are produced inblackduck_binary_analysis— the order the findings are produced insarif— the finding's tagslegitify— the URLs listed underreferencesdependency_track— the vulnerability ids: the first id becomes the finding's primary id (itscve), soPYTHONHASHSEEDdecided which identifier a finding was filed under. The report's ownvulnIdis now always first, aliases follow sorted.None of these values is a
hash_codefield for its scan type, so no identity changes there; they just stop reshuffling on every import.dependency_trackmay now show a different (and stable) CVE for findings that carry aliases — thevulnIdthe title has always used.A guard instead of one PR per parser
unittests/test_parsers.pygains an AST check overdojo/tools: it reports where the iteration order of a set decides what a parser emits —join()over a set,list()/tuple()of a set, iterating or comprehending over one, including through a local variable and through set algebra. It catches the exact shapes from both earlier PRs, including govulncheck's (assign the set to a variable first, join it later), which a regex over the source does not. A deliberate exception is annotated with# set-order-ok: <why>— there is one, inblackduck, where the loop only sums lengths. The checker has its own test pinning what it must catch and what it must not (lists, dicts,sorted(...), membership tests).Each of the three
hash_codefixes has a dedicated regression test inunittests/test_hash_code_location_ordering.py, which runs against both the locations and the legacy endpoints ingredient; every one of them was confirmed to fail with its fix reverted.Release notes
docs/content/releases/os_upgrading/3.2.mdcalls out the identity change: multi-endpoint findings on installs runningDD_V3_FEATURE_LOCATIONS=False. A single endpoint has one possible ordering, so those hashes do not move, and installs on the defaultTrueare unaffected. For the findings this does change there is no stable prior identity to preserve — the stored value was whatever order the report arrived in, and it never matched a recomputation.Targeting
devrather thanbugfix: these are bug fixes, but one of them changeshash_code, so it ships with a minor release.