Files
honeydany/app/threat_intel.go
T
2025-09-28 06:48:03 +01:00

489 lines
13 KiB
Go

package app
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"sync"
"time"
)
// ThreatIntel tracks malicious IPs and their activities
type ThreatIntel struct {
mu sync.RWMutex
maliciousIPs map[string]*IPThreatInfo
configPath string
autoSave bool
}
// IPThreatInfo contains information about a potentially malicious IP
type IPThreatInfo struct {
IP string `json:"ip"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
TotalConnections int `json:"total_connections"`
Services map[string]int `json:"services"` // service -> connection count
AuthAttempts int `json:"auth_attempts"` // total authentication attempts
UniqueUsernames map[string]int `json:"unique_usernames"` // username -> attempt count
UniquePasswords map[string]int `json:"unique_passwords"` // password -> attempt count
Countries map[string]int `json:"countries"` // country -> count (if GeoIP available)
ThreatScore int `json:"threat_score"` // calculated threat score
IsBlacklisted bool `json:"is_blacklisted"`
Notes []string `json:"notes"`
RawPayloads []string `json:"raw_payloads,omitempty"` // sample payloads
}
// NewThreatIntel creates a new threat intelligence tracker
func NewThreatIntel(configPath string, autoSave bool) *ThreatIntel {
ti := &ThreatIntel{
maliciousIPs: make(map[string]*IPThreatInfo),
configPath: configPath,
autoSave: autoSave,
}
// Try to load existing data
ti.Load()
// Start auto-save routine if enabled
if autoSave {
go ti.autoSaveRoutine()
}
return ti
}
// RecordActivity records activity from an IP address
func (ti *ThreatIntel) RecordActivity(record Record) {
ti.mu.Lock()
defer ti.mu.Unlock()
ip := record.RemoteAddr
if ip == "" {
return
}
// Get or create IP info
info, exists := ti.maliciousIPs[ip]
if !exists {
info = &IPThreatInfo{
IP: ip,
FirstSeen: record.Timestamp,
Services: make(map[string]int),
UniqueUsernames: make(map[string]int),
UniquePasswords: make(map[string]int),
Countries: make(map[string]int),
Notes: []string{},
RawPayloads: []string{},
}
ti.maliciousIPs[ip] = info
}
// Update basic info
info.LastSeen = record.Timestamp
info.TotalConnections++
info.Services[record.Service]++
// Process authentication attempts
if record.Details != nil {
if username, ok := record.Details["username"]; ok && username != "" {
info.UniqueUsernames[username]++
info.AuthAttempts++
}
if password, ok := record.Details["password"]; ok && password != "" {
info.UniquePasswords[password]++
}
// Check for specific attack patterns
ti.analyzeAttackPatterns(info, record)
}
// Store sample payloads (limit to 10)
if record.RawPayload != "" && len(info.RawPayloads) < 10 {
info.RawPayloads = append(info.RawPayloads, record.RawPayload)
}
// Recalculate threat score
info.ThreatScore = ti.calculateThreatScore(info)
// Auto-blacklist based on threat score
if info.ThreatScore >= 100 && !info.IsBlacklisted {
info.IsBlacklisted = true
info.Notes = append(info.Notes, fmt.Sprintf("Auto-blacklisted at %s (threat score: %d)", time.Now().Format(time.RFC3339), info.ThreatScore))
}
}
// analyzeAttackPatterns looks for specific attack patterns and adds notes
func (ti *ThreatIntel) analyzeAttackPatterns(info *IPThreatInfo, record Record) {
// Check for brute force patterns
if info.AuthAttempts > 10 {
if len(info.Notes) == 0 || info.Notes[len(info.Notes)-1] != "Brute force attack detected" {
info.Notes = append(info.Notes, "Brute force attack detected")
}
}
// Check for credential stuffing (many unique usernames)
if len(info.UniqueUsernames) > 20 {
if len(info.Notes) == 0 || info.Notes[len(info.Notes)-1] != "Credential stuffing attack detected" {
info.Notes = append(info.Notes, "Credential stuffing attack detected")
}
}
// Check for service scanning (multiple services)
if len(info.Services) > 5 {
if len(info.Notes) == 0 || info.Notes[len(info.Notes)-1] != "Port/service scanning detected" {
info.Notes = append(info.Notes, "Port/service scanning detected")
}
}
// Check for common attack usernames
commonAttackUsernames := []string{"admin", "root", "administrator", "user", "test", "guest", "oracle", "postgres", "mysql"}
if username, ok := record.Details["username"]; ok {
for _, attackUser := range commonAttackUsernames {
if username == attackUser {
info.Notes = append(info.Notes, fmt.Sprintf("Used common attack username: %s", username))
break
}
}
}
// Check for common attack passwords
commonAttackPasswords := []string{"password", "123456", "admin", "root", "toor", "password123", "qwerty"}
if password, ok := record.Details["password"]; ok {
for _, attackPass := range commonAttackPasswords {
if password == attackPass {
info.Notes = append(info.Notes, fmt.Sprintf("Used common attack password: %s", password))
break
}
}
}
}
// calculateThreatScore calculates a threat score based on various factors
func (ti *ThreatIntel) calculateThreatScore(info *IPThreatInfo) int {
score := 0
// Base score for any connection
score += 1
// Score based on number of connections
if info.TotalConnections > 100 {
score += 50
} else if info.TotalConnections > 50 {
score += 30
} else if info.TotalConnections > 10 {
score += 15
} else if info.TotalConnections > 5 {
score += 5
}
// Score based on authentication attempts
if info.AuthAttempts > 50 {
score += 40
} else if info.AuthAttempts > 20 {
score += 25
} else if info.AuthAttempts > 10 {
score += 15
} else if info.AuthAttempts > 5 {
score += 10
}
// Score based on unique usernames (credential stuffing)
if len(info.UniqueUsernames) > 50 {
score += 30
} else if len(info.UniqueUsernames) > 20 {
score += 20
} else if len(info.UniqueUsernames) > 10 {
score += 10
}
// Score based on service diversity (scanning)
if len(info.Services) > 10 {
score += 25
} else if len(info.Services) > 5 {
score += 15
} else if len(info.Services) > 3 {
score += 10
}
// Score based on time span (persistent attacker)
timeSpan := info.LastSeen.Sub(info.FirstSeen)
if timeSpan > 24*time.Hour {
score += 20
} else if timeSpan > 12*time.Hour {
score += 15
} else if timeSpan > 6*time.Hour {
score += 10
} else if timeSpan > 1*time.Hour {
score += 5
}
return score
}
// IsBlacklisted checks if an IP is blacklisted
func (ti *ThreatIntel) IsBlacklisted(ip string) bool {
ti.mu.RLock()
defer ti.mu.RUnlock()
if info, exists := ti.maliciousIPs[ip]; exists {
return info.IsBlacklisted
}
return false
}
// GetThreatInfo returns threat information for an IP
func (ti *ThreatIntel) GetThreatInfo(ip string) (*IPThreatInfo, bool) {
ti.mu.RLock()
defer ti.mu.RUnlock()
info, exists := ti.maliciousIPs[ip]
if !exists {
return nil, false
}
// Return a copy to avoid race conditions
infoCopy := *info
infoCopy.Services = make(map[string]int)
infoCopy.UniqueUsernames = make(map[string]int)
infoCopy.UniquePasswords = make(map[string]int)
infoCopy.Countries = make(map[string]int)
for k, v := range info.Services {
infoCopy.Services[k] = v
}
for k, v := range info.UniqueUsernames {
infoCopy.UniqueUsernames[k] = v
}
for k, v := range info.UniquePasswords {
infoCopy.UniquePasswords[k] = v
}
for k, v := range info.Countries {
infoCopy.Countries[k] = v
}
infoCopy.Notes = make([]string, len(info.Notes))
copy(infoCopy.Notes, info.Notes)
infoCopy.RawPayloads = make([]string, len(info.RawPayloads))
copy(infoCopy.RawPayloads, info.RawPayloads)
return &infoCopy, true
}
// GetTopThreats returns the top N threats by score
func (ti *ThreatIntel) GetTopThreats(n int) []*IPThreatInfo {
ti.mu.RLock()
defer ti.mu.RUnlock()
var threats []*IPThreatInfo
for _, info := range ti.maliciousIPs {
threats = append(threats, info)
}
// Sort by threat score (simple bubble sort for small datasets)
for i := 0; i < len(threats)-1; i++ {
for j := 0; j < len(threats)-i-1; j++ {
if threats[j].ThreatScore < threats[j+1].ThreatScore {
threats[j], threats[j+1] = threats[j+1], threats[j]
}
}
}
if n > len(threats) {
n = len(threats)
}
return threats[:n]
}
// GetBlacklistedIPs returns all blacklisted IPs
func (ti *ThreatIntel) GetBlacklistedIPs() []string {
ti.mu.RLock()
defer ti.mu.RUnlock()
var blacklisted []string
for ip, info := range ti.maliciousIPs {
if info.IsBlacklisted {
blacklisted = append(blacklisted, ip)
}
}
return blacklisted
}
// ManualBlacklist manually blacklists an IP
func (ti *ThreatIntel) ManualBlacklist(ip string, reason string) {
ti.mu.Lock()
defer ti.mu.Unlock()
info, exists := ti.maliciousIPs[ip]
if !exists {
info = &IPThreatInfo{
IP: ip,
FirstSeen: time.Now(),
Services: make(map[string]int),
UniqueUsernames: make(map[string]int),
UniquePasswords: make(map[string]int),
Countries: make(map[string]int),
Notes: []string{},
RawPayloads: []string{},
}
ti.maliciousIPs[ip] = info
}
info.IsBlacklisted = true
info.LastSeen = time.Now()
note := fmt.Sprintf("Manually blacklisted at %s", time.Now().Format(time.RFC3339))
if reason != "" {
note += fmt.Sprintf(" - Reason: %s", reason)
}
info.Notes = append(info.Notes, note)
}
// RemoveFromBlacklist removes an IP from the blacklist
func (ti *ThreatIntel) RemoveFromBlacklist(ip string) {
ti.mu.Lock()
defer ti.mu.Unlock()
if info, exists := ti.maliciousIPs[ip]; exists {
info.IsBlacklisted = false
info.Notes = append(info.Notes, fmt.Sprintf("Removed from blacklist at %s", time.Now().Format(time.RFC3339)))
}
}
// Save saves threat intelligence data to file
func (ti *ThreatIntel) Save() error {
ti.mu.RLock()
defer ti.mu.RUnlock()
if ti.configPath == "" {
return nil
}
// Create directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(ti.configPath), 0755); err != nil {
return fmt.Errorf("create threat intel dir: %w", err)
}
data, err := json.MarshalIndent(ti.maliciousIPs, "", " ")
if err != nil {
return fmt.Errorf("marshal threat intel data: %w", err)
}
return os.WriteFile(ti.configPath, data, 0644)
}
// Load loads threat intelligence data from file
func (ti *ThreatIntel) Load() error {
ti.mu.Lock()
defer ti.mu.Unlock()
if ti.configPath == "" {
return nil
}
data, err := os.ReadFile(ti.configPath)
if err != nil {
if os.IsNotExist(err) {
return nil // File doesn't exist yet, that's okay
}
return fmt.Errorf("read threat intel file: %w", err)
}
return json.Unmarshal(data, &ti.maliciousIPs)
}
// autoSaveRoutine periodically saves threat intelligence data
func (ti *ThreatIntel) autoSaveRoutine() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
if err := ti.Save(); err != nil {
// Log error but don't stop the routine
fmt.Printf("Error auto-saving threat intel: %v\n", err)
}
}
}
// GetStats returns overall statistics
func (ti *ThreatIntel) GetStats() map[string]interface{} {
ti.mu.RLock()
defer ti.mu.RUnlock()
totalIPs := len(ti.maliciousIPs)
blacklistedIPs := 0
totalConnections := 0
totalAuthAttempts := 0
serviceStats := make(map[string]int)
for _, info := range ti.maliciousIPs {
if info.IsBlacklisted {
blacklistedIPs++
}
totalConnections += info.TotalConnections
totalAuthAttempts += info.AuthAttempts
for service, count := range info.Services {
serviceStats[service] += count
}
}
return map[string]interface{}{
"total_ips": totalIPs,
"blacklisted_ips": blacklistedIPs,
"total_connections": totalConnections,
"total_auth_attempts": totalAuthAttempts,
"service_stats": serviceStats,
}
}
// ShouldBlock determines if a connection should be blocked based on IP reputation
func (ti *ThreatIntel) ShouldBlock(ip string) bool {
// Check if IP is blacklisted
if ti.IsBlacklisted(ip) {
return true
}
// Additional checks could be added here:
// - Rate limiting
// - Temporary blocks for high-frequency connections
// - Integration with external threat feeds
return false
}
// IsPrivateIP checks if an IP address is private/internal
func IsPrivateIP(ip string) bool {
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return false
}
// Check for private IP ranges
privateRanges := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8",
"169.254.0.0/16",
"::1/128",
"fc00::/7",
"fe80::/10",
}
for _, cidr := range privateRanges {
_, network, err := net.ParseCIDR(cidr)
if err != nil {
continue
}
if network.Contains(parsedIP) {
return true
}
}
return false
}