Files
god-eye/internal/network/asn.go
Vyntral 14c26dc726 feat: Add Multi-Agent AI Orchestration with 8 specialized agents
- 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
2025-11-21 15:23:11 +01:00

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
}