package service import ( "bufio" "fmt" "os" "os/exec" "path" "strconv" "strings" "time" "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/app/task" "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/utils/alert_push" "github.com/1Panel-dev/1Panel/agent/utils/clam" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/controller" "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/jinzhu/copier" "github.com/robfig/cron/v3" ) type ClamService struct { serviceName string freshClamService string } type IClamService interface { LoadBaseInfo() (dto.ClamBaseInfo, error) Operate(operate string) error SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error) Create(req dto.ClamCreate, operator string) error Update(req dto.ClamUpdate, operator string) error UpdateStatus(id uint, status string) error Delete(req dto.ClamDelete) error HandleOnce(id uint) error LoadFile(req dto.ClamFileReq) (string, error) UpdateFile(req dto.UpdateByNameAndFile) error SearchRecords(req dto.ClamLogSearch) (int64, interface{}, error) CleanRecord(id uint) error } func NewIClamService() IClamService { return &ClamService{} } func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) { var baseInfo dto.ClamBaseInfo baseInfo.Version = "-" baseInfo.FreshVersion = "-" clamSvc, err := controller.LoadServiceName("clam") if err != nil { baseInfo.IsExist = false return baseInfo, nil } c.serviceName = clamSvc exist, _ := controller.CheckExist(clamSvc) if exist { baseInfo.IsExist = true baseInfo.IsActive, _ = controller.CheckActive(clamSvc) } freshSvc, err := controller.LoadServiceName("freshclam") if err != nil { baseInfo.FreshIsExist = false return baseInfo, nil } c.freshClamService = freshSvc freshExist, _ := controller.CheckExist(freshSvc) if freshExist { baseInfo.FreshIsExist = true baseInfo.FreshIsActive, _ = controller.CheckActive(freshSvc) } if !cmd.Which("clamdscan") { baseInfo.IsActive = false } cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)) if baseInfo.IsActive { version, err := cmdMgr.RunWithStdout("clamdscan", "--version") if err == nil { if strings.Contains(version, "/") { baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") } else { baseInfo.Version = strings.TrimPrefix(version, "ClamAV ") } } } else { _ = clam.CheckWithStopAll(false, clamRepo) } if baseInfo.FreshIsActive { version, err := cmdMgr.RunWithStdout("freshclam", "--version") if err == nil { if strings.Contains(version, "/") { baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") } else { baseInfo.FreshVersion = strings.TrimPrefix(version, "ClamAV ") } } } return baseInfo, nil } func (c *ClamService) Operate(operate string) error { switch operate { case "start", "restart", "stop": if err := controller.Handle(operate, c.serviceName); err != nil { return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, err) } return nil case "fresh-start", "fresh-restart", "fresh-stop": if err := controller.Handle(strings.TrimPrefix(operate, "fresh-"), c.freshClamService); err != nil { return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, err) } return nil default: return fmt.Errorf("not support such operation: %v", operate) } } func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) { total, clams, err := clamRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info), repo.WithOrderRuleBy(req.OrderBy, req.Order)) if err != nil { return 0, nil, err } var datas []dto.ClamInfo for _, clam := range clams { var item dto.ClamInfo if err := copier.Copy(&item, &clam); err != nil { return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) } datas = append(datas, item) } for i := 0; i < len(datas); i++ { record, _ := clamRepo.RecordFirst(datas[i].ID) if record.ID != 0 { datas[i].LastRecordStatus = record.Status datas[i].LastRecordTime = record.StartTime.Format(constant.DateTimeLayout) } else { datas[i].LastRecordTime = "-" } alertBase := dto.AlertBase{ AlertType: "clams", EntryID: datas[i].ID, } alertInfo, _ := alertRepo.Get(alertRepo.WithByType(alertBase.AlertType), alertRepo.WithByProject(strconv.Itoa(int(alertBase.EntryID))), repo.WithByStatus(constant.AlertEnable)) datas[i].AlertMethod = alertInfo.Method if alertInfo.SendCount != 0 { datas[i].AlertCount = alertInfo.SendCount } else { datas[i].AlertCount = 0 } } return total, datas, err } func (c *ClamService) Create(req dto.ClamCreate, operator string) error { clam, _ := clamRepo.Get(repo.WithByName(req.Name)) if clam.ID != 0 { return buserr.New("ErrRecordExist") } if cmd.CheckIllegal(req.Path) { return buserr.New("ErrCmdIllegal") } if err := copier.Copy(&clam, &req); err != nil { return buserr.WithDetail("ErrStructTransform", err.Error(), nil) } if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" { clam.InfectedDir = "" } if len(req.Spec) != 0 { entryID, err := xpack.MultiNodeProvider.StartClam(&clam, false) if err != nil { return err } clam.EntryID = entryID clam.Status = constant.StatusEnable } if err := clamRepo.Create(&clam); err != nil { return err } if req.AlertCount != 0 && req.AlertTitle != "" && req.AlertMethod != "" { createAlert := dto.AlertCreate{ Title: req.AlertTitle, SendCount: req.AlertCount, Method: req.AlertMethod, Type: "clams", Project: strconv.Itoa(int(clam.ID)), Status: constant.AlertEnable, } err := NewIAlertService().CreateAlert(createAlert, operator) if err != nil { return err } } return nil } func (c *ClamService) Update(req dto.ClamUpdate, operator string) error { if cmd.CheckIllegal(req.Path) { return buserr.New("ErrCmdIllegal") } clam, _ := clamRepo.Get(repo.WithByName(req.Name)) if clam.ID == 0 { return buserr.New("ErrRecordNotFound") } if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" { req.InfectedDir = "" } var clamItem model.Clam if err := copier.Copy(&clamItem, &req); err != nil { return buserr.WithDetail("ErrStructTransform", err.Error(), nil) } clamItem.EntryID = clam.EntryID upMap := map[string]interface{}{} if len(clam.Spec) != 0 && clam.EntryID != 0 { global.Cron.Remove(cron.EntryID(clamItem.EntryID)) upMap["entry_id"] = 0 } if len(req.Spec) == 0 { upMap["status"] = "" upMap["entry_id"] = 0 } if len(req.Spec) != 0 && clam.Status != constant.StatusDisable { newEntryID, err := xpack.MultiNodeProvider.StartClam(&clamItem, true) if err != nil { return err } upMap["entry_id"] = newEntryID } if len(clam.Spec) == 0 && len(req.Spec) != 0 { upMap["status"] = constant.StatusEnable } upMap["name"] = req.Name upMap["path"] = req.Path upMap["infected_dir"] = req.InfectedDir upMap["infected_strategy"] = req.InfectedStrategy upMap["spec"] = req.Spec upMap["timeout"] = req.Timeout upMap["description"] = req.Description if err := clamRepo.Update(req.ID, upMap); err != nil { return err } updateAlert := dto.AlertCreate{ Title: req.AlertTitle, SendCount: req.AlertCount, Method: req.AlertMethod, Type: "clams", Project: strconv.Itoa(int(clam.ID)), } err := NewIAlertService().ExternalUpdateAlert(updateAlert, operator) if err != nil { return err } return nil } func (c *ClamService) UpdateStatus(id uint, status string) error { clam, _ := clamRepo.Get(repo.WithByID(id)) if clam.ID == 0 { return buserr.New("ErrRecordNotFound") } var ( entryID int err error ) if status == constant.StatusEnable { entryID, err = xpack.MultiNodeProvider.StartClam(&clam, true) if err != nil { return err } } else { global.Cron.Remove(cron.EntryID(clam.EntryID)) global.LOG.Infof("stop cronjob entryID: %v", clam.EntryID) } return clamRepo.Update(clam.ID, map[string]interface{}{"status": status, "entry_id": entryID}) } func (c *ClamService) Delete(req dto.ClamDelete) error { for _, id := range req.Ids { clam, _ := clamRepo.Get(repo.WithByID(id)) if clam.ID == 0 { continue } if len(clam.Spec) != 0 { global.Cron.Remove(cron.EntryID(clam.EntryID)) } _ = c.CleanRecord(clam.ID) if req.RemoveInfected { _ = os.RemoveAll(path.Join(clam.InfectedDir, "1panel-infected", clam.Name)) } if err := clamRepo.Delete(repo.WithByID(id)); err != nil { return err } err := alertRepo.Delete(alertRepo.WithByProject(strconv.Itoa(int(clam.ID))), alertRepo.WithByType("clams")) if err != nil { return err } } return nil } func (c *ClamService) HandleOnce(id uint) error { if active := clam.CheckWithStopAll(true, clamRepo); !active { return buserr.New("ErrClamdscanNotFound") } clamItem, _ := clamRepo.Get(repo.WithByID(id)) if clamItem.ID == 0 { return buserr.New("ErrRecordNotFound") } record := clamRepo.StartRecords(clamItem.ID) taskItem, err := task.NewTaskWithOps("clam-"+clamItem.Name, task.TaskScan, task.TaskScopeClam, record.TaskID, clamItem.ID) if err != nil { return fmt.Errorf("new task for exec shell failed, err: %v", err) } clam.AddScanTask(taskItem, clamItem, record.StartTime.Format(constant.DateTimeSlimLayout)) go func() { err := taskItem.Execute() taskRepo := repo.NewITaskRepo() taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(record.TaskID)) if len(taskItem.ID) == 0 { record.TaskID = "" } if err != nil { clamRepo.EndRecords(record, constant.StatusFailed, err.Error()) return } clam.AnalysisFromLog(taskItem.LogFile, &record) clamRepo.EndRecords(record, constant.StatusDone, "") handleAlert(record.InfectedFiles, clamItem.Name, clamItem.ID) }() return nil } func (c *ClamService) SearchRecords(req dto.ClamLogSearch) (int64, interface{}, error) { clam, _ := clamRepo.Get(repo.WithByID(req.ClamID)) if clam.ID == 0 { return 0, nil, buserr.New("ErrRecordNotFound") } loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd()) req.StartTime = req.StartTime.In(loc) req.EndTime = req.EndTime.In(loc) total, records, err := clamRepo.PageRecords(req.Page, req.PageSize, clamRepo.WithByClamID(req.ClamID), repo.WithByStatus(req.Status), repo.WithByCreatedAt(req.StartTime, req.EndTime)) if err != nil { return 0, nil, err } var datas []dto.ClamRecord for _, record := range records { var item dto.ClamRecord if err := copier.Copy(&item, &record); err != nil { return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) } datas = append(datas, item) } return int64(total), datas, nil } func (c *ClamService) CleanRecord(id uint) error { record, _ := clamRepo.ListRecord() for _, item := range record { if len(item.TaskID) != 0 { continue } taskItem, _ := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)) if len(taskItem.LogFile) != 0 { _ = os.Remove(taskItem.LogFile) } } return clamRepo.DeleteRecord(clamRepo.WithByClamID(id)) } func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) { filePath := "" switch req.Name { case "clamd": filePath = c.loadConfigPath("clamd") case "clamd-log": filePath = c.loadLogPath("clamd-log") case "freshclam": filePath = c.loadConfigPath("freshclam") case "freshclam-log": filePath = c.loadLogPath("freshclam-log") default: return "", fmt.Errorf("not support such type") } if _, err := os.Stat(filePath); err != nil { return "", buserr.New("ErrHttpReqNotFound") } var tail string if req.Tail != "0" { tail = req.Tail } else { tail = "+1" } cmd := exec.Command("tail", "-n", tail, filePath) stdout, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err) } return string(stdout), nil } func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error { filePath := "" switch req.Name { case "clamd": filePath = c.loadConfigPath("clamd") case "freshclam": filePath = c.loadConfigPath("freshclam") default: return fmt.Errorf("not support such type") } file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640) if err != nil { return err } defer file.Close() write := bufio.NewWriter(file) _, _ = write.WriteString(req.File) write.Flush() _ = controller.HandleRestart(c.serviceName) return nil } func (c *ClamService) loadLogPath(name string) string { configKey := "clamd" searchPrefix := "LogFile " if name != "clamd-log" { configKey = "freshclam" searchPrefix = "UpdateLogFile " } confPath := c.loadConfigPath(configKey) content, err := os.ReadFile(confPath) if err != nil { global.LOG.Debugf("read config of %s failed, err: %v", configKey, err) return "" } lines := strings.Split(string(content), "\n") for _, line := range lines { if strings.HasPrefix(line, searchPrefix) { return strings.Trim(strings.ReplaceAll(line, searchPrefix, ""), " ") } } if configKey == "clamd" { if _, err := os.Stat("/var/log/clamav/clamav.log"); err == nil { return "/var/log/clamav/clamav.log" } if _, err := os.Stat("/var/log/clamd.scan"); err == nil { return "/var/log/clamd.scan" } } if configKey == "freshclam" { if _, err := os.Stat("/var/log/clamav/freshclam.log"); err == nil { return "/var/log/clamav/freshclam.log" } if _, err := os.Stat("/var/log/freshclam.log"); err == nil { return "/var/log/freshclam.log" } } return "" } func (c *ClamService) loadConfigPath(confType string) string { switch confType { case "clamd": if _, err := os.Stat("/etc/clamav/clamd.conf"); err == nil { return "/etc/clamav/clamd.conf" } return "/etc/clamd.d/scan.conf" case "freshclam": if _, err := os.Stat("/etc/clamav/freshclam.conf"); err == nil { return "/etc/clamav/freshclam.conf" } return "/etc/freshclam.conf" default: return "" } } func handleAlert(infectedFiles, clamName string, clamId uint) { itemInfected, _ := strconv.Atoi(strings.TrimSpace(infectedFiles)) if itemInfected <= 0 { return } pushAlert := dto.PushAlert{ TaskName: clamName, AlertType: "clams", EntryID: clamId, Param: strconv.Itoa(itemInfected), } _ = alert_push.PushAlert(pushAlert) }