Skip to content

Commit 3344972

Browse files
authored
Merge pull request #46 from git-pkgs/tighten-threat-output
Tighten threat-model output
2 parents b8be950 + 2f36707 commit 3344972

7 files changed

Lines changed: 55 additions & 43 deletions

File tree

detect/threat.go

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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).
159171
func matchesAll(have map[string]bool, required []string) bool {

detect/threat_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestThreatModelRubyProject(t *testing.T) {
4242
threatIDs[th.ID] = true
4343
}
4444

45-
wantThreats := []string{"xss", "csrf", "ssti", "auth_bypass", "ssrf"}
45+
wantThreats := []string{"xss", "csrf", "ssti", "auth_bypass"}
4646
for _, w := range wantThreats {
4747
if !threatIDs[w] {
4848
t.Errorf("expected threat %q, got %v", w, tr.Threats)

knowledge/_shared/_threats.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ note = "Spawns OS processes from string arguments"
175175

176176
[[mappings]]
177177
match = ["role:framework", "layer:backend"]
178-
threats = ["xss", "csrf", "open_redirect", "ssrf", "path_traversal", "auth_bypass"]
178+
threats = ["xss", "csrf", "open_redirect", "path_traversal", "auth_bypass"]
179179
note = "Server-side framework handling user-controlled HTTP input"
180180

181181
[[mappings]]

report/markdown.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -354,15 +354,15 @@ func MissingMarkdown(w io.Writer, r *brief.MissingReport) {
354354

355355
// ThreatMarkdown writes the threat report in markdown format.
356356
func ThreatMarkdown(w io.Writer, r *brief.ThreatReport) {
357-
if len(r.Threats) == 0 {
358-
_, _ = fmt.Fprintln(w, "No security data available for detected tools.")
359-
return
360-
}
361-
362357
if len(r.Ecosystems) > 0 {
363358
_, _ = fmt.Fprintf(w, "**Detected:** %s\n\n", strings.Join(r.Ecosystems, ", "))
364359
}
365360

361+
if len(r.Threats) == 0 {
362+
_, _ = fmt.Fprintln(w, "No threat categories match the detected stack.")
363+
return
364+
}
365+
366366
_, _ = fmt.Fprintln(w, "| Threat | CWE | OWASP | Introduced by |")
367367
_, _ = fmt.Fprintln(w, "|--------|-----|-------|---------------|")
368368
for _, t := range r.Threats {

report/markdown_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ func TestThreatMarkdown(t *testing.T) {
296296
func TestThreatMarkdownEmpty(t *testing.T) {
297297
var buf bytes.Buffer
298298
ThreatMarkdown(&buf, &brief.ThreatReport{})
299-
if !strings.Contains(buf.String(), "No security data available") {
299+
if !strings.Contains(buf.String(), "No threat categories match") {
300300
t.Errorf("expected empty message\ngot:\n%s", buf.String())
301301
}
302302
}

report/report.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,6 @@ func ThreatJSON(w io.Writer, r *brief.ThreatReport) error {
361361

362362
// ThreatHuman writes the threat report in human-readable format.
363363
func ThreatHuman(w io.Writer, r *brief.ThreatReport) {
364-
if len(r.Threats) == 0 {
365-
_, _ = fmt.Fprintln(w, "No security data available for detected tools.")
366-
return
367-
}
368-
369364
if len(r.Ecosystems) > 0 {
370365
_, _ = fmt.Fprintf(w, "Detected: %s\n", strings.Join(r.Ecosystems, ", "))
371366
}
@@ -376,6 +371,11 @@ func ThreatHuman(w io.Writer, r *brief.ThreatReport) {
376371
}
377372
_, _ = fmt.Fprintf(w, "Stack: %s\n", strings.Join(names, ", "))
378373
}
374+
375+
if len(r.Threats) == 0 {
376+
_, _ = fmt.Fprintln(w, "\nNo threat categories match the detected stack.")
377+
return
378+
}
379379
_, _ = fmt.Fprintln(w)
380380

381381
for _, t := range r.Threats {

report/report_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ func TestThreatHuman(t *testing.T) {
252252
func TestThreatHumanEmpty(t *testing.T) {
253253
var buf bytes.Buffer
254254
ThreatHuman(&buf, &brief.ThreatReport{})
255-
if !strings.Contains(buf.String(), "No security data available") {
255+
if !strings.Contains(buf.String(), "No threat categories match") {
256256
t.Errorf("expected empty message, got:\n%s", buf.String())
257257
}
258258
}

0 commit comments

Comments
 (0)