582 lines
17 KiB
Go
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()
|
|
}
|