mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 19:11:25 -07:00
dbe79c495e
-crawl spiders same-host links/scripts/forms through the shared httpx client so proxy/headers/rate-limit and robots.txt are honored, bounded by -crawl-depth. -passive pulls subdomains from keyless ct feeds (crt.sh, certspotter) and historical urls from wayback, each source isolated so one feed being down doesn't sink the rest and the target sees no traffic.
164 lines
5.4 KiB
Go
164 lines
5.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"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// sample feed payloads. crt.sh packs several names per name_value (newline
|
|
// separated) and emits wildcards; certspotter returns expanded dns_names.
|
|
const (
|
|
crtshFixture = `[
|
|
{"name_value": "www.example.com\n*.example.com"},
|
|
{"name_value": "api.example.com"},
|
|
{"name_value": "WWW.example.com"}
|
|
]`
|
|
certspotterFixture = `[
|
|
{"dns_names": ["mail.example.com", "api.example.com"]},
|
|
{"dns_names": ["*.example.com"]}
|
|
]`
|
|
waybackFixture = "http://example.com/\n" +
|
|
"http://example.com/login\n" +
|
|
"http://example.com/login\n" +
|
|
"\n" +
|
|
"http://example.com/admin\n"
|
|
)
|
|
|
|
// fixtureServer serves each passive source on its own path and repoints the
|
|
// package base-url vars at it. the vars are restored on cleanup.
|
|
func fixtureServer(t *testing.T, crtsh, certspotter, wayback string) *httptest.Server {
|
|
t.Helper()
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/crtsh", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(crtsh))
|
|
})
|
|
mux.HandleFunc("/certspotter", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(certspotter))
|
|
})
|
|
mux.HandleFunc("/wayback", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(wayback))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
|
|
origCrtsh, origCertspotter, origWayback := crtshBaseURL, certspotterBaseURL, waybackBaseURL
|
|
// %s still consumes the domain so the production formatting path is exercised.
|
|
crtshBaseURL = srv.URL + "/crtsh?q=%s"
|
|
certspotterBaseURL = srv.URL + "/certspotter?domain=%s"
|
|
waybackBaseURL = srv.URL + "/wayback?url=%s"
|
|
t.Cleanup(func() {
|
|
crtshBaseURL, certspotterBaseURL, waybackBaseURL = origCrtsh, origCertspotter, origWayback
|
|
})
|
|
|
|
return srv
|
|
}
|
|
|
|
func TestPassive_ParsesAndDedupes(t *testing.T) {
|
|
fixtureServer(t, crtshFixture, certspotterFixture, waybackFixture)
|
|
|
|
result, err := Passive("https://example.com", 5*time.Second, "")
|
|
if err != nil {
|
|
t.Fatalf("Passive: %v", err)
|
|
}
|
|
|
|
// wildcards stripped, case-folded, and merged across both ct feeds.
|
|
wantSubs := map[string]bool{
|
|
"www.example.com": false,
|
|
"api.example.com": false,
|
|
"mail.example.com": false,
|
|
"example.com": false, // from "*.example.com"
|
|
}
|
|
for _, s := range result.Subdomains {
|
|
if _, ok := wantSubs[s]; !ok {
|
|
t.Errorf("unexpected subdomain %q", s)
|
|
continue
|
|
}
|
|
wantSubs[s] = true
|
|
}
|
|
for s, seen := range wantSubs {
|
|
if !seen {
|
|
t.Errorf("missing subdomain %q in %v", s, result.Subdomains)
|
|
}
|
|
}
|
|
if len(result.Subdomains) != len(wantSubs) {
|
|
t.Errorf("expected %d deduped subdomains, got %d: %v", len(wantSubs), len(result.Subdomains), result.Subdomains)
|
|
}
|
|
|
|
// wayback: blank line dropped, duplicate /login collapsed.
|
|
wantURLs := map[string]bool{
|
|
"http://example.com/": false,
|
|
"http://example.com/login": false,
|
|
"http://example.com/admin": false,
|
|
}
|
|
for _, u := range result.URLs {
|
|
if _, ok := wantURLs[u]; !ok {
|
|
t.Errorf("unexpected url %q", u)
|
|
continue
|
|
}
|
|
wantURLs[u] = true
|
|
}
|
|
if len(result.URLs) != len(wantURLs) {
|
|
t.Errorf("expected %d deduped urls, got %d: %v", len(wantURLs), len(result.URLs), result.URLs)
|
|
}
|
|
}
|
|
|
|
func TestPassive_SourceFailureIsIsolated(t *testing.T) {
|
|
// crt.sh serves garbage that fails to parse; the other feeds must still
|
|
// produce results.
|
|
fixtureServer(t, "not json", certspotterFixture, waybackFixture)
|
|
|
|
result, err := Passive("https://example.com", 5*time.Second, "")
|
|
if err != nil {
|
|
t.Fatalf("Passive should not fail when one source is down: %v", err)
|
|
}
|
|
|
|
if len(result.Subdomains) == 0 {
|
|
t.Error("expected certspotter subdomains despite crt.sh failure")
|
|
}
|
|
if len(result.URLs) == 0 {
|
|
t.Error("expected wayback urls despite crt.sh failure")
|
|
}
|
|
if urlsContain(result.Subdomains, "www.example.com") {
|
|
t.Error("crt.sh-only subdomain leaked despite parse failure")
|
|
}
|
|
}
|
|
|
|
func TestPassive_ResultType(t *testing.T) {
|
|
r := &PassiveResult{}
|
|
if r.ResultType() != "passive" {
|
|
t.Errorf("ResultType = %q, want passive", r.ResultType())
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHost(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"www.example.com", "www.example.com"},
|
|
{"*.example.com", "example.com"},
|
|
{" WWW.Example.COM ", "www.example.com"},
|
|
{"", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := normalizeHost(tt.in); got != tt.want {
|
|
t.Errorf("normalizeHost(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
}
|
|
}
|