From 3aa4bfaa820c7e630d0152f990a0003d46f0bc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=98=AD?= Date: Thu, 3 Sep 2026 18:33:07 +0800 Subject: [PATCH] ref: streamline SSH login log handling and remove unused functions (#13705) --- agent/app/service/alert_helper.go | 98 ++++++++++++++++------ agent/app/service/ssh.go | 60 ++++++++++---- agent/utils/alert/alert.go | 131 ------------------------------ 3 files changed, 117 insertions(+), 172 deletions(-) diff --git a/agent/app/service/alert_helper.go b/agent/app/service/alert_helper.go index e9687167a..4a7ed87d9 100644 --- a/agent/app/service/alert_helper.go +++ b/agent/app/service/alert_helper.go @@ -2,6 +2,7 @@ package service import ( "encoding/json" + "errors" "fmt" "math" "net" @@ -32,6 +33,7 @@ const ( ResourceAlertInterval = 30 CheckIntervalSec = 3 LoadCheckIntervalMin = 5 + sshIPLoginWindow = 30 * time.Minute ) type AlertTaskHelper struct { @@ -512,10 +514,28 @@ func loadPanelLogin(alert dto.AlertDTO) { } func loadSSHLogin(alert dto.AlertDTO) { - count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count) - if err != nil { - global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err) + now := time.Now() + failedWindow := time.Duration(alert.Cycle) * time.Minute + loadWindow := failedWindow + if loadWindow < sshIPLoginWindow { + loadWindow = sshIPLoginWindow } + location, err := time.LoadLocation(common.LoadTimeZoneByCmd()) + if err != nil { + global.LOG.Errorf("Failed to load timezone for ssh login logs: %v", err) + location = time.Local + } + histories, err := loadSSHAlertHistories(defaultSSHLogDir, now.Add(-loadWindow), now, location) + if err != nil { + global.LOG.Errorf("Failed to load ssh login logs: %v", err) + } + count, records := summarizeSSHLoginHistories( + histories, + now, + failedWindow, + strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"), + ) + isAlert := count >= int(alert.Count) if isAlert { params := []dto.Param{ { @@ -531,12 +551,6 @@ func loadSSHLogin(alert dto.AlertDTO) { } sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params) } - whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n") - records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist) - if err != nil { - global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err) - } - records = filterSSHLoginEntriesNotInWhitelist(records, whitelist) if len(records) > 0 { quota := strings.Join(records, "\n") params := []dto.Param{ @@ -565,20 +579,6 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) return filtered } -func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string { - filtered := make([]string, 0, len(records)) - for _, record := range records { - ip := record - if idx := strings.Index(record, "-"); idx >= 0 { - ip = record[:idx] - } - if !isIPInWhitelist(ip, whitelist) { - filtered = append(filtered, record) - } - } - return filtered -} - func isIPInWhitelist(ip string, whitelist []string) bool { targetIP := net.ParseIP(strings.TrimSpace(ip)) if targetIP == nil { @@ -1117,3 +1117,55 @@ func calculateMinutesDifference(newDate time.Time) int { minutesDifference := int(now.Sub(newDate).Minutes()) return minutesDifference } + +func loadSSHAlertHistories( + baseDir string, + startTime, endTime time.Time, + location *time.Location, +) ([]dto.SSHHistory, error) { + fileList, err := listSSHLogFiles(baseDir) + if err != nil { + return nil, err + } + + var ( + histories []dto.SSHHistory + loadErr error + ) + for _, file := range fileList { + items, err := loadSSHHistoriesFromFile(file.Name, "", "", startTime, endTime, file.Year, location) + if err != nil { + loadErr = errors.Join(loadErr, fmt.Errorf("load SSH log file %s: %w", file.Name, err)) + continue + } + histories = append(histories, items...) + } + return histories, loadErr +} + +func summarizeSSHLoginHistories( + histories []dto.SSHHistory, + now time.Time, + failedWindow time.Duration, + whitelist []string, +) (int, []string) { + failedStartTime := now.Add(-failedWindow) + successStartTime := now.Add(-sshIPLoginWindow) + failedCount := 0 + var abnormalLogins []string + + for _, item := range histories { + switch item.Status { + case constant.StatusFailed: + if isSSHLogWithinTimeRange(item.Date, failedStartTime, now) { + failedCount++ + } + case constant.StatusSuccess: + if !isSSHLogWithinTimeRange(item.Date, successStartTime, now) || isIPInWhitelist(item.Address, whitelist) { + continue + } + abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", item.Address, item.Date.Format(constant.DateTimeLayout))) + } + } + return failedCount, abnormalLogins +} diff --git a/agent/app/service/ssh.go b/agent/app/service/ssh.go index 1e5a80c69..87ef300bc 100644 --- a/agent/app/service/ssh.go +++ b/agent/app/service/ssh.go @@ -40,6 +40,7 @@ import ( const sshPath = "/etc/ssh/sshd_config" const defaultSSHPort = "22" const sshManagedMarker = "# config by 1panel" +const defaultSSHLogDir = "/var/log" type SSHService struct{} @@ -668,13 +669,11 @@ func isSSHLogFileName(name string) bool { return false } -func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) { +func listSSHLogFiles(baseDir string) ([]sshFileItem, error) { var fileList []sshFileItem - var data []dto.SSHHistory - baseDir := "/var/log" fileItems, err := os.ReadDir(baseDir) if err != nil { - return 0, data, err + return nil, err } for _, item := range fileItems { if item.IsDir() || !isSSHLogFileName(item.Name()) { @@ -682,7 +681,7 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d } info, err := item.Info() if err != nil { - return 0, data, err + return nil, err } if !info.Mode().IsRegular() { continue @@ -695,7 +694,15 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d } fileList = append(fileList, sshFileItem{Name: itemPath, Year: info.ModTime().Year()}) } - fileList = sortFileList(fileList) + return sortFileList(fileList), nil +} + +func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) { + var data []dto.SSHHistory + fileList, err := listSSHLogFiles(defaultSSHLogDir) + if err != nil { + return 0, data, err + } filter := "" if len(req.Info) != 0 { @@ -742,7 +749,7 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d } func (u *SSHService) CleanLog() error { - return cleanSSHLogFiles("/var/log") + return cleanSSHLogFiles(defaultSSHLogDir) } func cleanSSHLogFiles(baseDir string) error { @@ -1290,20 +1297,11 @@ func loadSSHData( if err != nil { return datas, 0, 0 } - lines, err := loadSSHLogLines(filePath) + histories, err := loadSSHHistoriesFromFile(filePath, status, filter, startTime, endTime, currentYear, nyc) if err != nil { return datas, 0, 0 } - items := collectSSHLogItems(lines, filter, status) - for i := len(items) - 1; i >= 0; i-- { - itemData := items[i].History - if !matchSSHLogStatus(status, itemData.Status) || !checkIsStandard(itemData) { - continue - } - itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) - if !isSSHLogWithinTimeRange(itemData.Date, startTime, endTime) { - continue - } + for _, itemData := range histories { if successCount+failedCount >= showCountFrom && (showCountTo == -1 || successCount+failedCount < showCountTo) { itemData.Area, _ = geo.GetIPLocation(getLoc, itemData.Address, common.GetLang(ctx)) datas = append(datas, itemData) @@ -1317,6 +1315,32 @@ func loadSSHData( return datas, successCount, failedCount } +func loadSSHHistoriesFromFile( + filePath, status, filter string, + startTime, endTime time.Time, + currentYear int, + location *time.Location, +) ([]dto.SSHHistory, error) { + lines, err := loadSSHLogLines(filePath) + if err != nil { + return nil, err + } + items := collectSSHLogItems(lines, filter, status) + histories := make([]dto.SSHHistory, 0, len(items)) + for i := len(items) - 1; i >= 0; i-- { + itemData := items[i].History + if !matchSSHLogStatus(status, itemData.Status) || !checkIsStandard(itemData) { + continue + } + itemData.Date = loadDate(currentYear, itemData.DateStr, location) + if !isSSHLogWithinTimeRange(itemData.Date, startTime, endTime) { + continue + } + histories = append(histories, itemData) + } + return histories, nil +} + func isSSHLogWithinTimeRange(itemTime, startTime, endTime time.Time) bool { if startTime.IsZero() || endTime.IsZero() { return true diff --git a/agent/utils/alert/alert.go b/agent/utils/alert/alert.go index 5ea18b7ce..5688436bf 100644 --- a/agent/utils/alert/alert.go +++ b/agent/utils/alert/alert.go @@ -2,13 +2,10 @@ package alert import ( "encoding/json" - "errors" "fmt" "mime" network "net" "net/http" - "os" - "os/exec" "strconv" "strings" "sync" @@ -24,7 +21,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/bark" "github.com/1Panel-dev/1Panel/agent/utils/email" "github.com/1Panel-dev/1Panel/agent/utils/psutil" - "github.com/1Panel-dev/1Panel/agent/utils/re" "github.com/jinzhu/copier" ) @@ -536,133 +532,6 @@ func FindRecentSuccessLoginsNotInWhitelist(minutes int, whitelist []string) ([]m return abnormalLogs, nil } -func CountRecentFailedSSHLog(minutes uint, maxAllowed uint) (int, bool, error) { - lines, err := grepSSHLog([]string{"Failed password", "Invalid user", "authentication failure"}) - if err != nil { - return 0, false, err - } - - thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute) - count := 0 - - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - t, err := parseLogTime(line) - if err != nil { - continue - } - if t.After(thresholdTime) { - count++ - } - } - return count, count >= int(maxAllowed), nil -} - -func FindRecentSuccessLoginNotInWhitelist(minutes int, whitelist []string) ([]string, error) { - lines, err := grepSSHLog([]string{"Accepted password", "Accepted publickey"}) - if err != nil { - return nil, err - } - - thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute) - var abnormalLogins []string - - whitelistMap := make(map[string]struct{}, len(whitelist)) - for _, ip := range whitelist { - whitelistMap[ip] = struct{}{} - } - - ipRegex := re.GetRegex(re.AlertIPPattern) - - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - t, err := parseLogTime(line) - if err != nil || t.Before(thresholdTime) { - continue - } - - match := ipRegex.FindStringSubmatch(line) - if len(match) >= 2 { - ip := match[1] - if _, ok := whitelistMap[ip]; !ok { - abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", ip, t.Format("2006-01-02 15:04:05"))) - } - } - } - - return abnormalLogins, nil -} - -func findGrepPath() (string, error) { - path, err := exec.LookPath("grep") - if err != nil { - return "", fmt.Errorf("grep not found in PATH: %w", err) - } - return path, nil -} - -func grepSSHLog(keywords []string) ([]string, error) { - logFiles := []string{"/var/log/secure", "/var/log/auth.log"} - var results []string - seen := make(map[string]struct{}) - - grepPath, err := findGrepPath() - if err != nil { - return nil, fmt.Errorf("find grep failed: %w", err) - } - - for _, logFile := range logFiles { - if _, err := os.Stat(logFile); err != nil { - continue - } - for _, keyword := range keywords { - cmd := exec.Command(grepPath, "-a", keyword, logFile) - output, err := cmd.Output() - if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - if exitErr.ExitCode() == 1 { - continue - } - } - return nil, fmt.Errorf("read log file fail [%s]: %w", logFile, err) - } - - lines := strings.Split(string(output), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line != "" { - if _, exists := seen[line]; !exists { - results = append(results, line) - seen[line] = struct{}{} - } - } - } - } - } - - return results, nil -} - -func parseLogTime(line string) (time.Time, error) { - if len(line) < 15 { - return time.Time{}, nil - } - timeStr := line[:15] - parsedTime, err := time.ParseInLocation("Jan 2 15:04:05", timeStr, time.Local) - if err != nil { - return time.Time{}, nil - } - return parsedTime.AddDate(time.Now().Year(), 0, 0), nil -} - func getNodeName(agentInfo *dto.AgentInfo) string { var nodeName string if agentInfo != nil && agentInfo.NodeName != "" {