🚀 God's Eye v0.1 - Initial Release

God's Eye is an ultra-fast subdomain enumeration and reconnaissance tool with AI-powered security analysis.

##  Key Features

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

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

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

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

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

## 🚀 Installation

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

## 📖 Quick Start

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

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

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

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

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

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

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

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

---

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vyntral
2025-11-20 10:41:05 +01:00
co-authored by Claude
commit 6f3bc2f952
26 changed files with 8052 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
package ai
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// CVEInfo represents CVE vulnerability information
type CVEInfo struct {
ID string `json:"id"`
Description string `json:"description"`
Severity string `json:"severity"`
Score float64 `json:"score"`
Published string `json:"published"`
References []string `json:"references"`
}
// NVDResponse represents the response from NVD API
type NVDResponse struct {
ResultsPerPage int `json:"resultsPerPage"`
StartIndex int `json:"startIndex"`
TotalResults int `json:"totalResults"`
Vulnerabilities []struct {
CVE struct {
ID string `json:"id"`
Published string `json:"published"`
Descriptions []struct {
Lang string `json:"lang"`
Value string `json:"value"`
} `json:"descriptions"`
Metrics struct {
CVSSMetricV31 []struct {
CVSSData struct {
BaseScore float64 `json:"baseScore"`
BaseSeverity string `json:"baseSeverity"`
} `json:"cvssData"`
} `json:"cvssMetricV31,omitempty"`
CVSSMetricV2 []struct {
CVSSData struct {
BaseScore float64 `json:"baseScore"`
} `json:"cvssData"`
BaseSeverity string `json:"baseSeverity"`
} `json:"cvssMetricV2,omitempty"`
} `json:"metrics,omitempty"`
References []struct {
URL string `json:"url"`
} `json:"references"`
} `json:"cve"`
} `json:"vulnerabilities"`
}
var (
nvdClient = &http.Client{
Timeout: 10 * time.Second,
}
nvdBaseURL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
)
// SearchCVE searches for CVE vulnerabilities using NVD API
func SearchCVE(technology string, version string) (string, error) {
// Normalize technology name
tech := normalizeTechnology(technology)
// Build search query
query := tech
if version != "" && version != "unknown" {
query = fmt.Sprintf("%s %s", tech, version)
}
// Query NVD API
cves, err := queryNVD(query)
if err != nil {
return fmt.Sprintf("Unable to search CVE database for %s: %v", technology, err), nil
}
if len(cves) == 0 {
return fmt.Sprintf("No known CVE vulnerabilities found for %s %s in the NVD database. This doesn't guarantee the software is secure - always keep software updated.", technology, version), nil
}
// Format results
result := fmt.Sprintf("CVE Vulnerabilities for %s %s:\n\n", technology, version)
result += fmt.Sprintf("Found %d CVE(s):\n\n", len(cves))
// Show top 5 most recent/critical CVEs
maxShow := 5
if len(cves) < maxShow {
maxShow = len(cves)
}
for i := 0; i < maxShow; i++ {
cve := cves[i]
result += fmt.Sprintf("🔴 %s (%s - Score: %.1f)\n", cve.ID, cve.Severity, cve.Score)
result += fmt.Sprintf(" Published: %s\n", cve.Published)
// Truncate description if too long
desc := cve.Description
if len(desc) > 200 {
desc = desc[:200] + "..."
}
result += fmt.Sprintf(" %s\n", desc)
if len(cve.References) > 0 {
result += fmt.Sprintf(" Reference: %s\n", cve.References[0])
}
result += "\n"
}
if len(cves) > maxShow {
result += fmt.Sprintf("... and %d more CVEs. Check https://nvd.nist.gov for complete details.\n", len(cves)-maxShow)
}
result += "\n⚠️ Recommendation: Update to the latest version to mitigate known vulnerabilities."
return result, nil
}
// queryNVD queries the NVD API for CVE information
func queryNVD(keyword string) ([]CVEInfo, error) {
// Build URL with query parameters
params := url.Values{}
params.Add("keywordSearch", keyword)
params.Add("resultsPerPage", "10") // Limit results
reqURL := fmt.Sprintf("%s?%s", nvdBaseURL, params.Encode())
// Create request
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// NVD recommends including a user agent
req.Header.Set("User-Agent", "GodEye-Security-Scanner/0.1")
// Execute request
resp, err := nvdClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to query NVD: %w", err)
}
defer resp.Body.Close()
// Check status code
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("NVD API returned status %d: %s", resp.StatusCode, string(body))
}
// Parse response
var nvdResp NVDResponse
if err := json.NewDecoder(resp.Body).Decode(&nvdResp); err != nil {
return nil, fmt.Errorf("failed to parse NVD response: %w", err)
}
// Convert to CVEInfo
var cves []CVEInfo
for _, vuln := range nvdResp.Vulnerabilities {
cve := CVEInfo{
ID: vuln.CVE.ID,
Published: formatDate(vuln.CVE.Published),
}
// Get description
for _, desc := range vuln.CVE.Descriptions {
if desc.Lang == "en" {
cve.Description = desc.Value
break
}
}
// Get severity and score (prefer CVSS v3.1)
if len(vuln.CVE.Metrics.CVSSMetricV31) > 0 {
metric := vuln.CVE.Metrics.CVSSMetricV31[0]
cve.Score = metric.CVSSData.BaseScore
cve.Severity = metric.CVSSData.BaseSeverity
} else if len(vuln.CVE.Metrics.CVSSMetricV2) > 0 {
metric := vuln.CVE.Metrics.CVSSMetricV2[0]
cve.Score = metric.CVSSData.BaseScore
cve.Severity = metric.BaseSeverity
}
// Get references
for _, ref := range vuln.CVE.References {
cve.References = append(cve.References, ref.URL)
}
cves = append(cves, cve)
}
return cves, nil
}
// normalizeTechnology normalizes technology names for better CVE search results
func normalizeTechnology(tech string) string {
tech = strings.ToLower(tech)
// Common normalizations
replacements := map[string]string{
"microsoft-iis": "iis",
"apache httpd": "apache",
"apache http server": "apache",
"nginx/": "nginx",
"wordpress": "wordpress",
"asp.net": "asp.net",
"next.js": "nextjs",
"react": "react",
"angular": "angular",
"vue": "vue",
"express": "express",
"django": "django",
"flask": "flask",
"spring": "spring",
"tomcat": "tomcat",
"jetty": "jetty",
"php": "php",
"mysql": "mysql",
"postgresql": "postgresql",
"mongodb": "mongodb",
"redis": "redis",
"elasticsearch": "elasticsearch",
"docker": "docker",
"kubernetes": "kubernetes",
"jenkins": "jenkins",
"gitlab": "gitlab",
"grafana": "grafana",
}
for old, new := range replacements {
if strings.Contains(tech, old) {
return new
}
}
// Remove version numbers and extra info
parts := strings.Fields(tech)
if len(parts) > 0 {
return parts[0]
}
return tech
}
// formatDate formats ISO 8601 date to a more readable format
func formatDate(isoDate string) string {
t, err := time.Parse(time.RFC3339, isoDate)
if err != nil {
return isoDate
}
return t.Format("2006-01-02")
}
+456
View File
@@ -0,0 +1,456 @@
package ai
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// OllamaClient handles communication with local Ollama instance
type OllamaClient struct {
BaseURL string
FastModel string // phi3.5:3.8b for quick triage
DeepModel string // qwen2.5-coder:7b for deep analysis
Timeout time.Duration
EnableCascade bool
}
// OllamaRequest represents the request payload for Ollama API
type OllamaRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt,omitempty"`
Stream bool `json:"stream"`
Tools []Tool `json:"tools,omitempty"`
Options map[string]interface{} `json:"options,omitempty"`
}
// OllamaResponse represents the response from Ollama API
type OllamaResponse struct {
Model string `json:"model"`
CreatedAt time.Time `json:"created_at"`
Response string `json:"response"`
Done bool `json:"done"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
// AnalysisResult contains AI analysis findings
type AnalysisResult struct {
Type string // "javascript", "http", "anomaly", "report"
Severity string // "critical", "high", "medium", "low", "info"
Findings []string
Model string
Duration time.Duration
}
// NewOllamaClient creates a new Ollama client
func NewOllamaClient(baseURL, fastModel, deepModel string, enableCascade bool) *OllamaClient {
if baseURL == "" {
baseURL = "http://localhost:11434"
}
if fastModel == "" {
fastModel = "phi3.5:3.8b"
}
if deepModel == "" {
deepModel = "qwen2.5-coder:7b"
}
return &OllamaClient{
BaseURL: baseURL,
FastModel: fastModel,
DeepModel: deepModel,
Timeout: 60 * time.Second,
EnableCascade: enableCascade,
}
}
// IsAvailable checks if Ollama is running and models are available
func (c *OllamaClient) IsAvailable() bool {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(c.BaseURL + "/api/tags")
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}
// QuickTriage performs fast classification using lightweight model
func (c *OllamaClient) QuickTriage(content, contextType string) (bool, string, error) {
prompt := fmt.Sprintf(`You are a security triage expert. Quickly classify if this %s contains security-relevant information.
Content:
%s
Respond with ONLY:
- "RELEVANT: <brief reason>" if it contains security issues, secrets, vulnerabilities, or suspicious patterns
- "SKIP: <brief reason>" if it's normal/benign
Be concise. One line response only.`, contextType, truncate(content, 2000))
start := time.Now()
response, err := c.query(c.FastModel, prompt, 10*time.Second)
if err != nil {
return false, "", err
}
duration := time.Since(start)
response = strings.TrimSpace(response)
// Parse response
isRelevant := strings.HasPrefix(strings.ToUpper(response), "RELEVANT:")
reason := strings.TrimPrefix(response, "RELEVANT:")
reason = strings.TrimPrefix(reason, "SKIP:")
reason = strings.TrimSpace(reason)
if duration > 5*time.Second {
// If fast model is too slow, disable it
c.EnableCascade = false
}
return isRelevant, reason, nil
}
// AnalyzeJavaScript performs deep analysis of JavaScript code
func (c *OllamaClient) AnalyzeJavaScript(code string) (*AnalysisResult, error) {
// Fast triage first if cascade enabled
if c.EnableCascade {
relevant, reason, err := c.QuickTriage(code, "JavaScript code")
if err == nil && !relevant {
return &AnalysisResult{
Type: "javascript",
Severity: "info",
Findings: []string{fmt.Sprintf("Skipped (triage: %s)", reason)},
Model: c.FastModel,
}, nil
}
}
prompt := fmt.Sprintf(`You are a security expert analyzing JavaScript code. Identify:
1. **Hardcoded Secrets**: API keys, tokens, passwords, private keys
2. **Vulnerabilities**: XSS, injection points, insecure functions
3. **Suspicious Patterns**: Obfuscation, backdoors, malicious logic
4. **Hidden Endpoints**: Undocumented APIs, internal URLs
JavaScript Code:
%s
Format your response as:
CRITICAL: <finding>
HIGH: <finding>
MEDIUM: <finding>
LOW: <finding>
INFO: <finding>
Only list actual findings. Be concise and specific.`, truncate(code, 3000))
start := time.Now()
response, err := c.query(c.DeepModel, prompt, 30*time.Second)
duration := time.Since(start)
if err != nil {
return nil, err
}
return parseFindings(response, "javascript", c.DeepModel, duration), nil
}
// AnalyzeHTTPResponse analyzes HTTP response for security issues
func (c *OllamaClient) AnalyzeHTTPResponse(subdomain string, statusCode int, headers []string, body string) (*AnalysisResult, error) {
// Fast triage
if c.EnableCascade {
content := fmt.Sprintf("Status: %d\nHeaders: %s\nBody: %s", statusCode, strings.Join(headers, ", "), truncate(body, 500))
relevant, reason, err := c.QuickTriage(content, "HTTP response")
if err == nil && !relevant {
return &AnalysisResult{
Type: "http",
Severity: "info",
Findings: []string{fmt.Sprintf("Normal response (triage: %s)", reason)},
Model: c.FastModel,
}, nil
}
}
prompt := fmt.Sprintf(`Analyze this HTTP response for security issues:
URL: %s
Status: %d
Headers: %s
Body (first 1000 chars): %s
Identify:
- Information disclosure
- Misconfigurations
- Debug/error information exposure
- Unusual behavior patterns
Format as: SEVERITY: finding`, subdomain, statusCode, strings.Join(headers, "\n"), truncate(body, 1000))
start := time.Now()
response, err := c.query(c.DeepModel, prompt, 20*time.Second)
duration := time.Since(start)
if err != nil {
return nil, err
}
return parseFindings(response, "http", c.DeepModel, duration), nil
}
// DetectAnomalies identifies unusual patterns across scan results
func (c *OllamaClient) DetectAnomalies(summary string) (*AnalysisResult, error) {
prompt := fmt.Sprintf(`You are analyzing subdomain enumeration results. Find anomalies and prioritize findings:
%s
Identify:
- Subdomains with unusual behavior vs others
- Potential high-value targets (admin, api, internal)
- Misconfigurations or exposed services
- Patterns suggesting vulnerabilities
Format: SEVERITY: finding`, truncate(summary, 4000))
start := time.Now()
response, err := c.query(c.DeepModel, prompt, 30*time.Second)
duration := time.Since(start)
if err != nil {
return nil, err
}
return parseFindings(response, "anomaly", c.DeepModel, duration), nil
}
// GenerateReport creates executive summary and recommendations
func (c *OllamaClient) GenerateReport(findings string, stats map[string]int) (string, error) {
prompt := fmt.Sprintf(`Create a concise security assessment report:
SCAN STATISTICS:
- Total subdomains: %d
- Active: %d
- Vulnerabilities: %d
- Takeovers: %d
KEY FINDINGS:
%s
Generate report with:
## Executive Summary (2-3 sentences)
## Critical Findings (prioritized list)
## Recommendations (actionable items)
Be concise and professional.`,
stats["total"], stats["active"], stats["vulns"], stats["takeovers"], truncate(findings, 3000))
response, err := c.query(c.DeepModel, prompt, 45*time.Second)
if err != nil {
return "", err
}
return response, nil
}
// CVEMatch checks for known vulnerabilities in detected technologies using function calling
func (c *OllamaClient) CVEMatch(technology, version string) (string, error) {
prompt := fmt.Sprintf(`Check if %s version %s has known CVE vulnerabilities. Use the search_cve tool to look up real CVE data from the NVD database.
After getting CVE results, analyze them and provide:
1. Summary of findings
2. Severity assessment
3. Specific recommendations
If version is unknown, still search using just the technology name.`, technology, version)
// Use function calling with tools
response, err := c.queryWithTools(c.DeepModel, prompt, 30*time.Second)
if err != nil {
return "", err
}
if strings.Contains(strings.ToLower(response), "no known cve") {
return "", nil
}
return response, nil
}
// query sends a request to Ollama API
func (c *OllamaClient) query(model, prompt string, timeout time.Duration) (string, error) {
reqBody := OllamaRequest{
Model: model,
Prompt: prompt,
Stream: false,
Options: map[string]interface{}{
"temperature": 0.3, // Low temperature for more focused responses
"top_p": 0.9,
},
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %v", err)
}
client := &http.Client{Timeout: timeout}
resp, err := client.Post(
c.BaseURL+"/api/generate",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
return "", fmt.Errorf("ollama request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("ollama returned status %d", resp.StatusCode)
}
var ollamaResp OllamaResponse
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
return "", fmt.Errorf("failed to decode response: %v", err)
}
return strings.TrimSpace(ollamaResp.Response), nil
}
// parseFindings extracts findings by severity from AI response
func parseFindings(response, findingType, model string, duration time.Duration) *AnalysisResult {
result := &AnalysisResult{
Type: findingType,
Severity: "info",
Findings: []string{},
Model: model,
Duration: duration,
}
lines := strings.Split(response, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Parse severity-prefixed findings
upper := strings.ToUpper(line)
if strings.HasPrefix(upper, "CRITICAL:") {
result.Severity = "critical"
result.Findings = append(result.Findings, strings.TrimPrefix(line, "CRITICAL:"))
} else if strings.HasPrefix(upper, "HIGH:") {
if result.Severity != "critical" {
result.Severity = "high"
}
result.Findings = append(result.Findings, strings.TrimPrefix(line, "HIGH:"))
} else if strings.HasPrefix(upper, "MEDIUM:") {
if result.Severity != "critical" && result.Severity != "high" {
result.Severity = "medium"
}
result.Findings = append(result.Findings, strings.TrimPrefix(line, "MEDIUM:"))
} else if strings.HasPrefix(upper, "LOW:") {
if result.Severity == "info" {
result.Severity = "low"
}
result.Findings = append(result.Findings, strings.TrimPrefix(line, "LOW:"))
} else if strings.HasPrefix(upper, "INFO:") {
result.Findings = append(result.Findings, strings.TrimPrefix(line, "INFO:"))
} else if len(line) > 0 && !strings.HasPrefix(line, "#") {
// Non-prefixed findings
result.Findings = append(result.Findings, line)
}
}
// Clean up findings
for i := range result.Findings {
result.Findings[i] = strings.TrimSpace(result.Findings[i])
}
return result
}
// queryWithTools sends a request to Ollama API with function calling support
func (c *OllamaClient) queryWithTools(model, prompt string, timeout time.Duration) (string, error) {
tools := GetAvailableTools()
reqBody := OllamaRequest{
Model: model,
Prompt: prompt,
Stream: false,
Tools: tools,
Options: map[string]interface{}{
"temperature": 0.3,
"top_p": 0.9,
},
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %v", err)
}
client := &http.Client{Timeout: timeout}
resp, err := client.Post(
c.BaseURL+"/api/generate",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
return "", fmt.Errorf("ollama request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("ollama returned status %d", resp.StatusCode)
}
var ollamaResp OllamaResponse
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
return "", fmt.Errorf("failed to decode response: %v", err)
}
// Check if AI requested tool calls
if len(ollamaResp.ToolCalls) > 0 {
// Execute tool calls and get results
toolResults := make(map[string]string)
for _, toolCall := range ollamaResp.ToolCalls {
result, err := ExecuteTool(toolCall)
if err != nil {
toolResults[toolCall.Function.Name] = fmt.Sprintf("Error: %v", err)
} else {
toolResults[toolCall.Function.Name] = result
}
}
// Send tool results back to AI for final analysis
followUpPrompt := fmt.Sprintf(`%s
Tool Results:
%s
Based on these results, provide your analysis.`, prompt, formatToolResults(toolResults))
return c.query(model, followUpPrompt, timeout)
}
return strings.TrimSpace(ollamaResp.Response), nil
}
// formatToolResults formats tool execution results for the AI
func formatToolResults(results map[string]string) string {
var formatted strings.Builder
for tool, result := range results {
formatted.WriteString(fmt.Sprintf("\n=== %s ===\n%s\n", tool, result))
}
return formatted.String()
}
// truncate limits string length for prompts
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "\n...(truncated)"
}
+257
View File
@@ -0,0 +1,257 @@
package ai
import (
"encoding/json"
"fmt"
)
// Tool represents a function that can be called by the AI
type Tool struct {
Type string `json:"type"`
Function ToolFunction `json:"function"`
}
// ToolFunction describes a callable function
type ToolFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
// ToolCall represents an AI request to call a function
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function ToolCallFunction `json:"function"`
}
// ToolCallFunction contains the function name and arguments
type ToolCallFunction struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
// GetAvailableTools returns the list of tools available for AI function calling
func GetAvailableTools() []Tool {
return []Tool{
{
Type: "function",
Function: ToolFunction{
Name: "search_cve",
Description: "Search for CVE vulnerabilities for a specific software/technology and version. Returns a list of known CVEs with descriptions and severity.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"technology": map[string]interface{}{
"type": "string",
"description": "The software or technology name (e.g., 'nginx', 'Apache', 'WordPress', 'IIS')",
},
"version": map[string]interface{}{
"type": "string",
"description": "The version number if known (e.g., '2.4.49', '10.0'). Use 'unknown' if version is not specified.",
},
},
"required": []string{"technology"},
},
},
},
{
Type: "function",
Function: ToolFunction{
Name: "check_security_headers",
Description: "Analyzes HTTP security headers and returns recommendations for missing or misconfigured headers.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"headers": map[string]interface{}{
"type": "object",
"description": "HTTP response headers as key-value pairs",
},
},
"required": []string{"headers"},
},
},
},
{
Type: "function",
Function: ToolFunction{
Name: "analyze_javascript",
Description: "Analyzes JavaScript code for potential security issues like hardcoded secrets, eval usage, or suspicious patterns.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"code": map[string]interface{}{
"type": "string",
"description": "JavaScript code snippet to analyze",
},
"url": map[string]interface{}{
"type": "string",
"description": "The URL where the JavaScript was found",
},
},
"required": []string{"code"},
},
},
},
}
}
// ExecuteTool executes a tool call and returns the result
func ExecuteTool(toolCall ToolCall) (string, error) {
switch toolCall.Function.Name {
case "search_cve":
var args struct {
Technology string `json:"technology"`
Version string `json:"version"`
}
if err := json.Unmarshal(toolCall.Function.Arguments, &args); err != nil {
return "", fmt.Errorf("failed to parse arguments: %w", err)
}
return SearchCVE(args.Technology, args.Version)
case "check_security_headers":
var args struct {
Headers map[string]string `json:"headers"`
}
if err := json.Unmarshal(toolCall.Function.Arguments, &args); err != nil {
return "", fmt.Errorf("failed to parse arguments: %w", err)
}
return CheckSecurityHeaders(args.Headers)
case "analyze_javascript":
var args struct {
Code string `json:"code"`
URL string `json:"url"`
}
if err := json.Unmarshal(toolCall.Function.Arguments, &args); err != nil {
return "", fmt.Errorf("failed to parse arguments: %w", err)
}
return AnalyzeJavaScript(args.Code, args.URL)
default:
return "", fmt.Errorf("unknown tool: %s", toolCall.Function.Name)
}
}
// CheckSecurityHeaders analyzes HTTP headers for security issues
func CheckSecurityHeaders(headers map[string]string) (string, error) {
var issues []string
var recommendations []string
// Check for important security headers
if _, ok := headers["Strict-Transport-Security"]; !ok {
issues = append(issues, "Missing HSTS header")
recommendations = append(recommendations, "Add 'Strict-Transport-Security: max-age=31536000; includeSubDomains'")
}
if _, ok := headers["X-Content-Type-Options"]; !ok {
issues = append(issues, "Missing X-Content-Type-Options header")
recommendations = append(recommendations, "Add 'X-Content-Type-Options: nosniff'")
}
if _, ok := headers["X-Frame-Options"]; !ok {
issues = append(issues, "Missing X-Frame-Options header")
recommendations = append(recommendations, "Add 'X-Frame-Options: DENY' or 'SAMEORIGIN'")
}
if csp, ok := headers["Content-Security-Policy"]; !ok {
issues = append(issues, "Missing Content-Security-Policy header")
recommendations = append(recommendations, "Add CSP header to prevent XSS attacks")
} else if csp == "" {
issues = append(issues, "Empty Content-Security-Policy header")
}
if xss, ok := headers["X-XSS-Protection"]; ok && xss == "0" {
issues = append(issues, "X-XSS-Protection is disabled")
recommendations = append(recommendations, "Enable XSS protection: '1; mode=block'")
}
// Check for information disclosure
if server, ok := headers["Server"]; ok {
issues = append(issues, fmt.Sprintf("Server header exposes technology: %s", server))
recommendations = append(recommendations, "Remove or obfuscate Server header")
}
if xPowered, ok := headers["X-Powered-By"]; ok {
issues = append(issues, fmt.Sprintf("X-Powered-By header exposes technology: %s", xPowered))
recommendations = append(recommendations, "Remove X-Powered-By header")
}
result := fmt.Sprintf("Security Headers Analysis:\n\nIssues Found (%d):\n", len(issues))
for i, issue := range issues {
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
if len(recommendations) > 0 {
result += fmt.Sprintf("\nRecommendations (%d):\n", len(recommendations))
for i, rec := range recommendations {
result += fmt.Sprintf("%d. %s\n", i+1, rec)
}
}
if len(issues) == 0 {
result = "Security headers look good! No major issues found."
}
return result, nil
}
// AnalyzeJavaScript performs basic security analysis on JavaScript code
func AnalyzeJavaScript(code string, url string) (string, error) {
var findings []string
// Simple pattern matching for security issues
patterns := map[string]string{
"eval(": "Usage of eval() - can lead to code injection",
"innerHTML": "Usage of innerHTML - potential XSS vulnerability",
"document.write": "Usage of document.write - can be dangerous",
"api_key": "Potential hardcoded API key",
"apikey": "Potential hardcoded API key",
"password": "Potential hardcoded password",
"secret": "Potential hardcoded secret",
"token": "Potential hardcoded token",
"access_token": "Potential hardcoded access token",
"AKIA": "Potential AWS access key",
"Bearer ": "Potential hardcoded bearer token",
"crypto.createCipheriv": "Cryptographic operations - review implementation",
"Math.random()": "Math.random() is not cryptographically secure",
"localStorage.setItem": "Data stored in localStorage - ensure no sensitive data",
"sessionStorage.setItem": "Data stored in sessionStorage - ensure no sensitive data",
"XMLHttpRequest": "Legacy XMLHttpRequest - consider using fetch API",
"dangerouslySetInnerHTML": "React dangerouslySetInnerHTML - XSS risk",
}
for pattern, description := range patterns {
if contains(code, pattern) {
findings = append(findings, fmt.Sprintf("⚠️ %s", description))
}
}
result := fmt.Sprintf("JavaScript Security Analysis for %s:\n\n", url)
if len(findings) == 0 {
result += "No obvious security issues detected in this code snippet."
} else {
result += fmt.Sprintf("Found %d potential security issues:\n", len(findings))
for i, finding := range findings {
result += fmt.Sprintf("%d. %s\n", i+1, finding)
}
result += "\nNote: These are automated findings. Manual review is recommended."
}
return result, nil
}
// contains checks if a string contains a substring (case-insensitive for simplicity)
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsAt(s, substr, 0))
}
func containsAt(s, substr string, start int) bool {
for i := start; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
+139
View File
@@ -0,0 +1,139 @@
package config
import (
"time"
)
// Config holds the scan configuration
type Config struct {
Domain string
Wordlist string
Concurrency int
Timeout int
Output string
Format string
Silent bool
Verbose bool
NoBrute bool
NoProbe bool
NoPorts bool
NoTakeover bool
Resolvers string
Ports string
OnlyActive bool
JsonOutput bool
// AI Configuration
EnableAI bool
AIUrl string
AIFastModel string
AIDeepModel string
AICascade bool
AIDeepAnalysis bool
}
// Stats holds scan statistics
type Stats struct {
TotalFound int32
TotalResolved int32
TotalActive int32
TakeoverFound int32
StartTime time.Time
}
// SubdomainResult holds all information about a subdomain
type SubdomainResult struct {
Subdomain string `json:"subdomain"`
IPs []string `json:"ips,omitempty"`
CNAME string `json:"cname,omitempty"`
PTR string `json:"ptr,omitempty"`
ASN string `json:"asn,omitempty"`
Org string `json:"org,omitempty"`
Country string `json:"country,omitempty"`
City string `json:"city,omitempty"`
StatusCode int `json:"status_code,omitempty"`
ContentLength int64 `json:"content_length,omitempty"`
RedirectURL string `json:"redirect_url,omitempty"`
Title string `json:"title,omitempty"`
Server string `json:"server,omitempty"`
Tech []string `json:"technologies,omitempty"`
Headers []string `json:"headers,omitempty"`
WAF string `json:"waf,omitempty"`
TLSVersion string `json:"tls_version,omitempty"`
TLSIssuer string `json:"tls_issuer,omitempty"`
TLSExpiry string `json:"tls_expiry,omitempty"`
Ports []int `json:"ports,omitempty"`
Takeover string `json:"takeover,omitempty"`
ResponseMs int64 `json:"response_ms,omitempty"`
FaviconHash string `json:"favicon_hash,omitempty"`
RobotsTxt bool `json:"robots_txt,omitempty"`
SitemapXml bool `json:"sitemap_xml,omitempty"`
MXRecords []string `json:"mx_records,omitempty"`
TXTRecords []string `json:"txt_records,omitempty"`
NSRecords []string `json:"ns_records,omitempty"`
// Security checks
SecurityHeaders []string `json:"security_headers,omitempty"`
MissingHeaders []string `json:"missing_headers,omitempty"`
OpenRedirect bool `json:"open_redirect,omitempty"`
CORSMisconfig string `json:"cors_misconfig,omitempty"`
AllowedMethods []string `json:"allowed_methods,omitempty"`
DangerousMethods []string `json:"dangerous_methods,omitempty"`
// Discovery checks
AdminPanels []string `json:"admin_panels,omitempty"`
GitExposed bool `json:"git_exposed,omitempty"`
SvnExposed bool `json:"svn_exposed,omitempty"`
BackupFiles []string `json:"backup_files,omitempty"`
APIEndpoints []string `json:"api_endpoints,omitempty"`
// Cloud and Email Security
CloudProvider string `json:"cloud_provider,omitempty"`
S3Buckets []string `json:"s3_buckets,omitempty"`
SPFRecord string `json:"spf_record,omitempty"`
DMARCRecord string `json:"dmarc_record,omitempty"`
EmailSecurity string `json:"email_security,omitempty"`
TLSAltNames []string `json:"tls_alt_names,omitempty"`
// JavaScript Analysis
JSFiles []string `json:"js_files,omitempty"`
JSSecrets []string `json:"js_secrets,omitempty"`
// AI Analysis
AIFindings []string `json:"ai_findings,omitempty"`
AISeverity string `json:"ai_severity,omitempty"`
AIModel string `json:"ai_model,omitempty"`
CVEFindings []string `json:"cve_findings,omitempty"`
}
// IPInfo holds IP geolocation data
type IPInfo struct {
ASN string `json:"as"`
Org string `json:"org"`
Country string `json:"country"`
City string `json:"city"`
}
// SourceResult holds passive source results
type SourceResult struct {
Name string
Subs []string
Err error
}
// Default values
var DefaultResolvers = []string{
"8.8.8.8:53",
"8.8.4.4:53",
"1.1.1.1:53",
"1.0.0.1:53",
"9.9.9.9:53",
}
var DefaultWordlist = []string{
"www", "mail", "ftp", "localhost", "webmail", "smtp", "pop", "ns1", "ns2",
"ns3", "ns4", "dns", "dns1", "dns2", "api", "dev", "staging", "prod",
"admin", "administrator", "app", "apps", "auth", "beta", "blog", "cdn",
"chat", "cloud", "cms", "cpanel", "dashboard", "db", "demo", "docs",
"email", "forum", "git", "gitlab", "help", "home", "host", "img",
"images", "imap", "internal", "intranet", "jenkins", "jira", "lab",
"legacy", "login", "m", "mobile", "monitor", "mx", "mysql", "new",
"news", "old", "panel", "portal", "preview", "private", "proxy", "remote",
"server", "shop", "smtp", "sql", "ssh", "ssl", "stage", "staging",
"static", "status", "store", "support", "test", "testing", "tools",
"vpn", "web", "webmail", "wiki", "www1", "www2", "www3",
}
+232
View File
@@ -0,0 +1,232 @@
package dns
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/miekg/dns"
"god-eye/internal/config"
)
func ResolveSubdomain(subdomain string, resolvers []string, timeout int) []string {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(subdomain), dns.TypeA)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
var ips []string
for _, ans := range r.Answer {
if a, ok := ans.(*dns.A); ok {
ips = append(ips, a.A.String())
}
}
if len(ips) > 0 {
return ips
}
}
return nil
}
func CheckWildcard(domain string, resolvers []string) []string {
// Test multiple random patterns for better wildcard detection
patterns := []string{
fmt.Sprintf("random%d.%s", time.Now().UnixNano(), domain),
fmt.Sprintf("xyz%d.%s", time.Now().UnixNano()%1000000, domain),
fmt.Sprintf("nonexistent-%s.%s", "abc123xyz", domain),
}
allIPs := make(map[string]int)
for _, pattern := range patterns {
ips := ResolveSubdomain(pattern, resolvers, 3)
for _, ip := range ips {
allIPs[ip]++
}
}
// If same IP(s) appear in multiple patterns, it's a wildcard
var wildcardIPs []string
for ip, count := range allIPs {
if count >= 2 {
wildcardIPs = append(wildcardIPs, ip)
}
}
return wildcardIPs
}
func ResolveCNAME(subdomain string, resolvers []string, timeout int) string {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(subdomain), dns.TypeCNAME)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
for _, ans := range r.Answer {
if cname, ok := ans.(*dns.CNAME); ok {
return strings.TrimSuffix(cname.Target, ".")
}
}
}
return ""
}
func ResolvePTR(ip string, resolvers []string, timeout int) string {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
// Convert IP to reverse DNS format
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return ""
}
reverseIP := fmt.Sprintf("%s.%s.%s.%s.in-addr.arpa.", parts[3], parts[2], parts[1], parts[0])
m := dns.Msg{}
m.SetQuestion(reverseIP, dns.TypePTR)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
for _, ans := range r.Answer {
if ptr, ok := ans.(*dns.PTR); ok {
return strings.TrimSuffix(ptr.Ptr, ".")
}
}
}
return ""
}
func ResolveMX(domain string, resolvers []string, timeout int) []string {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(domain), dns.TypeMX)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
var records []string
for _, ans := range r.Answer {
if mx, ok := ans.(*dns.MX); ok {
records = append(records, strings.TrimSuffix(mx.Mx, "."))
}
}
if len(records) > 0 {
return records
}
}
return nil
}
func ResolveTXT(domain string, resolvers []string, timeout int) []string {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
var records []string
for _, ans := range r.Answer {
if txt, ok := ans.(*dns.TXT); ok {
for _, t := range txt.Txt {
// Limit length for display
if len(t) > 100 {
t = t[:97] + "..."
}
records = append(records, t)
}
}
}
if len(records) > 0 {
return records
}
}
return nil
}
func ResolveNS(domain string, resolvers []string, timeout int) []string {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(domain), dns.TypeNS)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
var records []string
for _, ans := range r.Answer {
if ns, ok := ans.(*dns.NS); ok {
records = append(records, strings.TrimSuffix(ns.Ns, "."))
}
}
if len(records) > 0 {
return records
}
}
return nil
}
func GetIPInfo(ip string) (*config.IPInfo, error) {
client := &http.Client{Timeout: 5 * time.Second}
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=as,org,country,city", ip)
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var info config.IPInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, err
}
return &info, nil
}
+27
View File
@@ -0,0 +1,27 @@
package http
import (
"crypto/tls"
"net/http"
"time"
)
// SharedTransport is a global shared HTTP transport for connection pooling
var SharedTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true, // Keep Content-Length header for SPA detection
}
// GetSharedClient returns an HTTP client with connection pooling
func GetSharedClient(timeout int) *http.Client {
return &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: SharedTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
+221
View File
@@ -0,0 +1,221 @@
package http
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
"god-eye/internal/config"
)
func ProbeHTTP(subdomain string, timeout int) *config.SubdomainResult {
result := &config.SubdomainResult{}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
urls := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, url := range urls {
start := time.Now()
resp, err := client.Get(url)
if err != nil {
continue
}
defer resp.Body.Close()
result.StatusCode = resp.StatusCode
result.ResponseMs = time.Since(start).Milliseconds()
// Content-Length
if cl := resp.ContentLength; cl > 0 {
result.ContentLength = cl
}
// Redirect location
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
if loc := resp.Header.Get("Location"); loc != "" {
result.RedirectURL = loc
}
}
// Server header
if server := resp.Header.Get("Server"); server != "" {
result.Server = server
result.Tech = append(result.Tech, server)
}
// TLS/SSL info
if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
cert := resp.TLS.PeerCertificates[0]
result.TLSIssuer = cert.Issuer.CommonName
result.TLSExpiry = cert.NotAfter.Format("2006-01-02")
// TLS version
switch resp.TLS.Version {
case tls.VersionTLS13:
result.TLSVersion = "TLS 1.3"
case tls.VersionTLS12:
result.TLSVersion = "TLS 1.2"
case tls.VersionTLS11:
result.TLSVersion = "TLS 1.1"
case tls.VersionTLS10:
result.TLSVersion = "TLS 1.0"
}
}
// Interesting headers
interestingHeaders := []string{
"X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version",
"X-Generator", "X-Drupal-Cache", "X-Varnish",
"X-Cache", "X-Backend-Server", "X-Server",
}
for _, h := range interestingHeaders {
if val := resp.Header.Get(h); val != "" {
result.Headers = append(result.Headers, fmt.Sprintf("%s: %s", h, val))
if h == "X-Powered-By" {
result.Tech = append(result.Tech, val)
}
}
}
// WAF detection
result.WAF = DetectWAF(resp)
// Security headers check
result.SecurityHeaders, result.MissingHeaders = CheckSecurityHeaders(resp)
// Read body for title and tech detection
body, err := io.ReadAll(io.LimitReader(resp.Body, 100000))
if err == nil {
// Content-Length from body if not set
if result.ContentLength == 0 {
result.ContentLength = int64(len(body))
}
// Extract title
titleRe := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
if matches := titleRe.FindSubmatch(body); len(matches) > 1 {
result.Title = strings.TrimSpace(string(matches[1]))
}
// Detect technologies
bodyStr := string(body)
if strings.Contains(bodyStr, "wp-content") || strings.Contains(bodyStr, "wordpress") {
result.Tech = append(result.Tech, "WordPress")
}
if strings.Contains(bodyStr, "_next") || strings.Contains(bodyStr, "Next.js") {
result.Tech = append(result.Tech, "Next.js")
}
if strings.Contains(bodyStr, "react") || strings.Contains(bodyStr, "React") {
result.Tech = append(result.Tech, "React")
}
if strings.Contains(bodyStr, "laravel") || strings.Contains(bodyStr, "Laravel") {
result.Tech = append(result.Tech, "Laravel")
}
if strings.Contains(bodyStr, "django") || strings.Contains(bodyStr, "Django") {
result.Tech = append(result.Tech, "Django")
}
if strings.Contains(bodyStr, "angular") || strings.Contains(bodyStr, "ng-") {
result.Tech = append(result.Tech, "Angular")
}
if strings.Contains(bodyStr, "vue") || strings.Contains(bodyStr, "Vue.js") {
result.Tech = append(result.Tech, "Vue.js")
}
}
break
}
return result
}
func DetectWAF(resp *http.Response) string {
// Check headers for WAF signatures
serverHeader := strings.ToLower(resp.Header.Get("Server"))
// Cloudflare
if resp.Header.Get("CF-RAY") != "" || strings.Contains(serverHeader, "cloudflare") {
return "Cloudflare"
}
// AWS WAF/CloudFront
if resp.Header.Get("X-Amz-Cf-Id") != "" || resp.Header.Get("X-Amz-Cf-Pop") != "" {
return "AWS CloudFront"
}
// Akamai
if resp.Header.Get("X-Akamai-Transformed") != "" || strings.Contains(serverHeader, "akamai") {
return "Akamai"
}
// Sucuri
if resp.Header.Get("X-Sucuri-ID") != "" || strings.Contains(serverHeader, "sucuri") {
return "Sucuri"
}
// Imperva/Incapsula
if resp.Header.Get("X-Iinfo") != "" || resp.Header.Get("X-CDN") == "Incapsula" {
return "Imperva"
}
// F5 BIG-IP
if strings.Contains(serverHeader, "big-ip") || resp.Header.Get("X-WA-Info") != "" {
return "F5 BIG-IP"
}
// Barracuda
if strings.Contains(serverHeader, "barracuda") {
return "Barracuda"
}
// Fastly
if resp.Header.Get("X-Fastly-Request-ID") != "" || resp.Header.Get("Fastly-Debug-Digest") != "" {
return "Fastly"
}
// Varnish
if resp.Header.Get("X-Varnish") != "" {
return "Varnish"
}
return ""
}
func CheckSecurityHeaders(resp *http.Response) (present []string, missing []string) {
securityHeaders := map[string]string{
"Content-Security-Policy": "CSP",
"X-Frame-Options": "X-Frame",
"X-Content-Type-Options": "X-Content-Type",
"Strict-Transport-Security": "HSTS",
"X-XSS-Protection": "X-XSS",
"Referrer-Policy": "Referrer",
"Permissions-Policy": "Permissions",
}
for header, shortName := range securityHeaders {
if val := resp.Header.Get(header); val != "" {
present = append(present, shortName)
} else {
missing = append(missing, shortName)
}
}
return present, missing
}
+145
View File
@@ -0,0 +1,145 @@
package output
import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
"github.com/fatih/color"
"god-eye/internal/config"
)
var (
// Basic colors
Green = color.New(color.FgGreen).SprintFunc()
Red = color.New(color.FgRed).SprintFunc()
Blue = color.New(color.FgBlue).SprintFunc()
Yellow = color.New(color.FgYellow).SprintFunc()
Cyan = color.New(color.FgCyan).SprintFunc()
Magenta = color.New(color.FgMagenta).SprintFunc()
White = color.New(color.FgWhite).SprintFunc()
// Bold variants
BoldGreen = color.New(color.FgGreen, color.Bold).SprintFunc()
BoldRed = color.New(color.FgRed, color.Bold).SprintFunc()
BoldCyan = color.New(color.FgCyan, color.Bold).SprintFunc()
BoldYellow = color.New(color.FgYellow, color.Bold).SprintFunc()
BoldMagenta = color.New(color.FgMagenta, color.Bold).SprintFunc()
BoldWhite = color.New(color.FgWhite, color.Bold).SprintFunc()
// Dim/faint
Dim = color.New(color.Faint).SprintFunc()
// Background highlights
BgRed = color.New(color.BgRed, color.FgWhite, color.Bold).SprintFunc()
BgGreen = color.New(color.BgGreen, color.FgBlack, color.Bold).SprintFunc()
BgYellow = color.New(color.BgYellow, color.FgBlack, color.Bold).SprintFunc()
)
func PrintBanner() {
fmt.Println()
fmt.Println(BoldCyan(" ██████╗ ██████╗ ██████╗ ") + BoldWhite("███████╗") + BoldCyan(" ███████╗██╗ ██╗███████╗"))
fmt.Println(BoldCyan(" ██╔════╝ ██╔═══██╗██╔══██╗") + BoldWhite("██╔════╝") + BoldCyan(" ██╔════╝╚██╗ ██╔╝██╔════╝"))
fmt.Println(BoldCyan(" ██║ ███╗██║ ██║██║ ██║") + BoldWhite("███████╗") + BoldCyan(" █████╗ ╚████╔╝ █████╗ "))
fmt.Println(BoldCyan(" ██║ ██║██║ ██║██║ ██║") + BoldWhite("╚════██║") + BoldCyan(" ██╔══╝ ╚██╔╝ ██╔══╝ "))
fmt.Println(BoldCyan(" ╚██████╔╝╚██████╔╝██████╔╝") + BoldWhite("███████║") + BoldCyan(" ███████╗ ██║ ███████╗"))
fmt.Println(BoldCyan(" ╚═════╝ ╚═════╝ ╚═════╝ ") + BoldWhite("╚══════╝") + BoldCyan(" ╚══════╝ ╚═╝ ╚══════╝"))
fmt.Println()
fmt.Printf(" %s %s\n", BoldWhite("⚡"), Dim("Ultra-fast subdomain enumeration & reconnaissance"))
fmt.Printf(" %s %s %s %s %s %s\n",
Dim("Version:"), BoldGreen("0.1"),
Dim("By:"), Cyan("github.com/Vyntral"),
Dim("For:"), Yellow("github.com/Orizon-eu"))
fmt.Println()
}
func PrintSection(icon, title string) {
fmt.Printf("\n%s %s %s\n", BoldCyan("┌──"), BoldWhite(icon+" "+title), BoldCyan(strings.Repeat("─", 50)))
}
func PrintSubSection(text string) {
fmt.Printf("%s %s\n", Cyan("│"), text)
}
func PrintEndSection() {
fmt.Printf("%s\n", BoldCyan("└"+strings.Repeat("─", 60)))
}
func PrintProgress(current, total int, label string) {
width := 30
filled := int(float64(current) / float64(total) * float64(width))
if filled > width {
filled = width
}
bar := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
percent := float64(current) / float64(total) * 100
fmt.Printf("\r%s %s %s %s %.0f%% ", Cyan("│"), label, BoldGreen(bar), Dim(fmt.Sprintf("(%d/%d)", current, total)), percent)
}
func ClearLine() {
fmt.Print("\r\033[K")
}
func SaveOutput(path string, format string, results map[string]*config.SubdomainResult) {
file, err := os.Create(path)
if err != nil {
fmt.Printf("%s Failed to create output file: %v\n", Red("[-]"), err)
return
}
defer file.Close()
// Sort subdomains for consistent output
var sortedSubs []string
for sub := range results {
sortedSubs = append(sortedSubs, sub)
}
sort.Strings(sortedSubs)
switch format {
case "json":
var resultList []*config.SubdomainResult
for _, sub := range sortedSubs {
resultList = append(resultList, results[sub])
}
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
encoder.Encode(resultList)
case "csv":
writer := csv.NewWriter(file)
// Header
writer.Write([]string{"subdomain", "ips", "status_code", "title", "server", "technologies", "ports", "takeover", "response_ms"})
for _, sub := range sortedSubs {
r := results[sub]
var portStrs []string
for _, p := range r.Ports {
portStrs = append(portStrs, strconv.Itoa(p))
}
writer.Write([]string{
r.Subdomain,
strings.Join(r.IPs, ";"),
strconv.Itoa(r.StatusCode),
r.Title,
r.Server,
strings.Join(r.Tech, ";"),
strings.Join(portStrs, ";"),
r.Takeover,
strconv.FormatInt(r.ResponseMs, 10),
})
}
writer.Flush()
default: // txt
for _, sub := range sortedSubs {
file.WriteString(sub + "\n")
}
}
fmt.Printf("%s Results saved to %s\n", Green("[+]"), path)
}
+276
View File
@@ -0,0 +1,276 @@
package scanner
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/miekg/dns"
)
// DetectCloudProvider detects cloud provider based on IP/CNAME
func DetectCloudProvider(ips []string, cname string, asn string) string {
// Check CNAME patterns
cnamePatterns := map[string]string{
"amazonaws.com": "AWS",
"aws.com": "AWS",
"cloudfront.net": "AWS CloudFront",
"elasticbeanstalk.com": "AWS Elastic Beanstalk",
"elb.amazonaws.com": "AWS ELB",
"s3.amazonaws.com": "AWS S3",
"azure.com": "Azure",
"azurewebsites.net": "Azure App Service",
"cloudapp.net": "Azure",
"azurefd.net": "Azure Front Door",
"blob.core.windows.net": "Azure Blob",
"googleapis.com": "Google Cloud",
"appspot.com": "Google App Engine",
"storage.googleapis.com": "Google Cloud Storage",
"digitaloceanspaces.com": "DigitalOcean Spaces",
"ondigitalocean.app": "DigitalOcean App Platform",
"cloudflare.com": "Cloudflare",
"fastly.net": "Fastly",
"akamai.net": "Akamai",
"netlify.app": "Netlify",
"vercel.app": "Vercel",
"herokuapp.com": "Heroku",
}
for pattern, provider := range cnamePatterns {
if strings.Contains(cname, pattern) {
return provider
}
}
// Check ASN patterns
asnPatterns := map[string]string{
"AS14618": "AWS",
"AS16509": "AWS",
"AS8075": "Azure",
"AS15169": "Google Cloud",
"AS14061": "DigitalOcean",
"AS13335": "Cloudflare",
"AS54113": "Fastly",
"AS20940": "Akamai",
}
for pattern, provider := range asnPatterns {
if strings.Contains(asn, pattern) {
return provider
}
}
return ""
}
// CheckS3Buckets checks for exposed S3 buckets
func CheckS3Buckets(subdomain string, timeout int) []string {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Common S3 bucket URL patterns
parts := strings.Split(subdomain, ".")
bucketName := parts[0]
patterns := []string{
fmt.Sprintf("https://%s.s3.amazonaws.com", bucketName),
fmt.Sprintf("https://s3.amazonaws.com/%s", bucketName),
fmt.Sprintf("https://%s.s3.us-east-1.amazonaws.com", bucketName),
fmt.Sprintf("https://%s.s3.us-west-2.amazonaws.com", bucketName),
fmt.Sprintf("https://%s.s3.eu-west-1.amazonaws.com", bucketName),
}
var found []string
for _, url := range patterns {
resp, err := client.Get(url)
if err != nil {
continue
}
resp.Body.Close()
// Public bucket if 200 or 403 (exists but forbidden)
if resp.StatusCode == 200 {
found = append(found, url+" (PUBLIC)")
} else if resp.StatusCode == 403 {
found = append(found, url+" (exists)")
}
}
return found
}
// CheckEmailSecurity checks SPF/DKIM/DMARC records
func CheckEmailSecurity(domain string, resolvers []string, timeout int) (spf string, dmarc string, security string) {
c := dns.Client{
Timeout: time.Duration(timeout) * time.Second,
}
// Check SPF record
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m, resolver)
if err != nil || r == nil {
continue
}
for _, ans := range r.Answer {
if txt, ok := ans.(*dns.TXT); ok {
for _, t := range txt.Txt {
if strings.HasPrefix(t, "v=spf1") {
spf = t
if len(spf) > 80 {
spf = spf[:77] + "..."
}
break
}
}
}
}
if spf != "" {
break
}
}
// Check DMARC record
m2 := dns.Msg{}
m2.SetQuestion(dns.Fqdn("_dmarc."+domain), dns.TypeTXT)
for _, resolver := range resolvers {
r, _, err := c.Exchange(&m2, resolver)
if err != nil || r == nil {
continue
}
for _, ans := range r.Answer {
if txt, ok := ans.(*dns.TXT); ok {
for _, t := range txt.Txt {
if strings.HasPrefix(t, "v=DMARC1") {
dmarc = t
if len(dmarc) > 80 {
dmarc = dmarc[:77] + "..."
}
break
}
}
}
}
if dmarc != "" {
break
}
}
// Determine email security level
if spf != "" && dmarc != "" {
if strings.Contains(dmarc, "p=reject") || strings.Contains(dmarc, "p=quarantine") {
security = "Strong"
} else {
security = "Moderate"
}
} else if spf != "" || dmarc != "" {
security = "Weak"
} else {
security = "None"
}
return spf, dmarc, security
}
// GetTLSAltNames extracts Subject Alternative Names from TLS certificate
func GetTLSAltNames(subdomain string, timeout int) []string {
conn, err := tls.DialWithDialer(
&net.Dialer{Timeout: time.Duration(timeout) * time.Second},
"tcp",
subdomain+":443",
&tls.Config{InsecureSkipVerify: true},
)
if err != nil {
return nil
}
defer conn.Close()
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
return nil
}
var altNames []string
seen := make(map[string]bool)
for _, cert := range certs {
for _, name := range cert.DNSNames {
if !seen[name] && name != subdomain {
seen[name] = true
altNames = append(altNames, name)
}
}
}
// Limit to first 10
if len(altNames) > 10 {
altNames = altNames[:10]
}
return altNames
}
// CheckS3BucketsWithClient checks for exposed S3 buckets with shared client
func CheckS3BucketsWithClient(subdomain string, client *http.Client) []string {
parts := strings.Split(subdomain, ".")
if len(parts) < 2 {
return nil
}
subPrefix := parts[0]
// Get domain name (e.g., "finnat" from "ftp.finnat.it")
var domainName string
if len(parts) >= 2 {
domainName = parts[len(parts)-2]
}
// Skip generic subdomain names that cause false positives
genericNames := map[string]bool{
"www": true, "ftp": true, "mail": true, "smtp": true, "imap": true,
"pop": true, "webmail": true, "autodiscover": true, "test": true,
"dev": true, "staging": true, "api": true, "admin": true, "pop3": true,
}
var patterns []string
if genericNames[subPrefix] {
// For generic subdomains, use domain-specific bucket names
patterns = []string{
fmt.Sprintf("https://%s-%s.s3.amazonaws.com", domainName, subPrefix),
fmt.Sprintf("https://%s.s3.amazonaws.com", domainName),
}
} else {
// For specific subdomains, use combination
patterns = []string{
fmt.Sprintf("https://%s-%s.s3.amazonaws.com", domainName, subPrefix),
fmt.Sprintf("https://%s.s3.amazonaws.com", domainName),
}
}
var found []string
for _, url := range patterns {
resp, err := client.Get(url)
if err != nil {
continue
}
resp.Body.Close()
// Only report PUBLIC buckets (200), not just existing (403)
if resp.StatusCode == 200 {
found = append(found, url+" (PUBLIC)")
}
}
return found
}
+164
View File
@@ -0,0 +1,164 @@
package scanner
import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
)
// AnalyzeJSFiles finds JavaScript files and extracts potential secrets
func AnalyzeJSFiles(subdomain string, client *http.Client) ([]string, []string) {
var jsFiles []string
var secrets []string
urls := []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
resp, err := client.Get(baseURL)
if err != nil {
continue
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 500000))
resp.Body.Close()
if err != nil {
continue
}
// Find JS files referenced in HTML
jsRe := regexp.MustCompile(`src=["']([^"']*\.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
}
}
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
}
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 {
break
}
}
// Deduplicate and limit
jsFiles = UniqueStrings(jsFiles)
secrets = UniqueStrings(secrets)
if len(jsFiles) > 10 {
jsFiles = jsFiles[:10]
}
if len(secrets) > 10 {
secrets = secrets[:10]
}
return jsFiles, secrets
}
// UniqueStrings returns unique strings from a slice
func UniqueStrings(input []string) []string {
seen := make(map[string]bool)
var result []string
for _, s := range input {
if !seen[s] {
seen[s] = true
result = append(result, s)
}
}
return result
}
File diff suppressed because it is too large Load Diff
+254
View File
@@ -0,0 +1,254 @@
package scanner
import (
"crypto/md5"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/miekg/dns"
)
var TakeoverFingerprints = map[string]string{
// GitHub
"github.io": "There isn't a GitHub Pages site here",
"githubusercontent.com": "There isn't a GitHub Pages site here",
// Heroku
"herokuapp.com": "no-such-app.herokuapp.com",
"herokussl.com": "no-such-app.herokuapp.com",
// AWS
"s3.amazonaws.com": "NoSuchBucket",
"s3-website": "NoSuchBucket",
"elasticbeanstalk.com": "NoSuchBucket",
"cloudfront.net": "Bad Request",
"elb.amazonaws.com": "NXDOMAIN",
// Azure
"azurewebsites.net": "404 Web Site not found",
"cloudapp.net": "404 Web Site not found",
"cloudapp.azure.com": "404 Web Site not found",
"azurefd.net": "404 Web Site not found",
"blob.core.windows.net": "BlobNotFound",
"azure-api.net": "404 Resource not found",
"azurehdinsight.net": "404",
"azureedge.net": "404 Web Site not found",
"trafficmanager.net": "404 Web Site not found",
// Google Cloud
"appspot.com": "Error: Not Found",
"storage.googleapis.com": "NoSuchBucket",
"googleplex.com": "404. That's an error",
// Shopify
"myshopify.com": "Sorry, this shop is currently unavailable",
// Pantheon
"pantheonsite.io": "404 error unknown site",
// Zendesk
"zendesk.com": "Help Center Closed",
// Various services
"teamwork.com": "Oops - We didn't find your site",
"helpjuice.com": "We could not find what you're looking for",
"helpscoutdocs.com": "No settings were found for this company",
"ghost.io": "The thing you were looking for is no longer here",
"surge.sh": "project not found",
"bitbucket.io": "Repository not found",
"wordpress.com": "Do you want to register",
"smartling.com": "Domain is not configured",
"acquia.com": "Web Site Not Found",
"fastly.net": "Fastly error: unknown domain",
"uservoice.com": "This UserVoice subdomain is currently available",
"unbounce.com": "The requested URL was not found on this server",
"thinkific.com": "You may have mistyped the address",
"tilda.cc": "Please renew your subscription",
"mashery.com": "Unrecognized domain",
"intercom.help": "This page is reserved for",
"webflow.io": "The page you are looking for doesn't exist",
"wishpond.com": "https://www.wishpond.com/404",
"aftership.com": "Oops.</h2><p>The page you're looking for doesn't exist",
"aha.io": "There is no portal here",
"tictail.com": "to target URL: <a href=\"https://tictail.com",
"campaignmonitor.com": "Trying to access your account?",
"cargocollective.com": "404 Not Found",
"statuspage.io": "You are being <a href=\"https://www.statuspage.io\">",
"tumblr.com": "There's nothing here.",
"worksites.net": "Hello! Sorry, but the website you&rsquo;re looking for doesn&rsquo;t exist.",
"smugmug.com": "class=\"message-text\">Page Not Found<",
// Additional services
"netlify.app": "Not Found",
"netlify.com": "Not Found",
"vercel.app": "NOT_FOUND",
"now.sh": "NOT_FOUND",
"fly.dev": "404 Not Found",
"render.com": "NOT_FOUND",
"gitbook.io": "Domain not found",
"readme.io": "Project doesnt exist",
"desk.com": "Sorry, We Couldn't Find That Page",
"freshdesk.com": "There is no helpdesk here",
"tave.com": "Sorry, this profile doesn't exist",
"feedpress.me": "The feed has not been found",
"launchrock.com": "It looks like you may have taken a wrong turn",
"pingdom.com": "This public status page",
"surveygizmo.com": "data-html-name",
"tribepad.com": "Sorry, we could not find that page",
"uptimerobot.com": "This public status page",
"wufoo.com": "Profile not found",
"brightcove.com": "Error - Loss of soul",
"bigcartel.com": "Oops! We couldn't find that page",
"activehosted.com": "alt=\"LIGHTTPD - fly light.\"",
"createsend.com": "Double check the URL",
"flexbe.com": "Domain doesn't exist",
"agilecrm.com": "Sorry, this page is no longer available",
"anima.io": "not found",
"proposify.com": "If you need immediate assistance",
"simplebooklet.com": "We can't find this FlipBook",
"getresponse.com": "With GetResponse Landing Pages",
"vend.com": "Looks like you've traveled too far",
"strikingly.com": "But if you're looking to build your own website",
"airee.ru": "Ошибка 402. Сервис",
"anweb.ru": "Эта страница не существует",
"domain.ru": "К сожалению, не удалось",
"instapage.com": "Looks Like You're Lost",
"landingi.com": "Nie znaleziono strony",
"leadpages.net": "Oops - We Couldn't Find Your Page",
"pagewiz.com": "PAGE NOT FOUND",
"short.io": "Link does not exist",
"smartjobboard.com": "Company Not Found",
"uberflip.com": "Non-hub polygon detected",
"vingle.net": "해당 페이지가 존재하지 않습니다",
"ngrok.io": "Tunnel",
"kinsta.cloud": "No Site For Domain",
"canny.io": "There is no such company",
"hatena.ne.jp": "404 Blog is not found",
"medium.com": "This page doesn't exist",
"hatenablog.com": "404 Blog is not found",
"jetbrains.com": "is not a registered InCloud YouTrack",
}
func CheckTakeover(subdomain string, timeout int) string {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Check CNAME
c := dns.Client{Timeout: 3 * time.Second}
m := dns.Msg{}
m.SetQuestion(dns.Fqdn(subdomain), dns.TypeCNAME)
r, _, err := c.Exchange(&m, "8.8.8.8:53")
if err != nil || r == nil {
return ""
}
var cname string
for _, ans := range r.Answer {
if cn, ok := ans.(*dns.CNAME); ok {
cname = cn.Target
break
}
}
if cname == "" {
return ""
}
// Check if CNAME matches any vulnerable service
for service, fingerprint := range TakeoverFingerprints {
if strings.Contains(cname, service) {
// Verify by checking response
resp, err := client.Get(fmt.Sprintf("http://%s", subdomain))
if err != nil {
resp, err = client.Get(fmt.Sprintf("https://%s", subdomain))
}
if err == nil {
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 100000))
if strings.Contains(string(body), fingerprint) {
return service
}
}
// If can't reach, might still be vulnerable
if err != nil {
return service + " (unverified)"
}
}
}
return ""
}
// Helper functions for connection pooling
func CheckRobotsTxtWithClient(subdomain string, client *http.Client) bool {
urls := []string{
fmt.Sprintf("https://%s/robots.txt", subdomain),
fmt.Sprintf("http://%s/robots.txt", subdomain),
}
for _, url := range urls {
resp, err := client.Head(url)
if err != nil {
continue
}
resp.Body.Close()
if resp.StatusCode == 200 {
return true
}
}
return false
}
func CheckSitemapXmlWithClient(subdomain string, client *http.Client) bool {
urls := []string{
fmt.Sprintf("https://%s/sitemap.xml", subdomain),
fmt.Sprintf("http://%s/sitemap.xml", subdomain),
}
for _, url := range urls {
resp, err := client.Head(url)
if err != nil {
continue
}
resp.Body.Close()
if resp.StatusCode == 200 {
return true
}
}
return false
}
func GetFaviconHashWithClient(subdomain string, client *http.Client) string {
urls := []string{
fmt.Sprintf("https://%s/favicon.ico", subdomain),
fmt.Sprintf("http://%s/favicon.ico", subdomain),
}
for _, url := range urls {
resp, err := client.Get(url)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
continue
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 100000))
if err != nil || len(body) == 0 {
continue
}
hash := md5.Sum(body)
return hex.EncodeToString(hash[:])
}
return ""
}
+321
View File
@@ -0,0 +1,321 @@
package security
import (
"crypto/tls"
"fmt"
"net/http"
"strings"
"time"
)
// CheckOpenRedirect tests for open redirect vulnerabilities
func CheckOpenRedirect(subdomain string, timeout int) bool {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// Common open redirect parameters
testPayloads := []string{
"?url=https://evil.com",
"?redirect=https://evil.com",
"?next=https://evil.com",
"?return=https://evil.com",
"?dest=https://evil.com",
"?destination=https://evil.com",
"?rurl=https://evil.com",
"?target=https://evil.com",
}
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
for _, payload := range testPayloads {
testURL := baseURL + payload
resp, err := client.Get(testURL)
if err != nil {
continue
}
resp.Body.Close()
// Check if redirects to evil.com
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
if strings.Contains(location, "evil.com") {
return true
}
}
}
}
return false
}
// CheckCORS tests for CORS misconfiguration
func CheckCORS(subdomain string, timeout int) string {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
urls := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, url := range urls {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
continue
}
// Test with evil origin
req.Header.Set("Origin", "https://evil.com")
resp, err := client.Do(req)
if err != nil {
continue
}
resp.Body.Close()
acao := resp.Header.Get("Access-Control-Allow-Origin")
acac := resp.Header.Get("Access-Control-Allow-Credentials")
// Check for dangerous CORS configs
if acao == "*" {
if acac == "true" {
return "Wildcard + Credentials"
}
return "Wildcard Origin"
}
if acao == "https://evil.com" {
if acac == "true" {
return "Origin Reflection + Credentials"
}
return "Origin Reflection"
}
if strings.Contains(acao, "null") {
return "Null Origin Allowed"
}
}
return ""
}
// CheckHTTPMethods tests which HTTP methods are allowed
func CheckHTTPMethods(subdomain string, timeout int) (allowed []string, dangerous []string) {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
urls := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "TRACE"}
dangerousMethods := map[string]bool{
"PUT": true,
"DELETE": true,
"TRACE": true,
"PATCH": true,
}
for _, url := range urls {
// First try OPTIONS to get Allow header
req, err := http.NewRequest("OPTIONS", url, nil)
if err != nil {
continue
}
resp, err := client.Do(req)
if err != nil {
continue
}
resp.Body.Close()
// Check Allow header
allowHeader := resp.Header.Get("Allow")
if allowHeader != "" {
for _, method := range strings.Split(allowHeader, ",") {
method = strings.TrimSpace(method)
allowed = append(allowed, method)
if dangerousMethods[method] {
dangerous = append(dangerous, method)
}
}
return allowed, dangerous
}
// If no Allow header, test each method
for _, method := range methods {
req, err := http.NewRequest(method, url, nil)
if err != nil {
continue
}
resp, err := client.Do(req)
if err != nil {
continue
}
resp.Body.Close()
// Method is allowed if not 405 Method Not Allowed
if resp.StatusCode != 405 {
allowed = append(allowed, method)
if dangerousMethods[method] {
dangerous = append(dangerous, method)
}
}
}
if len(allowed) > 0 {
return allowed, dangerous
}
}
return allowed, dangerous
}
// WithClient versions for parallel execution with shared client
func CheckOpenRedirectWithClient(subdomain string, client *http.Client) bool {
testPayloads := []string{
"?url=https://evil.com",
"?redirect=https://evil.com",
"?next=https://evil.com",
"?return=https://evil.com",
}
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
for _, payload := range testPayloads {
testURL := baseURL + payload
resp, err := client.Get(testURL)
if err != nil {
continue
}
resp.Body.Close()
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
// Check if redirect actually goes to evil.com, not just contains it as parameter
if strings.HasPrefix(location, "https://evil.com") ||
strings.HasPrefix(location, "http://evil.com") ||
strings.HasPrefix(location, "//evil.com") {
return true
}
}
}
}
return false
}
func CheckCORSWithClient(subdomain string, client *http.Client) string {
urls := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, url := range urls {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
continue
}
req.Header.Set("Origin", "https://evil.com")
resp, err := client.Do(req)
if err != nil {
continue
}
resp.Body.Close()
acao := resp.Header.Get("Access-Control-Allow-Origin")
acac := resp.Header.Get("Access-Control-Allow-Credentials")
if acao == "*" {
if acac == "true" {
return "Wildcard + Credentials"
}
return "Wildcard Origin"
}
if acao == "https://evil.com" {
if acac == "true" {
return "Origin Reflection + Credentials"
}
return "Origin Reflection"
}
if strings.Contains(acao, "null") {
return "Null Origin Allowed"
}
}
return ""
}
func CheckHTTPMethodsWithClient(subdomain string, client *http.Client) (allowed []string, dangerous []string) {
urls := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
dangerousMethods := map[string]bool{
"PUT": true,
"DELETE": true,
"TRACE": true,
"PATCH": true,
}
for _, url := range urls {
req, err := http.NewRequest("OPTIONS", url, nil)
if err != nil {
continue
}
resp, err := client.Do(req)
if err != nil {
continue
}
resp.Body.Close()
// Only trust the Allow header from OPTIONS response
// Don't probe individual methods as this causes too many false positives
allowHeader := resp.Header.Get("Allow")
if allowHeader != "" {
for _, method := range strings.Split(allowHeader, ",") {
method = strings.TrimSpace(method)
allowed = append(allowed, method)
if dangerousMethods[method] {
dangerous = append(dangerous, method)
}
}
return allowed, dangerous
}
}
// If no Allow header found, don't report anything to avoid false positives
return allowed, dangerous
}
+407
View File
@@ -0,0 +1,407 @@
package security
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"strings"
"time"
)
func CheckAdminPanels(subdomain string, timeout int) []string {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// Common admin panel paths
paths := []string{
"/admin", "/administrator", "/admin.php", "/admin.html",
"/login", "/login.php", "/signin", "/auth",
"/wp-admin", "/wp-login.php",
"/phpmyadmin", "/pma", "/mysql",
"/cpanel", "/webmail",
"/manager", "/console", "/dashboard",
"/admin/login", "/user/login",
}
var found []string
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
for _, path := range paths {
testURL := baseURL + path
resp, err := client.Get(testURL)
if err != nil {
continue
}
resp.Body.Close()
// Found if 200, 301, 302, 401, 403 (not 404)
if resp.StatusCode != 404 && resp.StatusCode != 0 {
found = append(found, path)
}
}
if len(found) > 0 {
break
}
}
return found
}
// CheckGitSvnExposure checks for exposed .git or .svn directories
func CheckGitSvnExposure(subdomain string, timeout int) (gitExposed bool, svnExposed bool) {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
// Check .git
resp, err := client.Get(baseURL + "/.git/config")
if err == nil {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1000))
resp.Body.Close()
if resp.StatusCode == 200 && strings.Contains(string(body), "[core]") {
gitExposed = true
}
}
// Check .svn
resp, err = client.Get(baseURL + "/.svn/entries")
if err == nil {
resp.Body.Close()
if resp.StatusCode == 200 {
svnExposed = true
}
}
if gitExposed || svnExposed {
break
}
}
return gitExposed, svnExposed
}
// CheckBackupFiles checks for common backup files
func CheckBackupFiles(subdomain string, timeout int) []string {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Common backup file patterns
paths := []string{
"/backup.zip", "/backup.tar.gz", "/backup.sql",
"/db.sql", "/database.sql", "/dump.sql",
"/site.zip", "/www.zip", "/public.zip",
"/config.bak", "/config.old", "/.env.bak",
"/index.php.bak", "/index.php.old", "/index.html.bak",
"/web.config.bak", "/.htaccess.bak",
}
var found []string
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
for _, path := range paths {
resp, err := client.Head(baseURL + path)
if err != nil {
continue
}
resp.Body.Close()
if resp.StatusCode == 200 {
found = append(found, path)
}
}
if len(found) > 0 {
break
}
}
return found
}
// CheckAPIEndpoints checks for common API endpoints
func CheckAPIEndpoints(subdomain string, timeout int) []string {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// Common API endpoint patterns
paths := []string{
"/api", "/api/v1", "/api/v2", "/api/v3",
"/graphql", "/graphiql",
"/swagger", "/swagger-ui", "/swagger.json", "/swagger.yaml",
"/openapi.json", "/openapi.yaml",
"/docs", "/api-docs", "/redoc",
"/health", "/healthz", "/status",
"/metrics", "/actuator", "/actuator/health",
"/v1", "/v2", "/rest",
}
var found []string
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
for _, path := range paths {
resp, err := client.Get(baseURL + path)
if err != nil {
continue
}
resp.Body.Close()
// Found if not 404
if resp.StatusCode != 404 && resp.StatusCode != 0 {
found = append(found, path)
}
}
if len(found) > 0 {
break
}
}
return found
}
// WithClient versions for parallel execution
func CheckAdminPanelsWithClient(subdomain string, client *http.Client) []string {
paths := []string{
"/admin", "/administrator", "/admin.php", "/admin.html",
"/login", "/login.php", "/signin", "/auth",
"/wp-admin", "/wp-login.php",
"/phpmyadmin", "/pma", "/mysql",
"/cpanel", "/webmail",
"/manager", "/console", "/dashboard",
"/admin/login", "/user/login",
}
var found []string
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
// First, get the root page to detect SPA catch-all behavior
rootResp, err := client.Get(baseURL + "/")
var rootContentLength string
var rootContentType string
if err == nil {
rootContentLength = rootResp.Header.Get("Content-Length")
rootContentType = rootResp.Header.Get("Content-Type")
rootResp.Body.Close()
}
for _, path := range paths {
testURL := baseURL + path
resp, err := client.Get(testURL)
if err != nil {
continue
}
resp.Body.Close()
// Report 200 OK (found), 401/403 (protected but exists)
if resp.StatusCode == 200 {
// Check for SPA catch-all: same content-length and content-type as root
contentLength := resp.Header.Get("Content-Length")
contentType := resp.Header.Get("Content-Type")
// If response matches root page exactly, it's likely SPA catch-all
isSPACatchAll := rootContentLength != "" && contentLength == rootContentLength &&
strings.Contains(contentType, "text/html") && strings.Contains(rootContentType, "text/html")
if !isSPACatchAll {
found = append(found, path)
}
} else if resp.StatusCode == 401 || resp.StatusCode == 403 {
// Protected endpoint exists
found = append(found, path+" (protected)")
}
}
if len(found) > 0 {
break // Found results, no need to try HTTP
}
}
return found
}
func CheckGitSvnExposureWithClient(subdomain string, client *http.Client) (gitExposed bool, svnExposed bool) {
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
resp, err := client.Get(baseURL + "/.git/config")
if err == nil {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1000))
resp.Body.Close()
if resp.StatusCode == 200 && strings.Contains(string(body), "[core]") {
gitExposed = true
}
}
resp, err = client.Get(baseURL + "/.svn/entries")
if err == nil {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1000))
resp.Body.Close()
// SVN entries file starts with version number or contains specific format
// Must not be HTML (SPA catch-all returns HTML for all routes)
bodyStr := string(body)
if resp.StatusCode == 200 && !strings.Contains(bodyStr, "<html") && !strings.Contains(bodyStr, "<!DOCTYPE") {
// Old SVN format starts with version number, new format is XML
if len(bodyStr) > 0 && (bodyStr[0] >= '0' && bodyStr[0] <= '9' || strings.Contains(bodyStr, "<?xml")) {
svnExposed = true
}
}
}
if gitExposed || svnExposed {
break
}
}
return gitExposed, svnExposed
}
func CheckBackupFilesWithClient(subdomain string, client *http.Client) []string {
paths := []string{
"/backup.zip", "/backup.tar.gz", "/backup.sql",
"/db.sql", "/database.sql", "/dump.sql",
"/site.zip", "/www.zip", "/public.zip",
"/config.bak", "/config.old", "/.env.bak",
"/index.php.bak", "/index.php.old", "/index.html.bak",
"/web.config.bak", "/.htaccess.bak",
}
var found []string
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
for _, path := range paths {
resp, err := client.Head(baseURL + path)
if err != nil {
continue
}
resp.Body.Close()
if resp.StatusCode == 200 {
// Check content-type to avoid SPA catch-all false positives
contentType := resp.Header.Get("Content-Type")
// Backup files should NOT be text/html - that indicates SPA catch-all
if !strings.Contains(contentType, "text/html") {
found = append(found, path)
}
}
}
if len(found) > 0 {
break // Found results, no need to try HTTP
}
}
return found
}
func CheckAPIEndpointsWithClient(subdomain string, client *http.Client) []string {
paths := []string{
"/api", "/api/v1", "/api/v2", "/api/v3",
"/graphql", "/graphiql",
"/swagger", "/swagger-ui", "/swagger.json", "/swagger.yaml",
"/openapi.json", "/openapi.yaml",
"/docs", "/api-docs", "/redoc",
"/health", "/healthz", "/status",
"/metrics", "/actuator", "/actuator/health",
"/v1", "/v2", "/rest",
}
var found []string
baseURLs := []string{
fmt.Sprintf("https://%s", subdomain),
fmt.Sprintf("http://%s", subdomain),
}
for _, baseURL := range baseURLs {
// First, get the root page to detect SPA catch-all behavior
rootResp, err := client.Get(baseURL + "/")
var rootContentLength string
var rootContentType string
if err == nil {
rootContentLength = rootResp.Header.Get("Content-Length")
rootContentType = rootResp.Header.Get("Content-Type")
rootResp.Body.Close()
}
for _, path := range paths {
resp, err := client.Get(baseURL + path)
if err != nil {
continue
}
resp.Body.Close()
// Report 200 OK (found), 401/403 (protected but exists)
if resp.StatusCode == 200 {
// Check for SPA catch-all: same content-length and content-type as root
contentLength := resp.Header.Get("Content-Length")
contentType := resp.Header.Get("Content-Type")
// If response matches root page exactly, it's likely SPA catch-all
isSPACatchAll := rootContentLength != "" && contentLength == rootContentLength &&
strings.Contains(contentType, "text/html") && strings.Contains(rootContentType, "text/html")
if !isSPACatchAll {
found = append(found, path)
}
} else if resp.StatusCode == 401 || resp.StatusCode == 403 {
// Protected endpoint exists
found = append(found, path+" (protected)")
}
}
if len(found) > 0 {
break // Found results, no need to try HTTP
}
}
return found
}
File diff suppressed because it is too large Load Diff