v0.1.1: Major AI improvements, new security modules, and documentation fixes

## AI & CVE Improvements
- Fix AI report to display actual subdomain names instead of generic placeholders
- Add 10-year CVE filter to reduce false positives from outdated vulnerabilities
- Integrate CISA KEV (Known Exploited Vulnerabilities) database support
- Improve AI analysis prompt for more accurate security findings

## New Security Modules
- Add wildcard DNS detection with multi-phase validation (DNS + HTTP)
- Add TLS certificate analyzer for certificate chain inspection
- Add comprehensive rate limiting module for API requests
- Add retry mechanism with exponential backoff
- Add stealth mode for reduced detection during scans
- Add progress tracking module for better UX

## Code Refactoring
- Extract scanner output logic to dedicated module
- Add base source interface for consistent passive source implementation
- Reduce admin panel paths to common generic patterns only
- Improve HTTP client with connection pooling
- Add JSON output formatter

## Documentation Updates
- Correct passive source count to 20 (was incorrectly stated as 34)
- Fix AI model names: deepseek-r1:1.5b (fast) + qwen2.5-coder:7b (deep)
- Update all markdown files for consistency
- Relocate demo GIFs to assets/ directory
- Add benchmark disclaimer for test variability

## Files Changed
- 4 documentation files updated (README, AI_SETUP, BENCHMARK, EXAMPLES)
- 11 new source files added
- 12 existing files modified
This commit is contained in:
Vyntral committed 2025-11-21 12:00:58 +01:00
1 parent 2be93a0dbb
commit 45295bb262
28 files changed
+4650 -882

No files matched your search

