mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
772 lines
19 KiB
Go
772 lines
19 KiB
Go
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})
|
|
}
|