Files
CLICD/backend/internal/api/security.go
T

1002 lines
26 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"sort"
"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) 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 == "" {
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
}
alertBefore := ss.alertCount()
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)
// If new alerts were generated, snapshot the conntrack data for later retrieval.
if ss.alertCount() > alertBefore {
config.SaveConntrackSnapshot(ip, lines)
}
}
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()
now := time.Now()
cutoff := now.Add(-5 * time.Minute)
shouldShutdown := false
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
}
shouldShutdown = config.AppConfig.SecurityAutoShutdown
ss.mu.Unlock()
if shouldShutdown {
autoShutdownAlertContainer(name, alertType, 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")
shouldShutdown = config.AppConfig.SecurityAutoShutdown
if len(ss.alerts) > 200 {
ss.alerts = ss.alerts[len(ss.alerts)-200:]
}
ss.mu.Unlock()
if shouldShutdown {
autoShutdownAlertContainer(name, alertType, severity)
}
}
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
}
}
func autoShutdownAlertContainer(containerName, alertType, severity string) {
c := config.FindContainerByName(containerName)
if c == nil || c.Status != "running" {
return
}
reason := fmt.Sprintf("%s 告警触发策略临时封禁", alertType)
if severity != "" {
reason = fmt.Sprintf("[%s] %s", severity, reason)
}
config.SetContainerPolicyBlock(c.ID, true, reason)
taskID, queued := globalQueue.EnqueueSecurityStop(c.ID, c.Name)
if queued {
config.AddAuditLog("security_auto_shutdown", c.Name, fmt.Sprintf("[%s] %s 告警触发自动关机任务 %s", severity, alertType, taskID), "system")
}
}
// 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
}
if !requireScope(w, r, "security:read") {
return
}
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: filterSecurityAlertsForRequest(r, mergedSecurityAlerts())})
}
// HandleSecuritySettings returns or updates security automation settings.
func HandleSecuritySettings(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if !requireScope(w, r, "security:read") {
return
}
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: map[string]bool{
"auto_shutdown": config.AppConfig.SecurityAutoShutdown,
}})
case http.MethodPut:
if !requireScope(w, r, "security:settings") {
return
}
var req struct {
AutoShutdown bool `json:"auto_shutdown"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
return
}
config.AppConfig.SecurityAutoShutdown = req.AutoShutdown
if err := config.SaveConfig(); err != nil {
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()})
return
}
auditRequest(r, "security.settings", "auto_shutdown", fmt.Sprintf("auto_shutdown=%v", req.AutoShutdown), true, "")
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: map[string]bool{
"auto_shutdown": config.AppConfig.SecurityAutoShutdown,
}})
default:
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
}
}
// 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
}
if !requireScope(w, r, "security:check") {
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
}
if !isContainerAllowedForRequest(r, c.UUID) {
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Access denied to this container"})
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
}
if !requireScope(w, r, "security:read") {
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
}
if !isContainerAllowedForRequest(r, c.UUID) {
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Access denied to this container"})
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)
seen := map[string]bool{}
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)
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
}
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
}
if !requireScope(w, r, "security:read") {
return
}
critical := 0
high := 0
medium := 0
low := 0
alerts := filterSecurityAlertsForRequest(r, mergedSecurityAlerts())
for _, a := range alerts {
switch a.Severity {
case "critical":
critical++
case "high":
high++
case "medium":
medium++
case "low":
low++
}
}
total := len(alerts)
summary := map[string]interface{}{
"total_alerts": total,
"critical": critical,
"high": high,
"medium": medium,
"low": low,
}
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: summary})
}
func filterSecurityAlertsForRequest(r *http.Request, alerts []SecurityAlert) []SecurityAlert {
allowed, restricted := requestAllowedContainers(r)
if !restricted {
return alerts
}
filtered := make([]SecurityAlert, 0, len(alerts))
for _, alert := range alerts {
if c := config.FindContainerByName(alert.ContainerName); c != nil && isContainerAllowed(allowed, c) {
filtered = append(filtered, alert)
}
}
return filtered
}
func mergedSecurityAlerts() []SecurityAlert {
ss := ensureScanner()
ss.mu.Lock()
alerts := make([]SecurityAlert, len(ss.alerts))
copy(alerts, ss.alerts)
ss.mu.Unlock()
seen := make(map[string]bool)
for _, alert := range alerts {
seen[securityAlertKey(alert)] = true
}
for i, log := range config.AppConfig.AuditLogs {
alert, ok := alertFromSecurityAuditLog(log, i)
if !ok {
continue
}
key := securityAlertKey(alert)
if seen[key] {
continue
}
seen[key] = true
alerts = append(alerts, alert)
}
sort.SliceStable(alerts, func(i, j int) bool {
ti, errI := time.Parse("2006-01-02 15:04:05", alerts[i].Timestamp)
tj, errJ := time.Parse("2006-01-02 15:04:05", alerts[j].Timestamp)
if errI == nil && errJ == nil && !ti.Equal(tj) {
return ti.After(tj)
}
return alerts[i].Timestamp > alerts[j].Timestamp
})
if len(alerts) > 200 {
alerts = alerts[:200]
}
if alerts == nil {
return []SecurityAlert{}
}
return alerts
}
func securityAlertKey(alert SecurityAlert) string {
return strings.Join([]string{
alert.Timestamp,
alert.ContainerName,
alert.Type,
alert.Detail,
strconv.Itoa(alert.TargetPort),
}, "\x1f")
}
func alertFromSecurityAuditLog(log config.AuditLog, index int) (SecurityAlert, bool) {
if !strings.HasPrefix(log.Action, "security_") || log.Action == "security_auto_shutdown" || log.Action == "security_policy_unblock" {
return SecurityAlert{}, false
}
alertType := strings.TrimPrefix(log.Action, "security_")
severity, detail := parseSecurityAuditDetail(log.Detail)
targetPort := parseDetailPort(detail)
targetIP := ""
if targetPort > 0 || alertType == "horizontal_scan" || alertType == "brute_force" {
targetIP = "*"
}
return SecurityAlert{
ID: fmt.Sprintf("audit-security-%d", index),
ContainerName: log.Target,
Type: alertType,
Severity: severity,
SourceIP: "",
TargetIP: targetIP,
TargetPort: targetPort,
Detail: detail,
LogLine: "",
Timestamp: log.Time,
Count: 1,
}, true
}
func parseSecurityAuditDetail(detail string) (string, string) {
severity := "medium"
if strings.HasPrefix(detail, "[") {
if end := strings.Index(detail, "]"); end > 1 {
severity = detail[1:end]
detail = strings.TrimSpace(detail[end+1:])
}
}
return severity, detail
}
func parseDetailPort(detail string) int {
for _, marker := range []string{"端口 ", "端口"} {
idx := strings.Index(detail, marker)
if idx == -1 {
continue
}
start := idx + len(marker)
for start < len(detail) && (detail[start] == ' ' || detail[start] == ':' || detail[start] == '(') {
start++
}
end := start
for end < len(detail) && detail[end] >= '0' && detail[end] <= '9' {
end++
}
if end > start {
port, _ := strconv.Atoi(detail[start:end])
return port
}
}
return 0
}