Files
AetherForge/server/internal/recon/recon_test.go
AetherForge f3e5a9a07d
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add browser deploy recon backend with port scan, web crawl, and deploy lane recommendations.
POST /api/v1/recon/scan probes fleet ports from the server host, crawls owned HTTP targets, maps findings to spread lanes, and records optional oath ledger rows.
2026-06-07 11:23:10 -07:00

187 lines
4.4 KiB
Go

package recon
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
)
func TestScanPortsWithInject(t *testing.T) {
SetPortDialHook(func(host string, port int, _ time.Duration) bool {
if host != "10.0.0.5" {
t.Fatalf("host=%q", host)
}
return port == 22 || port == 443
})
t.Cleanup(func() { SetPortDialHook(nil) })
results := ScanPorts("10.0.0.5")
open := map[int]bool{}
for _, r := range results {
if r.Open {
open[r.Port] = true
}
}
if !open[22] || !open[443] {
t.Fatalf("open=%v", open)
}
if open[445] {
t.Fatal("445 should be closed")
}
}
func TestParseHTMLFixtures(t *testing.T) {
body := `<html><head><title>Upload</title></head><body>
<form action="/import" method="post" enctype="multipart/form-data">
<input type="file" name="payload">
<input type="text" name="webhook_url" value="">
</form>
<a href="/admin/login">Admin</a>
<link href="/wp-content/themes/x/style.css">
</body></html>`
files, multi, fields, score, cms := ParseHTML("http://lab/upload", body)
if len(files) != 1 || !files[0].HasFile {
t.Fatalf("file inputs: %+v", files)
}
if len(multi) != 1 || !multi[0].Multipart {
t.Fatalf("multipart: %+v", multi)
}
if len(fields) == 0 {
t.Fatal("expected url fields")
}
if score < 10 {
t.Fatalf("ssrf score=%d", score)
}
if !containsStr(cms, "wordpress") && !containsStr(cms, "admin_login") {
t.Fatalf("cms=%v", cms)
}
}
func TestCrawlSameOriginDepth(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
w.Write([]byte(`<html><a href="/page2">next</a><a href="http://evil.example/x">off</a></html>`))
case "/page2":
w.Write([]byte(`<html><a href="/page3">deep</a></html>`))
case "/page3":
w.Write([]byte(`<html>leaf</html>`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
u, err := url.Parse(srv.URL)
if err != nil {
t.Fatal(err)
}
port := 80
if p := u.Port(); p != "" {
port = atoi(p)
}
SetFetchPageHook(func(rawURL string) (int, string, error) {
resp, err := http.Get(rawURL)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
body, _ := readBodyLimited(resp.Body, maxHTMLBytes)
return resp.StatusCode, body, nil
})
t.Cleanup(func() { SetFetchPageHook(nil) })
report, err := Crawl(u.Hostname(), port, u.Scheme, []string{"/"})
if err != nil {
t.Fatal(err)
}
if report.PagesFetched < 2 {
t.Fatalf("pages=%d", report.PagesFetched)
}
for _, p := range report.Pages {
if strings.Contains(p.URL, "evil.example") {
t.Fatalf("followed off-origin %s", p.URL)
}
}
}
func TestScanReportRecommendations(t *testing.T) {
ports := []PortResult{
{Port: 22, Open: true},
{Port: 5985, Open: true},
{Port: 80, Open: true},
}
crawl := &CrawlReport{
MultipartForms: []FormFinding{{PageURL: "http://x/", Multipart: true}},
SSRFScore: 40,
}
recs := BuildRecommendations(ports, crawl)
if len(recs) < 5 {
t.Fatalf("recs=%+v", recs)
}
keys := map[string]bool{}
for _, r := range recs {
keys[r.Lane+"|"+r.Template] = true
}
for _, want := range []string{"linux_lotl|linux-lotl", "winrm|winrm", "bits_curl|", "stage_fetch|", "|ssrf_probe", "|public_waterhole"} {
if !keys[want] {
t.Fatalf("missing %q in %+v", want, recs)
}
}
}
func TestScanOwnedTarget(t *testing.T) {
SetPortDialHook(func(host string, port int, _ time.Duration) bool {
return port == 80
})
t.Cleanup(func() { SetPortDialHook(nil) })
SetFetchPageHook(func(rawURL string) (int, string, error) {
return 200, `<html><form enctype="multipart/form-data"><input type="file" name="f"></form></html>`, nil
})
t.Cleanup(func() { SetFetchPageHook(nil) })
report, err := Scan(ScanRequest{Host: "owned.lab", Port: 80, Scheme: "http"})
if err != nil {
t.Fatal(err)
}
if report.Host != "owned.lab" {
t.Fatalf("host=%q", report.Host)
}
if report.Crawl == nil || len(report.Recommendations) == 0 {
raw, _ := json.Marshal(report)
t.Fatalf("report=%s", raw)
}
}
func TestNormalizeOwnedHostRejectsEmpty(t *testing.T) {
if _, err := Scan(ScanRequest{}); err == nil {
t.Fatal("expected error")
}
}
func containsStr(list []string, want string) bool {
for _, s := range list {
if s == want {
return true
}
}
return false
}
func atoi(s string) int {
n := 0
for _, c := range s {
if c < '0' || c > '9' {
return 80
}
n = n*10 + int(c-'0')
}
if n == 0 {
return 80
}
return n
}