Browse Source

feat: 增加病毒扫描工具 (#5546)

pull/5551/head
ssongliu 5 months ago committed by GitHub
parent
commit
97b790a092
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 251
      backend/app/api/v1/clam.go
  2. 1
      backend/app/api/v1/entry.go
  3. 52
      backend/app/dto/clam.go
  4. 9
      backend/app/model/clam.go
  5. 58
      backend/app/repo/clam.go
  6. 366
      backend/app/service/clam.go
  7. 0
      backend/app/service/cronjob.go
  8. 1
      backend/app/service/entry.go
  9. 1
      backend/init/migration/migrate.go
  10. 10
      backend/init/migration/migrations/v_1_10.go
  11. 12
      backend/router/ro_toolbox.go
  12. 593
      cmd/server/docs/docs.go
  13. 593
      cmd/server/docs/swagger.json
  14. 378
      cmd/server/docs/swagger.yaml
  15. 38
      frontend/src/api/interface/toolbox.ts
  16. 35
      frontend/src/api/modules/toolbox.ts
  17. 1
      frontend/src/components/error-message/error_code.vue
  18. 14
      frontend/src/lang/modules/en.ts
  19. 14
      frontend/src/lang/modules/tw.ts
  20. 14
      frontend/src/lang/modules/zh.ts
  21. 20
      frontend/src/routers/modules/toolbox.ts
  22. 1
      frontend/src/views/database/mysql/setting/index.vue
  23. 246
      frontend/src/views/toolbox/clam/index.vue
  24. 133
      frontend/src/views/toolbox/clam/operate/index.vue
  25. 345
      frontend/src/views/toolbox/clam/record/index.vue
  26. 138
      frontend/src/views/toolbox/clam/setting/index.vue
  27. 129
      frontend/src/views/toolbox/clam/status/index.vue
  28. 4
      frontend/src/views/toolbox/index.vue

251
backend/app/api/v1/clam.go

@ -0,0 +1,251 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
// @Tags Clam
// @Summary Create clam
// @Description 创建扫描规则
// @Accept json
// @Param request body dto.ClamCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam [post]
// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建扫描规则 [name][path]","formatEN":"create clam [name][path]"}
func (b *BaseApi) CreateClam(c *gin.Context) {
var req dto.ClamCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Update clam
// @Description 修改扫描规则
// @Accept json
// @Param request body dto.ClamUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/update [post]
// @x-panel-log {"bodyKeys":["name","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改扫描规则 [name][path]","formatEN":"update clam [name][path]"}
func (b *BaseApi) UpdateClam(c *gin.Context) {
var req dto.ClamUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.Update(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Page clam
// @Description 获取扫描规则列表分页
// @Accept json
// @Param request body dto.SearchWithPage true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /toolbox/clam/search [post]
func (b *BaseApi) SearchClam(c *gin.Context) {
var req dto.SearchWithPage
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := clamService.SearchWithPage(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Clam
// @Summary Load clam base info
// @Description 获取 Clam 基础信息
// @Accept json
// @Success 200 {object} dto.ClamBaseInfo
// @Security ApiKeyAuth
// @Router /toolbox/clam/base [get]
func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) {
info, err := clamService.LoadBaseInfo()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, info)
}
// @Tags Clam
// @Summary Operate Clam
// @Description 修改 Clam 状态
// @Accept json
// @Param request body dto.Operate true "request"
// @Security ApiKeyAuth
// @Router /toolbox/clam/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Clam","formatEN":"[operation] FTP"}
func (b *BaseApi) OperateClam(c *gin.Context) {
var req dto.Operate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.Operate(req.Operation); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Clean clam record
// @Description 清空扫描报告
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Security ApiKeyAuth
// @Router /toolbox/clam/record/clean [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"清空扫描报告 [name]","formatEN":"clean clam record [name]"}
func (b *BaseApi) CleanClamRecord(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.CleanRecord(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Page clam record
// @Description 获取扫描结果列表分页
// @Accept json
// @Param request body dto.ClamLogSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /toolbox/clam/record/search [post]
func (b *BaseApi) SearchClamRecord(c *gin.Context) {
var req dto.ClamLogSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := clamService.LoadRecords(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Clam
// @Summary Load clam file
// @Description 获取扫描文件
// @Accept json
// @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /toolbox/clam/file/search [post]
func (b *BaseApi) SearchClamFile(c *gin.Context) {
var req dto.OperationWithName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
content, err := clamService.LoadFile(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, content)
}
// @Tags Clam
// @Summary Update clam file
// @Description 更新病毒扫描配置文件
// @Accept json
// @Param request body dto.UpdateByNameAndFile true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/file/update [post]
func (b *BaseApi) UpdateFile(c *gin.Context) {
var req dto.UpdateByNameAndFile
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.UpdateFile(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags Clam
// @Summary Delete clam
// @Description 删除扫描规则
// @Accept json
// @Param request body dto.BatchDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"clams","output_column":"name","output_value":"names"}],"formatZH":"删除扫描规则 [names]","formatEN":"delete clam [names]"}
func (b *BaseApi) DeleteClam(c *gin.Context) {
var req dto.BatchDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.Delete(req.Ids); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam
// @Summary Handle clam scan
// @Description 执行病毒扫描
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/handle [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"执行病毒扫描 [name]","formatEN":"handle clam scan [name]"}
func (b *BaseApi) HandleClamScan(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.HandleOnce(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

1
backend/app/api/v1/entry.go

@ -38,6 +38,7 @@ var (
deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService()
ftpService = service.NewIFtpService()
clamService = service.NewIClamService()
settingService = service.NewISettingService()
backupService = service.NewIBackupService()

52
backend/app/dto/clam.go

@ -0,0 +1,52 @@
package dto
import (
"time"
)
type ClamBaseInfo struct {
Version string `json:"version"`
IsActive bool `json:"isActive"`
IsExist bool `json:"isExist"`
}
type ClamInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Path string `json:"path"`
LastHandleDate string `json:"lastHandleDate"`
Description string `json:"description"`
}
type ClamLogSearch struct {
PageInfo
ClamID uint `json:"clamID"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
}
type ClamLog struct {
Name string `json:"name"`
ScanDate string `json:"scanDate"`
ScanTime string `json:"scanTime"`
InfectedFiles string `json:"infectedFiles"`
Log string `json:"log"`
Status string `json:"status"`
}
type ClamCreate struct {
Name string `json:"name"`
Path string `json:"path"`
Description string `json:"description"`
}
type ClamUpdate struct {
ID uint `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Description string `json:"description"`
}

9
backend/app/model/clam.go

@ -0,0 +1,9 @@
package model
type Clam struct {
BaseModel
Name string `gorm:"type:varchar(64);not null" json:"name"`
Path string `gorm:"type:varchar(64);not null" json:"path"`
Description string `gorm:"type:varchar(64);not null" json:"description"`
}

58
backend/app/repo/clam.go

@ -0,0 +1,58 @@
package repo
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
)
type ClamRepo struct{}
type IClamRepo interface {
Page(limit, offset int, opts ...DBOption) (int64, []model.Clam, error)
Create(clam *model.Clam) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
Get(opts ...DBOption) (model.Clam, error)
}
func NewIClamRepo() IClamRepo {
return &ClamRepo{}
}
func (u *ClamRepo) Get(opts ...DBOption) (model.Clam, error) {
var clam model.Clam
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&clam).Error
return clam, err
}
func (u *ClamRepo) Page(page, size int, opts ...DBOption) (int64, []model.Clam, error) {
var users []model.Clam
db := global.DB.Model(&model.Clam{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (u *ClamRepo) Create(clam *model.Clam) error {
return global.DB.Create(clam).Error
}
func (u *ClamRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Clam{}).Where("id = ?", id).Updates(vars).Error
}
func (u *ClamRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Clam{}).Error
}

366
backend/app/service/clam.go

@ -0,0 +1,366 @@
package service
import (
"bufio"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
const (
clamServiceNameCentOs = "clamd@scan.service"
clamServiceNameUbuntu = "clamav-daemon.service"
scanDir = "scan-result"
)
type ClamService struct {
serviceName string
}
type IClamService interface {
LoadBaseInfo() (dto.ClamBaseInfo, error)
Operate(operate string) error
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate) error
Update(req dto.ClamUpdate) error
Delete(ids []uint) error
HandleOnce(req dto.OperateByID) error
LoadFile(req dto.OperationWithName) (string, error)
UpdateFile(req dto.UpdateByNameAndFile) error
LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error)
CleanRecord(req dto.OperateByID) error
}
func NewIClamService() IClamService {
return &ClamService{}
}
func (f *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
var baseInfo dto.ClamBaseInfo
baseInfo.Version = "-"
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
if exist1 {
f.serviceName = clamServiceNameCentOs
baseInfo.IsExist = true
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
}
exist2, _ := systemctl.IsExist(clamServiceNameCentOs)
if exist2 {
f.serviceName = clamServiceNameCentOs
baseInfo.IsExist = true
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
}
if baseInfo.IsActive {
version, err := cmd.Exec("clamdscan --version")
if err != nil {
return baseInfo, nil
}
if strings.Contains(version, "/") {
baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
} else {
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
}
}
return baseInfo, nil
}
func (f *ClamService) Operate(operate string) error {
switch operate {
case "start", "restart", "stop":
stdout, err := cmd.Execf("systemctl %s %s", operate, f.serviceName)
if err != nil {
return fmt.Errorf("%s the %s failed, err: %s", operate, f.serviceName, stdout)
}
return nil
default:
return fmt.Errorf("not support such operation: %v", operate)
}
}
func (f *ClamService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info))
if err != nil {
return 0, nil, err
}
var datas []dto.ClamInfo
for _, command := range commands {
var item dto.ClamInfo
if err := copier.Copy(&item, &command); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
item.LastHandleDate = "-"
datas = append(datas, item)
}
nyc, _ := time.LoadLocation(common.LoadTimeZone())
for i := 0; i < len(datas); i++ {
logPaths := loadFileByName(datas[i].Name)
sort.Slice(logPaths, func(i, j int) bool {
return logPaths[i] > logPaths[j]
})
if len(logPaths) != 0 {
t1, err := time.ParseInLocation("20060102150405", logPaths[0], nyc)
if err != nil {
continue
}
datas[i].LastHandleDate = t1.Format("2006-01-02 15:04:05")
}
}
return total, datas, err
}
func (f *ClamService) Create(req dto.ClamCreate) error {
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name))
if clam.ID != 0 {
return constant.ErrRecordExist
}
if err := copier.Copy(&clam, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
if err := clamRepo.Create(&clam); err != nil {
return err
}
return nil
}
func (f *ClamService) Update(req dto.ClamUpdate) error {
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name))
if clam.ID == 0 {
return constant.ErrRecordNotFound
}
upMap := map[string]interface{}{}
upMap["name"] = req.Name
upMap["path"] = req.Path
upMap["description"] = req.Description
if err := clamRepo.Update(req.ID, upMap); err != nil {
return err
}
return nil
}
func (u *ClamService) Delete(ids []uint) error {
if len(ids) == 1 {
clam, _ := clamRepo.Get(commonRepo.WithByID(ids[0]))
if clam.ID == 0 {
return constant.ErrRecordNotFound
}
return clamRepo.Delete(commonRepo.WithByID(ids[0]))
}
return clamRepo.Delete(commonRepo.WithIdsIn(ids))
}
func (u *ClamService) HandleOnce(req dto.OperateByID) error {
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
if clam.ID == 0 {
return constant.ErrRecordNotFound
}
if cmd.CheckIllegal(clam.Path) {
return buserr.New(constant.ErrCmdIllegal)
}
logFile := path.Join(global.CONF.System.DataDir, scanDir, clam.Name, time.Now().Format("20060102150405"))
if _, err := os.Stat(path.Dir(logFile)); err != nil {
_ = os.MkdirAll(path.Dir(logFile), os.ModePerm)
}
go func() {
cmd := exec.Command("clamdscan", "--fdpass", clam.Path, "-l", logFile)
_, _ = cmd.CombinedOutput()
}()
return nil
}
func (u *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) {
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ClamID))
if clam.ID == 0 {
return 0, nil, constant.ErrRecordNotFound
}
logPaths := loadFileByName(clam.Name)
if len(logPaths) == 0 {
return 0, nil, nil
}
var filterFiles []string
nyc, _ := time.LoadLocation(common.LoadTimeZone())
for _, item := range logPaths {
t1, err := time.ParseInLocation("20060102150405", item, nyc)
if err != nil {
continue
}
if t1.After(req.StartTime) && t1.Before(req.EndTime) {
filterFiles = append(filterFiles, item)
}
}
if len(filterFiles) == 0 {
return 0, nil, nil
}
sort.Slice(filterFiles, func(i, j int) bool {
return filterFiles[i] > filterFiles[j]
})
var records []string
total, start, end := len(filterFiles), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]string, 0)
} else {
if end >= total {
end = total
}
records = filterFiles[start:end]
}
var datas []dto.ClamLog
for i := 0; i < len(records); i++ {
item := loadResultFromLog(path.Join(global.CONF.System.DataDir, scanDir, clam.Name, records[i]))
datas = append(datas, item)
}
return int64(total), datas, nil
}
func (u *ClamService) CleanRecord(req dto.OperateByID) error {
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
if clam.ID == 0 {
return constant.ErrRecordNotFound
}
pathItem := path.Join(global.CONF.System.DataDir, scanDir, clam.Name)
_ = os.RemoveAll(pathItem)
return nil
}
func (u *ClamService) LoadFile(req dto.OperationWithName) (string, error) {
filePath := ""
switch req.Name {
case "clamd":
if u.serviceName == clamServiceNameCentOs {
filePath = "/etc/clamav/clamd.conf"
} else {
filePath = "/etc/clamd.d/scan.conf"
}
case "clamd-log":
if u.serviceName == clamServiceNameCentOs {
filePath = "/var/log/clamav/clamav.log"
} else {
filePath = "/var/log/clamd.scan"
}
case "freshclam":
if u.serviceName == clamServiceNameCentOs {
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
}
case "freshclam-log":
if u.serviceName == clamServiceNameCentOs {
filePath = "/var/log/clamav/freshclam.log"
} else {
filePath = "/var/log/clamav/freshclam.log"
}
default:
return "", fmt.Errorf("not support such type")
}
if _, err := os.Stat(filePath); err != nil {
return "", buserr.New("ErrHttpReqNotFound")
}
content, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
return string(content), nil
}
func (u *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
filePath := ""
service := ""
switch req.Name {
case "clamd":
if u.serviceName == clamServiceNameCentOs {
service = clamServiceNameCentOs
filePath = "/etc/clamav/clamd.conf"
} else {
service = clamServiceNameCentOs
filePath = "/etc/clamd.d/scan.conf"
}
case "freshclam":
if u.serviceName == clamServiceNameCentOs {
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
}
service = "clamav-freshclam.service"
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()
_ = systemctl.Restart(service)
return nil
}
func loadFileByName(name string) []string {
var logPaths []string
pathItem := path.Join(global.CONF.System.DataDir, scanDir, name)
_ = filepath.Walk(pathItem, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() || info.Name() == name {
return nil
}
logPaths = append(logPaths, info.Name())
return nil
})
return logPaths
}
func loadResultFromLog(pathItem string) dto.ClamLog {
var data dto.ClamLog
data.Name = path.Base(pathItem)
data.Status = constant.StatusWaiting
file, err := os.ReadFile(pathItem)
if err != nil {
return data
}
data.Log = string(file)
lines := strings.Split(string(file), "\n")
for _, line := range lines {
if strings.Contains(line, "- SCAN SUMMARY -") {
data.Status = constant.StatusDone
}
if data.Status != constant.StatusDone {
continue
}
switch {
case strings.HasPrefix(line, "Infected files:"):
data.InfectedFiles = strings.TrimPrefix(line, "Infected files:")
case strings.HasPrefix(line, "Time:"):
if strings.Contains(line, "(") {
data.ScanTime = strings.ReplaceAll(strings.Split(line, "(")[1], ")", "")
continue
}
data.ScanTime = strings.TrimPrefix(line, "Time:")
case strings.HasPrefix(line, "Start Date:"):
data.ScanDate = strings.TrimPrefix(line, "Start Date:")
}
}
return data
}

0
backend/app/service/cornjob.go → backend/app/service/cronjob.go

1
backend/app/service/entry.go

@ -25,6 +25,7 @@ var (
groupRepo = repo.NewIGroupRepo()
commandRepo = repo.NewICommandRepo()
ftpRepo = repo.NewIFtpRepo()
clamRepo = repo.NewIClamRepo()
settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo()

1
backend/init/migration/migrate.go

@ -91,6 +91,7 @@ func Init() {
migrations.AddCronJobColumn,
migrations.AddForward,
migrations.AddShellColumn,
migrations.AddClam,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

10
backend/init/migration/migrations/v_1_10.go

@ -268,3 +268,13 @@ var AddShellColumn = &gormigrate.Migration{
return nil
},
}
var AddClam = &gormigrate.Migration{
ID: "20240624-add-clam",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Clam{}); err != nil {
return err
}
return nil
},
}

12
backend/router/ro_toolbox.go

@ -45,5 +45,17 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp)
toolboxRouter.POST("/ftp/del", baseApi.DeleteFtp)
toolboxRouter.POST("/ftp/sync", baseApi.SyncFtp)
toolboxRouter.POST("/clam/search", baseApi.SearchClam)
toolboxRouter.POST("/clam/record/search", baseApi.SearchClamRecord)
toolboxRouter.POST("/clam/record/clean", baseApi.CleanClamRecord)
toolboxRouter.POST("/clam/file/search", baseApi.SearchClamFile)
toolboxRouter.POST("/clam/file/update", baseApi.UpdateFile)
toolboxRouter.POST("/clam", baseApi.CreateClam)
toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo)
toolboxRouter.POST("/clam/operate", baseApi.OperateClam)
toolboxRouter.POST("/clam/update", baseApi.UpdateClam)
toolboxRouter.POST("/clam/del", baseApi.DeleteClam)
toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan)
}
}

