/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package fingerprint import ( "strings" "testing" ) // 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 pinned shodan mmh3 hash of goldenFaviconBytes: the python // base64.encodebytes byte stream (76-char lines + trailing newline) through murmur3-32, // reinterpreted as a signed int32. if the chunking or signedness regress, this test fails. const goldenFaviconHash int32 = -1554620260 // goldenHelloHash pins a short single-line case so a regression in the trailing // newline (which the small case still has) is caught independently. const goldenHelloHash int32 = 1155597304 func TestFaviconHashGolden(t *testing.T) { tests := []struct { name string in []byte want int32 }{ {name: "multi-line fixture", in: goldenFaviconBytes, want: goldenFaviconHash}, {name: "single-line hello", in: []byte("hello"), want: goldenHelloHash}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := FaviconHash(tt.in); got != tt.want { t.Errorf("FaviconHash = %d, want %d", got, tt.want) } }) } } // TestLookupFaviconTech proves the SSOT table is the single place all ten // facts are resolved: every entry round-trips, an unknown hash misses, and the // count is pinned so an accidental addition/removal is caught. func TestLookupFaviconTech(t *testing.T) { if len(faviconTech) != 10 { t.Fatalf("faviconTech has %d entries, want 10", len(faviconTech)) } for hash, want := range faviconTech { got, ok := LookupFaviconTech(hash) if !ok { t.Errorf("LookupFaviconTech(%d) ok = false, want true", hash) } if got != want { t.Errorf("LookupFaviconTech(%d) = %q, want %q", hash, got, want) } } if tech, ok := LookupFaviconTech(0); ok { t.Errorf("LookupFaviconTech(0) = (%q, true), want (\"\", false)", tech) } tests := []struct { hash int32 want string }{ {hash: -1255347784, want: "GitLab"}, {hash: 116323821, want: "Apache Tomcat"}, } for _, tt := range tests { if got, ok := LookupFaviconTech(tt.hash); !ok || got != tt.want { t.Errorf("LookupFaviconTech(%d) = (%q, %v), want (%q, true)", tt.hash, got, ok, tt.want) } } } // TestFaviconBase64Chunking pins the encode step against python's // base64.encodebytes: a 60-byte input encodes to 80 base64 chars, so it must // wrap into two newline-terminated lines. func TestFaviconBase64Chunking(t *testing.T) { in := []byte(strings.Repeat("A", 60)) got := string(encodeFaviconBase64(in)) lines := strings.Split(strings.TrimRight(got, "\n"), "\n") if len(lines) != 2 { t.Fatalf("expected 2 wrapped lines, got %d: %q", len(lines), got) } if len(lines[0]) != b64LineLen { t.Errorf("first line = %d chars, want %d", len(lines[0]), b64LineLen) } if !strings.HasSuffix(got, "\n") { t.Errorf("encoding must end in a trailing newline, got %q", got) } }