mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-21 15:36:48 +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.0 KiB
Go
170 lines
4.0 KiB
Go
package wizard
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// newPrompterWithInput builds a prompter whose stdin comes from the given
|
|
// string. Each line is terminated with \n.
|
|
func newPrompterWithInput(input string) (*prompter, *bytes.Buffer) {
|
|
buf := &bytes.Buffer{}
|
|
p := newPrompter(strings.NewReader(input), buf)
|
|
return p, buf
|
|
}
|
|
|
|
func TestChoose_DefaultAcceptsEmpty(t *testing.T) {
|
|
p, _ := newPrompterWithInput("\n")
|
|
n, err := p.choose("pick one", []string{"a", "b", "c"}, 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 2 {
|
|
t.Errorf("default = %d, want 2", n)
|
|
}
|
|
}
|
|
|
|
func TestChoose_ValidNumber(t *testing.T) {
|
|
p, _ := newPrompterWithInput("3\n")
|
|
n, _ := p.choose("pick one", []string{"a", "b", "c"}, 1)
|
|
if n != 3 {
|
|
t.Errorf("= %d, want 3", n)
|
|
}
|
|
}
|
|
|
|
func TestChoose_OutOfRangeReprompts(t *testing.T) {
|
|
p, _ := newPrompterWithInput("99\nfoo\n2\n")
|
|
n, err := p.choose("pick one", []string{"a", "b", "c"}, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 2 {
|
|
t.Errorf("= %d, want 2", n)
|
|
}
|
|
}
|
|
|
|
func TestChoose_MaxAttemptsExhausted(t *testing.T) {
|
|
p, _ := newPrompterWithInput("bad\nbad\nbad\n")
|
|
p.max = 3
|
|
if _, err := p.choose("pick one", []string{"a", "b"}, 1); err == nil {
|
|
t.Error("expected error after max attempts")
|
|
}
|
|
}
|
|
|
|
func TestChoose_EOFCancels(t *testing.T) {
|
|
p, _ := newPrompterWithInput("")
|
|
_, err := p.choose("pick", []string{"a"}, 1)
|
|
if !errors.Is(err, ErrCancelled) {
|
|
t.Errorf("want ErrCancelled, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestYesNo_Default(t *testing.T) {
|
|
p, _ := newPrompterWithInput("\n")
|
|
got, _ := p.yesNo("ok?", true)
|
|
if !got {
|
|
t.Error("empty input should return default true")
|
|
}
|
|
|
|
p2, _ := newPrompterWithInput("\n")
|
|
got2, _ := p2.yesNo("ok?", false)
|
|
if got2 {
|
|
t.Error("empty input should return default false")
|
|
}
|
|
}
|
|
|
|
func TestYesNo_ParsesYesNo(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"y\n": true,
|
|
"Y\n": true,
|
|
"yes\n": true,
|
|
"YES\n": true,
|
|
"s\n": true, // italian si
|
|
"si\n": true,
|
|
"n\n": false,
|
|
"no\n": false,
|
|
"NO\n": false,
|
|
}
|
|
for input, want := range cases {
|
|
p, _ := newPrompterWithInput(input)
|
|
got, err := p.yesNo("?", false)
|
|
if err != nil {
|
|
t.Errorf("input %q: %v", input, err)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("input %q: got %v want %v", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAskText_DefaultUsed(t *testing.T) {
|
|
p, _ := newPrompterWithInput("\n")
|
|
got, _ := p.askText("thing?", "sensible-default", false, nil)
|
|
if got != "sensible-default" {
|
|
t.Errorf("= %q, want sensible-default", got)
|
|
}
|
|
}
|
|
|
|
func TestAskText_OptionalEmpty(t *testing.T) {
|
|
p, _ := newPrompterWithInput("\n")
|
|
got, err := p.askText("optional?", "", false, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "" {
|
|
t.Errorf("= %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestAskText_RequiredEmptyReprompts(t *testing.T) {
|
|
p, _ := newPrompterWithInput("\n\n\n") // 3 empty attempts
|
|
_, err := p.askText("required", "", true, nil)
|
|
if err == nil {
|
|
t.Error("expected error after 3 empty attempts")
|
|
}
|
|
}
|
|
|
|
func TestAskText_Validation(t *testing.T) {
|
|
p, _ := newPrompterWithInput("bad\ngood\n")
|
|
got, err := p.askText("domain", "", true, func(s string) error {
|
|
if s == "bad" {
|
|
return errors.New("no")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "good" {
|
|
t.Errorf("= %q, want good", got)
|
|
}
|
|
}
|
|
|
|
func TestAskDomain_InvalidReprompts(t *testing.T) {
|
|
p, _ := newPrompterWithInput("not_a_domain\nexample.com\n")
|
|
got, err := p.askDomain()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "example.com" {
|
|
t.Errorf("= %q, want example.com", got)
|
|
}
|
|
}
|
|
|
|
func TestAskDomain_AcceptsSchemePrefixThroughSanitization(t *testing.T) {
|
|
// askDomain sanitizes the input before validation — https://example.com
|
|
// becomes example.com and passes on the first try. The caller is
|
|
// responsible for calling SanitizeDomain again on the returned value.
|
|
p, _ := newPrompterWithInput("https://example.com\n")
|
|
got, err := p.askDomain()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "https://example.com" {
|
|
t.Errorf("= %q, want raw input preserved", got)
|
|
}
|
|
}
|