593
cmd/server/docs/docs.go

@ -11065,6 +11065,445 @@ const docTemplate = `{
}
}
},
"/toolbox/clam": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Create clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "create clam [name][path]",
"formatZH": "创建扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clam/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Clam 基础信息",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ClamBaseInfo"
}
}
}
}
},
"/toolbox/clam/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Delete clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "ids",
"isList": true,
"output_column": "name",
"output_value": "names"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "delete clam [names]",
"formatZH": "删除扫描规则 [names]",
"paramKeys": []
}
}
},
"/toolbox/clam/file/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperationWithName"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/file/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新病毒扫描配置文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateByNameAndFile"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/clam/handle": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "执行病毒扫描",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Handle clam scan",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "handle clam scan [name]",
"formatZH": "执行病毒扫描 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Clam 状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Operate Clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] FTP",
"formatZH": "[operation] Clam",
"paramKeys": []
}
}
},
"/toolbox/clam/record/clean": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "清空扫描报告",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Clean clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "clean clam record [name]",
"formatZH": "清空扫描报告 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/record/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描结果列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamLogSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描规则列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchWithPage"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "update clam [name][path]",
"formatZH": "修改扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clean": {
"post": {
"security": [
@ -12748,6 +13187,73 @@ const docTemplate = `{
}
}
},
"/websites/default/html/:type": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get default html",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.FileInfo"
}
}
}
}
},
"/websites/default/html/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update default html",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteHtmlUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"type"
],
"formatEN": "Update default html",
"formatZH": "更新默认 html",
"paramKeys": []
}
}
},
"/websites/default/server": {
"post": {
"security": [
@ -14934,6 +15440,75 @@ const docTemplate = `{
}
}
},
"dto.ClamBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
},
"version": {
"type": "string"
}
}
},
"dto.ClamCreate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.ClamLogSearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"clamID": {
"type": "integer"
},
"endTime": {
"type": "string"
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"startTime": {
"type": "string"
}
}
},
"dto.ClamUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.Clean": {
"type": "object",
"properties": {
@ -20144,6 +20719,9 @@ const docTemplate = `{
"ID": {
"type": "integer"
},
"latest": {
"type": "boolean"
},
"name": {
"type": "string"
},
@ -21388,6 +21966,21 @@ const docTemplate = `{
}
}
},
"request.WebsiteHtmlUpdate": {
"type": "object",
"required": [
"content",
"type"
],
"properties": {
"content": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"request.WebsiteInstallCheckReq": {
"type": "object",
"properties": {

593
cmd/server/docs/swagger.json

@ -11058,6 +11058,445 @@
}
}
},
"/toolbox/clam": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Create clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "create clam [name][path]",
"formatZH": "创建扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clam/base": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Clam 基础信息",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam base info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ClamBaseInfo"
}
}
}
}
},
"/toolbox/clam/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Delete clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "ids",
"isList": true,
"output_column": "name",
"output_value": "names"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "delete clam [names]",
"formatZH": "删除扫描规则 [names]",
"paramKeys": []
}
}
},
"/toolbox/clam/file/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperationWithName"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/file/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新病毒扫描配置文件",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateByNameAndFile"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/clam/handle": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "执行病毒扫描",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Handle clam scan",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "handle clam scan [name]",
"formatZH": "执行病毒扫描 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 Clam 状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Operate Clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Operate"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operation"
],
"formatEN": "[operation] FTP",
"formatZH": "[operation] Clam",
"paramKeys": []
}
}
},
"/toolbox/clam/record/clean": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "清空扫描报告",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Clean clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperateByID"
}
}
],
"responses": {},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": true,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "clean clam record [name]",
"formatZH": "清空扫描报告 [name]",
"paramKeys": []
}
}
},
"/toolbox/clam/record/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描结果列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam record",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamLogSearch"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描规则列表分页",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Page clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchWithPage"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/clam/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name",
"path"
],
"formatEN": "update clam [name][path]",
"formatZH": "修改扫描规则 [name][path]",
"paramKeys": []
}
}
},
"/toolbox/clean": {
"post": {
"security": [
@ -12741,6 +13180,73 @@
}
}
},
"/websites/default/html/:type": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get default html",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.FileInfo"
}
}
}
}
},
"/websites/default/html/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新默认 html",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update default html",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteHtmlUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"type"
],
"formatEN": "Update default html",
"formatZH": "更新默认 html",
"paramKeys": []
}
}
},
"/websites/default/server": {
"post": {
"security": [
@ -14927,6 +15433,75 @@
}
}
},
"dto.ClamBaseInfo": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"isExist": {
"type": "boolean"
},
"version": {
"type": "string"
}
}
},
"dto.ClamCreate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.ClamLogSearch": {
"type": "object",
"required": [
"page",
"pageSize"
],
"properties": {
"clamID": {
"type": "integer"
},
"endTime": {
"type": "string"
},
"page": {
"type": "integer"
},
"pageSize": {
"type": "integer"
},
"startTime": {
"type": "string"
}
}
},
"dto.ClamUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"dto.Clean": {
"type": "object",
"properties": {
@ -20137,6 +20712,9 @@
"ID": {
"type": "integer"
},
"latest": {
"type": "boolean"
},
"name": {
"type": "string"
},
@ -21381,6 +21959,21 @@
}
}
},
"request.WebsiteHtmlUpdate": {
"type": "object",
"required": [
"content",
"type"
],
"properties": {
"content": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"request.WebsiteInstallCheckReq": {
"type": "object",
"properties": {

378
cmd/server/docs/swagger.yaml

@ -216,6 +216,51 @@ definitions:
required:
- database
type: object
dto.ClamBaseInfo:
properties:
isActive:
type: boolean
isExist:
type: boolean
version:
type: string
type: object
dto.ClamCreate:
properties:
description:
type: string
name:
type: string
path:
type: string
type: object
dto.ClamLogSearch:
properties:
clamID:
type: integer
endTime:
type: string
page:
type: integer
pageSize:
type: integer
startTime:
type: string
required:
- page
- pageSize
type: object
dto.ClamUpdate:
properties:
description:
type: string
id:
type: integer
name:
type: string
path:
type: string
type: object
dto.Clean:
properties:
name:
@ -3720,6 +3765,8 @@ definitions:
properties:
ID:
type: integer
latest:
type: boolean
name:
type: string
page:
@ -4565,6 +4612,16 @@ definitions:
required:
- websiteId
type: object
request.WebsiteHtmlUpdate:
properties:
content:
type: string
type:
type: string
required:
- content
- type
type: object
request.WebsiteInstallCheckReq:
properties:
InstallIds:
@ -12360,6 +12417,285 @@ paths:
formatEN: upgrade system => [version]
formatZH: 更新系统 => [version]
paramKeys: []
/toolbox/clam:
post:
consumes:
- application/json
description: 创建扫描规则
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Create clam
tags:
- Clam
x-panel-log:
BeforeFunctions: []
bodyKeys:
- name
- path
formatEN: create clam [name][path]
formatZH: 创建扫描规则 [name][path]
paramKeys: []
/toolbox/clam/base:
get:
consumes:
- application/json
description: 获取 Clam 基础信息
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.ClamBaseInfo'
security:
- ApiKeyAuth: []
summary: Load clam base info
tags:
- Clam
/toolbox/clam/del:
post:
consumes:
- application/json
description: 删除扫描规则
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.BatchDeleteReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Delete clam
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: ids
isList: true
output_column: name
output_value: names
bodyKeys:
- ids
formatEN: delete clam [names]
formatZH: 删除扫描规则 [names]
paramKeys: []
/toolbox/clam/file/search:
post:
consumes:
- application/json
description: 获取扫描文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OperationWithName'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Load clam file
tags:
- Clam
/toolbox/clam/file/update:
post:
consumes:
- application/json
description: 更新病毒扫描配置文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.UpdateByNameAndFile'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update clam file
tags:
- Clam
/toolbox/clam/handle:
post:
consumes:
- application/json
description: 执行病毒扫描
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OperateByID'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Handle clam scan
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: id
isList: true
output_column: name
output_value: name
bodyKeys:
- id
formatEN: handle clam scan [name]
formatZH: 执行病毒扫描 [name]
paramKeys: []
/toolbox/clam/operate:
post:
consumes:
- application/json
description: 修改 Clam 状态
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses: {}
security:
- ApiKeyAuth: []
summary: Operate Clam
tags:
- Clam
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] FTP'
formatZH: '[operation] Clam'
paramKeys: []
/toolbox/clam/record/clean:
post:
consumes:
- application/json
description: 清空扫描报告
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.OperateByID'
responses: {}
security:
- ApiKeyAuth: []
summary: Clean clam record
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: id
isList: true
output_column: name
output_value: name
bodyKeys:
- id
formatEN: clean clam record [name]
formatZH: 清空扫描报告 [name]
paramKeys: []
/toolbox/clam/record/search:
post:
consumes:
- application/json
description: 获取扫描结果列表分页
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamLogSearch'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Page clam record
tags:
- Clam
/toolbox/clam/search:
post:
consumes:
- application/json
description: 获取扫描规则列表分页
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchWithPage'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Page clam
tags:
- Clam
/toolbox/clam/update:
post:
consumes:
- application/json
description: 修改扫描规则
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update clam
tags:
- Clam
x-panel-log:
BeforeFunctions: []
bodyKeys:
- name
- path
formatEN: update clam [name][path]
formatZH: 修改扫描规则 [name][path]
paramKeys: []
/toolbox/clean:
post:
consumes:
@ -13422,6 +13758,48 @@ paths:
formatEN: Nginx conf update [domain]
formatZH: nginx 配置修改 [domain]
paramKeys: []
/websites/default/html/:type:
get:
consumes:
- application/json
description: 获取默认 html
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.FileInfo'
security:
- ApiKeyAuth: []
summary: Get default html
tags:
- Website
/websites/default/html/update:
post:
consumes:
- application/json
description: 更新默认 html
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteHtmlUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update default html
tags:
- Website
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: Update default html
formatZH: 更新默认 html
paramKeys: []
/websites/default/server:
post:
consumes:

38
frontend/src/api/interface/toolbox.ts

@ -116,4 +116,42 @@ export namespace Toolbox {
status: string;
size: string;
}
export interface ClamBaseInfo {
version: string;
isActive: boolean;
isExist: boolean;
}
export interface ClamInfo {
id: number;
name: string;
path: string;
lastHandleDate: string;
description: string;
}
export interface ClamCreate {
name: string;
path: string;
description: string;
}
export interface ClamUpdate {
id: number;
name: string;
path: string;
description: string;
}
export interface ClamSearchLog extends ReqPage {
clamID: number;
startTime: Date;
endTime: Date;
}
export interface ClamLog {
name: string;
scanDate: string;
scanTime: string;
scannedFiles: string;
infectedFiles: string;
log: string;
status: string;
}
}

35
frontend/src/api/modules/toolbox.ts

@ -106,3 +106,38 @@ export const updateFtp = (params: Toolbox.FtpUpdate) => {
export const deleteFtp = (params: { ids: number[] }) => {
return http.post(`/toolbox/ftp/del`, params);
};
// clam
export const cleanClamRecord = (id: number) => {
return http.post(`/toolbox/clam/record/clean`, { id: id });
};
export const searchClamRecord = (param: Toolbox.ClamSearchLog) => {
return http.post<ResPage<Toolbox.ClamLog>>(`/toolbox/clam/record/search`, param);
};
export const searchClamFile = (name: string) => {
return http.post<string>(`/toolbox/clam/file/search`, { name: name });
};
export const updateClamFile = (name: string, file: string) => {
return http.post(`/toolbox/clam/file/update`, { name: name, file: file });
};
export const searchClamBaseInfo = () => {
return http.post<Toolbox.ClamBaseInfo>(`/toolbox/clam/base`);
};
export const updateClamBaseInfo = (operate: string) => {
return http.post(`/toolbox/clam/operate`, { Operation: operate });
};
export const searchClam = (param: ReqPage) => {
return http.post<ResPage<Toolbox.ClamInfo>>(`/toolbox/clam/search`, param);
};
export const createClam = (params: Toolbox.ClamCreate) => {
return http.post(`/toolbox/clam`, params);
};
export const updateClam = (params: Toolbox.ClamUpdate) => {
return http.post(`/toolbox/clam/update`, params);
};
export const deleteClam = (params: { ids: number[] }) => {
return http.post(`/toolbox/clam/del`, params);
};
export const handleClamScan = (id: number) => {
return http.post(`/toolbox/clam/handle`, { id: id });
};

1
frontend/src/components/error-message/error_code.vue

@ -19,7 +19,6 @@ const props = defineProps({
code: String,
});
const loadErrInfo = () => {
console.log(props.code);
switch (props.code) {
case '400':
return '400 Bad Request';

14
frontend/src/lang/modules/en.ts

@ -229,6 +229,7 @@ const message = {
status: {
running: 'Running',
done: 'Done',
scanFailed: 'Incomplete',
success: 'Success',
waiting: 'Waiting',
waiting1: 'Waiting',
@ -1058,6 +1059,19 @@ const message = {
'Disabling the selected FTP account will revoke its access permissions. Do you want to continue?',
syncHelper: 'Sync FTP account data between server and database. Do you want to continue?',
},
clam: {
clam: 'Virus Scan',
clamCreate: 'Create Scan Rule',
scanDate: 'Scan Date',
scanTime: 'Elapsed Time',
scannedFiles: 'Number of Scanned Files',
infectedFiles: 'Number of Infected Files',
log: 'Details',
clamConf: 'Scan Configuration',
clamLog: 'Scan Log',
freshClam: 'Virus Database Refresh Configuration',
freshClamLog: 'Virus Database Refresh Log',
},
},
logs: {
panelLog: 'Panel logs',

14
frontend/src/lang/modules/tw.ts

@ -224,6 +224,7 @@ const message = {
status: {
running: '已啟動',
done: '已完成',
scanFailed: '未完成',
success: '成功',
waiting: '執行中',
waiting1: '等待中',
@ -1000,6 +1001,19 @@ const message = {
disableHelper: '停用選取的 FTP 帳號後 FTP 帳號將失去訪問權限是否繼續操作',
syncHelper: '同步伺服器與資料庫中的 FTP 帳戶資料是否繼續操作',
},
clam: {
clam: '病毒掃描',
clamCreate: '創建掃描規則',
scanDate: '掃描時間',
scanTime: '耗時',
scannedFiles: '掃描文件數',
infectedFiles: '危險文件數',
log: '詳情',
clamConf: '掃描配置',
clamLog: '掃描日誌',
freshClam: '病毒庫刷新配置',
freshClamLog: '病毒庫刷新日誌',
},
},
logs: {
panelLog: '面板日誌',

14
frontend/src/lang/modules/zh.ts

@ -225,6 +225,7 @@ const message = {
status: {
running: '已启动',
done: '已完成',
scanFailed: '未完成',
success: '成功',
waiting: '执行中',
waiting1: '等待中',
@ -1002,6 +1003,19 @@ const message = {
disableHelper: '停用选中的 FTP 账号后 FTP 账号将失去访问权限是否继续操作',
syncHelper: '同步服务器与数据库中的 FTP 账户数据是否继续操作',
},
clam: {
clam: '病毒扫描',
clamCreate: '创建扫描规则',
scanDate: '扫描时间',
scanTime: '耗时',
scannedFiles: '扫描文件数',
infectedFiles: '危险文件数',
log: '详情',
clamConf: '扫描配置',
clamLog: '扫描日志',
freshClam: '病毒库刷新配置',
freshClamLog: '病毒库刷新日志',
},
},
logs: {
panelLog: '面板日志',

20
frontend/src/routers/modules/toolbox.ts

@ -37,6 +37,26 @@ const toolboxRouter = {
requiresAuth: false,
},
},
{
path: 'clam',
name: 'Clam',
component: () => import('@/views/toolbox/clam/index.vue'),
hidden: true,
meta: {
activeMenu: '/toolbox',
requiresAuth: false,
},
},
{
path: 'clam/setting',
name: 'Clam-Setting',
component: () => import('@/views/toolbox/clam/setting/index.vue'),
hidden: true,
meta: {
activeMenu: '/toolbox',
requiresAuth: false,
},
},
{
path: 'ftp',
name: 'FTP',

1
frontend/src/views/database/mysql/setting/index.vue

@ -236,7 +236,6 @@ const onSavePort = async (formEl: FormInstance | undefined) => {
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmPortRef.value!.acceptParams(params);
return;
};
function callback(error: any) {
if (error) {

246
frontend/src/views/toolbox/clam/index.vue

@ -0,0 +1,246 @@
<template>
<div>
<LayoutContent v-loading="loading" v-if="!isRecordShow && !isSettingShow" :title="$t('toolbox.clam.clam')">
<template #app>
<ClamStatus
@setting="setting"
v-model:loading="loading"
@get-status="getStatus"
v-model:mask-show="maskShow"
/>
</template>
<template #toolbar>
<el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
<el-button type="primary" :disabled="!form.isActive" @click="onOpenDialog('add')">
{{ $t('toolbox.clam.clamCreate') }}
</el-button>
<el-button plain :disabled="selects.length === 0 || !form.isActive" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
<TableSearch @search="search()" v-model:searchName="searchName" />
</el-col>
</el-row>
</template>
<template #main>
<ComplexTable
v-if="!isSettingShow"
:pagination-config="paginationConfig"
v-model:selects="selects"
@sort-change="search"
@search="search"
:data="data"
>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
:min-width="60"
prop="name"
show-overflow-tooltip
/>
<el-table-column :label="$t('file.path')" :min-width="120" prop="path" show-overflow-tooltip>
<template #default="{ row }">
<el-button text type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button>
</template>
</el-table-column>
<el-table-column
:label="$t('cronjob.lastRecordTime')"
:min-width="100"
prop="lastHandleDate"
show-overflow-tooltip
/>
<el-table-column :label="$t('commons.table.description')" prop="description" show-overflow-tooltip>
<template #default="{ row }">
<fu-input-rw-switch v-model="row.description" @blur="onChange(row)" />
</template>
</el-table-column>
<fu-table-operations
width="200px"
:buttons="buttons"
:ellipsis="10"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()" />
<OperateDialog @search="search" ref="dialogRef" />
<LogDialog ref="dialogLogRef" />
<SettingDialog v-if="isSettingShow" />
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { deleteClam, handleClamScan, searchClam, updateClam } from '@/api/modules/toolbox';
import OperateDialog from '@/views/toolbox/clam/operate/index.vue';
import LogDialog from '@/views/toolbox/clam/record/index.vue';
import ClamStatus from '@/views/toolbox/clam/status/index.vue';
import SettingDialog from '@/views/toolbox/clam/setting/index.vue';
import { Toolbox } from '@/api/interface/toolbox';
import router from '@/routers';
const loading = ref();
const selects = ref<any>([]);
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'clam-page-size',
currentPage: 1,
pageSize: Number(localStorage.getItem('ftp-page-size')) || 10,
total: 0,
orderBy: 'created_at',
order: 'null',
});
const searchName = ref();
const form = reactive({
isActive: true,
isExist: true,
});
const opRef = ref();
const dialogRef = ref();
const operateIDs = ref();
const dialogLogRef = ref();
const isRecordShow = ref();
const isSettingShow = ref();
const maskShow = ref(true);
const clamStatus = ref({
isExist: false,
version: false,
isActive: true,
});
const search = async () => {
loading.value = true;
let params = {
info: searchName.value,
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
await searchClam(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const setting = () => {
router.push({ name: 'Clam-Setting' });
};
const getStatus = (status: any) => {
clamStatus.value = status;
search();
};
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });
};
const onChange = async (row: any) => {
await await updateClam(row);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
};
const onOpenDialog = async (title: string, rowData: Partial<Toolbox.ClamInfo> = {}) => {
let params = {
title,
rowData: { ...rowData },
};
dialogRef.value!.acceptParams(params);
};
const onDelete = async (row: Toolbox.ClamInfo | null) => {
let names = [];
let ids = [];
if (row) {
ids = [row.id];
names = [row.name];
} else {
for (const item of selects.value) {
names.push(item.name);
ids.push(item.id);
}
}
operateIDs.value = ids;
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('cronjob.cronTask'),
i18n.global.t('commons.button.delete'),
]),
api: null,
params: null,
});
};
const onSubmitDelete = async () => {
loading.value = true;
await deleteClam({ ids: operateIDs.value })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const buttons = [
{
label: i18n.global.t('commons.button.handle'),
click: async (row: Toolbox.ClamInfo) => {
loading.value = true;
await handleClamScan(row.id)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
},
},
{
label: i18n.global.t('commons.button.edit'),
click: (row: Toolbox.ClamInfo) => {
onOpenDialog('edit', row);
},
},
{
label: i18n.global.t('cronjob.record'),
click: (row: Toolbox.ClamInfo) => {
isRecordShow.value = true;
let params = {
rowData: { ...row },
};
dialogLogRef.value!.acceptParams(params);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: Toolbox.ClamInfo) => {
onDelete(row);
},
},
];
onMounted(() => {
search();
});
</script>

133
frontend/src/views/toolbox/clam/operate/index.vue

@ -0,0 +1,133 @@
<template>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="50%"
>
<template #header>
<DrawerHeader
:header="title"
:hideResource="dialogData.title === 'add'"
:resource="dialogData.rowData?.name"
:back="handleClose"
/>
</template>
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules" v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input
:disabled="dialogData.title === 'edit'"
clearable
v-model.trim="dialogData.rowData!.name"
/>
</el-form-item>
<el-form-item :label="$t('file.root')" prop="path">
<el-input v-model="dialogData.rowData!.path">
<template #prepend>
<FileList @choose="loadDir" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { Toolbox } from '@/api/interface/toolbox';
import { createClam, updateClam } from '@/api/modules/toolbox';
interface DialogProps {
title: string;
rowData?: Toolbox.ClamInfo;
getTableList?: () => Promise<any>;
}
const loading = ref();
const title = ref<string>('');
const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const handleClose = () => {
drawerVisible.value = false;
};
const rules = reactive({
name: [Rules.simpleName],
path: [Rules.requiredInput, Rules.noSpace],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const loadDir = async (path: string) => {
dialogData.value.rowData!.path = path;
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
if (dialogData.value.title === 'edit') {
await updateClam(dialogData.value.rowData)
.then(() => {
loading.value = false;
drawerVisible.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
})
.catch(() => {
loading.value = false;
});
return;
}
await createClam(dialogData.value.rowData)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

345
frontend/src/views/toolbox/clam/record/index.vue

@ -0,0 +1,345 @@
<template>
<div v-if="recordShow" v-loading="loading">
<div class="app-status p-mt-20">
<el-card>
<div>
<el-tag class="float-left" effect="dark" type="success">
{{ dialogData.rowData.name }}
</el-tag>
<el-popover
v-if="dialogData.rowData.path.length >= 35"
placement="top-start"
trigger="hover"
width="250"
:content="dialogData.rowData.path"
>
<template #reference>
<el-tag style="float: left" effect="dark" type="success">
{{ dialogData.rowData.path.substring(0, 20) }}...
</el-tag>
</template>
</el-popover>
<el-tag
v-if="dialogData.rowData.path.length < 35"
class="float-left ml-5"
effect="dark"
type="success"
>
{{ dialogData.rowData.path }}
</el-tag>
<span class="buttons">
<el-button type="primary" @click="onHandle(dialogData.rowData)" link>
{{ $t('commons.button.handle') }}
</el-button>
<el-divider direction="vertical" />
<el-button :disabled="!hasRecords" type="primary" @click="onClean" link>
{{ $t('commons.button.clean') }}
</el-button>
</span>
</div>
</el-card>
</div>
<LayoutContent :title="$t('cronjob.record')" :reload="true">
<template #search>
<el-row :gutter="20">
<el-col :span="8">
<el-date-picker
style="width: calc(100% - 20px)"
@change="search()"
v-model="timeRangeLoad"
type="datetimerange"
:range-separator="$t('commons.search.timeRange')"
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
></el-date-picker>
</el-col>
</el-row>
</template>
<template #main>
<div class="mainClass">
<el-row :gutter="20" v-show="hasRecords" class="mainRowClass">
<el-col :span="7">
<div class="infinite-list" style="overflow: auto">
<el-table
style="cursor: pointer"
:data="records"
border
:show-header="false"
@row-click="clickRow"
>
<el-table-column>
<template #default="{ row }">
<span v-if="row.name === currentRecord.name" class="select-sign"></span>
<el-tag v-if="row.status === 'Done'" type="success">
{{ $t('commons.status.done') }}
</el-tag>
<el-tag v-if="row.status === 'Waiting'" type="info">
{{ $t('commons.status.scanFailed') }}
</el-tag>
<span>
{{ row.name }}
</span>
</template>
</el-table-column>
</el-table>
</div>
<div class="page-item">
<el-pagination
:page-size="searchInfo.pageSize"
:current-page="searchInfo.page"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
:pager-count="3"
:page-sizes="[6, 8, 10, 12, 14]"
small
layout="total, sizes, prev, pager, next"
:total="searchInfo.recordTotal"
/>
</div>
</el-col>
<el-col :span="17">
<el-form label-position="top" :v-key="refresh">
<el-row v-if="currentRecord?.status === 'Done'">
<el-form-item class="descriptionWide">
<template #label>
<span class="status-label">{{ $t('toolbox.clam.scanTime') }}</span>
</template>
<span class="status-count">
{{ currentRecord?.scanTime }}
</span>
</el-form-item>
<el-form-item class="descriptionWide">
<template #label>
<span class="status-label">{{ $t('toolbox.clam.infectedFiles') }}</span>
</template>
<span class="status-count">
{{ currentRecord?.infectedFiles }}
</span>
</el-form-item>
</el-row>
<el-row v-if="currentRecord?.log">
<span>{{ $t('commons.table.records') }}</span>
<codemirror
ref="mymirror"
:autofocus="true"
:placeholder="$t('cronjob.noLogs')"
:indent-with-tab="true"
:tabSize="4"
style="height: calc(100vh - 488px); width: 100%; margin-top: 5px"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
@ready="handleReady"
v-model="currentRecord.log"
:disabled="true"
/>
</el-row>
</el-form>
</el-col>
</el-row>
</div>
<div class="app-warn" v-show="!hasRecords">
<div>
<span>{{ $t('cronjob.noRecord') }}</span>
<div>
<img src="@/assets/images/no_app.svg" />
</div>
</div>
</div>
</template>
</LayoutContent>
</div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { MsgSuccess } from '@/utils/message';
import { shortcuts } from '@/utils/shortcuts';
import { Toolbox } from '@/api/interface/toolbox';
import { cleanClamRecord, handleClamScan, searchClamRecord } from '@/api/modules/toolbox';
const loading = ref();
const refresh = ref(false);
const hasRecords = ref();
let timer: NodeJS.Timer | null = null;
const mymirror = ref();
const extensions = [javascript(), oneDark];
const view = shallowRef();
const handleReady = (payload) => {
view.value = payload.view;
};
const recordShow = ref(false);
interface DialogProps {
rowData: Toolbox.ClamInfo;
}
const dialogData = ref();
const records = ref<Array<Toolbox.ClamLog>>([]);
const currentRecord = ref<Toolbox.ClamLog>();
const acceptParams = async (params: DialogProps): Promise<void> => {
let itemSize = Number(localStorage.getItem(searchInfo.cacheSizeKey));
if (itemSize) {
searchInfo.pageSize = itemSize;
}
recordShow.value = true;
dialogData.value = params;
search();
timer = setInterval(() => {
search();
}, 1000 * 5);
};
const handleSizeChange = (val: number) => {
searchInfo.pageSize = val;
localStorage.setItem(searchInfo.cacheSizeKey, val + '');
search();
};
const handleCurrentChange = (val: number) => {
searchInfo.page = val;
search();
};
const timeRangeLoad = ref<[Date, Date]>([
new Date(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7).setHours(0, 0, 0, 0)),
new Date(new Date().setHours(23, 59, 59, 999)),
]);
const searchInfo = reactive({
cacheSizeKey: 'clam-record-page-size',
page: 1,
pageSize: 8,
recordTotal: 0,
cronjobID: 0,
startTime: new Date(),
endTime: new Date(),
});
const onHandle = async (row: Toolbox.ClamInfo) => {
loading.value = true;
await handleClamScan(row.id)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const search = async () => {
if (timeRangeLoad.value && timeRangeLoad.value.length === 2) {
searchInfo.startTime = timeRangeLoad.value[0];
searchInfo.endTime = timeRangeLoad.value[1];
} else {
searchInfo.startTime = new Date(new Date().setHours(0, 0, 0, 0));
searchInfo.endTime = new Date();
}
let params = {
page: searchInfo.page,
pageSize: searchInfo.pageSize,
clamID: dialogData.value.rowData!.id,
startTime: searchInfo.startTime,
endTime: searchInfo.endTime,
};
const res = await searchClamRecord(params);
records.value = res.data.items;
searchInfo.recordTotal = res.data.total;
hasRecords.value = searchInfo.recordTotal !== 0;
if (!hasRecords.value) {
return;
}
if (!currentRecord.value) {
currentRecord.value = records.value[0];
}
};
const clickRow = async (row: Toolbox.ClamLog) => {
currentRecord.value = row;
};
const onClean = async () => {
ElMessageBox.confirm(i18n.global.t('commons.msg.clean'), i18n.global.t('commons.msg.deleteTitle'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'warning',
}).then(async () => {
loading.value = true;
console.log(dialogData.value.id);
cleanClamRecord(dialogData.value.rowData.id)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
});
};
onBeforeUnmount(() => {
clearInterval(Number(timer));
timer = null;
});
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.infinite-list {
height: calc(100vh - 420px);
.select-sign {
&::before {
float: left;
margin-left: -3px;
position: relative;
width: 3px;
height: 24px;
content: '';
background: $primary-color;
border-radius: 20px;
}
}
.el-tag {
margin-left: 20px;
margin-right: 20px;
}
}
.descriptionWide {
width: 40%;
}
.description {
width: 30%;
}
.page-item {
margin-top: 10px;
font-size: 12px;
float: right;
}
@media only screen and (max-width: 1400px) {
.mainClass {
overflow: auto;
}
.mainRowClass {
min-width: 1200px;
}
}
</style>

138
frontend/src/views/toolbox/clam/setting/index.vue

@ -0,0 +1,138 @@
<template>
<div v-loading="loading">
<LayoutContent>
<template #app>
<ClamStatus v-model:loading="loading" />
</template>
<template #title>
<back-button name="Clam" header="Clamav">
<template #buttons>
<el-button type="primary" :plain="activeName !== 'clamd'" @click="search('clamd')">
{{ $t('toolbox.clam.clamConf') }}
</el-button>
<el-button type="primary" :plain="activeName !== 'freshclam'" @click="search('freshclam')">
{{ $t('toolbox.clam.freshClam') }}
</el-button>
<el-button type="primary" :plain="activeName !== 'clamd-log'" @click="search('clamd-log')">
{{ $t('toolbox.clam.clamLog') }}
</el-button>
<el-button
type="primary"
:plain="activeName !== 'freshclam-log'"
@click="search('freshclam-log')"
>
{{ $t('toolbox.clam.freshClamLog') }}
</el-button>
</template>
</back-button>
</template>
<template #main>
<div>
<codemirror
:autofocus="true"
:placeholder="$t('commons.msg.noneData')"
:indent-with-tab="true"
:tabSize="4"
:style="{ height: `calc(100vh - ${loadHeight()})`, 'margin-top': '10px' }"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
@ready="handleReady"
:extensions="extensions"
v-model="content"
:disabled="canUpdate()"
/>
<el-button type="primary" style="margin-top: 10px" v-if="!canUpdate()" @click="onSave">
{{ $t('commons.button.save') }}
</el-button>
</div>
</template>
</LayoutContent>
<ConfirmDialog ref="confirmRef" @confirm="onSubmit"></ConfirmDialog>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref, shallowRef } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import ClamStatus from '@/views/toolbox/clam/status/index.vue';
import { searchClamFile, updateClamFile } from '@/api/modules/toolbox';
import { oneDark } from '@codemirror/theme-one-dark';
import { GlobalStore } from '@/store';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const globalStore = GlobalStore();
const loading = ref(false);
const extensions = [javascript(), oneDark];
const view = shallowRef();
const handleReady = (payload) => {
view.value = payload.view;
};
const activeName = ref('clamd');
const content = ref();
const confirmRef = ref();
const loadHeight = () => {
let height = globalStore.openMenuTabs ? '405px' : '375px';
if (canUpdate()) {
height = globalStore.openMenuTabs ? '363px' : '333px';
}
return height;
};
const canUpdate = () => {
return activeName.value.indexOf('-log') !== -1;
};
const search = async (itemName: string) => {
loading.value = true;
activeName.value = itemName;
await searchClamFile(activeName.value)
.then((res) => {
loading.value = false;
content.value = res.data;
nextTick(() => {
const state = view.value.state;
view.value.dispatch({
selection: { anchor: state.doc.length, head: state.doc.length },
scrollIntoView: true,
});
});
})
.catch(() => {
loading.value = false;
});
};
const onSave = async () => {
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmRef.value!.acceptParams(params);
};
const onSubmit = async () => {
loading.value = true;
await updateClamFile(activeName.value, content.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search(activeName.value);
})
.catch(() => {
loading.value = false;
});
};
onMounted(() => {
search(activeName.value);
});
</script>

129
frontend/src/views/toolbox/clam/status/index.vue

@ -0,0 +1,129 @@
<template>
<div>
<div class="app-status tool-status" v-if="data.isExist">
<el-card>
<div>
<el-tag effect="dark" type="success">Clamav</el-tag>
<el-tag round class="status-content" v-if="data.isActive" type="success">
{{ $t('commons.status.running') }}
</el-tag>
<el-tag round class="status-content" v-if="!data.isActive" type="info">
{{ $t('commons.status.stopped') }}
</el-tag>
<el-tag class="status-content">{{ $t('app.version') }}:{{ data.version }}</el-tag>
<span class="buttons">
<el-button type="primary" v-if="!data.isActive" link @click="onOperate('start')">
{{ $t('app.start') }}
</el-button>
<el-button type="primary" v-if="data.isActive" link @click="onOperate('stop')">
{{ $t('app.stop') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link @click="onOperate('restart')">
{{ $t('app.restart') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link @click="setting">
{{ $t('commons.button.set') }}
</el-button>
</span>
</div>
</el-card>
</div>
<LayoutContent :title="$t('tool.supervisor.list')" :divider="true" v-if="!data.isExist" v-loading="loading">
<template #main>
<div class="app-warn">
<div>
<span v-if="!data.isExist">{{ $t('tool.supervisor.notSupport') }}</span>
<span @click="toDoc()" v-if="!data.isExist">
<el-icon class="ml-2"><Position /></el-icon>
{{ $t('firewall.quickJump') }}
</span>
<div>
<img alt="" src="@/assets/images/no_app.svg" />
</div>
</div>
</div>
</template>
</LayoutContent>
</div>
</template>
<script lang="ts" setup>
import { searchClamBaseInfo, updateClamBaseInfo } from '@/api/modules/toolbox';
import { onMounted, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const data = ref({
isExist: false,
isActive: false,
version: '',
});
const loading = ref(false);
const em = defineEmits(['setting', 'getStatus', 'update:loading', 'update:maskShow']);
const setting = () => {
em('setting', true);
};
const toDoc = async () => {
window.open('https://1panel.cn/docs/user_manual/toolbox/supervisor/', '_blank', 'noopener,noreferrer');
};
const onOperate = async (operation: string) => {
em('update:maskShow', false);
ElMessageBox.confirm(
i18n.global.t('commons.msg.operatorHelper', [' Clamav ', i18n.global.t('app.' + operation)]),
i18n.global.t('app.' + operation),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
)
.then(() => {
em('update:loading', true);
updateClamBaseInfo(operation)
.then(() => {
em('update:maskShow', true);
getStatus();
em('update:loading', false);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
em('update:loading', false);
});
})
.catch(() => {
em('update:maskShow', true);
});
};
const getStatus = async () => {
try {
loading.value = true;
em('update:loading', true);
const res = await searchClamBaseInfo();
data.value = res.data;
const status = {
isExist: data.value.isExist,
isRunning: data.value.isActive,
};
em('getStatus', status);
} catch (error) {}
em('update:loading', false);
loading.value = false;
};
onMounted(() => {
getStatus();
});
</script>
<style lang="scss" scoped>
.tool-status {
margin-top: 20px;
}
</style>

4
frontend/src/views/toolbox/index.vue

@ -61,6 +61,10 @@ const buttons = [
label: i18n.global.t('menu.supervisor'),
path: '/toolbox/supervisor',
},
{
label: i18n.global.t('toolbox.clam.clam'),
path: '/toolbox/clam',
},
{
label: 'FTP',
path: '/toolbox/ftp',

Loading…
Cancel
Save