Files
Vyntral 3a4c230aa7 feat: v2.0 full rewrite — event-driven pipeline, AI + Nuclei + proxy
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.
2026-04-18 16:48:41 +02:00

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)
}