mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-03-19 01:23:26 +00:00
- 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
650 lines
16 KiB
Go
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
|
|
}
|