mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-02-12 16:52:45 +00:00
- Implement 8 specialized AI agents (XSS, SQLi, Auth, API, Crypto, Secrets, Headers, General) - Add fast type-based routing for finding classification - Include OWASP-aligned knowledge bases per agent - Add agent handoff logic for cross-vulnerability detection - Optimize timeouts and parallelism for local LLM - Add new modules: cache, network, fingerprint, secrets, cloud, API, discovery - Update documentation with multi-agent feature
390 lines
9.0 KiB
Go
390 lines
9.0 KiB
Go
package network
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ASNInfo holds ASN information for an IP
|
|
type ASNInfo struct {
|
|
ASN string `json:"asn"`
|
|
Name string `json:"name"`
|
|
Country string `json:"country"`
|
|
CIDR string `json:"cidr"`
|
|
Range string `json:"range"`
|
|
NumHosts int `json:"num_hosts"`
|
|
RelatedIPs []string `json:"related_ips,omitempty"`
|
|
}
|
|
|
|
// ASNScanner discovers ASN/CIDR information and related IPs
|
|
type ASNScanner struct {
|
|
client *http.Client
|
|
timeout int
|
|
}
|
|
|
|
// NewASNScanner creates a new ASN scanner
|
|
func NewASNScanner(timeout int) *ASNScanner {
|
|
return &ASNScanner{
|
|
client: &http.Client{
|
|
Timeout: time.Duration(timeout) * time.Second,
|
|
},
|
|
timeout: timeout,
|
|
}
|
|
}
|
|
|
|
// GetASNInfo retrieves ASN information for an IP using free services
|
|
func (as *ASNScanner) GetASNInfo(ctx context.Context, ip string) (*ASNInfo, error) {
|
|
// Use ip-api.com (free, no API key needed, 45 requests/minute)
|
|
info, err := as.queryIPAPI(ctx, ip)
|
|
if err == nil && info != nil {
|
|
return info, nil
|
|
}
|
|
|
|
// Fallback to Team Cymru DNS-based ASN lookup (no rate limits)
|
|
return as.queryTeamCymruDNS(ip)
|
|
}
|
|
|
|
// queryIPAPI queries ip-api.com for ASN info
|
|
func (as *ASNScanner) queryIPAPI(ctx context.Context, ip string) (*ASNInfo, error) {
|
|
url := fmt.Sprintf("http://ip-api.com/line/%s?fields=as,org,country,query", ip)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := as.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("ip-api returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
lines := strings.Split(string(body), "\n")
|
|
|
|
if len(lines) < 3 {
|
|
return nil, fmt.Errorf("invalid response from ip-api")
|
|
}
|
|
|
|
// Parse ASN from "AS12345 Name" format
|
|
asnParts := strings.SplitN(lines[0], " ", 2)
|
|
asn := ""
|
|
name := ""
|
|
if len(asnParts) >= 1 {
|
|
asn = strings.TrimPrefix(asnParts[0], "AS")
|
|
}
|
|
if len(asnParts) >= 2 {
|
|
name = asnParts[1]
|
|
}
|
|
|
|
return &ASNInfo{
|
|
ASN: asn,
|
|
Name: name,
|
|
Country: lines[2],
|
|
}, nil
|
|
}
|
|
|
|
// queryTeamCymruDNS uses Team Cymru DNS for ASN lookup (free, no limits)
|
|
func (as *ASNScanner) queryTeamCymruDNS(ip string) (*ASNInfo, error) {
|
|
// Reverse IP for DNS query
|
|
parts := strings.Split(ip, ".")
|
|
if len(parts) != 4 {
|
|
return nil, fmt.Errorf("invalid IPv4 address")
|
|
}
|
|
|
|
// Reverse the IP
|
|
reversed := fmt.Sprintf("%s.%s.%s.%s", parts[3], parts[2], parts[1], parts[0])
|
|
|
|
// Query Team Cymru origin.asn.cymru.com
|
|
query := fmt.Sprintf("%s.origin.asn.cymru.com", reversed)
|
|
|
|
txtRecords, err := net.LookupTXT(query)
|
|
if err != nil || len(txtRecords) == 0 {
|
|
return nil, fmt.Errorf("DNS ASN lookup failed: %v", err)
|
|
}
|
|
|
|
// Parse response: "ASN | CIDR | Country | Registry | Date"
|
|
record := txtRecords[0]
|
|
fields := strings.Split(record, "|")
|
|
if len(fields) < 3 {
|
|
return nil, fmt.Errorf("invalid TXT record format")
|
|
}
|
|
|
|
asn := strings.TrimSpace(fields[0])
|
|
cidr := strings.TrimSpace(fields[1])
|
|
country := strings.TrimSpace(fields[2])
|
|
|
|
// Get ASN name from asn.cymru.com
|
|
name := ""
|
|
nameQuery := fmt.Sprintf("AS%s.asn.cymru.com", asn)
|
|
nameRecords, err := net.LookupTXT(nameQuery)
|
|
if err == nil && len(nameRecords) > 0 {
|
|
// Parse: "ASN | Country | Registry | Date | Name"
|
|
nameFields := strings.Split(nameRecords[0], "|")
|
|
if len(nameFields) >= 5 {
|
|
name = strings.TrimSpace(nameFields[4])
|
|
}
|
|
}
|
|
|
|
// Calculate number of hosts in CIDR
|
|
numHosts := 0
|
|
if cidr != "" {
|
|
numHosts = calculateCIDRHosts(cidr)
|
|
}
|
|
|
|
return &ASNInfo{
|
|
ASN: asn,
|
|
Name: name,
|
|
Country: country,
|
|
CIDR: cidr,
|
|
NumHosts: numHosts,
|
|
}, nil
|
|
}
|
|
|
|
// GetRelatedIPs discovers other IPs in the same CIDR range
|
|
// Only scans a subset for large ranges to avoid abuse
|
|
func (as *ASNScanner) GetRelatedIPs(ctx context.Context, cidr string, maxIPs int) []string {
|
|
if cidr == "" || maxIPs <= 0 {
|
|
return nil
|
|
}
|
|
|
|
_, ipnet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var relatedIPs []string
|
|
|
|
// Get network size
|
|
ones, bits := ipnet.Mask.Size()
|
|
hostBits := bits - ones
|
|
totalHosts := 1 << hostBits
|
|
|
|
// Limit scanning for large networks
|
|
if totalHosts > maxIPs {
|
|
// Sample IPs from the range instead of scanning all
|
|
return as.sampleCIDR(ipnet, maxIPs)
|
|
}
|
|
|
|
// For smaller ranges, enumerate all
|
|
ip := ipnet.IP
|
|
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); incrementIP(ip) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return relatedIPs
|
|
default:
|
|
}
|
|
|
|
// Skip network and broadcast addresses
|
|
if ip[3] == 0 || ip[3] == 255 {
|
|
continue
|
|
}
|
|
|
|
relatedIPs = append(relatedIPs, ip.String())
|
|
if len(relatedIPs) >= maxIPs {
|
|
break
|
|
}
|
|
}
|
|
|
|
return relatedIPs
|
|
}
|
|
|
|
// sampleCIDR samples IPs from a large CIDR range
|
|
func (as *ASNScanner) sampleCIDR(ipnet *net.IPNet, maxIPs int) []string {
|
|
var samples []string
|
|
|
|
ip := make(net.IP, len(ipnet.IP))
|
|
copy(ip, ipnet.IP)
|
|
|
|
ones, bits := ipnet.Mask.Size()
|
|
hostBits := bits - ones
|
|
totalHosts := 1 << hostBits
|
|
|
|
// Step size to get approximately maxIPs samples
|
|
step := totalHosts / maxIPs
|
|
if step < 1 {
|
|
step = 1
|
|
}
|
|
|
|
for i := 1; i < totalHosts && len(samples) < maxIPs; i += step {
|
|
// Calculate IP at position i
|
|
sampleIP := make(net.IP, 4)
|
|
baseIP := ipToInt(ipnet.IP)
|
|
sampleIP = intToIP(baseIP + uint32(i))
|
|
|
|
if ipnet.Contains(sampleIP) && sampleIP[3] != 0 && sampleIP[3] != 255 {
|
|
samples = append(samples, sampleIP.String())
|
|
}
|
|
}
|
|
|
|
return samples
|
|
}
|
|
|
|
// ExpandASN expands an ASN to find all related CIDR ranges using BGPView (free API)
|
|
func (as *ASNScanner) ExpandASN(ctx context.Context, asn string) ([]string, error) {
|
|
// Clean ASN format
|
|
asn = strings.TrimPrefix(strings.ToUpper(asn), "AS")
|
|
|
|
url := fmt.Sprintf("https://api.bgpview.io/asn/%s/prefixes", asn)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", "god-eye/1.0 (security scanner)")
|
|
|
|
resp, err := as.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("bgpview returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
|
|
|
|
// Simple parsing without json package
|
|
var cidrs []string
|
|
|
|
// Match IPv4 prefixes: "prefix": "1.2.3.0/24"
|
|
prefixRegex := regexp.MustCompile(`"prefix":\s*"([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+)"`)
|
|
matches := prefixRegex.FindAllStringSubmatch(string(body), -1)
|
|
|
|
for _, match := range matches {
|
|
if len(match) > 1 {
|
|
cidrs = append(cidrs, match[1])
|
|
}
|
|
}
|
|
|
|
return cidrs, nil
|
|
}
|
|
|
|
// ScanASNRange performs a concurrent scan of IPs in an ASN
|
|
func (as *ASNScanner) ScanASNRange(ctx context.Context, ips []string, concurrency int,
|
|
checkFunc func(string) bool) []string {
|
|
|
|
var activeIPs []string
|
|
var mu sync.Mutex
|
|
|
|
sem := make(chan struct{}, concurrency)
|
|
var wg sync.WaitGroup
|
|
|
|
for _, ip := range ips {
|
|
select {
|
|
case <-ctx.Done():
|
|
break
|
|
default:
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func(ipAddr string) {
|
|
defer wg.Done()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case sem <- struct{}{}:
|
|
defer func() { <-sem }()
|
|
}
|
|
|
|
if checkFunc(ipAddr) {
|
|
mu.Lock()
|
|
activeIPs = append(activeIPs, ipAddr)
|
|
mu.Unlock()
|
|
}
|
|
}(ip)
|
|
}
|
|
|
|
wg.Wait()
|
|
return activeIPs
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func calculateCIDRHosts(cidr string) int {
|
|
_, ipnet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
ones, bits := ipnet.Mask.Size()
|
|
return 1 << (bits - ones)
|
|
}
|
|
|
|
func incrementIP(ip net.IP) {
|
|
for j := len(ip) - 1; j >= 0; j-- {
|
|
ip[j]++
|
|
if ip[j] > 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func ipToInt(ip net.IP) uint32 {
|
|
ip = ip.To4()
|
|
if ip == nil {
|
|
return 0
|
|
}
|
|
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
|
|
}
|
|
|
|
func intToIP(n uint32) net.IP {
|
|
return net.IPv4(byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
|
|
}
|
|
|
|
// ReverseWhois performs reverse whois lookup to find related domains (uses ViewDNS free tier)
|
|
func (as *ASNScanner) ReverseWhois(ctx context.Context, domain string) ([]string, error) {
|
|
// Note: This is rate-limited but doesn't require API key
|
|
// Extract organization from domain whois and search for it
|
|
|
|
// For now, use HackerTarget free API (50 queries/day)
|
|
url := fmt.Sprintf("https://api.hackertarget.com/reverseiplookup/?q=%s", domain)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := as.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("hackertarget returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 100*1024))
|
|
bodyStr := string(body)
|
|
|
|
// Check for error response
|
|
if strings.Contains(bodyStr, "error") || strings.Contains(bodyStr, "API count exceeded") {
|
|
return nil, fmt.Errorf("API error: %s", bodyStr)
|
|
}
|
|
|
|
var domains []string
|
|
scanner := bufio.NewScanner(strings.NewReader(bodyStr))
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line != "" && !strings.Contains(line, "error") {
|
|
domains = append(domains, line)
|
|
}
|
|
}
|
|
|
|
return domains, nil
|
|
}
|