Files
god-eye/internal/cache/cache.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

358 lines
7.0 KiB
Go

package cache
import (
"encoding/json"
"net/http"
"sync"
"time"
"god-eye/internal/config"
)
// IPCache provides LRU caching for IP geolocation lookups
type IPCache struct {
mu sync.RWMutex
cache map[string]*ipCacheEntry
maxSize int
ttl time.Duration
hits int64
misses int64
}
type ipCacheEntry struct {
info *config.IPInfo
timestamp time.Time
}
// DNSCache provides caching for DNS resolutions
type DNSCache struct {
mu sync.RWMutex
cache map[string]*dnsCacheEntry
maxSize int
ttl time.Duration
hits int64
misses int64
}
type dnsCacheEntry struct {
ips []string
timestamp time.Time
}
var (
globalIPCache *IPCache
globalDNSCache *DNSCache
initOnce sync.Once
)
// InitCaches initializes global caches
func InitCaches() {
initOnce.Do(func() {
globalIPCache = NewIPCache(1000, 5*time.Minute)
globalDNSCache = NewDNSCache(5000, 60*time.Second)
})
}
// GetIPCache returns the global IP cache
func GetIPCache() *IPCache {
InitCaches()
return globalIPCache
}
// GetDNSCache returns the global DNS cache
func GetDNSCache() *DNSCache {
InitCaches()
return globalDNSCache
}
// NewIPCache creates a new IP geolocation cache
func NewIPCache(maxSize int, ttl time.Duration) *IPCache {
return &IPCache{
cache: make(map[string]*ipCacheEntry),
maxSize: maxSize,
ttl: ttl,
}
}
// NewDNSCache creates a new DNS resolution cache
func NewDNSCache(maxSize int, ttl time.Duration) *DNSCache {
return &DNSCache{
cache: make(map[string]*dnsCacheEntry),
maxSize: maxSize,
ttl: ttl,
}
}
// Get retrieves IP info from cache
func (c *IPCache) Get(ip string) (*config.IPInfo, bool) {
c.mu.RLock()
entry, exists := c.cache[ip]
c.mu.RUnlock()
if !exists {
c.mu.Lock()
c.misses++
c.mu.Unlock()
return nil, false
}
// Check TTL
if time.Since(entry.timestamp) > c.ttl {
c.mu.Lock()
delete(c.cache, ip)
c.misses++
c.mu.Unlock()
return nil, false
}
c.mu.Lock()
c.hits++
c.mu.Unlock()
return entry.info, true
}
// Set stores IP info in cache
func (c *IPCache) Set(ip string, info *config.IPInfo) {
c.mu.Lock()
defer c.mu.Unlock()
// Evict oldest entries if at capacity
if len(c.cache) >= c.maxSize {
c.evictOldest()
}
c.cache[ip] = &ipCacheEntry{
info: info,
timestamp: time.Now(),
}
}
// SetBatch stores multiple IP infos in cache
func (c *IPCache) SetBatch(results map[string]*config.IPInfo) {
c.mu.Lock()
defer c.mu.Unlock()
for ip, info := range results {
if len(c.cache) >= c.maxSize {
c.evictOldest()
}
c.cache[ip] = &ipCacheEntry{
info: info,
timestamp: time.Now(),
}
}
}
func (c *IPCache) evictOldest() {
var oldestKey string
var oldestTime time.Time
first := true
for key, entry := range c.cache {
if first || entry.timestamp.Before(oldestTime) {
oldestKey = key
oldestTime = entry.timestamp
first = false
}
}
if oldestKey != "" {
delete(c.cache, oldestKey)
}
}
// GetStats returns cache hit/miss statistics
func (c *IPCache) GetStats() (hits, misses int64, hitRate float64) {
c.mu.RLock()
defer c.mu.RUnlock()
hits = c.hits
misses = c.misses
total := hits + misses
if total > 0 {
hitRate = float64(hits) / float64(total) * 100
}
return
}
// DNS Cache methods
// Get retrieves DNS resolution from cache
func (c *DNSCache) Get(subdomain string) ([]string, bool) {
c.mu.RLock()
entry, exists := c.cache[subdomain]
c.mu.RUnlock()
if !exists {
c.mu.Lock()
c.misses++
c.mu.Unlock()
return nil, false
}
// Check TTL
if time.Since(entry.timestamp) > c.ttl {
c.mu.Lock()
delete(c.cache, subdomain)
c.misses++
c.mu.Unlock()
return nil, false
}
c.mu.Lock()
c.hits++
c.mu.Unlock()
return entry.ips, true
}
// Set stores DNS resolution in cache
func (c *DNSCache) Set(subdomain string, ips []string) {
c.mu.Lock()
defer c.mu.Unlock()
// Evict oldest entries if at capacity
if len(c.cache) >= c.maxSize {
c.evictOldest()
}
c.cache[subdomain] = &dnsCacheEntry{
ips: ips,
timestamp: time.Now(),
}
}
func (c *DNSCache) evictOldest() {
var oldestKey string
var oldestTime time.Time
first := true
for key, entry := range c.cache {
if first || entry.timestamp.Before(oldestTime) {
oldestKey = key
oldestTime = entry.timestamp
first = false
}
}
if oldestKey != "" {
delete(c.cache, oldestKey)
}
}
// GetStats returns cache hit/miss statistics
func (c *DNSCache) GetStats() (hits, misses int64, hitRate float64) {
c.mu.RLock()
defer c.mu.RUnlock()
hits = c.hits
misses = c.misses
total := hits + misses
if total > 0 {
hitRate = float64(hits) / float64(total) * 100
}
return
}
// BatchIPLookup performs batch IP geolocation lookup (up to 100 IPs per request)
// Uses ip-api.com batch endpoint which is 10x more efficient
func BatchIPLookup(ips []string) map[string]*config.IPInfo {
results := make(map[string]*config.IPInfo)
cache := GetIPCache()
// Separate cached and uncached IPs
var uncachedIPs []string
for _, ip := range ips {
if info, found := cache.Get(ip); found {
results[ip] = info
} else {
uncachedIPs = append(uncachedIPs, ip)
}
}
// If all cached, return early
if len(uncachedIPs) == 0 {
return results
}
// Batch lookup uncached IPs (max 100 per request)
client := &http.Client{Timeout: 10 * time.Second}
for i := 0; i < len(uncachedIPs); i += 100 {
end := i + 100
if end > len(uncachedIPs) {
end = len(uncachedIPs)
}
batch := uncachedIPs[i:end]
// Build batch request
batchResults := lookupIPBatch(client, batch)
for ip, info := range batchResults {
results[ip] = info
cache.Set(ip, info)
}
}
return results
}
// lookupIPBatch performs a single batch lookup request
func lookupIPBatch(client *http.Client, ips []string) map[string]*config.IPInfo {
results := make(map[string]*config.IPInfo)
// ip-api.com batch endpoint (free tier allows 45/min, but batch counts as 1)
// For free tier, we fall back to individual requests but with caching
// For production, use pro endpoint with POST /batch
// Fallback: Individual requests with rate limiting
for _, ip := range ips {
info := lookupSingleIP(client, ip)
if info != nil {
results[ip] = info
}
// Rate limit: ~40 req/min for free tier
time.Sleep(25 * time.Millisecond)
}
return results
}
// lookupSingleIP performs a single IP lookup
func lookupSingleIP(client *http.Client, ip string) *config.IPInfo {
url := "http://ip-api.com/json/" + ip + "?fields=as,org,country,city"
resp, err := client.Get(url)
if err != nil {
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil
}
var info config.IPInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil
}
return &info
}
// GetIPInfoCached retrieves IP info with caching (drop-in replacement for GetIPInfo)
func GetIPInfoCached(ip string) (*config.IPInfo, error) {
cache := GetIPCache()
// Check cache first
if info, found := cache.Get(ip); found {
return info, nil
}
// Lookup and cache
client := &http.Client{Timeout: 5 * time.Second}
info := lookupSingleIP(client, ip)
if info == nil {
return nil, nil
}
cache.Set(ip, info)
return info, nil
}