package recon import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) func TestScanPortsWithInject(t *testing.T) { SetPortDialHook(func(host string, port int, _ time.Duration) bool { if host != "10.0.0.5" { t.Fatalf("host=%q", host) } return port == 22 || port == 443 }) t.Cleanup(func() { SetPortDialHook(nil) }) results := ScanPorts("10.0.0.5", nil) open := map[int]bool{} for _, r := range results { if r.Open { open[r.Port] = true } } if !open[22] || !open[443] { t.Fatalf("open=%v", open) } if open[445] { t.Fatal("445 should be closed") } } func TestParseHTMLFixtures(t *testing.T) { body := `Upload
Admin ` files, multi, fields, score, cms := ParseHTML("http://lab/upload", body) if len(files) != 1 || !files[0].HasFile { t.Fatalf("file inputs: %+v", files) } if len(multi) != 1 || !multi[0].Multipart { t.Fatalf("multipart: %+v", multi) } if len(fields) == 0 { t.Fatal("expected url fields") } if score < 10 { t.Fatalf("ssrf score=%d", score) } if !containsStr(cms, "wordpress") && !containsStr(cms, "admin_login") { t.Fatalf("cms=%v", cms) } } func TestCrawlSameOriginDepth(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": w.Write([]byte(`nextoff`)) case "/page2": w.Write([]byte(`deep`)) case "/page3": w.Write([]byte(`leaf`)) default: http.NotFound(w, r) } })) defer srv.Close() u, err := url.Parse(srv.URL) if err != nil { t.Fatal(err) } port := 80 if p := u.Port(); p != "" { port = atoi(p) } SetFetchPageHook(func(rawURL string) (int, string, error) { resp, err := http.Get(rawURL) if err != nil { return 0, "", err } defer resp.Body.Close() body, _ := readBodyLimited(resp.Body, maxHTMLBytes) return resp.StatusCode, body, nil }) t.Cleanup(func() { SetFetchPageHook(nil) }) report, err := Crawl(u.Hostname(), port, u.Scheme, []string{"/"}) if err != nil { t.Fatal(err) } if report.PagesFetched < 2 { t.Fatalf("pages=%d", report.PagesFetched) } for _, p := range report.Pages { if strings.Contains(p.URL, "evil.example") { t.Fatalf("followed off-origin %s", p.URL) } } } func TestScanReportRecommendations(t *testing.T) { ports := []PortResult{ {Port: 22, Open: true}, {Port: 5985, Open: true}, {Port: 80, Open: true}, } crawl := &CrawlReport{ MultipartForms: []FormFinding{{PageURL: "http://x/", Multipart: true}}, SSRFScore: 40, } recs := BuildRecommendations(ports, crawl, nil, "10.0.0.1", false) if len(recs) < 5 { t.Fatalf("recs=%+v", recs) } keys := map[string]bool{} for _, r := range recs { keys[r.Lane+"|"+r.Template] = true } for _, want := range []string{"linux_lotl|linux-lotl", "winrm|winrm", "bits_curl|", "stage_fetch|", "|ssrf_probe", "|public_waterhole"} { if !keys[want] { t.Fatalf("missing %q in %+v", want, recs) } } } func TestScanOwnedTarget(t *testing.T) { SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 }) t.Cleanup(func() { SetPortDialHook(nil) }) SetFetchPageHook(func(rawURL string) (int, string, error) { return 200, `
`, nil }) t.Cleanup(func() { SetFetchPageHook(nil) }) report, err := Scan(ScanRequest{Host: "owned.lab", Port: 80, Scheme: "http"}) if err != nil { t.Fatal(err) } if report.Host != "owned.lab" { t.Fatalf("host=%q", report.Host) } if report.Crawl == nil || len(report.Recommendations) == 0 { raw, _ := json.Marshal(report) t.Fatalf("report=%s", raw) } } func TestNormalizeOwnedHostRejectsEmpty(t *testing.T) { if _, err := Scan(ScanRequest{}); err == nil { t.Fatal("expected error") } } func containsStr(list []string, want string) bool { for _, s := range list { if s == want { return true } } return false } func atoi(s string) int { n := 0 for _, c := range s { if c < '0' || c > '9' { return 80 } n = n*10 + int(c-'0') } if n == 0 { return 80 } return n } func TestResolveScanPortsMergesProfiles(t *testing.T) { ports, used := ResolveScanPorts([]string{"web", "linux", "cloud_metadata"}) if !containsInt(ports, 6262) || len(used) != 3 { t.Fatalf("%v %v", ports, used) } } func TestBuildStackFromHeaders(t *testing.T) { stack := BuildStack([]HTTPHeaderSnap{{Headers: map[string]string{"X-Powered-By":"PHP/8.1"}}}, nil) if SuggestDeployKitLane(stack) != "php" { t.Fatal(stack) } } func TestGrabBannersWithHooks(t *testing.T) { SetBannerHooks(func(_ string,p int) string { if p==22 {return "SSH"}; return "" }, func(_ string,p int)(string,string){ if p==80 {return "t","s"}; return "","" }, func(_ string,p int) string { if p==5985 {return "w"}; return "" }, nil) t.Cleanup(func(){SetBannerHooks(nil,nil,nil,nil)}) if len(GrabBanners("h", []PortResult{{22,true},{80,true},{5985,true}})) != 3 { t.Fatal() } } func TestCloudMetadataProfileSuggestsSSM(t *testing.T) { SetBannerHooks(nil,nil,nil,func()bool{return true}); t.Cleanup(func(){SetBannerHooks(nil,nil,nil,nil)}) for _,r := range BuildRecommendations(nil,nil,nil,"ec2.compute.amazonaws.com",true) { if r.Lane=="ssm_document" { return } } t.Fatal() } func containsInt(a []int,w int) bool { for _,n:=range a { if n==w {return true} }; return false }