func NormalizeHost(host string) (string, error) { return normalizeOwnedHost(host) } func runOwnedTargetScan(req ScanRequest, scanID string, emit StreamEmit) (*ScanReport, error) { host, err := normalizeOwnedHost(req.Host) if err != nil { return nil, err } uxProfile := NormalizeProfile(req.Profile) opts := ProfileOptions(uxProfile) portsToScan, profilesUsed := ResolveScanPortsFromRequest(req) if uxProfile == ProfileQuick && len(req.Profiles) == 0 && len(portsToScan) > 10 { portsToScan = portsToScan[:10] } report := &ScanReport{ScanID: scanID, Host: host, Profile: uxProfile, ProfilesUsed: profilesUsed, Status: "complete", ScannedAt: time.Now().UTC()} if !opts.SkipPorts { ports := scanPortsList(host, portsToScan) report.Ports = ports report.Banners = GrabBanners(host, ports) } if shouldCrawlProfile(req, uxProfile, report.Ports, opts) { crawl, err := crawlWithOptions(host, req.Port, req.Scheme, req.Paths, opts, scanID, emit) if err == nil && crawl != nil { report.Crawl = crawl report.Stack = MergeStack(crawl.Stack) } } report.DeployKitLane = SuggestDeployKitLane(report.Stack) cloudMeta := containsPortProfile(profilesUsed, PortProfileCloudMetadata) report.Recommendations = BuildRecommendations(report.Ports, report.Crawl, report.Stack, host, cloudMeta) return report, nil } const (ProfileQuick = "quick"; ProfileDeep = "deep"; ProfileSSRFOnly = "ssrf_only") type StreamEmit func(eventType string, payload map[string]interface{}) type ScanOptions struct { Ports []int; MaxPages, CrawlDepth int; SkipPorts, SkipCrawl bool } func NormalizeProfile(profile string) string { switch profile { case ProfileQuick, ProfileDeep, ProfileSSRFOnly: return profile; default: return "" } } func ProfileOptions(profile string) ScanOptions { switch NormalizeProfile(profile) { case ProfileQuick: p := FleetPorts if len(p) > 10 { p = p[:10] } return ScanOptions{Ports: p, MaxPages: 1, CrawlDepth: 0} case ProfileDeep: return ScanOptions{Ports: FleetPorts, MaxPages: 50, CrawlDepth: 2} case ProfileSSRFOnly: return ScanOptions{SkipPorts: true, MaxPages: 50, CrawlDepth: 2} default: return ScanOptions{Ports: FleetPorts, MaxPages: DefaultCrawlMaxPages, CrawlDepth: DefaultCrawlDepth} } } func ScanStream(req ScanRequest, scanID string, emit StreamEmit) (*ScanReport, error) { return runOwnedTargetScan(req, scanID, emit) } func scanPortsList(host string, ports []int) []PortResult { if len(ports) == 0 { return ScanPorts(host, nil) } out := make([]PortResult, 0, len(ports)) for _, port := range ports { out = append(out, PortResult{Port: port, Open: dialPort(host, port, DefaultPortDialTimeout)}) } return out } func shouldCrawlProfile(req ScanRequest, profile string, ports []PortResult, opts ScanOptions) bool { if opts.SkipCrawl { return false } if profile == ProfileSSRFOnly { return true } return shouldCrawl(req, ports) } func crawlWithOptions(host string, port int, scheme string, seedPaths []string, opts ScanOptions, scanID string, emit StreamEmit) (*CrawlReport, error) { scheme = normalizeScheme(scheme, port) if port <= 0 { port = defaultPortForScheme(scheme) } base := fmt.Sprintf("%s://%s", scheme, joinHostPort(host, port)) seeds := seedPaths if len(seeds) == 0 { seeds = []string{"/"} } maxPages, maxDepth := opts.MaxPages, opts.CrawlDepth if maxPages <= 0 { maxPages = DefaultCrawlMaxPages } if maxDepth < 0 { maxDepth = DefaultCrawlDepth } report := &CrawlReport{} var headerSnaps []HTTPHeaderSnap var htmlBodies []string visited := map[string]bool{} queue := []queuedURL{} for _, p := range seeds { if abs, err := resolveSameOrigin(base, p); err == nil { queue = append(queue, queuedURL{url: abs, depth: 0}) } } for len(queue) > 0 && report.PagesFetched < maxPages { item := queue[0]; queue = queue[1:] key := normalizeURLKey(item.url) if visited[key] { continue } visited[key] = true status, body, headers, err := fetchPage(item.url) if err != nil { continue } report.PagesFetched++ title, _ := htmlParseTitle(body) report.Pages = append(report.Pages, PageFinding{URL: item.url, StatusCode: status, Title: title}) if len(headers) > 0 { headerSnaps = append(headerSnaps, HTTPHeaderSnap{URL: item.url, Headers: headers}) } htmlBodies = append(htmlBodies, body) files, multi, fields, pageScore, cms := ParseHTML(item.url, body) report.FileInputs = append(report.FileInputs, files...) report.MultipartForms = append(report.MultipartForms, multi...) report.URLFields = append(report.URLFields, fields...) report.SSRFScore += pageScore report.CMSFingerprints = mergeCMS(report.CMSFingerprints, cms) if item.depth >= maxDepth { continue } for _, link := range extractLinks(body) { abs, err := resolveSameOrigin(base, link) if err != nil || !sameOrigin(base, abs) { continue } if !visited[normalizeURLKey(abs)] { queue = append(queue, queuedURL{url: abs, depth: item.depth + 1}) } } } if report.SSRFScore > 100 { report.SSRFScore = 100 } report.CMSFingerprints = mergeCMS(nil, report.CMSFingerprints) report.Stack = BuildStack(headerSnaps, htmlBodies) return report, nil } func OpenPorts(ports []PortResult) []int { var o []int; for _, p := range ports { if p.Open { o = append(o, p.Port) } }; return o } func DiffReports(prev, cur *ScanReport) *ReconScanDiff { if cur == nil { return nil } d := &ReconScanDiff{} if prev == nil { d.NewPorts = OpenPorts(cur.Ports); return d } po := map[int]bool{} for _, p := range prev.Ports { if p.Open { po[p.Port] = true } } for _, p := range cur.Ports { if p.Open && !po[p.Port] { d.NewPorts = append(d.NewPorts, p.Port) } } return d } func BuildHistory(rows []*ScanReport) []ReconHistoryEntry { out := make([]ReconHistoryEntry, 0, len(rows)) var prev *ScanReport for _, r := range rows { out = append(out, ReconHistoryEntry{ScanID: r.ScanID, Host: r.Host, Profile: r.Profile, Status: r.Status, ScannedAt: r.ScannedAt, Report: r, Diff: DiffReports(prev, r)}); prev = r } return out } func ReportToPDF(report *ScanReport) []byte { return []byte("AetherForge Recon\n") } var metaGeneratorRe = regexp.MustCompile(`(?is)]+name=["']generator["'][^>]+content=["']([^"']+)["']`) var bannerTitleRe = regexp.MustCompile(`(?is)