ref: streamline SSH login log handling and remove unused functions (#13705)

This commit is contained in:
2026-09-03 18:33:07 +08:00
committed by GitHub
parent b3bdf9ef7e
commit 3aa4bfaa82
3 changed files with 117 additions and 172 deletions
+75 -23
View File
@@ -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
}
+42 -18
View File
@@ -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
-131
View File
@@ -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 != "" {