+177
View File
@@ -0,0 +1,177 @@
package scanner
import (
"bufio"
"fmt"
"net"
"os"
"sort"
"strings"
"sync"
"time"
"god-eye/internal/config"
)
// LoadWordlist loads words from a file
func LoadWordlist(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var words []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
word := strings.TrimSpace(scanner.Text())
if word != "" && !strings.HasPrefix(word, "#") {
words = append(words, word)
}
}
return words, scanner.Err()
}
// ScanPorts scans ports on an IP address
func ScanPorts(ip string, ports []int, timeout int) []int {
var openPorts []int
var mu sync.Mutex
var wg sync.WaitGroup
for _, port := range ports {
wg.Add(1)
go func(p int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", ip, p)
conn, err := net.DialTimeout("tcp", address, time.Duration(timeout)*time.Second)
if err == nil {
conn.Close()
mu.Lock()
openPorts = append(openPorts, p)
mu.Unlock()
}
}(port)
}
wg.Wait()
sort.Ints(openPorts)
return openPorts
}
// Helper functions for AI analysis
func countSubdomainsWithAI(results map[string]*config.SubdomainResult) int {
count := 0
for _, r := range results {
if len(r.AIFindings) > 0 {
count++
}
}
return count
}
func countActive(results map[string]*config.SubdomainResult) int {
count := 0
for _, r := range results {
if r.StatusCode >= 200 && r.StatusCode < 400 {
count++
}
}
return count
}
func countVulns(results map[string]*config.SubdomainResult) int {
count := 0
for _, r := range results {
if r.OpenRedirect || r.CORSMisconfig != "" || len(r.DangerousMethods) > 0 ||
r.GitExposed || r.SvnExposed || len(r.BackupFiles) > 0 {
count++
}
}
return count
}
func buildAISummary(results map[string]*config.SubdomainResult) string {
var summary strings.Builder
criticalCount := 0
highCount := 0
mediumCount := 0
for sub, r := range results {
if len(r.AIFindings) == 0 {
continue
}
switch r.AISeverity {
case "critical":
criticalCount++
summary.WriteString(fmt.Sprintf("\n[CRITICAL] %s:\n", sub))
case "high":
highCount++
summary.WriteString(fmt.Sprintf("\n[HIGH] %s:\n", sub))
case "medium":
mediumCount++
summary.WriteString(fmt.Sprintf("\n[MEDIUM] %s:\n", sub))
default:
continue
}
// Add first 3 findings
for i, finding := range r.AIFindings {
if i >= 3 {
break
}
summary.WriteString(fmt.Sprintf(" - %s\n", finding))
}
// Add CVE findings
if len(r.CVEFindings) > 0 {
summary.WriteString(" CVEs:\n")
for _, cve := range r.CVEFindings {
summary.WriteString(fmt.Sprintf(" - %s\n", cve))
}
}
}
header := fmt.Sprintf("Summary: %d critical, %d high, %d medium findings\n", criticalCount, highCount, mediumCount)
return header + summary.String()
}
// ParseResolvers parses custom resolvers string
func ParseResolvers(resolversStr string) []string {
var resolvers []string
if resolversStr != "" {
for _, r := range strings.Split(resolversStr, ",") {
r = strings.TrimSpace(r)
if r != "" {
if !strings.Contains(r, ":") {
r = r + ":53"
}
resolvers = append(resolvers, r)
}
}
}
if len(resolvers) == 0 {
resolvers = config.DefaultResolvers
}
return resolvers
}
// ParsePorts parses custom ports string
func ParsePorts(portsStr string) []int {
var customPorts []int
if portsStr != "" {
for _, p := range strings.Split(portsStr, ",") {
p = strings.TrimSpace(p)
var port int
if _, err := fmt.Sscanf(p, "%d", &port); err == nil && port > 0 && port < 65536 {
customPorts = append(customPorts, port)
}
}
}
if len(customPorts) == 0 {
customPorts = []int{80, 443, 8080, 8443}
}
return customPorts
}
+298 -95
View File
@@ -6,52 +6,87 @@ import (
"net/http"
"regexp"
"strings"
"sync"
)
// SecretPattern defines a pattern for finding secrets
type SecretPattern struct {
Name string
Pattern *regexp.Regexp
}
// Secret patterns to search for in JS files
var secretPatterns = []SecretPattern{
// API Keys
{Name: "AWS Access Key", Pattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`)},
{Name: "AWS Secret Key", Pattern: regexp.MustCompile(`(?i)aws[_\-]?secret[_\-]?access[_\-]?key['"\s:=]+['"]?([A-Za-z0-9/+=]{40})['"]?`)},
{Name: "Google API Key", Pattern: regexp.MustCompile(`AIza[0-9A-Za-z\-_]{35}`)},
{Name: "Google OAuth", Pattern: regexp.MustCompile(`[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com`)},
{Name: "Firebase API Key", Pattern: regexp.MustCompile(`(?i)firebase[_\-]?api[_\-]?key['"\s:=]+['"]?([A-Za-z0-9_\-]{39})['"]?`)},
{Name: "Stripe Key", Pattern: regexp.MustCompile(`(?:sk|pk)_(?:test|live)_[0-9a-zA-Z]{24,}`)},
{Name: "Stripe Restricted", Pattern: regexp.MustCompile(`rk_(?:test|live)_[0-9a-zA-Z]{24,}`)},
{Name: "GitHub Token", Pattern: regexp.MustCompile(`(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}`)},
{Name: "GitHub OAuth", Pattern: regexp.MustCompile(`github[_\-]?oauth[_\-]?token['"\s:=]+['"]?([a-f0-9]{40})['"]?`)},
{Name: "Slack Token", Pattern: regexp.MustCompile(`xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*`)},
{Name: "Slack Webhook", Pattern: regexp.MustCompile(`https://hooks\.slack\.com/services/T[a-zA-Z0-9_]{8,}/B[a-zA-Z0-9_]{8,}/[a-zA-Z0-9_]{24}`)},
{Name: "Discord Webhook", Pattern: regexp.MustCompile(`https://discord(?:app)?\.com/api/webhooks/[0-9]{17,20}/[A-Za-z0-9_\-]{60,}`)},
{Name: "Twilio API Key", Pattern: regexp.MustCompile(`SK[a-f0-9]{32}`)},
{Name: "Twilio Account SID", Pattern: regexp.MustCompile(`AC[a-f0-9]{32}`)},
{Name: "SendGrid API Key", Pattern: regexp.MustCompile(`SG\.[a-zA-Z0-9_\-]{22}\.[a-zA-Z0-9_\-]{43}`)},
{Name: "Mailgun API Key", Pattern: regexp.MustCompile(`key-[0-9a-zA-Z]{32}`)},
{Name: "Mailchimp API Key", Pattern: regexp.MustCompile(`[0-9a-f]{32}-us[0-9]{1,2}`)},
{Name: "Heroku API Key", Pattern: regexp.MustCompile(`(?i)heroku[_\-]?api[_\-]?key['"\s:=]+['"]?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})['"]?`)},
{Name: "DigitalOcean Token", Pattern: regexp.MustCompile(`dop_v1_[a-f0-9]{64}`)},
{Name: "NPM Token", Pattern: regexp.MustCompile(`npm_[A-Za-z0-9]{36}`)},
{Name: "PyPI Token", Pattern: regexp.MustCompile(`pypi-AgEIcHlwaS5vcmc[A-Za-z0-9_\-]{50,}`)},
{Name: "Square Access Token", Pattern: regexp.MustCompile(`sq0atp-[0-9A-Za-z\-_]{22}`)},
{Name: "Square OAuth", Pattern: regexp.MustCompile(`sq0csp-[0-9A-Za-z\-_]{43}`)},
{Name: "Shopify Access Token", Pattern: regexp.MustCompile(`shpat_[a-fA-F0-9]{32}`)},
{Name: "Shopify Shared Secret", Pattern: regexp.MustCompile(`shpss_[a-fA-F0-9]{32}`)},
{Name: "Algolia API Key", Pattern: regexp.MustCompile(`(?i)algolia[_\-]?api[_\-]?key['"\s:=]+['"]?([a-zA-Z0-9]{32})['"]?`)},
{Name: "Auth0 Client Secret", Pattern: regexp.MustCompile(`(?i)auth0[_\-]?client[_\-]?secret['"\s:=]+['"]?([a-zA-Z0-9_\-]{32,})['"]?`)},
// Generic secrets
{Name: "Generic API Key", Pattern: regexp.MustCompile(`(?i)['"]?api[_\-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,64})['"]`)},
{Name: "Generic Secret", Pattern: regexp.MustCompile(`(?i)['"]?(?:client[_\-]?)?secret['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,64})['"]`)},
{Name: "Generic Token", Pattern: regexp.MustCompile(`(?i)['"]?(?:access[_\-]?)?token['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-\.]{20,500})['"]`)},
{Name: "Generic Password", Pattern: regexp.MustCompile(`(?i)['"]?password['"]?\s*[:=]\s*['"]([^'"]{8,64})['"]`)},
{Name: "Private Key", Pattern: regexp.MustCompile(`-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----`)},
{Name: "Bearer Token", Pattern: regexp.MustCompile(`(?i)['"]?authorization['"]?\s*[:=]\s*['"]Bearer\s+([a-zA-Z0-9_\-\.]+)['"]`)},
{Name: "Basic Auth", Pattern: regexp.MustCompile(`(?i)['"]?authorization['"]?\s*[:=]\s*['"]Basic\s+([a-zA-Z0-9+/=]+)['"]`)},
{Name: "JWT Token", Pattern: regexp.MustCompile(`eyJ[a-zA-Z0-9_\-]*\.eyJ[a-zA-Z0-9_\-]*\.[a-zA-Z0-9_\-]*`)},
// Database connection strings
{Name: "MongoDB URI", Pattern: regexp.MustCompile(`mongodb(?:\+srv)?://[^\s'"]+`)},
{Name: "PostgreSQL URI", Pattern: regexp.MustCompile(`postgres(?:ql)?://[^\s'"]+`)},
{Name: "MySQL URI", Pattern: regexp.MustCompile(`mysql://[^\s'"]+`)},
{Name: "Redis URI", Pattern: regexp.MustCompile(`redis://[^\s'"]+`)},
}
// Endpoint patterns for API discovery - only external/interesting URLs
// Note: We exclude relative paths like /api/... as they're not secrets
var endpointPatterns = []*regexp.Regexp{
regexp.MustCompile(`['"]https?://api\.[a-zA-Z0-9\-\.]+[a-zA-Z0-9/\-_]*['"]`), // External API domains
regexp.MustCompile(`['"]https?://[a-zA-Z0-9\-\.]+\.amazonaws\.com[^'"]*['"]`), // AWS endpoints
regexp.MustCompile(`['"]https?://[a-zA-Z0-9\-\.]+\.azure\.com[^'"]*['"]`), // Azure endpoints
regexp.MustCompile(`['"]https?://[a-zA-Z0-9\-\.]+\.googleapis\.com[^'"]*['"]`), // Google API
regexp.MustCompile(`['"]https?://[a-zA-Z0-9\-\.]+\.firebaseio\.com[^'"]*['"]`), // Firebase
}
// AnalyzeJSFiles finds JavaScript files and extracts potential secrets
func AnalyzeJSFiles(subdomain string, client *http.Client) ([]string, []string) {
var jsFiles []string
var secrets []string
var mu sync.Mutex
urls := []string{
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
// Common JS file paths
jsPaths := []string{
"/main.js", "/app.js", "/bundle.js", "/vendor.js",
"/static/js/main.js", "/static/js/app.js",
"/assets/js/app.js", "/js/main.js", "/js/app.js",
"/dist/main.js", "/dist/bundle.js",
"/_next/static/chunks/main.js",
"/build/static/js/main.js",
}
// Secret patterns to search for
secretPatterns := []*regexp.Regexp{
regexp.MustCompile(`(?i)['"]?api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,})['"]`),
regexp.MustCompile(`(?i)['"]?aws[_-]?access[_-]?key[_-]?id['"]?\s*[:=]\s*['"]([A-Z0-9]{20})['"]`),
regexp.MustCompile(`(?i)['"]?aws[_-]?secret[_-]?access[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9/+=]{40})['"]`),
regexp.MustCompile(`(?i)['"]?google[_-]?api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{39})['"]`),
regexp.MustCompile(`(?i)['"]?firebase[_-]?api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{39})['"]`),
regexp.MustCompile(`(?i)['"]?stripe[_-]?(publishable|secret)[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,})['"]`),
regexp.MustCompile(`(?i)['"]?github[_-]?token['"]?\s*[:=]\s*['"]([a-zA-Z0-9_]{36,})['"]`),
regexp.MustCompile(`(?i)['"]?slack[_-]?token['"]?\s*[:=]\s*['"]([a-zA-Z0-9\-]{30,})['"]`),
regexp.MustCompile(`(?i)['"]?private[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9/+=]{50,})['"]`),
regexp.MustCompile(`(?i)['"]?secret['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,})['"]`),
regexp.MustCompile(`(?i)['"]?password['"]?\s*[:=]\s*['"]([^'"]{8,})['"]`),
regexp.MustCompile(`(?i)['"]?authorization['"]?\s*[:=]\s*['"]Bearer\s+([a-zA-Z0-9_\-\.]+)['"]`),
}
// Also search for API endpoints in JS
endpointPatterns := []*regexp.Regexp{
regexp.MustCompile(`(?i)['"]https?://[a-zA-Z0-9\-\.]+/api/[a-zA-Z0-9/\-_]+['"]`),
regexp.MustCompile(`(?i)['"]https?://api\.[a-zA-Z0-9\-\.]+[a-zA-Z0-9/\-_]*['"]`),
}
for _, baseURL := range urls {
// First, get the main page and extract JS file references
// First, get the main page and extract JS file references
var foundJSURLs []string
for _, baseURL := range baseURLs {
resp, err := client.Get(baseURL)
if err != nil {
continue
@@ -64,92 +99,260 @@ func AnalyzeJSFiles(subdomain string, client *http.Client) ([]string, []string)
}
// Find JS files referenced in HTML
jsRe := regexp.MustCompile(`src=["']([^"']*\.js[^"']*)["']`)
jsRe := regexp.MustCompile(`(?:src|href)=["']([^"']*\.js(?:\?[^"']*)?)["']`)
matches := jsRe.FindAllStringSubmatch(string(body), -1)
for _, match := range matches {
if len(match) > 1 {
jsURL := match[1]
if !strings.HasPrefix(jsURL, "http") {
if strings.HasPrefix(jsURL, "/") {
jsURL = baseURL + jsURL
} else {
jsURL = baseURL + "/" + jsURL
}
jsURL := normalizeURL(match[1], baseURL)
if jsURL != "" && !contains(foundJSURLs, jsURL) {
foundJSURLs = append(foundJSURLs, jsURL)
}
jsFiles = append(jsFiles, jsURL)
}
}
// Also check common JS paths
for _, path := range jsPaths {
testURL := baseURL + path
resp, err := client.Get(testURL)
if err != nil {
continue
}
if resp.StatusCode == 200 {
jsFiles = append(jsFiles, path)
// Read JS content and search for secrets
jsBody, err := io.ReadAll(io.LimitReader(resp.Body, 500000))
resp.Body.Close()
if err != nil {
continue
// Also look for dynamic imports and webpack chunks
dynamicRe := regexp.MustCompile(`["']([^"']*(?:chunk|bundle|vendor|main|app)[^"']*\.js(?:\?[^"']*)?)["']`)
dynamicMatches := dynamicRe.FindAllStringSubmatch(string(body), -1)
for _, match := range dynamicMatches {
if len(match) > 1 {
jsURL := normalizeURL(match[1], baseURL)
if jsURL != "" && !contains(foundJSURLs, jsURL) {
foundJSURLs = append(foundJSURLs, jsURL)
}
jsContent := string(jsBody)
// Search for secrets
for _, pattern := range secretPatterns {
if matches := pattern.FindAllStringSubmatch(jsContent, 3); len(matches) > 0 {
for _, m := range matches {
if len(m) > 1 {
secret := m[0]
if len(secret) > 60 {
secret = secret[:57] + "..."
}
secrets = append(secrets, secret)
}
}
}
}
// Search for API endpoints
for _, pattern := range endpointPatterns {
if matches := pattern.FindAllString(jsContent, 5); len(matches) > 0 {
for _, m := range matches {
if len(m) > 60 {
m = m[:57] + "..."
}
secrets = append(secrets, "endpoint: "+m)
}
}
}
} else {
resp.Body.Close()
}
}
if len(jsFiles) > 0 || len(secrets) > 0 {
if len(foundJSURLs) > 0 {
break
}
}
// Deduplicate and limit
// Limit to first 15 JS files to avoid too many requests
if len(foundJSURLs) > 15 {
foundJSURLs = foundJSURLs[:15]
}
// Download and analyze each JS file concurrently
var wg sync.WaitGroup
semaphore := make(chan struct{}, 5) // Limit concurrent downloads
for _, jsURL := range foundJSURLs {
wg.Add(1)
go func(url string) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
fileSecrets := analyzeJSContent(url, client)
mu.Lock()
jsFiles = append(jsFiles, url)
secrets = append(secrets, fileSecrets...)
mu.Unlock()
}(jsURL)
}
wg.Wait()
// Deduplicate and limit results
jsFiles = UniqueStrings(jsFiles)
secrets = UniqueStrings(secrets)
if len(jsFiles) > 10 {
jsFiles = jsFiles[:10]
}
if len(secrets) > 10 {
secrets = secrets[:10]
if len(secrets) > 20 {
secrets = secrets[:20]
}
return jsFiles, secrets
}
// analyzeJSContent downloads and analyzes a JS file for secrets
func analyzeJSContent(jsURL string, client *http.Client) []string {
var secrets []string
resp, err := client.Get(jsURL)
if err != nil {
return secrets
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return secrets
}
// Read JS content (limit to 2MB)
body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024))
if err != nil {
return secrets
}
content := string(body)
// Skip minified files that are too large without meaningful content
if len(content) > 500000 && !containsInterestingPatterns(content) {
return secrets
}
// Search for secrets
for _, sp := range secretPatterns {
matches := sp.Pattern.FindAllStringSubmatch(content, 3)
for _, m := range matches {
var secret string
if len(m) > 1 && m[1] != "" {
secret = m[1]
} else {
secret = m[0]
}
// Skip common false positives
if isLikelyFalsePositive(secret) {
continue
}
// Truncate long secrets
if len(secret) > 80 {
secret = secret[:77] + "..."
}
finding := fmt.Sprintf("[%s] %s", sp.Name, secret)
secrets = append(secrets, finding)
}
}
// Search for API endpoints
for _, pattern := range endpointPatterns {
matches := pattern.FindAllString(content, 5)
for _, m := range matches {
// Clean up the match
m = strings.Trim(m, `'"`)
if len(m) > 80 {
m = m[:77] + "..."
}
secrets = append(secrets, fmt.Sprintf("[API Endpoint] %s", m))
}
}
return secrets
}
// normalizeURL converts relative URLs to absolute URLs
func normalizeURL(jsURL, baseURL string) string {
if jsURL == "" {
return ""
}
// Skip data URIs and blob URLs
if strings.HasPrefix(jsURL, "data:") || strings.HasPrefix(jsURL, "blob:") {
return ""
}
// Already absolute URL
if strings.HasPrefix(jsURL, "http://") || strings.HasPrefix(jsURL, "https://") {
return jsURL
}
// Protocol-relative URL
if strings.HasPrefix(jsURL, "//") {
if strings.HasPrefix(baseURL, "https") {
return "https:" + jsURL
}
return "http:" + jsURL
}
// Absolute path
if strings.HasPrefix(jsURL, "/") {
// Extract base (scheme + host)
parts := strings.SplitN(baseURL, "/", 4)
if len(parts) >= 3 {
return parts[0] + "//" + parts[2] + jsURL
}
return baseURL + jsURL
}
// Relative path
return strings.TrimSuffix(baseURL, "/") + "/" + jsURL
}
// containsInterestingPatterns checks if content might contain secrets
func containsInterestingPatterns(content string) bool {
interestingKeywords := []string{
"api_key", "apikey", "api-key",
"secret", "password", "token",
"authorization", "bearer",
"aws_", "firebase", "stripe",
"mongodb://", "postgres://", "mysql://",
"private_key", "privatekey",
}
contentLower := strings.ToLower(content)
for _, kw := range interestingKeywords {
if strings.Contains(contentLower, kw) {
return true
}
}
return false
}
// isLikelyFalsePositive performs basic pre-filtering before AI analysis
// Only filters obvious patterns - AI will handle context-aware filtering
func isLikelyFalsePositive(secret string) bool {
// Only filter obvious placeholder patterns
// AI will handle context-aware filtering (UI text, etc.)
obviousPlaceholders := []string{
"YOUR_API_KEY", "API_KEY_HERE", "REPLACE_ME",
"xxxxxxxx", "XXXXXXXX", "00000000",
}
secretLower := strings.ToLower(secret)
for _, fp := range obviousPlaceholders {
if strings.Contains(secretLower, strings.ToLower(fp)) {
return true
}
}
// Too short
if len(secret) < 8 {
return true
}
// Check for repeated characters (garbage data)
if isRepeatedChars(secret) {
return true
}
return false
}
// isRepeatedChars checks if string is mostly repeated characters
func isRepeatedChars(s string) bool {
if len(s) < 10 {
return false
}
charCount := make(map[rune]int)
for _, c := range s {
charCount[c]++
}
// If any single character is more than 60% of the string, it's likely garbage
for _, count := range charCount {
if float64(count)/float64(len(s)) > 0.6 {
return true
}
}
return false
}
// contains checks if a string slice contains a value
func contains(slice []string, val string) bool {
for _, s := range slice {
if s == val {
return true
}
}
return false
}
// UniqueStrings returns unique strings from a slice
func UniqueStrings(input []string) []string {
seen := make(map[string]bool)
+455
View File
@@ -0,0 +1,455 @@
package scanner
import (
"fmt"
"sort"
"strings"
"time"
"god-eye/internal/config"
"god-eye/internal/output"
)
// PrintResults displays scan results to stdout
func PrintResults(results map[string]*config.SubdomainResult, startTime time.Time, takeoverCount int32) {
elapsed := time.Since(startTime)
// Count statistics
var activeCount, vulnCount, cloudCount int
for _, r := range results {
if r.StatusCode >= 200 && r.StatusCode < 400 {
activeCount++
}
if r.OpenRedirect || r.CORSMisconfig != "" || len(r.DangerousMethods) > 0 || r.GitExposed || r.SvnExposed || len(r.BackupFiles) > 0 {
vulnCount++
}
if r.CloudProvider != "" {
cloudCount++
}
}
// Summary box
fmt.Println()
fmt.Println(output.BoldCyan("╔══════════════════════════════════════════════════════════════════════════════╗"))
fmt.Println(output.BoldCyan("║") + " " + output.BoldWhite("📊 SCAN SUMMARY") + " " + output.BoldCyan("║"))
fmt.Println(output.BoldCyan("╠══════════════════════════════════════════════════════════════════════════════╣"))
fmt.Printf("%s %-20s %s %-20s %s %-20s %s\n",
output.BoldCyan("║"),
fmt.Sprintf("🌐 Total: %s", output.BoldCyan(fmt.Sprintf("%d", len(results)))),
output.Dim("|"),
fmt.Sprintf("✅ Active: %s", output.BoldGreen(fmt.Sprintf("%d", activeCount))),
output.Dim("|"),
fmt.Sprintf("⏱️ Time: %s", output.BoldYellow(fmt.Sprintf("%.1fs", elapsed.Seconds()))),
output.BoldCyan("║"))
fmt.Printf("%s %-20s %s %-20s %s %-20s %s\n",
output.BoldCyan("║"),
fmt.Sprintf("⚠️ Vulns: %s", output.BoldRed(fmt.Sprintf("%d", vulnCount))),
output.Dim("|"),
fmt.Sprintf("☁️ Cloud: %s", output.Blue(fmt.Sprintf("%d", cloudCount))),
output.Dim("|"),
fmt.Sprintf("🎯 Takeover: %s", output.BoldRed(fmt.Sprintf("%d", takeoverCount))),
output.BoldCyan("║"))
fmt.Println(output.BoldCyan("╚══════════════════════════════════════════════════════════════════════════════╝"))
fmt.Println()
fmt.Println(output.BoldCyan("═══════════════════════════════════════════════════════════════════════════════"))
// Sort subdomains
var sortedSubs []string
for sub := range results {
sortedSubs = append(sortedSubs, sub)
}
sort.Strings(sortedSubs)
for _, sub := range sortedSubs {
r := results[sub]
printSubdomainResult(sub, r)
}
fmt.Println()
fmt.Println(output.BoldCyan("═══════════════════════════════════════════════════════════════════════════════"))
}
func printSubdomainResult(sub string, r *config.SubdomainResult) {
// Color code by status
var statusColor func(a ...interface{}) string
var statusIcon string
if r.StatusCode >= 200 && r.StatusCode < 300 {
statusColor = output.Green
statusIcon = "●"
} else if r.StatusCode >= 300 && r.StatusCode < 400 {
statusColor = output.Yellow
statusIcon = "◐"
} else if r.StatusCode >= 400 {
statusColor = output.Red
statusIcon = "○"
} else {
statusColor = output.Blue
statusIcon = "◌"
}
// Line 1: Subdomain name with status
statusBadge := ""
if r.StatusCode > 0 {
statusBadge = fmt.Sprintf(" %s", statusColor(fmt.Sprintf("[%d]", r.StatusCode)))
}
// Response time badge
timeBadge := ""
if r.ResponseMs > 0 {
if r.ResponseMs < 200 {
timeBadge = fmt.Sprintf(" %s", output.Green(fmt.Sprintf("⚡%dms", r.ResponseMs)))
} else if r.ResponseMs < 500 {
timeBadge = fmt.Sprintf(" %s", output.Yellow(fmt.Sprintf("⏱️%dms", r.ResponseMs)))
} else {
timeBadge = fmt.Sprintf(" %s", output.Red(fmt.Sprintf("🐢%dms", r.ResponseMs)))
}
}
fmt.Printf("\n%s %s%s%s\n", statusColor(statusIcon), output.BoldCyan(sub), statusBadge, timeBadge)
// IPs
if len(r.IPs) > 0 {
ips := r.IPs
if len(ips) > 3 {
ips = ips[:3]
}
fmt.Printf(" %s %s\n", output.Dim("IP:"), output.White(strings.Join(ips, ", ")))
}
// CNAME
if r.CNAME != "" {
fmt.Printf(" %s %s\n", output.Dim("CNAME:"), output.Blue(r.CNAME))
}
// Location + ASN
if r.Country != "" || r.City != "" || r.ASN != "" {
loc := ""
if r.City != "" && r.Country != "" {
loc = r.City + ", " + r.Country
} else if r.Country != "" {
loc = r.Country
} else if r.City != "" {
loc = r.City
}
asnStr := ""
if r.ASN != "" {
asnStr = r.ASN
if len(asnStr) > 40 {
asnStr = asnStr[:37] + "..."
}
}
if loc != "" && asnStr != "" {
fmt.Printf(" Location: %s | %s\n", output.Cyan(loc), output.Blue(asnStr))
} else if loc != "" {
fmt.Printf(" Location: %s\n", output.Cyan(loc))
} else if asnStr != "" {
fmt.Printf(" ASN: %s\n", output.Blue(asnStr))
}
}
// PTR
if r.PTR != "" {
fmt.Printf(" PTR: %s\n", output.Magenta(r.PTR))
}
// HTTP Info (Title, Size)
if r.Title != "" || r.ContentLength > 0 {
httpInfo := " HTTP: "
if r.Title != "" {
title := r.Title
if len(title) > 50 {
title = title[:47] + "..."
}
httpInfo += fmt.Sprintf("\"%s\"", title)
}
if r.ContentLength > 0 {
sizeStr := formatSize(r.ContentLength)
if r.Title != "" {
httpInfo += fmt.Sprintf(" (%s)", sizeStr)
} else {
httpInfo += sizeStr
}
}
fmt.Println(httpInfo)
}
// Redirect
if r.RedirectURL != "" {
redirectURL := r.RedirectURL
if len(redirectURL) > 60 {
redirectURL = redirectURL[:57] + "..."
}
fmt.Printf(" Redirect: %s\n", output.Yellow(redirectURL))
}
// Tech
if len(r.Tech) > 0 {
techMap := make(map[string]bool)
var uniqueTech []string
for _, t := range r.Tech {
if !techMap[t] {
techMap[t] = true
uniqueTech = append(uniqueTech, t)
}
}
if len(uniqueTech) > 5 {
uniqueTech = uniqueTech[:5]
}
if len(uniqueTech) > 0 {
fmt.Printf(" Tech: %s\n", output.Yellow(strings.Join(uniqueTech, ", ")))
}
}
// Security (WAF, TLS)
var securityInfo []string
if r.WAF != "" {
securityInfo = append(securityInfo, fmt.Sprintf("WAF: %s", output.Red(r.WAF)))
}
if r.TLSVersion != "" {
tlsInfo := fmt.Sprintf("TLS: %s", output.Cyan(r.TLSVersion))
if r.TLSSelfSigned {
tlsInfo += " " + output.Yellow("(self-signed)")
}
securityInfo = append(securityInfo, tlsInfo)
}
if len(securityInfo) > 0 {
fmt.Printf(" Security: %s\n", strings.Join(securityInfo, " | "))
}
// TLS Fingerprint (appliance detection)
if r.TLSFingerprint != nil {
fp := r.TLSFingerprint
if fp.Vendor != "" {
applianceInfo := fmt.Sprintf("%s %s", fp.Vendor, fp.Product)
if fp.Version != "" {
applianceInfo += " v" + fp.Version
}
if fp.ApplianceType != "" {
applianceInfo += fmt.Sprintf(" (%s)", fp.ApplianceType)
}
fmt.Printf(" %s %s\n", output.BoldYellow("APPLIANCE:"), output.Yellow(applianceInfo))
}
// Show internal hostnames found in certificate
if len(fp.InternalHosts) > 0 {
hosts := fp.InternalHosts
if len(hosts) > 5 {
hosts = hosts[:5]
}
fmt.Printf(" %s %s\n", output.BoldMagenta("INTERNAL:"), output.Magenta(strings.Join(hosts, ", ")))
}
// Show certificate subject info if no vendor matched but has org info
if fp.Vendor == "" && (fp.SubjectOrg != "" || fp.SubjectOU != "") {
certInfo := ""
if fp.SubjectOrg != "" {
certInfo = "Org: " + fp.SubjectOrg
}
if fp.SubjectOU != "" {
if certInfo != "" {
certInfo += ", "
}
certInfo += "OU: " + fp.SubjectOU
}
fmt.Printf(" Cert: %s\n", output.Dim(certInfo))
}
}
// Ports
if len(r.Ports) > 0 {
var portStrs []string
for _, p := range r.Ports {
portStrs = append(portStrs, fmt.Sprintf("%d", p))
}
fmt.Printf(" Ports: %s\n", output.Magenta(strings.Join(portStrs, ", ")))
}
// Extra files
var extras []string
if r.RobotsTxt {
extras = append(extras, "robots.txt")
}
if r.SitemapXml {
extras = append(extras, "sitemap.xml")
}
if r.FaviconHash != "" {
extras = append(extras, fmt.Sprintf("favicon:%s", r.FaviconHash[:8]))
}
if len(extras) > 0 {
fmt.Printf(" Files: %s\n", output.Green(strings.Join(extras, ", ")))
}
// DNS Records
if len(r.MXRecords) > 0 {
mx := r.MXRecords
if len(mx) > 2 {
mx = mx[:2]
}
fmt.Printf(" MX: %s\n", strings.Join(mx, ", "))
}
// Security Headers
if len(r.MissingHeaders) > 0 && len(r.MissingHeaders) < 7 {
if len(r.SecurityHeaders) > 0 {
fmt.Printf(" Headers: %s | Missing: %s\n",
output.Green(strings.Join(r.SecurityHeaders, ", ")),
output.Yellow(strings.Join(r.MissingHeaders, ", ")))
}
} else if len(r.SecurityHeaders) > 0 {
fmt.Printf(" Headers: %s\n", output.Green(strings.Join(r.SecurityHeaders, ", ")))
}
// Cloud Provider
if r.CloudProvider != "" {
fmt.Printf(" Cloud: %s\n", output.Cyan(r.CloudProvider))
}
// Email Security
if r.EmailSecurity != "" {
emailColor := output.Green
if r.EmailSecurity == "Weak" {
emailColor = output.Yellow
} else if r.EmailSecurity == "None" {
emailColor = output.Red
}
fmt.Printf(" Email: %s\n", emailColor(r.EmailSecurity))
}
// TLS Alt Names
if len(r.TLSAltNames) > 0 {
altNames := r.TLSAltNames
if len(altNames) > 5 {
altNames = altNames[:5]
}
fmt.Printf(" TLS Alt: %s\n", output.Blue(strings.Join(altNames, ", ")))
}
// S3 Buckets
if len(r.S3Buckets) > 0 {
for _, bucket := range r.S3Buckets {
if strings.Contains(bucket, "PUBLIC") {
fmt.Printf(" %s %s\n", output.Red("S3:"), output.Red(bucket))
} else {
fmt.Printf(" S3: %s\n", output.Yellow(bucket))
}
}
}
// Security Issues (vulnerabilities)
var vulns []string
if r.OpenRedirect {
vulns = append(vulns, "Open Redirect")
}
if r.CORSMisconfig != "" {
vulns = append(vulns, fmt.Sprintf("CORS: %s", r.CORSMisconfig))
}
if len(r.DangerousMethods) > 0 {
vulns = append(vulns, fmt.Sprintf("Methods: %s", strings.Join(r.DangerousMethods, ", ")))
}
if r.GitExposed {
vulns = append(vulns, ".git Exposed")
}
if r.SvnExposed {
vulns = append(vulns, ".svn Exposed")
}
if len(r.BackupFiles) > 0 {
files := r.BackupFiles
if len(files) > 3 {
files = files[:3]
}
vulns = append(vulns, fmt.Sprintf("Backup: %s", strings.Join(files, ", ")))
}
if len(vulns) > 0 {
fmt.Printf(" %s %s\n", output.Red("VULNS:"), output.Red(strings.Join(vulns, " | ")))
}
// Discovery (admin panels, API endpoints)
var discoveries []string
if len(r.AdminPanels) > 0 {
panels := r.AdminPanels
if len(panels) > 5 {
panels = panels[:5]
}
discoveries = append(discoveries, fmt.Sprintf("Admin: %s", strings.Join(panels, ", ")))
}
if len(r.APIEndpoints) > 0 {
endpoints := r.APIEndpoints
if len(endpoints) > 5 {
endpoints = endpoints[:5]
}
discoveries = append(discoveries, fmt.Sprintf("API: %s", strings.Join(endpoints, ", ")))
}
if len(discoveries) > 0 {
fmt.Printf(" %s %s\n", output.Magenta("FOUND:"), output.Magenta(strings.Join(discoveries, " | ")))
}
// JavaScript Analysis
if len(r.JSFiles) > 0 {
files := r.JSFiles
if len(files) > 3 {
files = files[:3]
}
fmt.Printf(" JS Files: %s\n", output.Blue(strings.Join(files, ", ")))
}
if len(r.JSSecrets) > 0 {
for _, secret := range r.JSSecrets {
fmt.Printf(" %s %s\n", output.Red("JS SECRET:"), output.Red(secret))
}
}
// Takeover
if r.Takeover != "" {
fmt.Printf(" %s %s\n", output.BgRed(" TAKEOVER "), output.BoldRed(r.Takeover))
}
// AI Findings
if len(r.AIFindings) > 0 {
severityColor := output.Cyan
severityLabel := "AI"
if r.AISeverity == "critical" {
severityColor = output.BoldRed
severityLabel = "AI:CRITICAL"
} else if r.AISeverity == "high" {
severityColor = output.Red
severityLabel = "AI:HIGH"
} else if r.AISeverity == "medium" {
severityColor = output.Yellow
severityLabel = "AI:MEDIUM"
}
for i, finding := range r.AIFindings {
if i == 0 {
fmt.Printf(" %s %s\n", severityColor(severityLabel+":"), finding)
} else {
fmt.Printf(" %s %s\n", output.Dim(" "), finding)
}
if i >= 4 {
remaining := len(r.AIFindings) - 5
if remaining > 0 {
fmt.Printf(" %s (%d more findings...)\n", output.Dim(" "), remaining)
}
break
}
}
if r.AIModel != "" {
fmt.Printf(" %s model: %s\n", output.Dim(" "), output.Dim(r.AIModel))
}
}
// CVE Findings
if len(r.CVEFindings) > 0 {
for _, cve := range r.CVEFindings {
fmt.Printf(" %s %s\n", output.BoldRed("CVE:"), output.Red(cve))
}
}
}
func formatSize(size int64) string {
if size > 1024*1024 {
return fmt.Sprintf("%.1fMB", float64(size)/(1024*1024))
} else if size > 1024 {
return fmt.Sprintf("%.1fKB", float64(size)/1024)
}
return fmt.Sprintf("%dB", size)
}
+180 -609
View File
@@ -1,13 +1,8 @@
package scanner
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -18,50 +13,57 @@ import (
"god-eye/internal/dns"
gohttp "god-eye/internal/http"
"god-eye/internal/output"
"god-eye/internal/progress"
"god-eye/internal/ratelimit"
"god-eye/internal/security"
"god-eye/internal/sources"
"god-eye/internal/stealth"
)
func Run(cfg config.Config) {
startTime := time.Now()
// Parse custom resolvers
var resolvers []string
if cfg.Resolvers != "" {
for _, r := range strings.Split(cfg.Resolvers, ",") {
r = strings.TrimSpace(r)
if r != "" {
if !strings.Contains(r, ":") {
r = r + ":53"
}
resolvers = append(resolvers, r)
// Pre-load KEV database if AI is enabled (auto-downloads if not present)
if cfg.EnableAI && !cfg.Silent && !cfg.JsonOutput {
kevStore := ai.GetKEVStore()
if !kevStore.IsLoaded() {
if err := kevStore.LoadWithProgress(true); err != nil {
fmt.Printf("%s Failed to load KEV database: %v\n", output.Yellow("⚠️"), err)
fmt.Println(output.Dim(" CVE lookups will use NVD API only (slower)"))
}
fmt.Println()
}
}
if len(resolvers) == 0 {
resolvers = config.DefaultResolvers
}
// Parse custom ports
var customPorts []int
if cfg.Ports != "" {
for _, p := range strings.Split(cfg.Ports, ",") {
p = strings.TrimSpace(p)
if port, err := strconv.Atoi(p); err == nil && port > 0 && port < 65536 {
customPorts = append(customPorts, port)
}
}
}
if len(customPorts) == 0 {
customPorts = []int{80, 443, 8080, 8443}
}
// Parse custom resolvers and ports using helpers
resolvers := ParseResolvers(cfg.Resolvers)
customPorts := ParsePorts(cfg.Ports)
// Initialize stealth manager
stealthMode := stealth.ParseMode(cfg.StealthMode)
stealthMgr := stealth.NewManager(stealthMode)
// Adjust concurrency based on stealth mode
effectiveConcurrency := stealthMgr.GetEffectiveConcurrency(cfg.Concurrency)
if !cfg.Silent && !cfg.JsonOutput {
output.PrintBanner()
output.PrintSection("🎯", "TARGET CONFIGURATION")
output.PrintSubSection(fmt.Sprintf("%s %s", output.Dim("Target:"), output.BoldCyan(cfg.Domain)))
// Show stealth status
if stealthMode != stealth.ModeOff {
stealthColor := output.Yellow
if stealthMode >= stealth.ModeAggressive {
stealthColor = output.Red
}
output.PrintSubSection(fmt.Sprintf("%s %s %s %s",
output.Dim("Stealth:"), stealthColor(stealthMgr.GetModeName()),
output.Dim("Effective Threads:"), output.BoldGreen(fmt.Sprintf("%d", effectiveConcurrency))))
}
output.PrintSubSection(fmt.Sprintf("%s %s %s %s %s %s",
output.Dim("Threads:"), output.BoldGreen(fmt.Sprintf("%d", cfg.Concurrency)),
output.Dim("Threads:"), output.BoldGreen(fmt.Sprintf("%d", effectiveConcurrency)),
output.Dim("Timeout:"), output.Yellow(fmt.Sprintf("%ds", cfg.Timeout)),
output.Dim("Resolvers:"), output.Blue(fmt.Sprintf("%d", len(resolvers)))))
if !cfg.NoPorts {
@@ -186,49 +188,72 @@ func Run(cfg config.Config) {
}
}()
// Wildcard detection (always run for JSON output accuracy)
var wildcardInfo *dns.WildcardInfo
wildcardDetector := dns.NewWildcardDetector(resolvers, cfg.Timeout)
wildcardInfo = wildcardDetector.Detect(cfg.Domain)
// DNS Brute-force
var bruteWg sync.WaitGroup
if !cfg.NoBrute {
// Check wildcard
wildcardIPs := dns.CheckWildcard(cfg.Domain, resolvers)
// Display wildcard detection results
if !cfg.Silent && !cfg.JsonOutput {
if len(wildcardIPs) > 0 {
output.PrintSubSection(fmt.Sprintf("%s Wildcard DNS: %s", output.Yellow("⚠"), output.BoldYellow("DETECTED")))
if wildcardInfo.IsWildcard {
output.PrintSubSection(fmt.Sprintf("%s Wildcard DNS: %s (confidence: %.0f%%)",
output.Yellow("⚠"), output.BoldYellow("DETECTED"), wildcardInfo.Confidence*100))
if len(wildcardInfo.WildcardIPs) > 0 {
ips := wildcardInfo.WildcardIPs
if len(ips) > 3 {
ips = ips[:3]
}
output.PrintSubSection(fmt.Sprintf(" %s Wildcard IPs: %s", output.Dim("→"), output.Yellow(strings.Join(ips, ", "))))
}
if wildcardInfo.HTTPStatusCode > 0 {
output.PrintSubSection(fmt.Sprintf(" %s HTTP response: %d (%d bytes)",
output.Dim("→"), wildcardInfo.HTTPStatusCode, wildcardInfo.HTTPBodySize))
}
} else {
output.PrintSubSection(fmt.Sprintf("%s Wildcard DNS: %s", output.Green("✓"), output.Green("not detected")))
}
}
// Brute-force
semaphore := make(chan struct{}, cfg.Concurrency)
for _, word := range wordlist {
// Brute-force with wildcard filtering
semaphore := make(chan struct{}, effectiveConcurrency)
wildcardIPSet := make(map[string]bool)
if wildcardInfo != nil {
for _, ip := range wildcardInfo.WildcardIPs {
wildcardIPSet[ip] = true
}
}
// Shuffle wordlist if stealth mode randomization is enabled
shuffledWordlist := stealthMgr.ShuffleSlice(wordlist)
for _, word := range shuffledWordlist {
bruteWg.Add(1)
go func(word string) {
defer bruteWg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
// Apply stealth delay
stealthMgr.Wait()
subdomain := fmt.Sprintf("%s.%s", word, cfg.Domain)
ips := dns.ResolveSubdomain(subdomain, resolvers, cfg.Timeout)
if len(ips) > 0 {
// Check if wildcard
isWildcard := false
if len(wildcardIPs) > 0 {
for _, ip := range ips {
for _, wip := range wildcardIPs {
if ip == wip {
isWildcard = true
break
}
}
if isWildcard {
break
}
// Check if ALL IPs are wildcard IPs
allWildcard := true
for _, ip := range ips {
if !wildcardIPSet[ip] {
allWildcard = false
break
}
}
if !isWildcard {
// Only add if not all IPs are wildcards
if !allWildcard || len(wildcardIPSet) == 0 {
seenMu.Lock()
if !seen[subdomain] {
seen[subdomain] = true
@@ -267,18 +292,24 @@ func Run(cfg config.Config) {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🌐", "DNS RESOLUTION")
output.PrintSubSection(fmt.Sprintf("Resolving %s subdomains...", output.BoldCyan(fmt.Sprintf("%d", len(subdomains)))))
}
// Create progress bar for DNS resolution
dnsBar := progress.New(len(subdomains), "DNS", cfg.Silent || cfg.JsonOutput)
var resolveWg sync.WaitGroup
semaphore := make(chan struct{}, cfg.Concurrency)
dnsSemaphore := make(chan struct{}, effectiveConcurrency)
for _, subdomain := range subdomains {
resolveWg.Add(1)
go func(sub string) {
defer resolveWg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
defer dnsBar.Increment()
dnsSemaphore <- struct{}{}
defer func() { <-dnsSemaphore }()
// Apply stealth delay for DNS
stealthMgr.Wait()
ips := dns.ResolveSubdomain(sub, resolvers, cfg.Timeout)
if len(ips) > 0 {
@@ -332,22 +363,36 @@ func Run(cfg config.Config) {
}(subdomain)
}
resolveWg.Wait()
dnsBar.Finish()
// HTTP Probing
if !cfg.NoProbe && len(results) > 0 {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🌍", "HTTP PROBING & SECURITY CHECKS")
output.PrintSubSection(fmt.Sprintf("Probing %s subdomains with %s parallel checks...", output.BoldCyan(fmt.Sprintf("%d", len(results))), output.BoldGreen("13")))
}
// Create progress bar and rate limiter for HTTP probing
httpBar := progress.New(len(results), "HTTP", cfg.Silent || cfg.JsonOutput)
httpLimiter := ratelimit.NewHostRateLimiter(ratelimit.DefaultConfig())
httpSemaphore := make(chan struct{}, effectiveConcurrency)
var probeWg sync.WaitGroup
for sub := range results {
probeWg.Add(1)
go func(subdomain string) {
defer probeWg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
defer httpBar.Increment()
httpSemaphore <- struct{}{}
defer func() { <-httpSemaphore }()
// Apply stealth delay and host-specific throttling
stealthMgr.Wait()
stealthMgr.WaitForHost(subdomain)
// Apply adaptive rate limiting
limiter := httpLimiter.Get(subdomain)
limiter.Wait()
// Use shared client for connection pooling
client := gohttp.GetSharedClient(cfg.Timeout)
@@ -542,6 +587,16 @@ func Run(cfg config.Config) {
}(sub)
}
probeWg.Wait()
httpBar.Finish()
// Log rate limiting stats if verbose
if cfg.Verbose && !cfg.JsonOutput {
hosts, requests, errors := httpLimiter.GetStats()
if errors > 0 {
output.PrintSubSection(fmt.Sprintf("%s Rate limiting: %d hosts, %d requests, %d errors",
output.Yellow("⚠️"), hosts, requests, errors))
}
}
}
// Port Scanning
@@ -549,9 +604,17 @@ func Run(cfg config.Config) {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🔌", "PORT SCANNING")
output.PrintSubSection(fmt.Sprintf("Scanning %s ports on %s hosts...", output.BoldMagenta(fmt.Sprintf("%d", len(customPorts))), output.BoldCyan(fmt.Sprintf("%d", len(results)))))
}
// Count hosts with IPs
hostCount := 0
for _, result := range results {
if len(result.IPs) > 0 {
hostCount++
}
}
portBar := progress.New(hostCount, "Ports", cfg.Silent || cfg.JsonOutput)
var portWg sync.WaitGroup
for sub, result := range results {
@@ -561,6 +624,7 @@ func Run(cfg config.Config) {
portWg.Add(1)
go func(subdomain string, ip string) {
defer portWg.Done()
defer portBar.Increment()
openPorts := ScanPorts(ip, customPorts, cfg.Timeout)
resultsMu.Lock()
if r, ok := results[subdomain]; ok {
@@ -570,6 +634,7 @@ func Run(cfg config.Config) {
}(sub, result.IPs[0])
}
portWg.Wait()
portBar.Finish()
}
// Subdomain Takeover Check
@@ -578,14 +643,15 @@ func Run(cfg config.Config) {
if !cfg.Silent && !cfg.JsonOutput {
output.PrintEndSection()
output.PrintSection("🎯", "SUBDOMAIN TAKEOVER")
output.PrintSubSection(fmt.Sprintf("Checking %s fingerprints against %s subdomains...", output.BoldRed("110+"), output.BoldCyan(fmt.Sprintf("%d", len(results)))))
}
takeoverBar := progress.New(len(results), "Takeover", cfg.Silent || cfg.JsonOutput)
var takeoverWg sync.WaitGroup
for sub := range results {
takeoverWg.Add(1)
go func(subdomain string) {
defer takeoverWg.Done()
defer takeoverBar.Increment()
if takeover := CheckTakeover(subdomain, cfg.Timeout); takeover != "" {
resultsMu.Lock()
if r, ok := results[subdomain]; ok {
@@ -593,15 +659,13 @@ func Run(cfg config.Config) {
}
resultsMu.Unlock()
atomic.AddInt32(&takeoverCount, 1)
if !cfg.JsonOutput {
output.PrintSubSection(fmt.Sprintf("%s %s → %s", output.BgRed(" TAKEOVER "), output.BoldWhite(subdomain), output.BoldRed(takeover)))
}
}
}(sub)
}
takeoverWg.Wait()
takeoverBar.Finish()
if takeoverCount > 0 && !cfg.JsonOutput {
if takeoverCount > 0 && !cfg.Silent && !cfg.JsonOutput {
output.PrintSubSection(fmt.Sprintf("%s Found %s potential takeover(s)!", output.Red("⚠"), output.BoldRed(fmt.Sprintf("%d", takeoverCount))))
}
if !cfg.Silent && !cfg.JsonOutput {
@@ -638,45 +702,62 @@ func Run(cfg config.Config) {
aiSemaphore := make(chan struct{}, 5) // Limit concurrent AI requests
for sub, result := range results {
// Only analyze interesting findings
shouldAnalyze := false
// Determine what types of analysis to perform
shouldAnalyzeVulns := false
shouldAnalyzeCVE := len(result.Tech) > 0 // CVE for ALL subdomains with tech
// Analyze JS files if found
if len(result.JSFiles) > 0 || len(result.JSSecrets) > 0 {
shouldAnalyze = true
shouldAnalyzeVulns = true
}
// Analyze if vulnerabilities detected
if result.OpenRedirect || result.CORSMisconfig != "" ||
len(result.DangerousMethods) > 0 || result.GitExposed ||
result.SvnExposed || len(result.BackupFiles) > 0 {
shouldAnalyze = true
shouldAnalyzeVulns = true
}
// Analyze takeovers
if result.Takeover != "" {
shouldAnalyze = true
shouldAnalyzeVulns = true
}
// Deep analysis mode: analyze everything
if cfg.AIDeepAnalysis {
shouldAnalyze = true
shouldAnalyzeVulns = true
}
if !shouldAnalyze {
// Skip if nothing to analyze
if !shouldAnalyzeVulns && !shouldAnalyzeCVE {
continue
}
analyzeVulns := shouldAnalyzeVulns // Capture for goroutine
aiWg.Add(1)
go func(subdomain string, r *config.SubdomainResult) {
go func(subdomain string, r *config.SubdomainResult, doVulnAnalysis bool) {
defer aiWg.Done()
aiSemaphore <- struct{}{}
defer func() { <-aiSemaphore }()
var aiResults []*ai.AnalysisResult
// Analyze JavaScript if present
if len(r.JSFiles) > 0 && len(r.JSSecrets) > 0 {
// Filter JS secrets using AI before analysis
if len(r.JSSecrets) > 0 {
if filteredSecrets, err := aiClient.FilterSecrets(r.JSSecrets); err == nil && len(filteredSecrets) > 0 {
resultsMu.Lock()
r.JSSecrets = filteredSecrets // Replace with AI-filtered secrets
resultsMu.Unlock()
} else if err == nil {
// No real secrets found after filtering
resultsMu.Lock()
r.JSSecrets = nil
resultsMu.Unlock()
}
}
// Analyze JavaScript if present (only if vuln analysis enabled)
if doVulnAnalysis && len(r.JSFiles) > 0 && len(r.JSSecrets) > 0 {
// Build context from secrets
jsContext := strings.Join(r.JSSecrets, "\n")
if analysis, err := aiClient.AnalyzeJavaScript(jsContext); err == nil {
@@ -684,15 +765,15 @@ func Run(cfg config.Config) {
}
}
// Analyze HTTP response for misconfigurations
if r.StatusCode > 0 && (len(r.MissingHeaders) > 3 || r.GitExposed || r.SvnExposed) {
// Analyze HTTP response for misconfigurations (only if vuln analysis enabled)
if doVulnAnalysis && r.StatusCode > 0 && (len(r.MissingHeaders) > 3 || r.GitExposed || r.SvnExposed) {
bodyContext := r.Title
if analysis, err := aiClient.AnalyzeHTTPResponse(subdomain, r.StatusCode, r.Headers, bodyContext); err == nil {
aiResults = append(aiResults, analysis)
}
}
// CVE matching for detected technologies
// CVE matching for detected technologies (always done if tech detected)
if len(r.Tech) > 0 {
for _, tech := range r.Tech {
if cve, err := aiClient.CVEMatch(tech, ""); err == nil && cve != "" {
@@ -748,7 +829,7 @@ func Run(cfg config.Config) {
output.Dim(fmt.Sprintf("%d findings", len(r.AIFindings)))))
}
}
}(sub, result)
}(sub, result, analyzeVulns)
}
aiWg.Wait()
@@ -795,543 +876,33 @@ func Run(cfg config.Config) {
results = filtered
}
// Sort subdomains
var sortedSubs []string
for sub := range results {
sortedSubs = append(sortedSubs, sub)
}
sort.Strings(sortedSubs)
// JSON output to stdout
// JSON output to stdout (structured report format)
if cfg.JsonOutput {
var resultList []*config.SubdomainResult
for _, sub := range sortedSubs {
resultList = append(resultList, results[sub])
// Build structured JSON report with metadata
reportBuilder := output.NewReportBuilder(cfg.Domain, cfg)
// Set wildcard info if available
if wildcardInfo != nil {
reportBuilder.SetWildcard(
wildcardInfo.IsWildcard,
wildcardInfo.WildcardIPs,
wildcardInfo.WildcardCNAME,
wildcardInfo.HTTPStatusCode,
wildcardInfo.Confidence,
)
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
encoder.Encode(resultList)
// Finalize and output the report
reportBuilder.Finalize(results)
reportBuilder.WriteJSON(os.Stdout, true)
return
}
// Print results
elapsed := time.Since(startTime)
// Count statistics
var activeCount, vulnCount, cloudCount int
for _, r := range results {
if r.StatusCode >= 200 && r.StatusCode < 400 {
activeCount++
}
if r.OpenRedirect || r.CORSMisconfig != "" || len(r.DangerousMethods) > 0 || r.GitExposed || r.SvnExposed || len(r.BackupFiles) > 0 {
vulnCount++
}
if r.CloudProvider != "" {
cloudCount++
}
}
// Summary box
fmt.Println()
fmt.Println(output.BoldCyan("╔══════════════════════════════════════════════════════════════════════════════╗"))
fmt.Println(output.BoldCyan("║") + " " + output.BoldWhite("📊 SCAN SUMMARY") + " " + output.BoldCyan("║"))
fmt.Println(output.BoldCyan("╠══════════════════════════════════════════════════════════════════════════════╣"))
fmt.Printf("%s %-20s %s %-20s %s %-20s %s\n",
output.BoldCyan("║"),
fmt.Sprintf("🌐 Total: %s", output.BoldCyan(fmt.Sprintf("%d", len(results)))),
output.Dim("|"),
fmt.Sprintf("✅ Active: %s", output.BoldGreen(fmt.Sprintf("%d", activeCount))),
output.Dim("|"),
fmt.Sprintf("⏱️ Time: %s", output.BoldYellow(fmt.Sprintf("%.1fs", elapsed.Seconds()))),
output.BoldCyan("║"))
fmt.Printf("%s %-20s %s %-20s %s %-20s %s\n",
output.BoldCyan("║"),
fmt.Sprintf("⚠️ Vulns: %s", output.BoldRed(fmt.Sprintf("%d", vulnCount))),
output.Dim("|"),
fmt.Sprintf("☁️ Cloud: %s", output.Blue(fmt.Sprintf("%d", cloudCount))),
output.Dim("|"),
fmt.Sprintf("🎯 Takeover: %s", output.BoldRed(fmt.Sprintf("%d", takeoverCount))),
output.BoldCyan("║"))
fmt.Println(output.BoldCyan("╚══════════════════════════════════════════════════════════════════════════════╝"))
fmt.Println()
fmt.Println(output.BoldCyan("═══════════════════════════════════════════════════════════════════════════════"))
for _, sub := range sortedSubs {
r := results[sub]
// Color code by status
var statusColor func(a ...interface{}) string
var statusIcon string
if r.StatusCode >= 200 && r.StatusCode < 300 {
statusColor = output.Green
statusIcon = "●"
} else if r.StatusCode >= 300 && r.StatusCode < 400 {
statusColor = output.Yellow
statusIcon = "◐"
} else if r.StatusCode >= 400 {
statusColor = output.Red
statusIcon = "○"
} else {
statusColor = output.Blue
statusIcon = "◌"
}
// Line 1: Subdomain name with status (modern box style)
statusBadge := ""
if r.StatusCode > 0 {
statusBadge = fmt.Sprintf(" %s", statusColor(fmt.Sprintf("[%d]", r.StatusCode)))
}
// Response time badge
timeBadge := ""
if r.ResponseMs > 0 {
if r.ResponseMs < 200 {
timeBadge = fmt.Sprintf(" %s", output.Green(fmt.Sprintf("⚡%dms", r.ResponseMs)))
} else if r.ResponseMs < 500 {
timeBadge = fmt.Sprintf(" %s", output.Yellow(fmt.Sprintf("⏱️%dms", r.ResponseMs)))
} else {
timeBadge = fmt.Sprintf(" %s", output.Red(fmt.Sprintf("🐢%dms", r.ResponseMs)))
}
}
fmt.Printf("\n%s %s%s%s\n", statusColor(statusIcon), output.BoldCyan(sub), statusBadge, timeBadge)
// Line 2: IPs
if len(r.IPs) > 0 {
ips := r.IPs
if len(ips) > 3 {
ips = ips[:3]
}
fmt.Printf(" %s %s\n", output.Dim("IP:"), output.White(strings.Join(ips, ", ")))
}
// Line 3: CNAME
if r.CNAME != "" {
fmt.Printf(" %s %s\n", output.Dim("CNAME:"), output.Blue(r.CNAME))
}
// Line 4: Location + ASN
if r.Country != "" || r.City != "" || r.ASN != "" {
loc := ""
if r.City != "" && r.Country != "" {
loc = r.City + ", " + r.Country
} else if r.Country != "" {
loc = r.Country
} else if r.City != "" {
loc = r.City
}
asnStr := ""
if r.ASN != "" {
asnStr = r.ASN
if len(asnStr) > 40 {
asnStr = asnStr[:37] + "..."
}
}
if loc != "" && asnStr != "" {
fmt.Printf(" Location: %s | %s\n", output.Cyan(loc), output.Blue(asnStr))
} else if loc != "" {
fmt.Printf(" Location: %s\n", output.Cyan(loc))
} else if asnStr != "" {
fmt.Printf(" ASN: %s\n", output.Blue(asnStr))
}
}
// Line 5: PTR
if r.PTR != "" {
fmt.Printf(" PTR: %s\n", output.Magenta(r.PTR))
}
// Line 6: HTTP Info (Title, Size)
if r.Title != "" || r.ContentLength > 0 {
httpInfo := " HTTP: "
if r.Title != "" {
title := r.Title
if len(title) > 50 {
title = title[:47] + "..."
}
httpInfo += fmt.Sprintf("\"%s\"", title)
}
if r.ContentLength > 0 {
sizeStr := ""
if r.ContentLength > 1024*1024 {
sizeStr = fmt.Sprintf("%.1fMB", float64(r.ContentLength)/(1024*1024))
} else if r.ContentLength > 1024 {
sizeStr = fmt.Sprintf("%.1fKB", float64(r.ContentLength)/1024)
} else {
sizeStr = fmt.Sprintf("%dB", r.ContentLength)
}
if r.Title != "" {
httpInfo += fmt.Sprintf(" (%s)", sizeStr)
} else {
httpInfo += sizeStr
}
}
fmt.Println(httpInfo)
}
// Line 7: Redirect
if r.RedirectURL != "" {
redirectURL := r.RedirectURL
if len(redirectURL) > 60 {
redirectURL = redirectURL[:57] + "..."
}
fmt.Printf(" Redirect: %s\n", output.Yellow(redirectURL))
}
// Line 8: Tech + Server
if len(r.Tech) > 0 || r.Server != "" {
techMap := make(map[string]bool)
var uniqueTech []string
for _, t := range r.Tech {
if !techMap[t] {
techMap[t] = true
uniqueTech = append(uniqueTech, t)
}
}
if len(uniqueTech) > 5 {
uniqueTech = uniqueTech[:5]
}
if len(uniqueTech) > 0 {
fmt.Printf(" Tech: %s\n", output.Yellow(strings.Join(uniqueTech, ", ")))
}
}
// Line 9: Security (WAF, TLS)
var securityInfo []string
if r.WAF != "" {
securityInfo = append(securityInfo, fmt.Sprintf("WAF: %s", output.Red(r.WAF)))
}
if r.TLSVersion != "" {
securityInfo = append(securityInfo, fmt.Sprintf("TLS: %s", output.Cyan(r.TLSVersion)))
}
if len(securityInfo) > 0 {
fmt.Printf(" Security: %s\n", strings.Join(securityInfo, " | "))
}
// Line 10: Ports
if len(r.Ports) > 0 {
var portStrs []string
for _, p := range r.Ports {
portStrs = append(portStrs, fmt.Sprintf("%d", p))
}
fmt.Printf(" Ports: %s\n", output.Magenta(strings.Join(portStrs, ", ")))
}
// Line 11: Extra files
var extras []string
if r.RobotsTxt {
extras = append(extras, "robots.txt")
}
if r.SitemapXml {
extras = append(extras, "sitemap.xml")
}
if r.FaviconHash != "" {
extras = append(extras, fmt.Sprintf("favicon:%s", r.FaviconHash[:8]))
}
if len(extras) > 0 {
fmt.Printf(" Files: %s\n", output.Green(strings.Join(extras, ", ")))
}
// Line 12: DNS Records
if len(r.MXRecords) > 0 {
mx := r.MXRecords
if len(mx) > 2 {
mx = mx[:2]
}
fmt.Printf(" MX: %s\n", strings.Join(mx, ", "))
}
// Line 13: Security Headers
if len(r.MissingHeaders) > 0 && len(r.MissingHeaders) < 7 {
// Only show if some headers are present (not all missing)
if len(r.SecurityHeaders) > 0 {
fmt.Printf(" Headers: %s | Missing: %s\n",
output.Green(strings.Join(r.SecurityHeaders, ", ")),
output.Yellow(strings.Join(r.MissingHeaders, ", ")))
}
} else if len(r.SecurityHeaders) > 0 {
fmt.Printf(" Headers: %s\n", output.Green(strings.Join(r.SecurityHeaders, ", ")))
}
// Line 14: Cloud Provider
if r.CloudProvider != "" {
fmt.Printf(" Cloud: %s\n", output.Cyan(r.CloudProvider))
}
// Line 15: Email Security (only for root domain)
if r.EmailSecurity != "" {
emailColor := output.Green
if r.EmailSecurity == "Weak" {
emailColor = output.Yellow
} else if r.EmailSecurity == "None" {
emailColor = output.Red
}
fmt.Printf(" Email: %s\n", emailColor(r.EmailSecurity))
}
// Line 16: TLS Alt Names
if len(r.TLSAltNames) > 0 {
altNames := r.TLSAltNames
if len(altNames) > 5 {
altNames = altNames[:5]
}
fmt.Printf(" TLS Alt: %s\n", output.Blue(strings.Join(altNames, ", ")))
}
// Line 17: S3 Buckets
if len(r.S3Buckets) > 0 {
for _, bucket := range r.S3Buckets {
if strings.Contains(bucket, "PUBLIC") {
fmt.Printf(" %s %s\n", output.Red("S3:"), output.Red(bucket))
} else {
fmt.Printf(" S3: %s\n", output.Yellow(bucket))
}
}
}
// Line 18: Security Issues (vulnerabilities found)
var vulns []string
if r.OpenRedirect {
vulns = append(vulns, "Open Redirect")
}
if r.CORSMisconfig != "" {
vulns = append(vulns, fmt.Sprintf("CORS: %s", r.CORSMisconfig))
}
if len(r.DangerousMethods) > 0 {
vulns = append(vulns, fmt.Sprintf("Methods: %s", strings.Join(r.DangerousMethods, ", ")))
}
if r.GitExposed {
vulns = append(vulns, ".git Exposed")
}
if r.SvnExposed {
vulns = append(vulns, ".svn Exposed")
}
if len(r.BackupFiles) > 0 {
files := r.BackupFiles
if len(files) > 3 {
files = files[:3]
}
vulns = append(vulns, fmt.Sprintf("Backup: %s", strings.Join(files, ", ")))
}
if len(vulns) > 0 {
fmt.Printf(" %s %s\n", output.Red("VULNS:"), output.Red(strings.Join(vulns, " | ")))
}
// Line 19: Discovery (admin panels, API endpoints)
var discoveries []string
if len(r.AdminPanels) > 0 {
panels := r.AdminPanels
if len(panels) > 5 {
panels = panels[:5]
}
discoveries = append(discoveries, fmt.Sprintf("Admin: %s", strings.Join(panels, ", ")))
}
if len(r.APIEndpoints) > 0 {
endpoints := r.APIEndpoints
if len(endpoints) > 5 {
endpoints = endpoints[:5]
}
discoveries = append(discoveries, fmt.Sprintf("API: %s", strings.Join(endpoints, ", ")))
}
if len(discoveries) > 0 {
fmt.Printf(" %s %s\n", output.Magenta("FOUND:"), output.Magenta(strings.Join(discoveries, " | ")))
}
// Line 20: JavaScript Analysis
if len(r.JSFiles) > 0 {
files := r.JSFiles
if len(files) > 3 {
files = files[:3]
}
fmt.Printf(" JS Files: %s\n", output.Blue(strings.Join(files, ", ")))
}
if len(r.JSSecrets) > 0 {
for _, secret := range r.JSSecrets {
fmt.Printf(" %s %s\n", output.Red("JS SECRET:"), output.Red(secret))
}
}
// Line 21: Takeover
if r.Takeover != "" {
fmt.Printf(" %s %s\n", output.BgRed(" TAKEOVER "), output.BoldRed(r.Takeover))
}
// Line 22: AI Findings
if len(r.AIFindings) > 0 {
severityColor := output.Cyan
severityLabel := "AI"
if r.AISeverity == "critical" {
severityColor = output.BoldRed
severityLabel = "AI:CRITICAL"
} else if r.AISeverity == "high" {
severityColor = output.Red
severityLabel = "AI:HIGH"
} else if r.AISeverity == "medium" {
severityColor = output.Yellow
severityLabel = "AI:MEDIUM"
}
for i, finding := range r.AIFindings {
if i == 0 {
fmt.Printf(" %s %s\n", severityColor(severityLabel+":"), finding)
} else {
fmt.Printf(" %s %s\n", output.Dim(" "), finding)
}
if i >= 4 { // Limit displayed findings
remaining := len(r.AIFindings) - 5
if remaining > 0 {
fmt.Printf(" %s (%d more findings...)\n", output.Dim(" "), remaining)
}
break
}
}
// Show model used
if r.AIModel != "" {
fmt.Printf(" %s model: %s\n", output.Dim(" "), output.Dim(r.AIModel))
}
}
// Line 23: CVE Findings
if len(r.CVEFindings) > 0 {
for _, cve := range r.CVEFindings {
fmt.Printf(" %s %s\n", output.BoldRed("CVE:"), output.Red(cve))
}
}
}
fmt.Println()
fmt.Println(output.BoldCyan("═══════════════════════════════════════════════════════════════════════════════"))
// Print results using output module
PrintResults(results, startTime, takeoverCount)
// Save output
if cfg.Output != "" {
output.SaveOutput(cfg.Output, cfg.Format, results)
}
}
// LoadWordlist loads words from a file
func LoadWordlist(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var words []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
word := strings.TrimSpace(scanner.Text())
if word != "" && !strings.HasPrefix(word, "#") {
words = append(words, word)
}
}
return words, scanner.Err()
}
// ScanPorts scans ports on an IP address
func ScanPorts(ip string, ports []int, timeout int) []int {
var openPorts []int
var mu sync.Mutex
var wg sync.WaitGroup
for _, port := range ports {
wg.Add(1)
go func(p int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", ip, p)
conn, err := net.DialTimeout("tcp", address, time.Duration(timeout)*time.Second)
if err == nil {
conn.Close()
mu.Lock()
openPorts = append(openPorts, p)
mu.Unlock()
}
}(port)
}
wg.Wait()
sort.Ints(openPorts)
return openPorts
}
// Helper functions for AI analysis
func countSubdomainsWithAI(results map[string]*config.SubdomainResult) int {
count := 0
for _, r := range results {
if len(r.AIFindings) > 0 {
count++
}
}
return count
}
func countActive(results map[string]*config.SubdomainResult) int {
count := 0
for _, r := range results {
if r.StatusCode >= 200 && r.StatusCode < 400 {
count++
}
}
return count
}
func countVulns(results map[string]*config.SubdomainResult) int {
count := 0
for _, r := range results {
if r.OpenRedirect || r.CORSMisconfig != "" || len(r.DangerousMethods) > 0 ||
r.GitExposed || r.SvnExposed || len(r.BackupFiles) > 0 {
count++
}
}
return count
}
func buildAISummary(results map[string]*config.SubdomainResult) string {
var summary strings.Builder
criticalCount := 0
highCount := 0
mediumCount := 0
for sub, r := range results {
if len(r.AIFindings) == 0 {
continue
}
switch r.AISeverity {
case "critical":
criticalCount++
summary.WriteString(fmt.Sprintf("\n[CRITICAL] %s:\n", sub))
case "high":
highCount++
summary.WriteString(fmt.Sprintf("\n[HIGH] %s:\n", sub))
case "medium":
mediumCount++
summary.WriteString(fmt.Sprintf("\n[MEDIUM] %s:\n", sub))
default:
continue // Skip low/info for summary
}
// Add first 3 findings
for i, finding := range r.AIFindings {
if i >= 3 {
break
}
summary.WriteString(fmt.Sprintf(" - %s\n", finding))
}
// Add CVE findings
if len(r.CVEFindings) > 0 {
summary.WriteString(" CVEs:\n")
for _, cve := range r.CVEFindings {
summary.WriteString(fmt.Sprintf(" - %s\n", cve))
}
}
}
header := fmt.Sprintf("Summary: %d critical, %d high, %d medium findings\n", criticalCount, highCount, mediumCount)
return header + summary.String()
}