Files
1Panel/agent/app/service/clam.go

508 lines
14 KiB
Go

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)
}