yellow.git


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 }