package api import ( "context" "encoding/json" "fmt" "net/http" "os" "os/exec" "strconv" "strings" "sync" "time" "clicd/internal/config" ) // SecurityAlert represents a detected abuse event. type SecurityAlert struct { ID string `json:"id"` ContainerName string `json:"container_name"` Type string `json:"type"` // port_scan, horizontal_scan, brute_force, ddos, spam, malware, mining, proxy, reflection Severity string `json:"severity"` // low, medium, high, critical SourceIP string `json:"source_ip"` TargetIP string `json:"target_ip"` TargetPort int `json:"target_port"` Detail string `json:"detail"` LogLine string `json:"log_line"` Timestamp string `json:"timestamp"` Count int `json:"count"` } // SecurityScanner monitors container network activity for abuse patterns. type SecurityScanner struct { mu sync.Mutex alerts []SecurityAlert nextID int scanCount map[string]int stopChan chan struct{} } type connEntry struct { dstIP string dstPort int proto string state string line string } type trafficStats struct { total int totalSynSent int destCounts map[string]int destPorts map[string]map[int]int portDestCounts map[int]map[string]int portTotalCounts map[int]int udpDestCounts map[int]map[string]int udpTotalCounts map[int]int synSentByDst map[string]int } var scanner *SecurityScanner var scannerStarted bool var bruteForcePorts = map[int]string{ 21: "FTP", 22: "SSH", 23: "Telnet", 135: "MS-RPC", 139: "NetBIOS", 445: "SMB", 3306: "MySQL", 3389: "RDP", 5432: "PostgreSQL", 5900: "VNC", 5901: "VNC", 5985: "WinRM", 5986: "WinRM", 6379: "Redis", 9200: "Elasticsearch", 27017: "MongoDB", } var smtpPorts = map[int]string{ 25: "SMTP", 465: "SMTPS", 587: "SMTP submission", 2525: "SMTP alternate", } var reflectionPorts = map[int]string{ 17: "QOTD", 19: "Chargen", 53: "DNS", 69: "TFTP", 111: "Portmap", 123: "NTP", 137: "NetBIOS", 161: "SNMP", 389: "CLDAP", 500: "IKE", 1900: "SSDP", 3702: "WS-Discovery", 4500: "IPsec NAT-T", 5353: "mDNS", 11211: "Memcached", } var miningPorts = map[int]string{ 3333: "Stratum", 3334: "Stratum", 3335: "Stratum", 4444: "Stratum", 5555: "Stratum", 7777: "Stratum", 8888: "Stratum", 9999: "Stratum", 14433: "Stratum", 14444: "Stratum", } var proxyPorts = map[int]string{ 1080: "SOCKS", 3128: "HTTP proxy", 8118: "Privoxy", 9001: "Tor OR", 9030: "Tor directory", 9050: "Tor SOCKS", 1194: "OpenVPN", 51820: "WireGuard", } var malwarePorts = map[int]string{ 1337: "common backdoor", 31337: "Back Orifice", 4444: "Metasploit/reverse shell", 5555: "Android debug/reverse shell", 6666: "IRC botnet", 6667: "IRC botnet", 6697: "IRC over TLS", 9050: "Tor/C2 proxy", } func InitScanner() { if scannerStarted { return } scannerStarted = true scanner = newSecurityScanner() go scanner.monitorLoop() } func newSecurityScanner() *SecurityScanner { return &SecurityScanner{ alerts: make([]SecurityAlert, 0), scanCount: make(map[string]int), stopChan: make(chan struct{}), } } func ensureScanner() *SecurityScanner { if scanner == nil { scanner = newSecurityScanner() } return scanner } func (ss *SecurityScanner) monitorLoop() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ss.stopChan: return case <-ticker.C: ss.checkAllContainers() } } } func (ss *SecurityScanner) checkAllContainers() { for _, c := range config.AppConfig.Containers { if c.Status != "running" || c.IP == "" { continue } ss.checkContainer(c.Name, c.IP) } } func (ss *SecurityScanner) checkContainer(name, ip string) { lines := readConntrackLines(ip) if len(lines) == 0 { return } stats := newTrafficStats() for _, line := range lines { conn, ok := parseConntrackLine(line, ip) if !ok || conn.dstIP == "" || conn.dstIP == ip { continue } stats.add(conn) } if stats.total == 0 { return } ss.detectPortScans(name, ip, stats) ss.detectBruteForce(name, ip, stats) ss.detectSpam(name, ip, stats) ss.detectMassAbuse(name, ip, stats) ss.detectReflectionAbuse(name, ip, stats) ss.detectMining(name, ip, stats) ss.detectProxyAndTor(name, ip, stats) ss.detectMalware(name, ip, stats) } func newTrafficStats() *trafficStats { return &trafficStats{ destCounts: make(map[string]int), destPorts: make(map[string]map[int]int), portDestCounts: make(map[int]map[string]int), portTotalCounts: make(map[int]int), udpDestCounts: make(map[int]map[string]int), udpTotalCounts: make(map[int]int), synSentByDst: make(map[string]int), } } func (ts *trafficStats) add(conn connEntry) { ts.total++ ts.destCounts[conn.dstIP]++ if conn.dstPort > 0 { if ts.destPorts[conn.dstIP] == nil { ts.destPorts[conn.dstIP] = make(map[int]int) } ts.destPorts[conn.dstIP][conn.dstPort]++ if ts.portDestCounts[conn.dstPort] == nil { ts.portDestCounts[conn.dstPort] = make(map[string]int) } ts.portDestCounts[conn.dstPort][conn.dstIP]++ ts.portTotalCounts[conn.dstPort]++ if conn.proto == "udp" { if ts.udpDestCounts[conn.dstPort] == nil { ts.udpDestCounts[conn.dstPort] = make(map[string]int) } ts.udpDestCounts[conn.dstPort][conn.dstIP]++ ts.udpTotalCounts[conn.dstPort]++ } } if conn.state == "SYN_SENT" { ts.totalSynSent++ ts.synSentByDst[conn.dstIP]++ } } func (ss *SecurityScanner) detectPortScans(name, ip string, stats *trafficStats) { for dstIP, portCounts := range stats.destPorts { uniquePorts := len(portCounts) switch { case uniquePorts >= 20: ss.addAlert(name, "port_scan", "high", ip, dstIP, 0, fmt.Sprintf("端口扫描: 同一目标 %s 出现 %d 个不同目标端口", dstIP, uniquePorts), "") case uniquePorts >= 8: ss.addAlert(name, "port_scan", "medium", ip, dstIP, 0, fmt.Sprintf("可疑端口探测: 同一目标 %s 出现 %d 个不同目标端口", dstIP, uniquePorts), "") } } for port, targets := range stats.portDestCounts { uniqueTargets := len(targets) if service, ok := bruteForcePorts[port]; ok { if uniqueTargets >= 30 { ss.addAlert(name, "brute_force", "critical", ip, "*", port, fmt.Sprintf("横向爆破: 目标服务 %s(%d) 覆盖 %d 个不同 IP", service, port, uniqueTargets), "") } else if uniqueTargets >= 10 { ss.addAlert(name, "brute_force", "high", ip, "*", port, fmt.Sprintf("疑似横向爆破: 目标服务 %s(%d) 覆盖 %d 个不同 IP", service, port, uniqueTargets), "") } continue } if uniqueTargets >= 40 { ss.addAlert(name, "horizontal_scan", "high", ip, "*", port, fmt.Sprintf("横向扫描: 同一端口 %d 覆盖 %d 个不同目标", port, uniqueTargets), "") } else if uniqueTargets >= 15 { ss.addAlert(name, "horizontal_scan", "medium", ip, "*", port, fmt.Sprintf("可疑横向探测: 同一端口 %d 覆盖 %d 个不同目标", port, uniqueTargets), "") } } } func (ss *SecurityScanner) detectBruteForce(name, ip string, stats *trafficStats) { for dstIP, portCounts := range stats.destPorts { for port, count := range portCounts { service, sensitive := bruteForcePorts[port] if !sensitive { continue } if count >= 20 { ss.addAlert(name, "brute_force", "critical", ip, dstIP, port, fmt.Sprintf("暴力破解: %s(%d) 当前连接数 %d", service, port, count), "") } else if count >= 10 { ss.addAlert(name, "brute_force", "high", ip, dstIP, port, fmt.Sprintf("疑似暴力破解: %s(%d) 当前连接数 %d", service, port, count), "") } } } } func (ss *SecurityScanner) detectSpam(name, ip string, stats *trafficStats) { total, targets := countPorts(stats.portTotalCounts, stats.portDestCounts, smtpPorts) if total == 0 { return } if targets >= 10 || total >= 30 { ss.addAlert(name, "spam", "critical", ip, "*", 25, fmt.Sprintf("疑似垃圾邮件: SMTP 相关端口当前连接 %d 条,覆盖 %d 个目标", total, targets), "") } else if targets >= 2 || total >= 5 { ss.addAlert(name, "spam", "high", ip, "*", 25, fmt.Sprintf("可疑邮件发送: SMTP 相关端口当前连接 %d 条,覆盖 %d 个目标", total, targets), "") } } func (ss *SecurityScanner) detectMassAbuse(name, ip string, stats *trafficStats) { targets := len(stats.destCounts) switch { case targets >= 100: ss.addAlert(name, "ddos", "critical", ip, "*", 0, fmt.Sprintf("大规模对外连接: 当前覆盖 %d 个不同目标", targets), "") case targets >= 35: ss.addAlert(name, "ddos", "high", ip, "*", 0, fmt.Sprintf("大量对外连接: 当前覆盖 %d 个不同目标", targets), "") } switch { case stats.total >= 500: ss.addAlert(name, "ddos", "critical", ip, "*", 0, fmt.Sprintf("异常大量连接: 当前 conntrack 出站记录 %d 条", stats.total), "") case stats.total >= 200: ss.addAlert(name, "ddos", "high", ip, "*", 0, fmt.Sprintf("高连接数: 当前 conntrack 出站记录 %d 条", stats.total), "") } if stats.totalSynSent >= 100 { ss.addAlert(name, "ddos", "critical", ip, "*", 0, fmt.Sprintf("大量半开连接: 当前 SYN_SENT %d 条", stats.totalSynSent), "") } for dstIP, count := range stats.synSentByDst { if count >= 50 { ss.addAlert(name, "ddos", "critical", ip, dstIP, 0, fmt.Sprintf("SYN 洪水: 单一目标半开连接 %d 条", count), "") } else if count >= 20 { ss.addAlert(name, "ddos", "high", ip, dstIP, 0, fmt.Sprintf("可疑 SYN 洪水: 单一目标半开连接 %d 条", count), "") } } } func (ss *SecurityScanner) detectReflectionAbuse(name, ip string, stats *trafficStats) { for port, service := range reflectionPorts { total := stats.udpTotalCounts[port] targets := len(stats.udpDestCounts[port]) if total == 0 { continue } if targets >= 30 || total >= 100 { ss.addAlert(name, "reflection", "critical", ip, "*", port, fmt.Sprintf("UDP 反射放大: %s(%d) 当前 UDP 连接 %d 条,覆盖 %d 个目标", service, port, total, targets), "") } else if targets >= 10 || total >= 30 { ss.addAlert(name, "reflection", "high", ip, "*", port, fmt.Sprintf("疑似 UDP 反射放大: %s(%d) 当前 UDP 连接 %d 条,覆盖 %d 个目标", service, port, total, targets), "") } } } func (ss *SecurityScanner) detectMining(name, ip string, stats *trafficStats) { for port, service := range miningPorts { total := stats.portTotalCounts[port] if total == 0 { continue } severity := "high" if total >= 5 { severity = "critical" } ss.addAlert(name, "mining", severity, ip, "*", port, fmt.Sprintf("疑似挖矿连接: %s/%d 当前连接 %d 条", service, port, total), "") } } func (ss *SecurityScanner) detectProxyAndTor(name, ip string, stats *trafficStats) { for port, service := range proxyPorts { total := stats.portTotalCounts[port] targets := len(stats.portDestCounts[port]) if total == 0 { continue } if port == 1194 || port == 51820 { if targets < 3 && total < 10 { continue } } severity := "high" if targets >= 10 || total >= 30 { severity = "critical" } ss.addAlert(name, "proxy", severity, ip, "*", port, fmt.Sprintf("疑似代理/VPN/Tor 滥用: %s(%d) 当前连接 %d 条,覆盖 %d 个目标", service, port, total, targets), "") } total8080 := stats.portTotalCounts[8080] targets8080 := len(stats.portDestCounts[8080]) if targets8080 >= 5 || total8080 >= 20 { ss.addAlert(name, "proxy", "high", ip, "*", 8080, fmt.Sprintf("疑似开放代理流量: HTTP 代理常用端口 8080 当前连接 %d 条,覆盖 %d 个目标", total8080, targets8080), "") } } func (ss *SecurityScanner) detectMalware(name, ip string, stats *trafficStats) { for port, label := range malwarePorts { total := stats.portTotalCounts[port] if total == 0 { continue } ss.addAlert(name, "malware", "critical", ip, "*", port, fmt.Sprintf("疑似恶意软件/C2 连接: %s 端口 %d 当前连接 %d 条", label, port, total), "") } } func readConntrackLines(ip string) []string { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "conntrack", "-L", "-s", ip) output, err := cmd.Output() if err == nil && len(output) > 0 { return splitNonEmptyLines(string(output)) } var lines []string for _, path := range []string{"/proc/net/nf_conntrack", "/proc/net/ip_conntrack"} { data, readErr := os.ReadFile(path) if readErr != nil { continue } for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line == "" { continue } if strings.Contains(line, "src="+ip+" ") { lines = append(lines, line) } } } return lines } func splitNonEmptyLines(raw string) []string { lines := make([]string, 0) for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) if line != "" { lines = append(lines, line) } } return lines } func parseConntrackLine(line, containerIP string) (connEntry, bool) { srcIP := extractField(line, "src=") if srcIP != containerIP { return connEntry{}, false } dstIP := extractField(line, "dst=") dstPort, _ := strconv.Atoi(extractField(line, "dport=")) return connEntry{ dstIP: dstIP, dstPort: dstPort, proto: extractProtocol(line), state: extractConnState(line), line: line, }, true } func extractProtocol(line string) string { for _, field := range strings.Fields(line) { switch field { case "tcp", "udp", "icmp", "icmpv6", "sctp": return field } } return "" } func extractConnState(line string) string { for _, field := range strings.Fields(line) { switch field { case "SYN_SENT", "SYN_RECV", "ESTABLISHED", "TIME_WAIT", "CLOSE", "CLOSE_WAIT", "FIN_WAIT", "LAST_ACK", "UNREPLIED": return field } } return "" } func countPorts(totalCounts map[int]int, destCounts map[int]map[string]int, ports map[int]string) (int, int) { total := 0 targets := make(map[string]struct{}) for port := range ports { total += totalCounts[port] for dstIP := range destCounts[port] { targets[dstIP] = struct{}{} } } return total, len(targets) } func (ss *SecurityScanner) addAlert(name, alertType, severity, srcIP, dstIP string, port int, detail, logLine string) { ss.mu.Lock() defer ss.mu.Unlock() now := time.Now() cutoff := now.Add(-5 * time.Minute) for i := range ss.alerts { a := &ss.alerts[i] if a.ContainerName != name || a.Type != alertType || a.TargetIP != dstIP || a.TargetPort != port { continue } t, err := time.Parse("2006-01-02 15:04:05", a.Timestamp) if err != nil || t.Before(cutoff) { continue } a.Count++ a.Detail = detail a.LogLine = logLine a.Timestamp = now.Format("2006-01-02 15:04:05") if severityRank(severity) > severityRank(a.Severity) { a.Severity = severity } return } ss.nextID++ alert := SecurityAlert{ ID: fmt.Sprintf("alert-%d", ss.nextID), ContainerName: name, Type: alertType, Severity: severity, SourceIP: srcIP, TargetIP: dstIP, TargetPort: port, Detail: detail, LogLine: logLine, Timestamp: now.Format("2006-01-02 15:04:05"), Count: 1, } ss.alerts = append(ss.alerts, alert) config.AddAuditLog("security_"+alertType, name, fmt.Sprintf("[%s] %s", severity, detail), "system") if len(ss.alerts) > 200 { ss.alerts = ss.alerts[len(ss.alerts)-200:] } } func severityRank(severity string) int { switch severity { case "critical": return 4 case "high": return 3 case "medium": return 2 case "low": return 1 default: return 0 } } // HandleSecurityAlerts returns all security alerts. func HandleSecurityAlerts(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"}) return } ss := ensureScanner() ss.mu.Lock() reversed := make([]SecurityAlert, len(ss.alerts)) for i, a := range ss.alerts { reversed[len(ss.alerts)-1-i] = a } ss.mu.Unlock() if reversed == nil { reversed = []SecurityAlert{} } jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: reversed}) } // HandleSecurityCheck triggers immediate security check for a container. func HandleSecurityCheck(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"}) return } var req struct { ContainerName string `json:"container_name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"}) return } c := config.FindContainerByName(req.ContainerName) if c == nil || c.IP == "" { jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found or not running"}) return } ensureScanner().checkContainer(c.Name, c.IP) jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Security check completed"}) } // HandleSecurityLogs returns connection logs for a container. func HandleSecurityLogs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"}) return } containerName := r.URL.Query().Get("container") if containerName == "" { jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Container name required"}) return } c := config.FindContainerByName(containerName) if c == nil || c.IP == "" { jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: []map[string]interface{}{}}) return } jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: getConnectionLogs(c.IP)}) } func getConnectionLogs(ip string) []map[string]interface{} { logs := make([]map[string]interface{}, 0) for _, line := range readConntrackLines(ip) { 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{}{ "src_ip": srcIP, "dst_ip": dstIP, "src_port": sPort, "dst_port": dPort, "protocol": extractProtocol(line), "state": extractConnState(line), }) if len(logs) >= 100 { break } } return logs } func extractField(line, prefix string) string { idx := strings.Index(line, prefix) if idx == -1 { return "" } start := idx + len(prefix) end := start for end < len(line) && line[end] != ' ' && line[end] != '\t' { end++ } return line[start:end] } // HandleContainerSecuritySummary returns security status for dashboard. func HandleContainerSecuritySummary(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"}) return } ss := ensureScanner() ss.mu.Lock() critical := 0 high := 0 medium := 0 low := 0 for _, a := range ss.alerts { switch a.Severity { case "critical": critical++ case "high": high++ case "medium": medium++ case "low": low++ } } total := len(ss.alerts) ss.mu.Unlock() summary := map[string]interface{}{ "total_alerts": total, "critical": critical, "high": high, "medium": medium, "low": low, } jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: summary}) }