mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-02-12 16:52:45 +00:00
- Implement 8 specialized AI agents (XSS, SQLi, Auth, API, Crypto, Secrets, Headers, General) - Add fast type-based routing for finding classification - Include OWASP-aligned knowledge bases per agent - Add agent handoff logic for cross-vulnerability detection - Optimize timeouts and parallelism for local LLM - Add new modules: cache, network, fingerprint, secrets, cloud, API, discovery - Update documentation with multi-agent feature
270 lines
7.8 KiB
Go
270 lines
7.8 KiB
Go
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
|
|
},
|
|
}
|
|
return CheckAdminPanelsWithClient(subdomain, client)
|
|
}
|
|
|
|
// 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},
|
|
},
|
|
}
|
|
return CheckGitSvnExposureWithClient(subdomain, client)
|
|
}
|
|
|
|
// 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},
|
|
},
|
|
}
|
|
return CheckBackupFilesWithClient(subdomain, client)
|
|
}
|
|
|
|
// 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
|
|
},
|
|
}
|
|
return CheckAPIEndpointsWithClient(subdomain, client)
|
|
}
|
|
|
|
// WithClient versions for parallel execution
|
|
|
|
func CheckAdminPanelsWithClient(subdomain string, client *http.Client) []string {
|
|
// Generic admin paths (common across all platforms)
|
|
paths := []string{
|
|
"/admin", "/administrator",
|
|
"/login", "/signin", "/auth",
|
|
"/manager", "/console", "/dashboard",
|
|
"/admin/login", "/user/login",
|
|
}
|
|
// Note: We removed platform-specific paths like /wp-admin, /admin.php, /login.php
|
|
// These generate false positives on non-PHP/WordPress sites
|
|
// The tech detection should be used to check platform-specific paths
|
|
|
|
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
|
|
}
|