@@ -19,6 +19,7 @@ package finding
1919
2020import (
2121 "fmt"
22+ "sort"
2223 "strings"
2324
2425 "github.com/projectdiscovery/nuclei/v3/pkg/output"
@@ -82,78 +83,79 @@ func key(module, identifier string) string {
8283// keyed "module:unhandled" so a new scanner surfaces loudly instead of
8384// vanishing - the guard test asserts this never happens for a known type.
8485func Flatten (target , module string , result any ) []Finding {
86+ var out []Finding
8587 switch r := result .(type ) {
8688 case * scan.ShodanResult :
87- return flattenShodan (target , r )
89+ out = flattenShodan (target , r )
8890 case * scan.SQLResult :
89- return flattenSQL (target , r )
91+ out = flattenSQL (target , r )
9092 case * scan.LFIResult :
91- return flattenLFI (target , r )
93+ out = flattenLFI (target , r )
9294 case * scan.JWTResult :
93- return flattenJWT (target , r )
95+ out = flattenJWT (target , r )
9496 case * scan.OpenAPIResult :
95- return flattenOpenAPI (target , r )
97+ out = flattenOpenAPI (target , r )
9698 case * scan.FaviconResult :
97- return flattenFavicon (target , r )
99+ out = flattenFavicon (target , r )
98100 case * scan.CMSResult :
99- return flattenCMS (target , r )
101+ out = flattenCMS (target , r )
100102 case * scan.SecurityTrailsResult :
101- return flattenSecurityTrails (target , r )
103+ out = flattenSecurityTrails (target , r )
102104 case * scan.CORSResult :
103- return flattenCORS (target , r )
105+ out = flattenCORS (target , r )
104106 case * scan.RedirectResult :
105- return flattenRedirect (target , r )
107+ out = flattenRedirect (target , r )
106108 case * scan.XSSResult :
107- return flattenXSS (target , r )
109+ out = flattenXSS (target , r )
108110 case * scan.CrawlResult :
109- return flattenCrawl (target , r )
111+ out = flattenCrawl (target , r )
110112 case * scan.PassiveResult :
111- return flattenPassive (target , r )
113+ out = flattenPassive (target , r )
112114 case * scan.ProbeResult :
113- return flattenProbe (target , r )
115+ out = flattenProbe (target , r )
114116 case scan.HeaderResults :
115- return flattenHeaders (target , r )
117+ out = flattenHeaders (target , r )
116118 case []scan.HeaderResult :
117119 // the headers module appends a literal []HeaderResult, not the named
118120 // slice type; both reach here so cover both.
119- return flattenHeaders (target , r )
121+ out = flattenHeaders (target , r )
120122 case scan.SecurityHeaderResults :
121- return flattenSecurityHeaders (target , r )
123+ out = flattenSecurityHeaders (target , r )
122124 case []scan.SecurityHeaderResult :
123- return flattenSecurityHeaders (target , r )
125+ out = flattenSecurityHeaders (target , r )
124126 case scan.DirectoryResults :
125- return flattenDirlist (target , r )
127+ out = flattenDirlist (target , r )
126128 case []scan.DirectoryResult :
127- return flattenDirlist (target , r )
129+ out = flattenDirlist (target , r )
128130 case scan.CloudStorageResults :
129- return flattenCloudStorage (target , r )
131+ out = flattenCloudStorage (target , r )
130132 case []scan.CloudStorageResult :
131- return flattenCloudStorage (target , r )
133+ out = flattenCloudStorage (target , r )
132134 case scan.DorkResults :
133- return flattenDork (target , r )
135+ out = flattenDork (target , r )
134136 case []scan.DorkResult :
135- return flattenDork (target , r )
137+ out = flattenDork (target , r )
136138 case scan.SubdomainTakeoverResults :
137- return flattenTakeover (target , r )
139+ out = flattenTakeover (target , r )
138140 case []scan.SubdomainTakeoverResult :
139- return flattenTakeover (target , r )
141+ out = flattenTakeover (target , r )
140142 case * frameworks.FrameworkResult :
141- return flattenFramework (target , r )
143+ out = flattenFramework (target , r )
142144 case * js.JavascriptScanResult :
143- return flattenJS (target , r )
145+ out = flattenJS (target , r )
144146 case * modules.Result :
145147 // yaml/builtin modules carry their own module id; honor it over the
146148 // passed-in module so per-module findings stay attributed correctly.
147- return flattenModule (target , r )
149+ out = flattenModule (target , r )
148150 case []output.ResultEvent :
149- return flattenNuclei (target , r )
151+ out = flattenNuclei (target , r )
150152 case []string :
151153 // dnslist/portscan/git all hand back a bare []string of discovered
152154 // items; module disambiguates which inventory it is.
153- return flattenStrings (target , module , r )
155+ out = flattenStrings (target , module , r )
154156 default :
155157 // unknown type: emit a loud placeholder rather than dropping it.
156- return []Finding {{
158+ out = []Finding {{
157159 Target : target ,
158160 Module : module ,
159161 Severity : SeverityUnknown ,
@@ -162,6 +164,13 @@ func Flatten(target, module string, result any) []Finding {
162164 Raw : fmt .Sprintf ("%T" , result ),
163165 }}
164166 }
167+ // some flatten* funcs source findings off Go map iteration (js env vars,
168+ // header names upstream in the headers scanner), which is randomized per
169+ // run. sort on the stable Key here, once, so every report surface (json,
170+ // sarif, markdown, -silent) renders the same order across identical scans
171+ // instead of churning on nothing but map-order luck.
172+ sort .SliceStable (out , func (i , j int ) bool { return out [i ].Key < out [j ].Key })
173+ return out
165174}
166175
167176func flattenShodan (target string , r * scan.ShodanResult ) []Finding {
@@ -477,11 +486,14 @@ func flattenHeaders(target string, rs []scan.HeaderResult) []Finding {
477486 out := make ([]Finding , 0 , len (rs ))
478487 for i := 0 ; i < len (rs ); i ++ {
479488 h := rs [i ]
489+ // a multi-valued header (Set-Cookie is the canonical case) emits one
490+ // HeaderResult per value; the value must ride in the identifier or
491+ // every value but the first collapses onto one dedup Key.
480492 out = append (out , Finding {
481493 Target : target ,
482494 Module : "headers" ,
483495 Severity : sevRecon ,
484- Key : key ("headers" , h .Name ),
496+ Key : key ("headers" , h .Name + ":" + h . Value ),
485497 Title : h .Name ,
486498 Raw : h .Value ,
487499 })
@@ -656,8 +668,7 @@ func flattenJS(target string, r *js.JavascriptScanResult) []Finding {
656668 Raw : e ,
657669 })
658670 }
659- // env vars are a map; sort-free since the Key carries the name, and diff
660- // keys on the Key not on iteration order.
671+ // map order is random here; Flatten sorts by Key (see comment above sort.SliceStable).
661672 for name , value := range r .FoundEnvironmentVars {
662673 out = append (out , Finding {
663674 Target : target ,
0 commit comments