Files
god-eye/internal/scanner/advanced.go
Vyntral 14c26dc726 feat: Add Multi-Agent AI Orchestration with 8 specialized agents
- Implement 8 specialized AI agents (XSS, SQLi, Auth, API, Crypto, Secrets, Headers, General)
- Add fast type-based routing for finding classification
- Include OWASP-aligned knowledge bases per agent
- Add agent handoff logic for cross-vulnerability detection
- Optimize timeouts and parallelism for local LLM
- Add new modules: cache, network, fingerprint, secrets, cloud, API, discovery
- Update documentation with multi-agent feature
2025-11-21 15:23:11 +01:00

650 lines
16 KiB
Go

package scanner
import (
"context"
"strings"
"sync"
"god-eye/internal/api"
"god-eye/internal/cloud"
"god-eye/internal/config"
"god-eye/internal/fingerprint"
"god-eye/internal/network"
"god-eye/internal/output"
"god-eye/internal/progress"
"god-eye/internal/secrets"
)
// AdvancedConfig holds configuration for advanced scanning
type AdvancedConfig struct {
Domain string
Timeout int
Concurrency int
CloudScan bool
APIScan bool
SecretsScan bool
TechScan bool
ASNScan bool
VHostScan bool
Silent bool
JsonOutput bool
}
// AdvancedResults holds results from advanced scanning
type AdvancedResults struct {
CloudAssets []cloud.CloudAsset
APIFindings []api.APIFinding
Secrets []secrets.SecretFinding
Technologies map[string][]fingerprint.Technology // host -> technologies
ASNInfo map[string]*network.ASNInfo // ip -> ASN info
VHosts map[string]*network.VHostResult // ip -> virtual hosts
}
// RunAdvancedScans performs cloud, API, and secrets scanning (sequential for ordered output)
func RunAdvancedScans(ctx context.Context, results map[string]*config.SubdomainResult,
resultsMu *sync.Mutex, cfg AdvancedConfig) *AdvancedResults {
advResults := &AdvancedResults{}
// 1. Cloud Asset Discovery (first)
if cfg.CloudScan {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("☁️", "CLOUD ASSET DISCOVERY")
}
cloudScanner := cloud.NewCloudScanner(cfg.Domain, cfg.Timeout)
assets := cloudScanner.ScanAll(ctx)
// Also check for Lambda/Cloud Functions
lambdaAssets := cloudScanner.CheckLambdaEndpoints(ctx)
assets = append(assets, lambdaAssets...)
advResults.CloudAssets = assets
if !cfg.Silent && !cfg.JsonOutput {
publicCount := 0
privateCount := 0
for _, asset := range assets {
if asset.Status == "public" {
publicCount++
} else if asset.Status == "private" {
privateCount++
}
}
if len(assets) > 0 {
output.PrintSubSection(output.Green("✓") + " Found " +
output.BoldRed(intToString(publicCount)) + " public, " +
output.BoldYellow(intToString(privateCount)) + " private cloud assets")
// Show top findings
shown := 0
for _, asset := range assets {
if shown >= 5 {
break
}
if asset.Status == "public" {
output.PrintSubSection(" " + output.Red("⚠ ") +
output.BoldWhite(asset.Type) + " " +
output.Cyan(asset.Name) + " - " +
output.Red("PUBLIC"))
shown++
}
}
} else {
output.PrintSubSection(output.Dim("No public cloud assets found"))
}
}
}
// 2. API Intelligence (second)
if cfg.APIScan {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🔌", "API INTELLIGENCE")
}
apiScanner := api.NewAPIScanner(cfg.Timeout)
// Scan each active subdomain
resultsMu.Lock()
hosts := make([]string, 0)
for sub, result := range results {
if result.StatusCode >= 200 && result.StatusCode < 500 {
hosts = append(hosts, sub)
}
}
resultsMu.Unlock()
var allFindings []api.APIFinding
var findingsMu sync.Mutex
// Limit concurrent API scans
sem := make(chan struct{}, 5)
var apiWg sync.WaitGroup
for _, host := range hosts {
apiWg.Add(1)
go func(h string) {
defer apiWg.Done()
select {
case <-ctx.Done():
return
case sem <- struct{}{}:
defer func() { <-sem }()
}
findings := apiScanner.ScanHost(ctx, h)
if len(findings) > 0 {
findingsMu.Lock()
allFindings = append(allFindings, findings...)
findingsMu.Unlock()
}
}(host)
}
apiWg.Wait()
advResults.APIFindings = allFindings
if !cfg.Silent && !cfg.JsonOutput {
graphqlCount := 0
swaggerCount := 0
sensitiveCount := 0
for _, f := range allFindings {
switch f.Type {
case "graphql":
graphqlCount++
case "swagger":
swaggerCount++
case "rest":
if f.Issue == "sensitive_endpoint" {
sensitiveCount++
}
}
}
if len(allFindings) > 0 {
output.PrintSubSection(output.Green("✓") + " Found " +
output.BoldCyan(intToString(graphqlCount)) + " GraphQL, " +
output.BoldCyan(intToString(swaggerCount)) + " Swagger, " +
output.BoldYellow(intToString(sensitiveCount)) + " sensitive endpoints")
// Show critical findings
for _, f := range allFindings {
if f.Issue == "introspection_enabled" {
output.PrintSubSection(" " + output.Red("⚠ ") +
"GraphQL introspection enabled at " + output.Cyan(f.URL))
}
if f.Issue == "api_documentation_exposed" {
output.PrintSubSection(" " + output.Yellow("! ") +
"API docs exposed at " + output.Cyan(f.URL))
}
}
} else {
output.PrintSubSection(output.Dim("No critical API findings"))
}
}
}
// 3. Secrets Discovery (third)
if cfg.SecretsScan {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🔑", "PASSIVE CREDENTIAL DISCOVERY")
}
secretScanner := secrets.NewSecretScanner(cfg.Domain, cfg.Timeout)
secretFindings := secretScanner.ScanAll(ctx)
advResults.Secrets = secretFindings
if !cfg.Silent && !cfg.JsonOutput {
criticalCount := 0
highCount := 0
for _, s := range secretFindings {
if s.Severity == "critical" {
criticalCount++
} else if s.Severity == "high" {
highCount++
}
}
if len(secretFindings) > 0 {
output.PrintSubSection(output.Green("✓") + " Found " +
output.BoldRed(intToString(criticalCount)) + " critical, " +
output.BoldYellow(intToString(highCount)) + " high severity findings")
// Show critical findings
shown := 0
for _, s := range secretFindings {
if shown >= 5 {
break
}
if s.Severity == "critical" || s.Severity == "high" {
output.PrintSubSection(" " + output.Red("⚠ ") +
output.BoldWhite(s.Type) + " in " +
output.Cyan(s.Source) + ": " +
output.Dim(s.Description))
shown++
}
}
} else {
output.PrintSubSection(output.Dim("No secrets found in public sources"))
}
}
}
// 4. Technology Fingerprinting
if cfg.TechScan {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🔍", "TECHNOLOGY FINGERPRINTING")
}
techScanner := fingerprint.NewTechScanner(cfg.Timeout)
// Get active hosts
resultsMu.Lock()
hosts := make([]string, 0)
for sub, result := range results {
if result.StatusCode >= 200 && result.StatusCode < 500 {
hosts = append(hosts, sub)
}
}
resultsMu.Unlock()
// Scan for technologies
advResults.Technologies = techScanner.ScanMultipleHosts(ctx, hosts, 10)
// Enrich with CVEs
for host, techs := range advResults.Technologies {
advResults.Technologies[host] = fingerprint.EnrichWithCVEs(techs)
}
if !cfg.Silent && !cfg.JsonOutput {
totalTechs := 0
techCounts := make(map[string]int)
var criticalCVEs []fingerprint.CVEMatch
for _, techs := range advResults.Technologies {
totalTechs += len(techs)
for _, tech := range techs {
techCounts[tech.Category]++
}
criticalCVEs = append(criticalCVEs, fingerprint.GetCriticalCVEs(techs)...)
}
if totalTechs > 0 {
output.PrintSubSection(output.Green("✓") + " Detected " +
output.BoldCyan(intToString(totalTechs)) + " technologies across " +
output.BoldWhite(intToString(len(advResults.Technologies))) + " hosts")
// Show category breakdown
for category, count := range techCounts {
if count > 0 {
output.PrintSubSection(" " + output.Dim(category+": ") + output.Cyan(intToString(count)))
}
}
// Show critical CVEs
if len(criticalCVEs) > 0 {
output.PrintSubSection("")
output.PrintSubSection(output.BoldRed("⚠ ") + output.BoldRed(intToString(len(criticalCVEs))) +
output.Red(" CRITICAL CVEs found (actively exploited):"))
shown := 0
for _, cve := range criticalCVEs {
if shown >= 5 {
break
}
ransomware := ""
if cve.Ransomware {
ransomware = output.Red(" [RANSOMWARE]")
}
output.PrintSubSection(" " + output.Red("• ") +
output.BoldYellow(cve.CVEID) + " - " +
output.Cyan(cve.Product) + ransomware)
shown++
}
}
} else {
output.PrintSubSection(output.Dim("No technologies detected"))
}
}
}
// 5. ASN/CIDR Expansion
if cfg.ASNScan {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🌐", "ASN/CIDR EXPANSION")
}
asnScanner := network.NewASNScanner(cfg.Timeout)
advResults.ASNInfo = make(map[string]*network.ASNInfo)
// Get unique IPs from results
resultsMu.Lock()
seenIPs := make(map[string]bool)
var uniqueIPs []string
for _, result := range results {
for _, ip := range result.IPs {
if !seenIPs[ip] {
seenIPs[ip] = true
uniqueIPs = append(uniqueIPs, ip)
}
}
}
resultsMu.Unlock()
// Limit to first 10 unique IPs for ASN lookups (rate limit friendly)
if len(uniqueIPs) > 10 {
uniqueIPs = uniqueIPs[:10]
}
var asnMu sync.Mutex
var asnWg sync.WaitGroup
asnSem := make(chan struct{}, 3) // Conservative concurrency
for _, ip := range uniqueIPs {
asnWg.Add(1)
go func(ipAddr string) {
defer asnWg.Done()
select {
case <-ctx.Done():
return
case asnSem <- struct{}{}:
defer func() { <-asnSem }()
}
info, err := asnScanner.GetASNInfo(ctx, ipAddr)
if err == nil && info != nil {
asnMu.Lock()
advResults.ASNInfo[ipAddr] = info
asnMu.Unlock()
}
}(ip)
}
asnWg.Wait()
if !cfg.Silent && !cfg.JsonOutput {
if len(advResults.ASNInfo) > 0 {
// Count unique ASNs
asnSet := make(map[string]bool)
for _, info := range advResults.ASNInfo {
if info.ASN != "" {
asnSet[info.ASN] = true
}
}
output.PrintSubSection(output.Green("✓") + " Discovered " +
output.BoldCyan(intToString(len(asnSet))) + " unique ASNs across " +
output.BoldWhite(intToString(len(advResults.ASNInfo))) + " IPs")
// Show ASN details
shown := 0
shownASN := make(map[string]bool)
for ip, info := range advResults.ASNInfo {
if shown >= 5 {
break
}
if info.ASN != "" && !shownASN[info.ASN] {
shownASN[info.ASN] = true
cidrInfo := ""
if info.CIDR != "" {
cidrInfo = output.Dim(" (") + output.Yellow(info.CIDR) + output.Dim(")")
}
output.PrintSubSection(" " + output.Cyan("AS"+info.ASN) + " - " +
output.BoldWhite(info.Name) + cidrInfo +
output.Dim(" ["+ip+"]"))
shown++
}
}
} else {
output.PrintSubSection(output.Dim("No ASN information discovered"))
}
}
}
// 6. Virtual Host Discovery
if cfg.VHostScan {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🏠", "VIRTUAL HOST DISCOVERY")
}
vhostScanner := network.NewVHostScanner(cfg.Timeout)
advResults.VHosts = make(map[string]*network.VHostResult)
// Get unique IPs from results
resultsMu.Lock()
seenIPs := make(map[string]bool)
var uniqueIPs []string
for _, result := range results {
for _, ip := range result.IPs {
if !seenIPs[ip] {
seenIPs[ip] = true
uniqueIPs = append(uniqueIPs, ip)
}
}
}
resultsMu.Unlock()
// Limit to first 5 IPs for vhost discovery (rate limit friendly)
if len(uniqueIPs) > 5 {
uniqueIPs = uniqueIPs[:5]
}
advResults.VHosts = vhostScanner.DiscoverMultipleIPs(ctx, uniqueIPs, 3)
if !cfg.Silent && !cfg.JsonOutput {
totalVHosts := 0
for _, vhost := range advResults.VHosts {
totalVHosts += len(vhost.Domains)
}
if totalVHosts > 0 {
output.PrintSubSection(output.Green("✓") + " Found " +
output.BoldCyan(intToString(totalVHosts)) + " virtual hosts across " +
output.BoldWhite(intToString(len(advResults.VHosts))) + " IPs")
// Show top vhosts
shown := 0
for ip, vhost := range advResults.VHosts {
if shown >= 3 {
break
}
if len(vhost.Domains) > 0 {
domainList := ""
for i, d := range vhost.Domains {
if i >= 3 {
domainList += output.Dim(", +"+intToString(len(vhost.Domains)-3)+" more")
break
}
if i > 0 {
domainList += ", "
}
domainList += output.Cyan(d)
}
output.PrintSubSection(" " + output.Yellow(ip) + ": " + domainList)
shown++
}
}
} else {
output.PrintSubSection(output.Dim("No additional virtual hosts discovered"))
}
}
}
// Update results with findings
updateResultsWithAdvanced(results, resultsMu, advResults)
return advResults
}
// updateResultsWithAdvanced adds advanced findings to subdomain results
func updateResultsWithAdvanced(results map[string]*config.SubdomainResult, resultsMu *sync.Mutex, adv *AdvancedResults) {
resultsMu.Lock()
defer resultsMu.Unlock()
// Add cloud assets to relevant subdomains
for _, asset := range adv.CloudAssets {
// Add to the main domain result or first result
for _, result := range results {
result.CloudAssets = append(result.CloudAssets, config.CloudAssetResult{
Type: asset.Type,
Name: asset.Name,
URL: asset.URL,
Provider: asset.Provider,
Status: asset.Status,
Permissions: asset.Permissions,
})
break // Add to first result only to avoid duplication
}
}
// Add API findings to relevant subdomains
for _, finding := range adv.APIFindings {
// Find the subdomain this finding belongs to
for sub, result := range results {
if containsHost(finding.URL, sub) {
result.APIFindings = append(result.APIFindings, config.APIFindingResult{
Type: finding.Type,
URL: finding.URL,
Issue: finding.Issue,
Severity: finding.Severity,
Endpoints: finding.Endpoints,
})
break
}
}
}
// Add secrets to relevant subdomains
for _, secret := range adv.Secrets {
// Add to the main domain result
for _, result := range results {
result.SecretsFound = append(result.SecretsFound, config.SecretResult{
Type: secret.Type,
Source: secret.Source,
Match: secret.Match,
Severity: secret.Severity,
Description: secret.Description,
})
break // Add to first result only
}
}
}
// containsHost checks if a URL contains a specific host
func containsHost(urlStr, host string) bool {
if len(urlStr) == 0 || len(host) == 0 {
return false
}
// Simple string contains check
return strings.Contains(urlStr, host)
}
// intToString converts int to string
func intToString(n int) string {
if n == 0 {
return "0"
}
var digits []byte
negative := n < 0
if negative {
n = -n
}
for n > 0 {
digits = append([]byte{byte('0' + n%10)}, digits...)
n /= 10
}
if negative {
digits = append([]byte{'-'}, digits...)
}
return string(digits)
}
// RunAdvancedWithProgress runs advanced scans with progress bar
func RunAdvancedWithProgress(ctx context.Context, results map[string]*config.SubdomainResult,
resultsMu *sync.Mutex, cfg AdvancedConfig) *AdvancedResults {
// Count total scans to perform
total := 0
if cfg.CloudScan {
total++
}
if cfg.APIScan {
total++
}
if cfg.SecretsScan {
total++
}
if total == 0 {
return &AdvancedResults{}
}
bar := progress.New(total, "Advanced", cfg.Silent || cfg.JsonOutput)
defer bar.Finish()
advResults := &AdvancedResults{}
var mu sync.Mutex
// Cloud scanning
if cfg.CloudScan {
cloudScanner := cloud.NewCloudScanner(cfg.Domain, cfg.Timeout)
assets := cloudScanner.ScanAll(ctx)
lambdaAssets := cloudScanner.CheckLambdaEndpoints(ctx)
assets = append(assets, lambdaAssets...)
mu.Lock()
advResults.CloudAssets = assets
mu.Unlock()
bar.Increment()
}
// API scanning
if cfg.APIScan {
apiScanner := api.NewAPIScanner(cfg.Timeout)
resultsMu.Lock()
hosts := make([]string, 0)
for sub, result := range results {
if result.StatusCode >= 200 && result.StatusCode < 500 {
hosts = append(hosts, sub)
}
}
resultsMu.Unlock()
var allFindings []api.APIFinding
for _, host := range hosts {
select {
case <-ctx.Done():
break
default:
}
findings := apiScanner.ScanHost(ctx, host)
allFindings = append(allFindings, findings...)
}
mu.Lock()
advResults.APIFindings = allFindings
mu.Unlock()
bar.Increment()
}
// Secrets scanning
if cfg.SecretsScan {
secretScanner := secrets.NewSecretScanner(cfg.Domain, cfg.Timeout)
secretFindings := secretScanner.ScanAll(ctx)
mu.Lock()
advResults.Secrets = secretFindings
mu.Unlock()
bar.Increment()
}
updateResultsWithAdvanced(results, resultsMu, advResults)
return advResults
}