Files
god-eye/internal/scanner/cloud.go
Vyntral 14718dd75f 🚀 God's Eye v0.1 - Initial Release
God's Eye is an ultra-fast subdomain enumeration and reconnaissance tool with AI-powered security analysis.

##  Key Features

### 🔍 Comprehensive Enumeration
- 20+ passive sources (crt.sh, Censys, URLScan, etc.)
- DNS brute-force with smart wordlists
- Wildcard detection and filtering
- 1000 concurrent workers for maximum speed

### 🌐 Deep Reconnaissance
- HTTP probing with 13+ security checks
- Port scanning (configurable)
- TLS/SSL fingerprinting
- Technology detection (Wappalyzer-style)
- WAF detection (Cloudflare, Akamai, etc.)
- Security header analysis
- JavaScript secrets extraction
- Admin panel & API discovery
- Backup file detection
- robots.txt & sitemap.xml checks

### 🎯 Subdomain Takeover Detection
- 110+ fingerprints (AWS, Azure, GitHub Pages, Heroku, etc.)
- CNAME validation
- Dead DNS detection

### 🤖 AI-Powered Analysis (NEW!)
- Local AI using Ollama - No API costs, complete privacy
- Real-time CVE detection via function calling (queries NVD database)
- Cascade architecture: phi3.5 (fast triage) + qwen2.5-coder (deep analysis)
- JavaScript security analysis
- HTTP response anomaly detection
- Executive summary reports

### 📊 Output Formats
- Pretty terminal output with colors
- JSON export
- CSV export
- TXT (simple subdomain list)
- Silent mode for piping

## 🚀 Installation

bash
go install github.com/Vyntral/god-eye@latest

## 📖 Quick Start

bash
# Basic scan
god-eye -d example.com

# With AI analysis
god-eye -d example.com --enable-ai

# Only active hosts
god-eye -d example.com --active

# Export to JSON
god-eye -d example.com -o results.json -f json

## 🎯 Use Cases
- Bug bounty reconnaissance
- Penetration testing
- Security audits
- Attack surface mapping
- Red team operations

## ⚠️ Legal Notice
This tool is for authorized security testing only. Users must obtain explicit permission before scanning any targets. Unauthorized access is illegal.

## 📄 License
MIT License with additional security tool terms - see LICENSE file

## 🙏 Credits
Built with ❤️ by Vyntral for Orizon
Powered by Go, Ollama, and the security community

---

🤖 Generated with Claude Code
https://claude.com/claude-code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 10:41:05 +01:00

277 lines
6.6 KiB
Go

