Skip to content

fix(report): dedup header findings by key with deterministic sort#335

Open
TBX3D wants to merge 2 commits into
vmfunc:mainfrom
TBX3D:harden-dedup-clean
Open

fix(report): dedup header findings by key with deterministic sort#335
TBX3D wants to merge 2 commits into
vmfunc:mainfrom
TBX3D:harden-dedup-clean

Conversation

@TBX3D

@TBX3D TBX3D commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

header findings were deduped by name alone, so two Set-Cookie (or other multi-valued) headers with different values collapsed onto one finding.

the key now disambiguates by how many times the header name has been seen, not by the value. Key is documented to be run-stable (finding.go:41), and folding the value in would destabilize every volatile single-valued header: Date rides on nearly every response, alongside ETag, Age, CF-Ray and X-Request-Id, so each scan would mint a fresh key and the diff/notify layer would report them added+removed every run.

the count is per name rather than the slice index: headers.go ranges over resp.Header, so a name's slice position is randomized per run while the order of values within one name is preserved.

Flatten's output is also sorted by key, since several flatten paths (in particular the JS env-var map) source from randomized Go map iteration and produced a different finding order on every run.

@TBX3D
TBX3D requested a review from vmfunc as a code owner July 9, 2026 21:17
@github-actions github-actions Bot added tests test changes size/m <200 lines changed labels Jul 9, 2026
@codecov-commenter

codecov-commenter commented Jul 9, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 87.80488% with 5 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@a38ba0a). Learn more about missing BASE report.

Files with missing lines Patch % Lines
internal/finding/finding.go 87.80% 5 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #335   +/-   ##
=======================================
  Coverage        ?   65.02%           
=======================================
  Files           ?       88           
  Lines           ?     7866           
  Branches        ?        0           
=======================================
  Hits            ?     5115           
  Misses          ?     2359           
  Partials        ?      392           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

pr summary

2 files changed (+158 -35)

category files
go source 2
tests 1

@TBX3D
TBX3D force-pushed the harden-dedup-clean branch 2 times, most recently from 046eebe to befd27a Compare July 9, 2026 23:52

@vmfunc vmfunc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the sort in Flatten plus the js env-var stability test are the right call, that map iteration needed pinning and the 200-run test is a good way to lock it.

the header key change is the wrong fix though. folding the full value into the key breaks the run-stable identity the Key field is documented to guarantee (finding.go:39, "the same finding across two runs produces the same Key"). the collapse you're actually hitting is only repeated header names (Set-Cookie), but headers.go emits every resp.Header, so you're now also destabilizing every single-valued volatile header: Date is on basically every response, plus ETag/Age/Server-with-version/CF-Ray/X-Request-Id. all of those were name-stable before and now churn a new key each run, so the diff/notify layer that keys off Finding.Key reports them added+removed on every scan of every target.

disambiguate by occurrence index within the same name instead, e.g. key("headers", h.Name+"#"+strconv.Itoa(i)). splits the two Set-Cookies, leaves Date and friends stable across runs. adjust the test's wantKey accordingly and it's in.

Copilot AI review requested due to automatic review settings July 22, 2026 22:18
@TBX3D

TBX3D commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

right, the value was the wrong axis and Date/ETag/Age/CF-Ray would have churned
a fresh key every run. added TestVolatileHeaderKeyIsRunStable, which fails on
the old behaviour with two Date values one second apart.

one adjustment to what you sketched: the disambiguator counts occurrences within
the name rather than using the slice index. headers.go builds its slice with
for name, values := range resp.Header, so a header's slice position is
randomized per run and key("headers", h.Name+"#"+strconv.Itoa(i)) would churn on
any multi-header response. the order of values within one name is preserved, so
a per-name counter is stable. TestHeaderKeyIndependentOfEmissionOrder pins that
by flattening the same headers in two different orders.

the two Set-Cookies still split, Date and friends stay put, wantKey updated.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes nondeterministic and overly-aggressive deduplication of header findings by ensuring multi-valued headers produce distinct, run-stable keys and by making Flatten output ordering deterministic.

Changes:

  • Add run-stable, per-occurrence disambiguation for multi-valued headers (e.g., repeated Set-Cookie).
  • Sort Flatten output by Key to avoid nondeterministic ordering from Go map iteration (notably JS env vars).
  • Add regression tests for header key behavior and stable JS env-var ordering.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
internal/finding/finding.go Adds global Flatten key sort + per-occurrence header key disambiguation.
internal/finding/finding_test.go Adds tests asserting distinct header keys, run-stable keys, and deterministic ordering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +498 to +502
identifier := h.Name
if n := seen[h.Name]; n > 0 {
identifier += "#" + strconv.Itoa(n)
}
seen[h.Name]++
Comment on lines +485 to +490
// a multi-valued header (Set-Cookie is the canonical case) emits one
// HeaderResult per value, and keying on the name alone collapses every
// value but the first onto one dedup Key. disambiguate by how many times
// the name has been seen rather than by the value: the value would
// destabilize every volatile single-valued header (Date, ETag, Age, CF-Ray)
// against the run-stable Key contract.
Comment on lines +168 to +171
// some flatten* funcs iterate a Go map (js env vars), which randomizes
// order per run; sort by Key here once so all report output is stable.
sort.SliceStable(out, func(i, j int) bool { return out[i].Key < out[j].Key })
return out
Comment on lines +168 to +171
// some flatten* funcs iterate a Go map (js env vars), which randomizes
// order per run; sort by Key here once so all report output is stable.
sort.SliceStable(out, func(i, j int) bool { return out[i].Key < out[j].Key })
return out
@TBX3D

TBX3D commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

one correction on my own fix: the occurrence suffix used "#", and "#" is a valid
header field-name character under rfc7230, so a real header named "Foo#1" took
the key of the second "Foo".

switched the separator to ":", which rfc7230 excludes from a field-name, so the
suffix cannot collide with any real header. TestHeaderOccurrenceSuffixCannotCollide
covers it and fails on the old separator.

@TBX3D

TBX3D commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

CI on this branch is red for a reason unrelated to the change: main does not
build at 2ce359a, internal/modules/executor.go:197 references MaxBodySize after
#355 moved it into internal/httpx, so every branch merged against main fails the
same way:

internal/modules/executor.go:197:57: undefined: MaxBodySize

#374 fixes it. this branch builds and tests clean with that one-line fix applied
on top; no action needed here.

TBX3D added 2 commits July 22, 2026 16:42
…istically

multi-valued headers (set-cookie) emitted one finding per value but keyed
on name alone, so store.Diff collapsed every value but the first onto one
key. fold the value into the header key so distinct values stay distinct.

several flatten funcs source findings off randomized go map iteration (js
env vars, header names), leaving report output order non-deterministic
across identical scans. sort the flattened slice on the stable key once in
Flatten so json/sarif/markdown/-silent all render a stable order.
folding the value into the header key gave every multi-valued header a
distinct key, but it also destabilized the single-valued volatile ones.
Date rides on nearly every response, alongside ETag, Age, CF-Ray and
X-Request-Id, so each scan minted a fresh key and the diff/notify layer
reported them added+removed every run. Key is documented to be run-stable
(finding.go:41), so that was the wrong axis.

count occurrences of the header name instead. the two Set-Cookies still
split, Date and friends stay put.

the count is per name rather than the slice index: headers.go ranges over
resp.Header, so a name's slice position is randomized per run while the
order of values within one name is preserved.
@TBX3D
TBX3D force-pushed the harden-dedup-clean branch from 683bb4b to e8de5f8 Compare July 22, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m <200 lines changed tests test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants