Files
sif/internal/scan/favicon_test.go
T
TigahandGitHub 059d7356bf refactor(fingerprint): make the favicon tech table the single source (#358)
* refactor(fingerprint): make the favicon tech table the single source

move the hash->tech map from internal/scan into internal/fingerprint
beside the hash function, exposed as LookupFaviconTech. the scan
Favicon path now resolves tech through it instead of a private map.

drop the demo-sync guard test: it existed only to keep scan's map in
sync with the yaml demo module, and that map no longer exists.

* feat(modules): name the matched tech in favicon evidence

favicon module findings now read the tech straight from the shared
fingerprint table, so a canonical hit reports "favicon mmh3=<n>
tech=<name>" instead of a bare hash. unknown hashes are unaffected.
2026-07-22 12:57:09 -07:00

149 lines
5.6 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"
"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 <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 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())
}
}