/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package scan import ( "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/vmfunc/sif/internal/fingerprint" ) // goldenFaviconBytes is a fixed payload long enough to span multiple base64 // lines, so the python-style 76-char chunking is actually exercised by the hash. var goldenFaviconBytes = []byte(strings.Repeat("sif-favicon-golden-test-bytes-", 8)) // goldenFaviconHash is the shodan mmh3 hash of goldenFaviconBytes. it is pinned: // the value comes from feeding the python base64.encodebytes byte stream (newline // every 76 chars + trailing newline) through murmur3-32 and reinterpreting the // result as a signed int32 - exactly what shodan stores. if the chunking or the // signedness regress, this number changes and the test fails. const goldenFaviconHash int32 = -1554620260 // fixtureFaviconServer serves the golden bytes at /favicon.ico. func fixtureFaviconServer() *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/favicon.ico" { w.Header().Set("Content-Type", "image/x-icon") _, _ = w.Write(goldenFaviconBytes) return } w.WriteHeader(http.StatusNotFound) })) } func TestFavicon_FetchAndHash(t *testing.T) { srv := fixtureFaviconServer() defer srv.Close() result, err := Favicon(srv.URL, 5*time.Second, "") if err != nil { t.Fatalf("Favicon: %v", err) } if result == nil { t.Fatal("expected a favicon result, got nil") } if result.Hash != goldenFaviconHash { t.Errorf("Hash = %d, want %d", result.Hash, goldenFaviconHash) } wantQ := "http.favicon.hash:-1554620260" if result.ShodanQ != wantQ { t.Errorf("ShodanQ = %q, want %q", result.ShodanQ, wantQ) } wantTech, _ := fingerprint.LookupFaviconTech(fingerprint.FaviconHash(goldenFaviconBytes)) if result.Tech != wantTech { t.Errorf("Tech = %q, want %q", result.Tech, wantTech) } } // TestFavicon_LinkFallback covers the path when /favicon.ico is // absent: the homepage points at /static/icon.png and that's what gets hashed. func TestFavicon_LinkFallback(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/favicon.ico": w.WriteHeader(http.StatusNotFound) case "/static/icon.png": _, _ = w.Write(goldenFaviconBytes) default: _, _ = w.Write([]byte(`
`)) } })) defer srv.Close() result, err := Favicon(srv.URL, 5*time.Second, "") if err != nil { t.Fatalf("Favicon: %v", err) } if result == nil { t.Fatal("expected a favicon result via link fallback, got nil") } if !strings.HasSuffix(result.FaviconURL, "/static/icon.png") { t.Errorf("FaviconURL = %q, want it to end in /static/icon.png", result.FaviconURL) } if result.Hash != goldenFaviconHash { t.Errorf("Hash = %d, want %d", result.Hash, goldenFaviconHash) } } // TestFavicon_NoIcon confirms a target with no favicon at all yields no result // and no error. func TestFavicon_NoIcon(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer srv.Close() result, err := Favicon(srv.URL, 5*time.Second, "") if err != nil { t.Fatalf("Favicon: %v", err) } if result != nil { t.Errorf("expected nil result for missing favicon, got %+v", result) } } func TestResolveFaviconURL(t *testing.T) { cases := []struct { name string base string href string want string }{ // see resolveFaviconURL's doc comment (favicon.go) for the anchoring rule. {"root-relative against pathful base", "https://example.com/app", "/favicon.ico", "https://example.com/favicon.ico"}, {"root-relative against bare base", "https://example.com", "/static/icon.png", "https://example.com/static/icon.png"}, {"absolute href kept", "https://example.com", "https://cdn.example.net/f.ico", "https://cdn.example.net/f.ico"}, {"scheme-relative inherits https", "https://example.com", "//cdn.example.net/f.ico", "https://cdn.example.net/f.ico"}, {"scheme-relative inherits http", "http://example.com", "//cdn.example.net/f.ico", "http://cdn.example.net/f.ico"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := resolveFaviconURL(tc.base, tc.href); got != tc.want { t.Errorf("resolveFaviconURL(%q, %q) = %q, want %q", tc.base, tc.href, got, tc.want) } }) } } func TestFaviconResult_ResultType(t *testing.T) { r := &FaviconResult{} if r.ResultType() != "favicon" { t.Errorf("expected result type 'favicon', got %q", r.ResultType()) } }