mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-19 06:38:06 +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.
170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
// Package wizard drives the interactive CLI setup flow. When god-eye is
|
|
// launched from a terminal with no target, the wizard asks a handful of
|
|
// questions (AI tier, model download consent, target, scan profile,
|
|
// live view, output) and returns a Choice the caller applies to Config.
|
|
//
|
|
// The wizard is terminal-only by design and uses the output package for
|
|
// consistent colors with the rest of the tool. It never modifies state
|
|
// itself — that's the caller's job.
|
|
package wizard
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mattn/go-isatty"
|
|
|
|
"god-eye/internal/output"
|
|
"god-eye/internal/validator"
|
|
)
|
|
|
|
// ErrCancelled is returned when the user aborts the wizard (Ctrl-C or
|
|
// by answering "no" at the confirmation step).
|
|
var ErrCancelled = errors.New("wizard: cancelled")
|
|
|
|
// IsInteractive reports whether stdin/stdout are attached to a terminal.
|
|
// Callers use this to decide whether to prompt or fall back to
|
|
// non-interactive defaults.
|
|
func IsInteractive() bool {
|
|
return isatty.IsTerminal(os.Stdin.Fd()) && isatty.IsTerminal(os.Stdout.Fd())
|
|
}
|
|
|
|
// prompter is a thin wrapper around a bufio.Reader + writer with helpers
|
|
// that respect defaults and re-prompt on invalid input.
|
|
type prompter struct {
|
|
r *bufio.Reader
|
|
w io.Writer
|
|
max int // max re-prompts before giving up (default 3)
|
|
}
|
|
|
|
func newPrompter(in io.Reader, out io.Writer) *prompter {
|
|
return &prompter{r: bufio.NewReader(in), w: out, max: 3}
|
|
}
|
|
|
|
// printf writes to the prompter's writer. Allows swapping out os.Stdout
|
|
// in tests for a bytes.Buffer.
|
|
func (p *prompter) printf(format string, args ...interface{}) {
|
|
fmt.Fprintf(p.w, format, args...)
|
|
}
|
|
|
|
// readLine reads one line, trimming trailing whitespace. Returns EOF as
|
|
// a cancellation.
|
|
func (p *prompter) readLine() (string, error) {
|
|
line, err := p.r.ReadString('\n')
|
|
if err != nil && line == "" {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(line), nil
|
|
}
|
|
|
|
// choose presents a numbered menu and returns the selected index. Empty
|
|
// input accepts def (1-based). Re-prompts on invalid input up to max.
|
|
func (p *prompter) choose(question string, options []string, def int) (int, error) {
|
|
p.printf("\n%s %s\n", output.BoldCyan("?"), output.BoldWhite(question))
|
|
for i, opt := range options {
|
|
marker := " "
|
|
if i+1 == def {
|
|
marker = output.Green("▸")
|
|
}
|
|
p.printf(" %s %s %s\n", marker, output.Yellow(fmt.Sprintf("%d)", i+1)), opt)
|
|
}
|
|
prompt := fmt.Sprintf(" %s ", output.Dim(fmt.Sprintf("Choice [%d]:", def)))
|
|
for attempt := 0; attempt < p.max; attempt++ {
|
|
p.printf("%s", prompt)
|
|
raw, err := p.readLine()
|
|
if err != nil {
|
|
return 0, ErrCancelled
|
|
}
|
|
if raw == "" {
|
|
return def, nil
|
|
}
|
|
n, err := strconv.Atoi(raw)
|
|
if err == nil && n >= 1 && n <= len(options) {
|
|
return n, nil
|
|
}
|
|
p.printf(" %s not a valid option\n", output.Red("✘"))
|
|
}
|
|
return 0, fmt.Errorf("too many invalid attempts")
|
|
}
|
|
|
|
// askText reads free-form text. When required is false an empty answer
|
|
// (after max attempts) returns "".
|
|
func (p *prompter) askText(question, def string, required bool, validate func(string) error) (string, error) {
|
|
defHint := ""
|
|
if def != "" {
|
|
defHint = fmt.Sprintf(" [%s]", def)
|
|
} else if !required {
|
|
defHint = " (empty to skip)"
|
|
}
|
|
p.printf("\n%s %s%s\n", output.BoldCyan("?"), output.BoldWhite(question), output.Dim(defHint))
|
|
|
|
for attempt := 0; attempt < p.max; attempt++ {
|
|
p.printf(" %s ", output.Dim(">"))
|
|
raw, err := p.readLine()
|
|
if err != nil {
|
|
return "", ErrCancelled
|
|
}
|
|
if raw == "" {
|
|
if def != "" {
|
|
return def, nil
|
|
}
|
|
if !required {
|
|
return "", nil
|
|
}
|
|
p.printf(" %s required\n", output.Red("✘"))
|
|
continue
|
|
}
|
|
if validate != nil {
|
|
if err := validate(raw); err != nil {
|
|
p.printf(" %s %v\n", output.Red("✘"), err)
|
|
continue
|
|
}
|
|
}
|
|
return raw, nil
|
|
}
|
|
return "", fmt.Errorf("too many invalid attempts")
|
|
}
|
|
|
|
// yesNo prompts for a boolean with a default. Accepts y/yes/n/no/empty
|
|
// (case-insensitive).
|
|
func (p *prompter) yesNo(question string, def bool) (bool, error) {
|
|
hint := "y/N"
|
|
if def {
|
|
hint = "Y/n"
|
|
}
|
|
p.printf("\n%s %s %s\n", output.BoldCyan("?"), output.BoldWhite(question), output.Dim("["+hint+"]"))
|
|
for attempt := 0; attempt < p.max; attempt++ {
|
|
p.printf(" %s ", output.Dim(">"))
|
|
raw, err := p.readLine()
|
|
if err != nil {
|
|
return false, ErrCancelled
|
|
}
|
|
if raw == "" {
|
|
return def, nil
|
|
}
|
|
switch strings.ToLower(raw) {
|
|
case "y", "yes", "si", "sì", "s":
|
|
return true, nil
|
|
case "n", "no":
|
|
return false, nil
|
|
}
|
|
p.printf(" %s answer y or n\n", output.Red("✘"))
|
|
}
|
|
return false, fmt.Errorf("too many invalid attempts")
|
|
}
|
|
|
|
// askDomain is a specialization of askText that validates via the
|
|
// validator package and strips URL-style prefixes.
|
|
func (p *prompter) askDomain() (string, error) {
|
|
return p.askText("Target domain", "", true, func(s string) error {
|
|
s = validator.SanitizeDomain(s)
|
|
v := validator.DefaultDomainValidator()
|
|
return v.ValidateDomain(s)
|
|
})
|
|
}
|