mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
* 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.
149 lines
5.6 KiB
Go
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())
|
|
}
|
|
}
|