package api import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/go-chi/chi/v5" "crypto-miner-server/internal/db" "crypto-miner-server/internal/recon" ) func waitForReconScan(t *testing.T, database *db.Database, scanID string) *recon.ScanReport { t.Helper() deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) { report, err := database.GetReconScan(scanID) if err == nil && report != nil { return report } time.Sleep(20 * time.Millisecond) } t.Fatalf("scan %s not persisted", scanID) return nil } func withScanIDParam(r *http.Request, scanID string) *http.Request { ctx := chi.NewRouteContext() ctx.URLParams.Add("scan_id", scanID) return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx)) } func TestReconScanEndpoint(t *testing.T) { recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 }) t.Cleanup(func() { recon.SetPortDialHook(nil) }) recon.SetFetchPageHook(func(rawURL string) (int, string, error) { return 200, `
`, nil }) t.Cleanup(func() { recon.SetFetchPageHook(nil) }) database, _ := db.New(t.TempDir()) t.Cleanup(func() { _ = database.Close() }) h := NewReconHandler(database, NewWSHub(database)) body, _ := json.Marshal(map[string]interface{}{"host": "recon.lab", "port": 80, "scheme": "http"}) w := httptest.NewRecorder() h.Scan(w, httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader(body))) if w.Code != http.StatusAccepted { t.Fatalf("%d %s", w.Code, w.Body.String()) } var started map[string]string _ = json.Unmarshal(w.Body.Bytes(), &started) report := waitForReconScan(t, database, started["scan_id"]) if report.Host != "recon.lab" || report.Crawl == nil { t.Fatalf("%+v", report) } } func TestReconSSRfCanaryFlow(t *testing.T) { recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 }) t.Cleanup(func() { recon.SetPortDialHook(nil) }) recon.SetFetchPageHook(func(rawURL string) (int, string, error) { return 200, ``, nil }) t.Cleanup(func() { recon.SetFetchPageHook(nil) }) database, _ := db.New(t.TempDir()) t.Cleanup(func() { _ = database.Close() }) h := NewReconHandler(database, nil, func() string { return "https://canary.test" }) body, _ := json.Marshal(map[string]interface{}{"host": "recon.lab", "port": 80, "scheme": "http", "ssrf_canary": true}) w := httptest.NewRecorder() h.Scan(w, httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader(body))) if w.Code != http.StatusAccepted { t.Fatalf("%d", w.Code) } var started map[string]string _ = json.Unmarshal(w.Body.Bytes(), &started) report := waitForReconScan(t, database, started["scan_id"]) if report.Canary == nil || report.Canary.PasteFieldName != "callback_url" { t.Fatalf("%+v", report.Canary) } pw := httptest.NewRecorder() h.CanaryPing(pw, withScanIDParam(httptest.NewRequest(http.MethodGet, "/recon/ping/"+report.ScanID, nil), report.ScanID)) sw := httptest.NewRecorder() h.CanaryStatus(sw, withScanIDParam(httptest.NewRequest(http.MethodGet, "/api/v1/recon/canary/"+report.ScanID, nil), report.ScanID)) var st recon.SSRFCanaryInfo _ = json.Unmarshal(sw.Body.Bytes(), &st) if st.Status != "confirmed" { t.Fatalf("%+v", st) } if !strings.Contains(report.Canary.PasteTarget, report.ScanID) { t.Fatal(report.Canary.PasteTarget) } } func TestReconHistoryAndExport(t *testing.T) { recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 80 }) t.Cleanup(func() { recon.SetPortDialHook(nil) }) recon.SetFetchPageHook(func(rawURL string) (int, string, error) { return 200, ``, nil }) t.Cleanup(func() { recon.SetFetchPageHook(nil) }) database, _ := db.New(t.TempDir()) t.Cleanup(func() { _ = database.Close() }) h := NewReconHandler(database, nil) body, _ := json.Marshal(map[string]interface{}{"host": "hist.lab", "port": 80, "scheme": "http", "profile": "quick"}) w := httptest.NewRecorder() h.Scan(w, httptest.NewRequest(http.MethodPost, "/api/v1/recon/scan", bytes.NewReader(body))) var started map[string]string _ = json.Unmarshal(w.Body.Bytes(), &started) waitForReconScan(t, database, started["scan_id"]) histW := httptest.NewRecorder() h.History(histW, httptest.NewRequest(http.MethodGet, "/api/v1/recon/history?host=hist.lab", nil)) if histW.Code != http.StatusOK { t.Fatalf("history %d", histW.Code) } exportW := httptest.NewRecorder() h.Export(exportW, withScanIDParam(httptest.NewRequest(http.MethodGet, "/api/v1/recon/export/"+started["scan_id"]+"?format=pdf", nil), started["scan_id"])) if exportW.Code != http.StatusOK { t.Fatalf("export %d %s", exportW.Code, exportW.Body.String()) } }