mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-24 00:24:01 +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.
193 lines
6.0 KiB
Go
193 lines
6.0 KiB
Go
// Package supplychain enumerates npm and PyPI packages that reference the
|
|
// target domain in their source, then flags packages as potential supply
|
|
// chain assets. Useful for discovering internal-only tools published by
|
|
// mistake to public registries and for finding branded utility packages
|
|
// that could reveal internal endpoints/secrets.
|
|
//
|
|
// This is a discovery-oriented check. Actually downloading + scanning
|
|
// package contents for secrets is a Fase 2 follow-up; here we just surface
|
|
// the packages and the URLs they point at.
|
|
package supplychain
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"god-eye/internal/eventbus"
|
|
"god-eye/internal/module"
|
|
"god-eye/internal/sources"
|
|
"god-eye/internal/store"
|
|
)
|
|
|
|
const ModuleName = "vuln.supply-chain"
|
|
|
|
type scModule struct{}
|
|
|
|
func Register() { module.Register(&scModule{}) }
|
|
|
|
func (*scModule) Name() string { return ModuleName }
|
|
func (*scModule) Phase() module.Phase { return module.PhaseDiscovery }
|
|
func (*scModule) Consumes() []eventbus.EventType { return nil }
|
|
func (*scModule) Produces() []eventbus.EventType {
|
|
return []eventbus.EventType{eventbus.EventSubdomainDiscovered, eventbus.EventAPIFinding}
|
|
}
|
|
func (*scModule) DefaultEnabled() bool { return true }
|
|
|
|
func (*scModule) Run(mctx module.Context) error {
|
|
target := mctx.Target
|
|
if target == "" {
|
|
return nil
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func() { defer wg.Done(); checkNPM(mctx, target) }()
|
|
go func() { defer wg.Done(); checkPyPI(mctx, target) }()
|
|
wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// checkNPM uses npm's registry search API. Packages matching "<target>"
|
|
// or "<target-suffix>" are surfaced.
|
|
func checkNPM(mctx module.Context, target string) {
|
|
q := extractBrand(target)
|
|
if q == "" {
|
|
return
|
|
}
|
|
url := fmt.Sprintf("https://registry.npmjs.org/-/v1/search?text=%s&size=100", q)
|
|
body, err := fetchJSON(mctx.Ctx, url, 15*time.Second)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var parsed struct {
|
|
Objects []struct {
|
|
Package struct {
|
|
Name string `json:"name"`
|
|
Links map[string]string `json:"links"`
|
|
Description string `json:"description"`
|
|
} `json:"package"`
|
|
} `json:"objects"`
|
|
}
|
|
_ = json.Unmarshal(body, &parsed)
|
|
|
|
for _, obj := range parsed.Objects {
|
|
pkg := obj.Package
|
|
text := pkg.Name + " " + pkg.Description
|
|
for _, link := range pkg.Links {
|
|
text += " " + link
|
|
}
|
|
if !strings.Contains(strings.ToLower(text), target) {
|
|
continue
|
|
}
|
|
// Emit an APIFinding for discovery context.
|
|
mctx.Bus.Publish(mctx.Ctx, eventbus.APIFinding{
|
|
EventMeta: eventbus.EventMeta{At: time.Now(), Source: ModuleName, Target: target},
|
|
Kind: "supply-chain:npm",
|
|
URL: "https://www.npmjs.com/package/" + pkg.Name,
|
|
Issue: "npm package references target: " + pkg.Name + " — " + pkg.Description,
|
|
Severity: eventbus.SeverityInfo,
|
|
})
|
|
// If the description or links contain subdomains of the target,
|
|
// also feed them into discovery.
|
|
for _, sub := range sources.ExtractSubdomains(text, target) {
|
|
_ = mctx.Store.Upsert(mctx.Ctx, sub, func(h *store.Host) {
|
|
store.AddDiscoveryMethod(h, "supply-chain:npm:"+pkg.Name)
|
|
})
|
|
mctx.Bus.Publish(mctx.Ctx, eventbus.SubdomainDiscovered{
|
|
EventMeta: eventbus.EventMeta{At: time.Now(), Source: ModuleName, Target: sub},
|
|
Subdomain: sub,
|
|
Method: "supply-chain:npm:" + pkg.Name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkPyPI(mctx module.Context, target string) {
|
|
// PyPI no longer supports XML-RPC search; use the simple index
|
|
// (all packages) scanning is too expensive. Instead query a few
|
|
// likely branded package prefixes via the JSON index.
|
|
q := extractBrand(target)
|
|
if q == "" {
|
|
return
|
|
}
|
|
// Try exact-name lookups for common variants.
|
|
candidates := []string{q, q + "-cli", q + "-sdk", q + "-api", q + "-client"}
|
|
for _, name := range candidates {
|
|
url := "https://pypi.org/pypi/" + name + "/json"
|
|
body, err := fetchJSON(mctx.Ctx, url, 10*time.Second)
|
|
if err != nil || len(body) < 50 {
|
|
continue
|
|
}
|
|
var parsed struct {
|
|
Info struct {
|
|
Name string `json:"name"`
|
|
Summary string `json:"summary"`
|
|
HomePage string `json:"home_page"`
|
|
ProjectURL string `json:"project_url"`
|
|
ProjectURLs map[string]string `json:"project_urls"`
|
|
} `json:"info"`
|
|
}
|
|
_ = json.Unmarshal(body, &parsed)
|
|
info := parsed.Info
|
|
if info.Name == "" {
|
|
continue
|
|
}
|
|
text := info.Name + " " + info.Summary + " " + info.HomePage + " " + info.ProjectURL
|
|
for _, u := range info.ProjectURLs {
|
|
text += " " + u
|
|
}
|
|
if !strings.Contains(strings.ToLower(text), target) {
|
|
continue
|
|
}
|
|
mctx.Bus.Publish(mctx.Ctx, eventbus.APIFinding{
|
|
EventMeta: eventbus.EventMeta{At: time.Now(), Source: ModuleName, Target: target},
|
|
Kind: "supply-chain:pypi",
|
|
URL: "https://pypi.org/project/" + info.Name + "/",
|
|
Issue: "PyPI package references target: " + info.Name + " — " + info.Summary,
|
|
Severity: eventbus.SeverityInfo,
|
|
})
|
|
for _, sub := range sources.ExtractSubdomains(text, target) {
|
|
_ = mctx.Store.Upsert(mctx.Ctx, sub, func(h *store.Host) {
|
|
store.AddDiscoveryMethod(h, "supply-chain:pypi:"+info.Name)
|
|
})
|
|
mctx.Bus.Publish(mctx.Ctx, eventbus.SubdomainDiscovered{
|
|
EventMeta: eventbus.EventMeta{At: time.Now(), Source: ModuleName, Target: sub},
|
|
Subdomain: sub,
|
|
Method: "supply-chain:pypi:" + info.Name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// extractBrand returns the "brand" (second-to-last label) from example.com →
|
|
// "example". Used as the package-search query term.
|
|
func extractBrand(domain string) string {
|
|
labels := strings.Split(strings.TrimSuffix(domain, "."), ".")
|
|
if len(labels) < 2 {
|
|
return ""
|
|
}
|
|
return strings.ToLower(labels[len(labels)-2])
|
|
}
|
|
|
|
func fetchJSON(ctx context.Context, url string, timeout time.Duration) ([]byte, error) {
|
|
c := &http.Client{Timeout: timeout}
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", "god-eye-v2")
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
return io.ReadAll(io.LimitReader(resp.Body, 4*1024*1024))
|
|
}
|