mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-15 21:28:18 +02:00
3a4c230aa7
Complete architectural overhaul. Replaces the v0.1 monolithic scanner with an event-driven pipeline of auto-registered modules. Foundation (internal/): - eventbus: typed pub/sub, 20 event types, race-safe, drop counter - module: registry with phase-based selection - store: thread-safe host store with per-host locks + deep-copy reads - pipeline: coordinator with phase barriers + panic recovery - config: 5 scan profiles + 3 AI tiers + YAML loader + auto-discovery Modules (26 auto-registered across 6 phases): - Discovery: passive (26 sources), bruteforce, recursive, AXFR, GitHub dorks, CT streaming, permutation, reverse DNS, vhost, ASN, supply chain (npm + PyPI) - Enrichment: HTTP probe + tech fingerprint + TLS appliance ID, ports - Analysis: security checks, takeover (110+ sigs), cloud, JavaScript, GraphQL, JWT, headers (OWASP), HTTP smuggling, AI cascade, Nuclei - Reporting: TXT/JSON/CSV writer + AI scan brief AI layer (internal/ai/ + internal/modules/ai/): - Three profiles: lean (16 GB), balanced (32 GB MoE), heavy (64 GB) - Six event-driven handlers: CVE, JS file, HTTP response, secret filter, multi-agent vuln enrichment, anomaly + executive report - Content-hash cache dedups Ollama calls across hosts - Auto-pull of missing models via /api/pull with streaming progress - End-of-scan AI SCAN BRIEF in terminal with top chains + next actions Nuclei compat layer (internal/nucleitpl/): - Executes ~13k community templates (HTTP subset) - Auto-download of nuclei-templates ZIP to ~/.god-eye/nuclei-templates - Scope filter rejects off-host templates (eliminates OSINT FPs) Operations: - Interactive wizard (internal/wizard/) — zero-flag launch - LivePrinter (internal/tui/) — colorized event stream - Diff engine + scheduler (internal/diff, internal/scheduler) for continuous ASM monitoring with webhook alerts - Proxy support (internal/proxyconf/): http / https / socks5 / socks5h + basic auth Fixes #1 — native SOCKS5 / Tor compatibility via --proxy flag. 185 unit tests across 15 packages, all race-detector clean.
175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
// Package proxyconf centralises outbound-proxy configuration for the
|
|
// HTTP and (where possible) DNS clients used across God's Eye modules.
|
|
//
|
|
// Why this lives in its own package: every source/probe/module needs to
|
|
// honour the same proxy setting, and duplicating URL parsing + dialer
|
|
// wiring across `internal/http`, `internal/sources`, and individual
|
|
// modules would be a fountain of bugs. This package is the single
|
|
// source of truth.
|
|
//
|
|
// Supported schemes:
|
|
//
|
|
// "" → direct (no proxy)
|
|
// http://host:port → HTTP CONNECT proxy (e.g. Burp, ZAP, mitmproxy)
|
|
// https://host:port → HTTPS CONNECT proxy
|
|
// socks5://host:port → SOCKS5 (DNS resolved locally by god-eye)
|
|
// socks5h://host:port → SOCKS5 (DNS resolved by the proxy — Tor convention)
|
|
//
|
|
// Basic auth (http://user:pass@host) is honoured for every scheme.
|
|
//
|
|
// DNS-over-SOCKS caveat: Go's net package uses the OS resolver by default,
|
|
// which does NOT route through SOCKS. `socks5h://` only applies to HTTP
|
|
// requests — the brute-force DNS resolver (`internal/dns`) continues to
|
|
// hit its configured resolvers directly. Users who need full Tor
|
|
// isolation for DNS should run god-eye inside a torsocks-wrapped shell
|
|
// or a netns with all traffic captured.
|
|
package proxyconf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"golang.org/x/net/proxy"
|
|
)
|
|
|
|
// DialFunc is the signature used by http.Transport.DialContext.
|
|
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
|
|
|
|
// ProxyFunc is the signature used by http.Transport.Proxy.
|
|
type ProxyFunc func(*http.Request) (*url.URL, error)
|
|
|
|
// Validate returns a descriptive error if proxyURL is non-empty and
|
|
// doesn't parse to a supported scheme. Call this early (e.g. during
|
|
// validator.ValidateXxx) so bad flags fail before module startup.
|
|
func Validate(proxyURL string) error {
|
|
proxyURL = strings.TrimSpace(proxyURL)
|
|
if proxyURL == "" {
|
|
return nil
|
|
}
|
|
u, err := url.Parse(proxyURL)
|
|
if err != nil {
|
|
return fmt.Errorf("proxy URL malformed: %w", err)
|
|
}
|
|
if u.Host == "" {
|
|
return errors.New("proxy URL missing host:port")
|
|
}
|
|
switch strings.ToLower(u.Scheme) {
|
|
case "http", "https", "socks5", "socks5h":
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported proxy scheme %q (use http/https/socks5/socks5h)", u.Scheme)
|
|
}
|
|
}
|
|
|
|
// BuildDialer returns a DialFunc that routes TCP through the configured
|
|
// proxy. For HTTP(S) CONNECT proxies (handled at the transport layer via
|
|
// Proxy field), this returns a direct dialer — the transport layer does
|
|
// the CONNECT dance itself.
|
|
//
|
|
// For empty proxyURL, returns the direct-dialer from net.Dialer.
|
|
func BuildDialer(proxyURL string, base *net.Dialer) (DialFunc, error) {
|
|
if base == nil {
|
|
base = &net.Dialer{}
|
|
}
|
|
if strings.TrimSpace(proxyURL) == "" {
|
|
return base.DialContext, nil
|
|
}
|
|
u, err := url.Parse(proxyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch strings.ToLower(u.Scheme) {
|
|
case "http", "https":
|
|
// CONNECT proxy — direct TCP, Transport.Proxy handles the handshake.
|
|
return base.DialContext, nil
|
|
case "socks5", "socks5h":
|
|
var auth *proxy.Auth
|
|
if u.User != nil {
|
|
pass, _ := u.User.Password()
|
|
auth = &proxy.Auth{User: u.User.Username(), Password: pass}
|
|
}
|
|
// proxy.Direct is the fallthrough dialer — we pass our base so
|
|
// timeouts/keepalive settings are preserved.
|
|
dialer, err := proxy.SOCKS5("tcp", u.Host, auth, &directAdapter{base: base})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create SOCKS5 dialer: %w", err)
|
|
}
|
|
if ctxDialer, ok := dialer.(proxy.ContextDialer); ok {
|
|
return ctxDialer.DialContext, nil
|
|
}
|
|
// Older x/net versions: wrap non-context Dial with ctx-aware shim.
|
|
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
type result struct {
|
|
conn net.Conn
|
|
err error
|
|
}
|
|
ch := make(chan result, 1)
|
|
go func() {
|
|
c, e := dialer.Dial(network, addr)
|
|
ch <- result{c, e}
|
|
}()
|
|
select {
|
|
case r := <-ch:
|
|
return r.conn, r.err
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported proxy scheme: %s", u.Scheme)
|
|
}
|
|
}
|
|
|
|
// BuildProxyFunc returns the http.Transport.Proxy callback for HTTP(S)
|
|
// CONNECT proxies. Returns nil for SOCKS5 (handled by the dialer) and
|
|
// for empty proxyURL.
|
|
func BuildProxyFunc(proxyURL string) (ProxyFunc, error) {
|
|
if strings.TrimSpace(proxyURL) == "" {
|
|
return nil, nil
|
|
}
|
|
u, err := url.Parse(proxyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch strings.ToLower(u.Scheme) {
|
|
case "http", "https":
|
|
return http.ProxyURL(u), nil
|
|
case "socks5", "socks5h":
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("unsupported proxy scheme: %s", u.Scheme)
|
|
}
|
|
|
|
// Humanize returns a redacted, user-facing description of the proxy.
|
|
// Strips credentials so logs don't leak tokens.
|
|
func Humanize(proxyURL string) string {
|
|
proxyURL = strings.TrimSpace(proxyURL)
|
|
if proxyURL == "" {
|
|
return "direct (no proxy)"
|
|
}
|
|
u, err := url.Parse(proxyURL)
|
|
if err != nil {
|
|
return "invalid"
|
|
}
|
|
auth := ""
|
|
if u.User != nil {
|
|
auth = "(auth)@"
|
|
}
|
|
return fmt.Sprintf("%s://%s%s", u.Scheme, auth, u.Host)
|
|
}
|
|
|
|
// directAdapter adapts a *net.Dialer to the proxy.Dialer interface so
|
|
// our configured timeouts/keepalive flow through to the socks hop.
|
|
type directAdapter struct {
|
|
base *net.Dialer
|
|
}
|
|
|
|
func (d *directAdapter) Dial(network, addr string) (net.Conn, error) {
|
|
return d.base.Dial(network, addr)
|
|
}
|