mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-10-12 04:37:07 +02:00
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:
1 parent
2be93a0dbb
commit
45295bb262
28 files changed
+4650
-882
No files matched your search
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user