@@ -36,41 +36,16 @@ func (e *Engine) ThreatModel(r *brief.Report) *brief.ThreatReport {
3636 }
3737 }
3838
39- for _ , d := range allDetections (r ) {
40- tool := e .KB .ByName [d .Name ]
41- if tool == nil {
42- continue
43- }
39+ contributes := e .resolveThreats (allDetections (r ), addThreat )
4440
45- // Build the tool's tag set for conjunctive matching.
46- tags := make (map [string ]bool )
47- for _ , t := range tool .Taxonomy .Tags () {
48- tags [t ] = true
49- }
50-
51- // Skip stack entry if the tool has no taxonomy and no security data:
52- // it contributes nothing and would just be noise.
53- hasSecurityData := len (tool .Security .Threats ) > 0 || len (tool .Security .Sinks ) > 0
54- if len (tags ) > 0 || hasSecurityData {
41+ // Build stack from tools that actually contribute threats or sinks.
42+ for _ , d := range allDetections (r ) {
43+ if contributes [d .Name ] {
5544 tr .Stack = append (tr .Stack , brief.StackEntry {
5645 Name : d .Name ,
5746 Taxonomy : d .Taxonomy ,
5847 })
5948 }
60-
61- // Check each mapping for a conjunctive match against this tool's tags.
62- for _ , m := range e .KB .ThreatMappings {
63- if matchesAll (tags , m .Match ) {
64- for _ , id := range m .Threats {
65- addThreat (id , d .Name , m .Note )
66- }
67- }
68- }
69-
70- // Explicit threats on the tool definition.
71- for _ , id := range tool .Security .Threats {
72- addThreat (id , d .Name , "" )
73- }
7449 }
7550
7651 // Resolve threat IDs against the registry and sort.
@@ -154,6 +129,43 @@ func allDetections(r *brief.Report) []brief.Detection {
154129 return all
155130}
156131
132+ // resolveThreats iterates detections, matches taxonomy tags against threat
133+ // mappings, and calls addThreat for each hit. Returns a set of tool names
134+ // that contribute threats or sinks (for stack filtering).
135+ func (e * Engine ) resolveThreats (detections []brief.Detection , addThreat func (id , tool , note string )) map [string ]bool {
136+ contributes := make (map [string ]bool )
137+ for _ , d := range detections {
138+ tool := e .KB .ByName [d .Name ]
139+ if tool == nil {
140+ continue
141+ }
142+
143+ tags := make (map [string ]bool )
144+ for _ , t := range tool .Taxonomy .Tags () {
145+ tags [t ] = true
146+ }
147+
148+ for _ , m := range e .KB .ThreatMappings {
149+ if matchesAll (tags , m .Match ) {
150+ contributes [d .Name ] = true
151+ for _ , id := range m .Threats {
152+ addThreat (id , d .Name , m .Note )
153+ }
154+ }
155+ }
156+
157+ for _ , id := range tool .Security .Threats {
158+ contributes [d .Name ] = true
159+ addThreat (id , d .Name , "" )
160+ }
161+
162+ if len (tool .Security .Sinks ) > 0 {
163+ contributes [d .Name ] = true
164+ }
165+ }
166+ return contributes
167+ }
168+
157169// matchesAll reports whether the tag set contains every required tag.
158170// An empty required slice matches nothing (vacuous mappings shouldn't fire).
159171func matchesAll (have map [string ]bool , required []string ) bool {
0 commit comments