1Panel/agent/utils/alert/alert.go

508 lines
15 KiB
Go

package alert
import (
"encoding/json"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/jinzhu/copier"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"time"
)
var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"}
func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport) error {
params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param))
alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method)
alertRule := ProcessAlertRule(alert)
create.AlertRule = alertRule
create.AlertDetail = alertDetail
return CreateEmailAlertLog(create, alert, params, transport)
}
func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport) error {
var alertLog model.AlertLog
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.CommonConfig))
if err != nil {
return err
}
var cfg dto.AlertCommonConfig
err = json.Unmarshal([]byte(config.Config), &cfg)
if err != nil {
return err
}
create.Method = constant.Email
// 获取远端推送信息
if !global.IsMaster && cfg.IsOffline == constant.StatusEnable {
create.Status = constant.AlertPushing
return SaveAlertLog(create, &alertLog)
} else {
emailConfig, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig))
if err != nil {
return err
}
var emailInfo dto.AlertEmailConfig
err = json.Unmarshal([]byte(emailConfig.Config), &emailInfo)
if err != nil {
return err
}
smtpConfig := email.SMTPConfig{
Host: emailInfo.Host,
Port: emailInfo.Port,
Username: emailInfo.Sender,
Password: emailInfo.Password,
From: fmt.Sprintf("%s <%s>", emailInfo.DisplayName, emailInfo.Sender),
Encryption: emailInfo.Encryption,
Recipient: emailInfo.Recipient,
}
content := i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title})
if GetEmailContent(alert.Type, params) != "" {
content = GetEmailContent(alert.Type, params)
}
msg := email.EmailMessage{
Subject: i18n.GetMsgByKey("PanelAlertTitle"),
Body: content,
IsHTML: true,
}
if err = email.SendMail(smtpConfig, msg, transport); err != nil {
create.Message = err.Error()
create.Status = constant.AlertError
return SaveAlertLog(create, &alertLog)
}
create.Status = constant.AlertSuccess
return SaveAlertLog(create, &alertLog)
}
}
func SaveAlertLog(create dto.AlertLogCreate, alertLog *model.AlertLog) error {
alertRepo := repo.NewIAlertRepo()
if err := copier.Copy(&alertLog, &create); err != nil {
return buserr.WithErr("ErrStructTransform", err)
}
if err := alertRepo.CreateLog(alertLog); err != nil {
global.LOG.Errorf("Error creating alert logs, err: %v", err)
return err
}
return nil
}
func CreateNewAlertTask(quota, alertType, quotaType, method string) {
alertRepo := repo.NewIAlertRepo()
taskBase := model.AlertTask{
Type: alertType,
Quota: quota,
QuotaType: quotaType,
Method: method,
}
err := alertRepo.CreateAlertTask(&taskBase)
if err != nil {
global.LOG.Errorf("error creating alert tasks, err: %v", err)
}
}
func ProcessAlertDetail(alert dto.AlertDTO, project string, params []dto.Param, method string) string {
alertDetail := dto.AlertDetail{
Type: GetCronJobType(alert.Type),
SubType: alert.Type,
Title: alert.Title,
Method: method,
Project: project,
Params: params,
}
marshal, err := json.Marshal(alertDetail)
if err != nil {
global.LOG.Errorf("error processing alert detail, err: %v", err)
return ""
}
return string(marshal)
}
func ProcessAlertRule(alert dto.AlertDTO) string {
marshal, err := json.Marshal(alert)
if err != nil {
global.LOG.Errorf("error processing alert rule, err: %v", err)
return ""
}
return string(marshal)
}
func GetCronJobType(alertType string) string {
for _, at := range cronJobAlertTypes {
if at == alertType {
return "cronJob"
}
}
return alertType
}
func GetCronJobTypeName(cronJobType string) string {
module := cronJobType
switch cronJobType {
case "shell":
module = "Shell 脚本"
case "app":
module = "备份应用"
case "website":
module = "备份网站"
case "database":
module = "备份数据库"
case "log":
module = "备份日志"
case "directory":
module = "备份目录"
case "curl":
module = "访问 URL"
case "cutWebsiteLog":
module = "切割网站日志"
case "clean":
module = "缓存清理"
case "snapshot":
module = "系统快照"
case "ntp":
module = "同步服务器时间"
default:
}
return module
}
func CreateAlertParams(param string) []dto.Param {
return []dto.Param{
{
Index: "1",
Key: "param",
Value: param,
},
}
}
var checkTaskMutex sync.Mutex
func CheckSMSSendLimit(method string) bool {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig))
if err != nil {
return false
}
var cfg dto.AlertSmsConfig
err = json.Unmarshal([]byte(config.Config), &cfg)
if err != nil {
return false
}
limitCount := cfg.AlertDailyNum
checkTaskMutex.Lock()
defer checkTaskMutex.Unlock()
todayCount, err := alertRepo.GetLicensePushCount(method)
if err != nil {
global.LOG.Errorf("error getting license push count info, err: %v", err)
return false
}
if todayCount >= limitCount {
return false
}
return true
}
type Settings struct {
NoticeAlert Category `json:"noticeAlert"`
ResourceAlert Category `json:"resourceAlert"`
}
type Category struct {
SendTimeRange string `json:"sendTimeRange"`
Type []string `json:"type"`
}
// CheckSendTimeRange 是否在时间范围内
func CheckSendTimeRange(alertType string) bool {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.CommonConfig))
if err != nil {
return false
}
var cfg dto.AlertCommonConfig
err = json.Unmarshal([]byte(config.Config), &cfg)
if err != nil {
return false
}
var timeRange string
if contains(cfg.AlertSendTimeRange.NoticeAlert.Type, alertType) {
timeRange = cfg.AlertSendTimeRange.NoticeAlert.SendTimeRange
} else if contains(cfg.AlertSendTimeRange.ResourceAlert.Type, alertType) {
timeRange = cfg.AlertSendTimeRange.ResourceAlert.SendTimeRange
} else {
global.LOG.Warnf("Alert type not found in sendTimeRange: %s", alertType)
return false
}
if !isWithinTimeRange(timeRange) {
return false
}
return true
}
func contains(arr []string, target string) bool {
for _, item := range arr {
if item == target {
return true
}
}
return false
}
func isWithinTimeRange(savedTimeString string) bool {
now := time.Now()
timeParts := strings.Split(savedTimeString, " - ")
if len(timeParts) != 2 {
global.LOG.Info("Time range string format error, should be: 'HH:MM:SS - HH:MM:SS'")
return false
}
startTime, err1 := time.Parse("15:04:05", strings.TrimSpace(timeParts[0]))
endTime, err2 := time.Parse("15:04:05", strings.TrimSpace(timeParts[1]))
if err1 != nil || err2 != nil {
global.LOG.Infof("Invalid time format in range: %s, errors: %v, %v", savedTimeString, err1, err2)
return false
}
skipTime := time.Date(now.Year(), now.Month(), now.Day(), startTime.Hour(), startTime.Minute(), startTime.Second(), 0, now.Location())
endSkipTime := time.Date(now.Year(), now.Month(), now.Day(), endTime.Hour(), endTime.Minute(), endTime.Second(), 0, now.Location())
if endSkipTime.Before(skipTime) {
return now.After(skipTime) || now.Before(endSkipTime)
}
return now.After(skipTime) && now.Before(endSkipTime)
}
func GetEmailContent(alertType string, params []dto.Param) string {
switch GetCronJobType(alertType) {
case "ssl":
return i18n.GetMsgWithMap("SSLAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "day": getValueByIndex(params, "2")})
case "siteEndTime":
return i18n.GetMsgWithMap("WebSiteAlert", map[string]interface{}{"num": getValueByIndex(params, "1"), "day": getValueByIndex(params, "2")})
case "panelPwdEndTime":
return i18n.GetMsgWithMap("PanelPwdExpirationAlert", map[string]interface{}{"day": getValueByIndex(params, "1")})
case "licenseTime":
return i18n.GetMsgWithMap("LicenseExpirationAlert", map[string]interface{}{"day": getValueByIndex(params, "1")})
case "panelUpdate":
return i18n.GetMsgByKey("PanelVersionAlert")
case "cpu":
return i18n.GetMsgWithMap("ResourceAlert", map[string]interface{}{"time": getValueByIndex(params, "1"), "name": getValueByIndex(params, "2"), "used": getValueByIndex(params, "3")})
case "memory":
return i18n.GetMsgWithMap("ResourceAlert", map[string]interface{}{"time": getValueByIndex(params, "1"), "name": getValueByIndex(params, "2"), "used": getValueByIndex(params, "3")})
case "load":
return i18n.GetMsgWithMap("ResourceAlert", map[string]interface{}{"time": getValueByIndex(params, "1"), "name": getValueByIndex(params, "2"), "used": getValueByIndex(params, "3")})
case "disk":
return i18n.GetMsgWithMap("DiskUsedAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "used": getValueByIndex(params, "2")})
case "cronJob":
return i18n.GetMsgWithMap("CronJobFailedAlert", map[string]interface{}{"name": getValueByIndex(params, "1")})
case "clams":
return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1")})
case "panelLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "sshLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "panelIpLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "sshIpLogin":
return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")})
case "nodeException":
return i18n.GetMsgWithMap("NodeExceptionAlert", map[string]interface{}{"num": getValueByIndex(params, "1")})
case "licenseException":
return i18n.GetMsgWithMap("LicenseExceptionAlert", map[string]interface{}{"num": getValueByIndex(params, "1")})
default:
return ""
}
}
func getValueByIndex(params []dto.Param, index string) string {
for _, p := range params {
if p.Index == index {
return p.Value
}
}
return ""
}
func CountRecentFailedLoginLogs(minutes uint, failCount uint) (int, bool, error) {
now := time.Now()
startTime := now.Add(-time.Duration(minutes) * time.Minute)
db := global.CoreDB.Model(&model.LoginLog{})
var count int64
err := db.Where("created_at >= ? AND status = ?", startTime, constant.StatusFailed).
Count(&count).Error
if err != nil {
return 0, false, err
}
return int(count), int(count) >= int(failCount), nil
}
func FindRecentSuccessLoginsNotInWhitelist(minutes int, whitelist []string) ([]model.LoginLog, error) {
now := time.Now()
startTime := now.Add(-time.Duration(minutes) * time.Minute)
whitelistMap := make(map[string]struct{})
for _, ip := range whitelist {
whitelistMap[ip] = struct{}{}
}
var logs []model.LoginLog
err := global.CoreDB.Model(&model.LoginLog{}).
Where("created_at >= ? AND status = ?", startTime, constant.StatusSuccess).
Find(&logs).Error
if err != nil {
return nil, err
}
var abnormalLogs []model.LoginLog
for _, log := range logs {
if _, ok := whitelistMap[log.IP]; !ok {
abnormalLogs = append(abnormalLogs, log)
}
}
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 := regexp.MustCompile(`from\s+([0-9.]+)\s+port\s+(\d+)`)
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
}