package scanner
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/miekg/dns"
)
// DetectCloudProvider detects cloud provider based on IP/CNAME
func DetectCloudProvider(ips []string, cname string, asn string) string {
// Check CNAME patterns
cnamePatterns := map[string]string{
"amazonaws.com": "AWS",
"aws.com": "AWS",
"cloudfront.net": "AWS CloudFront",
"elasticbeanstalk.com": "AWS Elastic Beanstalk",
"elb.amazonaws.com": "AWS ELB",
"s3.amazonaws.com": "AWS S3",
"azure.com": "Azure",
"azurewebsites.net": "Azure App Service",
"cloudapp.net": "Azure",
"azurefd.net": "Azure Front Door",
"blob.core.windows.net": "Azure Blob",
"googleapis.com": "Google Cloud",
"appspot.com": "Google App Engine",
"storage.googleapis.com": "Google Cloud Storage",
"digitaloceanspaces.com": "DigitalOcean Spaces",
"ondigitalocean.app": "DigitalOcean App Platform",
"cloudflare.com": "Cloudflare",
"fastly.net": "Fastly",
"akamai.net": "Akamai",
"netlify.app": "Netlify",
"vercel.app": "Vercel",
"herokuapp.com": "Heroku",
}
for pattern, provider := range cnamePatterns {
if strings.Contains(cname, pattern) {
return provider
}
}
// Check ASN patterns
asnPatterns := map[string]string{
"AS14618": "AWS",
"AS16509": "AWS",
"AS8075": "Azure",
"AS15169": "Google Cloud",
"AS14061": "DigitalOcean",
"AS13335": "Cloudflare",
"AS54113": "Fastly",
"AS20940": "Akamai",
}
for pattern, provider := range asnPatterns {
if strings.Contains(asn, pattern) {
return provider
}
}
return ""
}
// CheckS3Buckets checks for exposed S3 buckets
func CheckS3Buckets(subdomain string, timeout int) []string {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Common S3 bucket URL patterns
parts := strings.Split(subdomain, ".")
bucketName := parts[0]
patterns := []string{
fmt.Sprintf("https://%s.s3.amazonaws.com", bucketName),
fmt.Sprintf("https://s3.amazonaws.com/%s", bucketName),
fmt.Sprintf("https://%s.s3.us-east-1.amazonaws.com", bucketName),
fmt.Sprintf("https://%s.s3.us-west-2.amazonaws.com", bucketName),
fmt.Sprintf("https://%s.s3.eu-west-1.amazonaws.com", bucketName),
}
var found []string
for _, url := range patterns {
resp, err := client.Get(url)
if err != nil {
continue
}
resp.Body.Close()
// Public bucket if 200 or 403 (exists but forbidden)
if resp.StatusCode == 200 {
found = append(found, url+" (PUBLIC)")
} else if resp.StatusCode == 403 {
found = append(found, url+" (exists)")
}
}
return found
}
// CheckEmailSecurity checks SPF/DKIM/DMARC records
func CheckEmailSecurity(domain string, resolvers []string, timeout int) (spf string, dmarc string, security string) {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
// Check SPF record
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
for _, ans := range r.Answer {
if txt, ok := ans.(*dns.TXT); ok {
for _, t := range txt.Txt {
if strings.HasPrefix(t, "v=spf1") {
spf = t
if len(spf) > 80 {
spf = spf[:77] + "..."
}
break
}
}
}
}
if spf != "" {
break
}
}
// Check DMARC record
m2 := dns.Msg{}
m2.SetQuestion(dns.Fqdn("_dmarc."+domain), dns.TypeTXT)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m2, resolver)
if err != nil || r == nil {
continue
}
for _, ans := range r.Answer {
if txt, ok := ans.(*dns.TXT); ok {
for _, t := range txt.Txt {
if strings.HasPrefix(t, "v=DMARC1") {
dmarc = t
if len(dmarc) > 80 {
dmarc = dmarc[:77] + "..."
}
break
}
}
}
}
if dmarc != "" {
break
}
}
// Determine email security level
if spf != "" && dmarc != "" {
if strings.Contains(dmarc, "p=reject") || strings.Contains(dmarc, "p=quarantine") {
security = "Strong"
} else {
security = "Moderate"
}
} else if spf != "" || dmarc != "" {
security = "Weak"
} else {
security = "None"
}
return spf, dmarc, security
}
// GetTLSAltNames extracts Subject Alternative Names from TLS certificate
func GetTLSAltNames(subdomain string, timeout int) []string {
conn, err := tls.DialWithDialer(
&net.Dialer{Timeout: time.Duration(timeout) * time.Second},
"tcp",
subdomain+":443",
&tls.Config{InsecureSkipVerify: true},
)
if err != nil {
return nil
}
defer conn.Close()
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
return nil
}
var altNames []string
seen := make(map[string]bool)
for _, cert := range certs {
for _, name := range cert.DNSNames {
if !seen[name] && name != subdomain {
seen[name] = true
altNames = append(altNames, name)
}
}
}
// Limit to first 10
if len(altNames) > 10 {
altNames = altNames[:10]
}
return altNames
}
// CheckS3BucketsWithClient checks for exposed S3 buckets with shared client
func CheckS3BucketsWithClient(subdomain string, client *http.Client) []string {
parts := strings.Split(subdomain, ".")
if len(parts) < 2 {
return nil
}
subPrefix := parts[0]
// Get domain name (e.g., "finnat" from "ftp.finnat.it")
var domainName string
if len(parts) >= 2 {
domainName = parts[len(parts)-2]
}
// Skip generic subdomain names that cause false positives
genericNames := map[string]bool{
"www": true, "ftp": true, "mail": true, "smtp": true, "imap": true,
"pop": true, "webmail": true, "autodiscover": true, "test": true,
"dev": true, "staging": true, "api": true, "admin": true, "pop3": true,
}
var patterns []string
if genericNames[subPrefix] {
// For generic subdomains, use domain-specific bucket names
patterns = []string{
fmt.Sprintf("https://%s-%s.s3.amazonaws.com", domainName, subPrefix),
fmt.Sprintf("https://%s.s3.amazonaws.com", domainName),
}
} else {
// For specific subdomains, use combination
patterns = []string{
fmt.Sprintf("https://%s-%s.s3.amazonaws.com", domainName, subPrefix),
fmt.Sprintf("https://%s.s3.amazonaws.com", domainName),
}
}
var found []string
for _, url := range patterns {
resp, err := client.Get(url)
if err != nil {
continue
}
resp.Body.Close()
// Only report PUBLIC buckets (200), not just existing (403)
if resp.StatusCode == 200 {
found = append(found, url+" (PUBLIC)")
}
}
return found
}