Files
sif/internal/scan/favicon_test.go
T
TigahandGitHub 0f11283b1e refactor(scan): extract favicon hashing into internal/fingerprint (#182)
move FaviconHash and its base64 chunking into a leaf package so the scan check and the module engine share one implementation instead of each carrying a copy. no behavior change; the golden test moves with it.
2026-06-22 17:43:30 -07:00

119 lines
4.4 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · 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"
)
// 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)
}
}
// TestFavicon_LinkFallback covers the <link rel=icon> 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(`<html><head><link rel="icon" href="/static/icon.png"></head></html>`))
}
}))
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 TestFaviconResult_ResultType(t *testing.T) {
r := &FaviconResult{}
if r.ResultType() != "favicon" {
t.Errorf("expected result type 'favicon', got %q", r.ResultType())
}
}