Files
sif/internal/fingerprint/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

104 lines
4.0 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · 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)
}
}