diff --git a/backend/internal/api/security.go b/backend/internal/api/security.go index f8937c9..f53a9cd 100644 --- a/backend/internal/api/security.go +++ b/backend/internal/api/security.go @@ -180,6 +180,12 @@ func (ss *SecurityScanner) monitorLoop() { } } +func (ss *SecurityScanner) alertCount() int { + ss.mu.Lock() + defer ss.mu.Unlock() + return len(ss.alerts) +} + func (ss *SecurityScanner) checkAllContainers() { for _, c := range config.AppConfig.Containers { if c.Status != "running" || c.IP == "" { @@ -208,6 +214,7 @@ func (ss *SecurityScanner) checkContainer(name, ip string) { return } + alertBefore := ss.alertCount() ss.detectPortScans(name, ip, stats) ss.detectBruteForce(name, ip, stats) ss.detectSpam(name, ip, stats) @@ -216,6 +223,11 @@ func (ss *SecurityScanner) checkContainer(name, ip string) { ss.detectMining(name, ip, stats) ss.detectProxyAndTor(name, ip, stats) ss.detectMalware(name, ip, stats) + + // If new alerts were generated, snapshot the conntrack data for later retrieval. + if ss.alertCount() > alertBefore { + config.SaveConntrackSnapshot(ip, lines) + } } func newTrafficStats() *trafficStats { @@ -759,28 +771,49 @@ func HandleSecurityLogs(w http.ResponseWriter, r *http.Request) { func getConnectionLogs(ip string) []map[string]interface{} { logs := make([]map[string]interface{}, 0) + seen := map[string]bool{} - for _, line := range readConntrackLines(ip) { + parseLine := func(line string) map[string]interface{} { srcIP := extractField(line, "src=") dstIP := extractField(line, "dst=") srcPort := extractField(line, "sport=") dstPort := extractField(line, "dport=") - sPort, _ := strconv.Atoi(srcPort) dPort, _ := strconv.Atoi(dstPort) - - logs = append(logs, map[string]interface{}{ + return map[string]interface{}{ "src_ip": srcIP, "dst_ip": dstIP, "src_port": sPort, "dst_port": dPort, "protocol": extractProtocol(line), "state": extractConnState(line), - }) + } + } + // First, load stored snapshots from database (persisted at alert time). + for _, line := range config.GetConntrackSnapshotLines(ip) { if len(logs) >= 100 { break } + key := strings.TrimSpace(line) + if key == "" || seen[key] { + continue + } + seen[key] = true + logs = append(logs, parseLine(line)) + } + + // Then, merge live conntrack data (deduplicated). + for _, line := range readConntrackLines(ip) { + if len(logs) >= 100 { + break + } + key := strings.TrimSpace(line) + if key == "" || seen[key] { + continue + } + seen[key] = true + logs = append(logs, parseLine(line)) } return logs diff --git a/backend/internal/config/store_sqlite.go b/backend/internal/config/store_sqlite.go index 9cb9dcc..459076b 100644 --- a/backend/internal/config/store_sqlite.go +++ b/backend/internal/config/store_sqlite.go @@ -254,6 +254,14 @@ func ensureSchema() error { success INTEGER, error TEXT )`, + `CREATE TABLE IF NOT EXISTS security_conntrack_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + container_ip TEXT NOT NULL, + line TEXT NOT NULL, + captured_at TEXT NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_conntrack_snapshots_ip_time + ON security_conntrack_snapshots(container_ip, captured_at)`, `CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, type TEXT, @@ -642,6 +650,59 @@ func saveAPIKeys(tx *sql.Tx) error { return nil } +// SaveConntrackSnapshot stores raw conntrack lines for a container IP. +func SaveConntrackSnapshot(containerIP string, lines []string) { + if db == nil || len(lines) == 0 || strings.TrimSpace(containerIP) == "" { + return + } + now := time.Now().Format("2006-01-02 15:04:05") + tx, err := db.Begin() + if err != nil { + return + } + defer tx.Rollback() + stmt, err := tx.Prepare(`INSERT INTO security_conntrack_snapshots (container_ip, line, captured_at) VALUES (?, ?, ?)`) + if err != nil { + return + } + defer stmt.Close() + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + stmt.Exec(containerIP, line, now) + } + tx.Commit() + + // Cleanup old snapshots (>1 hour) + db.Exec(`DELETE FROM security_conntrack_snapshots WHERE captured_at < ?`, + time.Now().Add(-1*time.Hour).Format("2006-01-02 15:04:05")) +} + +// GetConntrackSnapshotLines returns stored conntrack lines for a container IP. +func GetConntrackSnapshotLines(containerIP string) []string { + if db == nil || strings.TrimSpace(containerIP) == "" { + return nil + } + rows, err := db.Query( + `SELECT line FROM security_conntrack_snapshots WHERE container_ip = ? ORDER BY captured_at DESC LIMIT 200`, + containerIP, + ) + if err != nil { + return nil + } + defer rows.Close() + var lines []string + for rows.Next() { + var line string + if rows.Scan(&line) == nil { + lines = append(lines, line) + } + } + return lines +} + func saveAuditLogs(tx *sql.Tx) error { for _, log := range AppConfig.AuditLogs { successSet := 0