Files
honeydany/app/dashboard/threat_analysis.go
T

582 lines
17 KiB
Go

package dashboard
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
// ThreatRule represents an automated threat detection rule
type ThreatRule struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Service string `json:"service"` // "*" for all services
Condition string `json:"condition"` // "connection_count", "auth_attempts", "scan_pattern"
Threshold int `json:"threshold"` // numeric threshold
TimeWindow int `json:"time_window"` // minutes
Action string `json:"action"` // "block", "alert", "monitor"
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ThreatEvent represents a detected threat event
type ThreatEvent struct {
ID int `json:"id"`
IP string `json:"ip"`
Service string `json:"service"`
EventType string `json:"event_type"` // "brute_force", "port_scan", "suspicious_activity"
Severity string `json:"severity"` // "low", "medium", "high", "critical"
Count int `json:"count"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
Details map[string]interface{} `json:"details"`
RuleID *int `json:"rule_id,omitempty"`
Blocked bool `json:"blocked"`
CreatedAt time.Time `json:"created_at"`
}
// IPReport represents comprehensive IP analysis
type IPReport struct {
IP string `json:"ip"`
TotalConnections int `json:"total_connections"`
TotalAuthAttempts int `json:"total_auth_attempts"`
Services []string `json:"services"`
ThreatEvents []ThreatEvent `json:"threat_events"`
ThreatScore int `json:"threat_score"`
IsBlocked bool `json:"is_blocked"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
GeoLocation map[string]interface{} `json:"geo_location,omitempty"`
}
// ThreatAnalyzer handles advanced threat detection and analysis
type ThreatAnalyzer struct {
db *sql.DB
}
// NewThreatAnalyzer creates a new threat analyzer instance
func NewThreatAnalyzer(dbPath string) (*ThreatAnalyzer, error) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
ta := &ThreatAnalyzer{db: db}
if err := ta.initDatabase(); err != nil {
return nil, fmt.Errorf("failed to initialize database: %w", err)
}
return ta, nil
}
// initDatabase creates the necessary tables
func (ta *ThreatAnalyzer) initDatabase() error {
queries := []string{
`CREATE TABLE IF NOT EXISTS threat_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
service TEXT NOT NULL,
condition TEXT NOT NULL,
threshold INTEGER NOT NULL,
time_window INTEGER NOT NULL,
action TEXT NOT NULL,
enabled BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS threat_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT NOT NULL,
service TEXT NOT NULL,
event_type TEXT NOT NULL,
severity TEXT NOT NULL,
count INTEGER DEFAULT 1,
first_seen DATETIME NOT NULL,
last_seen DATETIME NOT NULL,
details TEXT, -- JSON
rule_id INTEGER,
blocked BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (rule_id) REFERENCES threat_rules(id)
)`,
`CREATE TABLE IF NOT EXISTS ip_analysis (
ip TEXT PRIMARY KEY,
total_connections INTEGER DEFAULT 0,
total_auth_attempts INTEGER DEFAULT 0,
services TEXT, -- JSON array
threat_score INTEGER DEFAULT 0,
is_blocked BOOLEAN DEFAULT 0,
first_seen DATETIME,
last_seen DATETIME,
geo_location TEXT, -- JSON
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_threat_events_ip ON threat_events(ip)`,
`CREATE INDEX IF NOT EXISTS idx_threat_events_service ON threat_events(service)`,
`CREATE INDEX IF NOT EXISTS idx_threat_events_last_seen ON threat_events(last_seen)`,
`CREATE INDEX IF NOT EXISTS idx_ip_analysis_threat_score ON ip_analysis(threat_score DESC)`,
}
for _, query := range queries {
if _, err := ta.db.Exec(query); err != nil {
return fmt.Errorf("failed to execute query: %s, error: %w", query, err)
}
}
// Insert default rules if none exist
return ta.insertDefaultRules()
}
// insertDefaultRules creates some default threat detection rules
func (ta *ThreatAnalyzer) insertDefaultRules() error {
defaultRules := []ThreatRule{
{
Name: "SSH Brute Force",
Description: "Detect SSH brute force attempts",
Service: "ssh",
Condition: "auth_attempts",
Threshold: 5,
TimeWindow: 60, // 1 hour
Action: "alert",
Enabled: true,
},
{
Name: "HTTP Scanning",
Description: "Detect HTTP scanning/crawling behavior",
Service: "http",
Condition: "connection_count",
Threshold: 50,
TimeWindow: 30, // 30 minutes
Action: "alert",
Enabled: true,
},
{
Name: "Port Scanner",
Description: "Detect port scanning across multiple services",
Service: "*",
Condition: "service_diversity",
Threshold: 3, // 3 different services
TimeWindow: 15, // 15 minutes
Action: "alert",
Enabled: true,
},
{
Name: "FTP Brute Force",
Description: "Detect FTP brute force attempts",
Service: "ftp",
Condition: "auth_attempts",
Threshold: 5,
TimeWindow: 60,
Action: "alert",
Enabled: true,
},
}
for _, rule := range defaultRules {
exists, err := ta.ruleExists(rule.Name)
if err != nil {
return err
}
if !exists {
if err := ta.CreateRule(rule); err != nil {
log.Printf("Failed to create default rule %s: %v", rule.Name, err)
}
}
}
return nil
}
// ruleExists checks if a rule with the given name already exists
func (ta *ThreatAnalyzer) ruleExists(name string) (bool, error) {
var count int
err := ta.db.QueryRow("SELECT COUNT(*) FROM threat_rules WHERE name = ?", name).Scan(&count)
return count > 0, err
}
// CreateRule creates a new threat detection rule
func (ta *ThreatAnalyzer) CreateRule(rule ThreatRule) error {
query := `INSERT INTO threat_rules (name, description, service, condition, threshold, time_window, action, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
_, err := ta.db.Exec(query, rule.Name, rule.Description, rule.Service, rule.Condition,
rule.Threshold, rule.TimeWindow, rule.Action, rule.Enabled)
return err
}
// GetRules retrieves all threat detection rules
func (ta *ThreatAnalyzer) GetRules() ([]ThreatRule, error) {
query := `SELECT id, name, description, service, condition, threshold, time_window, action, enabled, created_at, updated_at
FROM threat_rules ORDER BY created_at DESC`
rows, err := ta.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var rules []ThreatRule
for rows.Next() {
var rule ThreatRule
err := rows.Scan(&rule.ID, &rule.Name, &rule.Description, &rule.Service, &rule.Condition,
&rule.Threshold, &rule.TimeWindow, &rule.Action, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt)
if err != nil {
return nil, err
}
rules = append(rules, rule)
}
return rules, nil
}
// AnalyzeIP performs comprehensive analysis of an IP address
func (ta *ThreatAnalyzer) AnalyzeIP(ip string) (*IPReport, error) {
report := &IPReport{IP: ip}
// Get basic IP statistics
query := `SELECT total_connections, total_auth_attempts, services, threat_score, is_blocked, first_seen, last_seen, geo_location
FROM ip_analysis WHERE ip = ?`
var servicesJSON, geoJSON sql.NullString
err := ta.db.QueryRow(query, ip).Scan(&report.TotalConnections, &report.TotalAuthAttempts,
&servicesJSON, &report.ThreatScore, &report.IsBlocked, &report.FirstSeen, &report.LastSeen, &geoJSON)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
// Parse services JSON
if servicesJSON.Valid {
json.Unmarshal([]byte(servicesJSON.String), &report.Services)
}
// Parse geo location JSON
if geoJSON.Valid {
json.Unmarshal([]byte(geoJSON.String), &report.GeoLocation)
}
// Get threat events for this IP
report.ThreatEvents, err = ta.GetThreatEventsByIP(ip)
if err != nil {
return nil, err
}
return report, nil
}
// GetThreatEventsByIP retrieves all threat events for a specific IP
func (ta *ThreatAnalyzer) GetThreatEventsByIP(ip string) ([]ThreatEvent, error) {
query := `SELECT id, ip, service, event_type, severity, count, first_seen, last_seen, details, rule_id, blocked, created_at
FROM threat_events WHERE ip = ? ORDER BY last_seen DESC`
rows, err := ta.db.Query(query, ip)
if err != nil {
return nil, err
}
defer rows.Close()
var events []ThreatEvent
for rows.Next() {
var event ThreatEvent
var detailsJSON sql.NullString
var ruleID sql.NullInt64
err := rows.Scan(&event.ID, &event.IP, &event.Service, &event.EventType, &event.Severity,
&event.Count, &event.FirstSeen, &event.LastSeen, &detailsJSON, &ruleID, &event.Blocked, &event.CreatedAt)
if err != nil {
return nil, err
}
if detailsJSON.Valid {
json.Unmarshal([]byte(detailsJSON.String), &event.Details)
}
if ruleID.Valid {
id := int(ruleID.Int64)
event.RuleID = &id
}
events = append(events, event)
}
return events, nil
}
// GetIPReports retrieves IP reports with filtering options
func (ta *ThreatAnalyzer) GetIPReports(filters map[string]interface{}) ([]IPReport, error) {
query := `SELECT ip, total_connections, total_auth_attempts, services, threat_score, is_blocked, first_seen, last_seen, geo_location
FROM ip_analysis WHERE 1=1`
var args []interface{}
var conditions []string
// Apply filters
if service, ok := filters["service"].(string); ok && service != "" {
conditions = append(conditions, "services LIKE ?")
args = append(args, "%\""+service+"\"%")
}
if minThreatScore, ok := filters["min_threat_score"].(int); ok {
conditions = append(conditions, "threat_score >= ?")
args = append(args, minThreatScore)
}
if blocked, ok := filters["blocked"].(bool); ok {
conditions = append(conditions, "is_blocked = ?")
args = append(args, blocked)
}
if len(conditions) > 0 {
query += " AND " + strings.Join(conditions, " AND ")
}
query += " ORDER BY threat_score DESC, last_seen DESC"
if limit, ok := filters["limit"].(int); ok && limit > 0 {
query += " LIMIT ?"
args = append(args, limit)
}
rows, err := ta.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var reports []IPReport
for rows.Next() {
var report IPReport
var servicesJSON, geoJSON sql.NullString
err := rows.Scan(&report.IP, &report.TotalConnections, &report.TotalAuthAttempts,
&servicesJSON, &report.ThreatScore, &report.IsBlocked, &report.FirstSeen, &report.LastSeen, &geoJSON)
if err != nil {
return nil, err
}
// Parse JSON fields
if servicesJSON.Valid {
json.Unmarshal([]byte(servicesJSON.String), &report.Services)
}
if geoJSON.Valid {
json.Unmarshal([]byte(geoJSON.String), &report.GeoLocation)
}
reports = append(reports, report)
}
return reports, nil
}
// ProcessLogRecord analyzes a log record and updates threat intelligence
func (ta *ThreatAnalyzer) ProcessLogRecord(record LogRecord) error {
// Update IP analysis
if err := ta.updateIPAnalysis(record); err != nil {
return err
}
// Check against threat rules
return ta.checkThreatRules(record)
}
// LogRecord represents a honeypot log entry (simplified version)
type LogRecord struct {
IP string `json:"ip"`
Service string `json:"service"`
Timestamp time.Time `json:"timestamp"`
Details map[string]interface{} `json:"details"`
}
// updateIPAnalysis updates the IP analysis table with new log data
func (ta *ThreatAnalyzer) updateIPAnalysis(record LogRecord) error {
// Check if IP exists
var exists bool
err := ta.db.QueryRow("SELECT EXISTS(SELECT 1 FROM ip_analysis WHERE ip = ?)", record.IP).Scan(&exists)
if err != nil {
return err
}
if exists {
// Update existing record
query := `UPDATE ip_analysis SET
total_connections = total_connections + 1,
last_seen = ?,
updated_at = CURRENT_TIMESTAMP
WHERE ip = ?`
_, err = ta.db.Exec(query, record.Timestamp, record.IP)
} else {
// Insert new record
servicesJSON, _ := json.Marshal([]string{record.Service})
query := `INSERT INTO ip_analysis (ip, total_connections, services, first_seen, last_seen)
VALUES (?, 1, ?, ?, ?)`
_, err = ta.db.Exec(query, record.IP, string(servicesJSON), record.Timestamp, record.Timestamp)
}
return err
}
// checkThreatRules evaluates log records against threat detection rules
func (ta *ThreatAnalyzer) checkThreatRules(record LogRecord) error {
rules, err := ta.GetRules()
if err != nil {
return err
}
for _, rule := range rules {
if !rule.Enabled {
continue
}
// Check if rule applies to this service
if rule.Service != "*" && rule.Service != record.Service {
continue
}
// Evaluate rule condition
triggered, err := ta.evaluateRule(rule, record)
if err != nil {
log.Printf("Error evaluating rule %s: %v", rule.Name, err)
continue
}
if triggered {
if err := ta.createThreatEvent(rule, record); err != nil {
log.Printf("Error creating threat event: %v", err)
}
}
}
return nil
}
// evaluateRule checks if a rule condition is met
func (ta *ThreatAnalyzer) evaluateRule(rule ThreatRule, record LogRecord) (bool, error) {
timeWindow := time.Now().Add(-time.Duration(rule.TimeWindow) * time.Minute)
switch rule.Condition {
case "connection_count":
var count int
query := `SELECT COUNT(*) FROM ip_analysis WHERE ip = ? AND last_seen >= ?`
err := ta.db.QueryRow(query, record.IP, timeWindow).Scan(&count)
return count >= rule.Threshold, err
case "auth_attempts":
// This would need to be tracked separately based on log details
// For now, we'll use a simplified approach
if authAttempts, ok := record.Details["auth_attempts"].(float64); ok {
return int(authAttempts) >= rule.Threshold, nil
}
return false, nil
case "service_diversity":
var serviceCount int
query := `SELECT COUNT(DISTINCT service) FROM threat_events WHERE ip = ? AND last_seen >= ?`
err := ta.db.QueryRow(query, record.IP, timeWindow).Scan(&serviceCount)
return serviceCount >= rule.Threshold, err
default:
return false, fmt.Errorf("unknown rule condition: %s", rule.Condition)
}
}
// createThreatEvent creates a new threat event
func (ta *ThreatAnalyzer) createThreatEvent(rule ThreatRule, record LogRecord) error {
detailsJSON, _ := json.Marshal(record.Details)
// Determine event type and severity based on rule
eventType := "suspicious_activity"
severity := "medium"
if strings.Contains(strings.ToLower(rule.Name), "brute") {
eventType = "brute_force"
severity = "high"
} else if strings.Contains(strings.ToLower(rule.Name), "scan") {
eventType = "port_scan"
severity = "medium"
}
query := `INSERT INTO threat_events (ip, service, event_type, severity, first_seen, last_seen, details, rule_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(ip, service, event_type) DO UPDATE SET
count = count + 1,
last_seen = ?,
details = ?`
_, err := ta.db.Exec(query, record.IP, record.Service, eventType, severity,
record.Timestamp, record.Timestamp, string(detailsJSON), rule.ID,
record.Timestamp, string(detailsJSON))
// If this is a blocking rule, add to blocklist
if rule.Action == "block" {
ta.blockIP(record.IP, rule.ID)
}
return err
}
// blockIP adds an IP to the blocklist
func (ta *ThreatAnalyzer) blockIP(ip string, ruleID int) error {
// Update IP analysis to mark as blocked
query := `UPDATE ip_analysis SET is_blocked = 1 WHERE ip = ?`
_, err := ta.db.Exec(query, ip)
// Update threat events to mark as blocked
query2 := `UPDATE threat_events SET blocked = 1 WHERE ip = ? AND rule_id = ?`
_, err2 := ta.db.Exec(query2, ip, ruleID)
if err != nil {
return err
}
return err2
}
// GetBlockedIPs returns all currently blocked IPs
func (ta *ThreatAnalyzer) GetBlockedIPs() ([]string, error) {
query := `SELECT ip FROM ip_analysis WHERE is_blocked = 1 ORDER BY last_seen DESC`
rows, err := ta.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var ips []string
for rows.Next() {
var ip string
if err := rows.Scan(&ip); err != nil {
return nil, err
}
ips = append(ips, ip)
}
return ips, nil
}
// UnblockIP removes an IP from the blocklist
func (ta *ThreatAnalyzer) UnblockIP(ip string) error {
query := `UPDATE ip_analysis SET is_blocked = 0 WHERE ip = ?`
_, err := ta.db.Exec(query, ip)
query2 := `UPDATE threat_events SET blocked = 0 WHERE ip = ?`
_, err2 := ta.db.Exec(query2, ip)
if err != nil {
return err
}
return err2
}
// Close closes the database connection
func (ta *ThreatAnalyzer) Close() error {
return ta.db.Close()
}