yellow~ opinionated web scanning pipeline for pentesters in a hurry |
| git clone https://github.com/djnnvx/yellow.git |
| log | files | refs | archive | README | LICENSE |
nuclei_test.go (3577B)
1 package scan 2 3 import ( 4 "fmt" 5 "testing" 6 7 "github.com/projectdiscovery/nuclei/v3/pkg/output" 8 ) 9 10 func TestMatchLocation(t *testing.T) { 11 cases := []struct { 12 name string 13 event output.ResultEvent 14 want string 15 }{ 16 {"prefers matched", output.ResultEvent{Matched: "https://x.tld/a", URL: "https://x.tld", Host: "x.tld"}, "https://x.tld/a"}, 17 {"falls back to url", output.ResultEvent{URL: "https://x.tld", Host: "x.tld"}, "https://x.tld"}, 18 {"falls back to host", output.ResultEvent{Host: "x.tld"}, "x.tld"}, 19 {"never empty", output.ResultEvent{}, "unknown"}, 20 } 21 22 for _, c := range cases { 23 if got := matchLocation(&c.event); got != c.want { 24 t.Errorf("%s: got %q, want %q", c.name, got, c.want) 25 } 26 } 27 } 28 29 // The exact shape nuclei emits once its host-error cache gives up on a target. 30 // Recording these as findings is what turned one rate-limited host into 95913 31 // "findings", 16283 of them critical, on a static site. 32 func TestFindingSetRejectsNonMatches(t *testing.T) { 33 s := newFindingSet() 34 35 skipped := &output.ResultEvent{ 36 TemplateID: "dbgate-anonymous-access", 37 Host: "https://evil.djnn.sh", 38 MatcherStatus: false, 39 Error: "host was skipped as it was found unresponsive", 40 } 41 if f, _ := s.add(skipped); f != nil { 42 t.Error("skipped-host event must not become a finding") 43 } 44 if f, _ := s.add(nil); f != nil { 45 t.Error("nil event must not become a finding") 46 } 47 if f, _ := s.add(&output.ResultEvent{TemplateID: "x", Matched: "https://x.tld/a"}); f != nil { 48 t.Error("MatcherStatus false must not become a finding") 49 } 50 51 if s.hits() != 0 || len(s.sorted()) != 0 { 52 t.Errorf("set must stay empty, got %d hit(s) in %d group(s)", s.hits(), len(s.sorted())) 53 } 54 55 real := &output.ResultEvent{TemplateID: "x", Matched: "https://x.tld/a", MatcherStatus: true} 56 if f, isNew := s.add(real); f == nil || !isNew { 57 t.Error("a real match must be recorded") 58 } 59 } 60 61 func TestFindingSetGroups(t *testing.T) { 62 event := func(id, matched string) *output.ResultEvent { 63 return &output.ResultEvent{TemplateID: id, Matched: matched, MatcherStatus: true} 64 } 65 66 s := newFindingSet() 67 if _, isNew := s.add(event("wp-optimize", "https://evil.tld/a")); !isNew { 68 t.Error("first hit must open a new group") 69 } 70 if _, isNew := s.add(event("wp-optimize", "https://evil.tld/b")); isNew { 71 t.Error("same template on same host must join the existing group") 72 } 73 s.add(event("wp-optimize", "https://evil.tld/c")) 74 s.add(event("wp-optimize", "https://other.tld/a")) 75 s.add(event("aem-bypass", "https://evil.tld/a")) 76 77 if s.hits() != 5 { 78 t.Errorf("hits: got %d, want 5", s.hits()) 79 } 80 81 got := s.sorted() 82 if len(got) != 3 { 83 t.Fatalf("groups: got %d, want 3", len(got)) 84 } 85 if got[0].Host != "evil.tld" || got[0].TemplateID != "wp-optimize" || len(got[0].Matched) != 3 { 86 t.Errorf("noisiest group first: got %+v", got[0]) 87 } 88 if len(got[1].Matched) != 1 || len(got[2].Matched) != 1 { 89 t.Errorf("tail groups should hold one hit each: got %+v, %+v", got[1], got[2]) 90 } 91 } 92 93 func TestFindingSetConcurrent(t *testing.T) { 94 s := newFindingSet() 95 done := make(chan struct{}) 96 97 for i := range 8 { 98 go func(i int) { 99 defer func() { done <- struct{}{} }() 100 for j := range 50 { 101 s.add(&output.ResultEvent{ 102 TemplateID: "tpl", 103 Matched: fmt.Sprintf("https://x.tld/%d-%d", i, j), 104 MatcherStatus: true, 105 }) 106 } 107 }(i) 108 } 109 for range 8 { 110 <-done 111 } 112 113 if s.hits() != 400 { 114 t.Errorf("hits: got %d, want 400", s.hits()) 115 } 116 if got := s.sorted(); len(got) != 1 { 117 t.Errorf("all hits share template+host, want 1 group, got %d", len(got)) 118 } 119 }