mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-16 13:39:10 +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.
230 lines
5.1 KiB
Go
230 lines
5.1 KiB
Go
package nucleitpl
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
const sampleSupported = `
|
|
id: test-basic-word-match
|
|
info:
|
|
name: Test Basic Word Match
|
|
author: vyntral
|
|
severity: high
|
|
description: Fires when response body contains 'phpinfo'
|
|
tags: exposure,php
|
|
reference:
|
|
- https://example.com/advisory/CVE-2021-12345
|
|
requests:
|
|
- method: GET
|
|
path:
|
|
- "{{BaseURL}}/phpinfo.php"
|
|
matchers:
|
|
- type: word
|
|
part: body
|
|
words:
|
|
- "PHP Version"
|
|
- type: status
|
|
status:
|
|
- 200
|
|
matchers-condition: and
|
|
`
|
|
|
|
const sampleUnsupportedDNS = `
|
|
id: test-dns
|
|
info:
|
|
name: Test DNS
|
|
severity: medium
|
|
dns:
|
|
- name: "{{FQDN}}"
|
|
type: TXT
|
|
matchers:
|
|
- type: word
|
|
words: ["v=spf"]
|
|
`
|
|
|
|
const sampleUnsupportedPayloads = `
|
|
id: test-payloads
|
|
info:
|
|
name: Test Payloads
|
|
severity: low
|
|
requests:
|
|
- method: GET
|
|
path:
|
|
- "{{BaseURL}}/{{word}}"
|
|
payloads:
|
|
word:
|
|
- admin
|
|
- backup
|
|
matchers:
|
|
- type: status
|
|
status: [200]
|
|
`
|
|
|
|
const sampleBadYAML = `
|
|
id: [unclosed
|
|
info:
|
|
name:
|
|
`
|
|
|
|
func writeTmp(t *testing.T, name, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestLoad_Supported(t *testing.T) {
|
|
path := writeTmp(t, "ok.yaml", sampleSupported)
|
|
tpl, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tpl.ID != "test-basic-word-match" {
|
|
t.Errorf("ID = %q", tpl.ID)
|
|
}
|
|
if tpl.Info.Severity != "high" {
|
|
t.Errorf("Severity = %q", tpl.Info.Severity)
|
|
}
|
|
if len(tpl.Requests) != 1 {
|
|
t.Fatalf("Requests len = %d", len(tpl.Requests))
|
|
}
|
|
r := tpl.Requests[0]
|
|
if r.Path[0] != "{{BaseURL}}/phpinfo.php" {
|
|
t.Errorf("Path[0] = %q", r.Path[0])
|
|
}
|
|
if len(r.Matchers) != 2 {
|
|
t.Errorf("Matchers len = %d", len(r.Matchers))
|
|
}
|
|
if r.MatchersCondition != "and" {
|
|
t.Errorf("MatchersCondition = %q", r.MatchersCondition)
|
|
}
|
|
if ok, reason := tpl.IsSupported(); !ok {
|
|
t.Errorf("should be supported; reason=%q", reason)
|
|
}
|
|
if tags := tpl.Tags(); len(tags) != 2 || tags[0] != "exposure" {
|
|
t.Errorf("Tags = %v", tags)
|
|
}
|
|
}
|
|
|
|
func TestLoad_DNSUnsupported(t *testing.T) {
|
|
path := writeTmp(t, "dns.yaml", sampleUnsupportedDNS)
|
|
tpl, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ok, reason := tpl.IsSupported()
|
|
if ok {
|
|
t.Error("dns template should be unsupported")
|
|
}
|
|
if reason == "" {
|
|
t.Error("expected non-empty reason")
|
|
}
|
|
}
|
|
|
|
func TestLoad_PayloadsUnsupported(t *testing.T) {
|
|
path := writeTmp(t, "payloads.yaml", sampleUnsupportedPayloads)
|
|
tpl, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ok, reason := tpl.IsSupported()
|
|
if ok {
|
|
t.Error("payloads template should be unsupported")
|
|
}
|
|
if reason == "" {
|
|
t.Error("expected non-empty reason")
|
|
}
|
|
}
|
|
|
|
func TestLoad_BadYAML(t *testing.T) {
|
|
path := writeTmp(t, "bad.yaml", sampleBadYAML)
|
|
if _, err := Load(path); err == nil {
|
|
t.Error("expected parse error")
|
|
}
|
|
}
|
|
|
|
func TestLoad_MissingID(t *testing.T) {
|
|
path := writeTmp(t, "noid.yaml", "info:\n severity: low\n")
|
|
if _, err := Load(path); err == nil {
|
|
t.Error("expected missing id error")
|
|
}
|
|
}
|
|
|
|
func TestLoadDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(dir, "a.yaml"), []byte(sampleSupported), 0o644)
|
|
_ = os.WriteFile(filepath.Join(dir, "b.yaml"), []byte(sampleUnsupportedDNS), 0o644)
|
|
_ = os.WriteFile(filepath.Join(dir, "c.yml"), []byte(sampleSupported), 0o644)
|
|
_ = os.WriteFile(filepath.Join(dir, "d.bad"), []byte("???"), 0o644)
|
|
_ = os.WriteFile(filepath.Join(dir, "e.yaml"), []byte(sampleBadYAML), 0o644)
|
|
|
|
sub := filepath.Join(dir, "nested")
|
|
_ = os.MkdirAll(sub, 0o755)
|
|
_ = os.WriteFile(filepath.Join(sub, "f.yaml"), []byte(sampleSupported), 0o644)
|
|
|
|
tpls, diags, err := LoadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 3 supported (a, c, nested/f), 1 dns (b), 1 parse error (e). .bad ignored.
|
|
if got := len(tpls); got != 4 {
|
|
t.Errorf("loaded = %d, want 4 (3 supported + 1 dns)", got)
|
|
}
|
|
if len(diags) != 1 {
|
|
t.Errorf("diags = %d, want 1", len(diags))
|
|
}
|
|
}
|
|
|
|
func TestSeverity_Default(t *testing.T) {
|
|
tpl := &Template{Info: Info{Severity: "UNKNOWN"}}
|
|
if sev := tpl.Severity(); sev != "info" {
|
|
t.Errorf("got %q, want info", sev)
|
|
}
|
|
}
|
|
|
|
func TestSeverity_Normalized(t *testing.T) {
|
|
for input, want := range map[string]string{
|
|
"critical": "critical",
|
|
"HIGH": "high",
|
|
" Medium ": "medium",
|
|
"LOW": "low",
|
|
"info": "info",
|
|
"": "info",
|
|
} {
|
|
tpl := &Template{Info: Info{Severity: input}}
|
|
if got := tpl.Severity(); got != want {
|
|
t.Errorf("Severity(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHTTPAlias(t *testing.T) {
|
|
content := `
|
|
id: http-alias
|
|
info:
|
|
severity: low
|
|
http:
|
|
- method: GET
|
|
path: ["{{BaseURL}}/"]
|
|
matchers:
|
|
- type: status
|
|
status: [200]
|
|
`
|
|
path := writeTmp(t, "http.yaml", content)
|
|
tpl, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tpl.Requests) != 1 {
|
|
t.Errorf("expected http: to be aliased to Requests, got %d", len(tpl.Requests))
|
|
}
|
|
if ok, _ := tpl.IsSupported(); !ok {
|
|
t.Error("http alias template should be supported")
|
|
}
|
|
}
|