diff --git a/backend/app/api/v1/clam.go b/backend/app/api/v1/clam.go new file mode 100644 index 000000000..6f26f2904 --- /dev/null +++ b/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) +} diff --git a/backend/app/api/v1/entry.go b/backend/app/api/v1/entry.go index 481a6917c..f93b92061 100644 --- a/backend/app/api/v1/entry.go +++ b/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() diff --git a/backend/app/dto/clam.go b/backend/app/dto/clam.go new file mode 100644 index 000000000..f2059d1aa --- /dev/null +++ b/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"` +} diff --git a/backend/app/model/clam.go b/backend/app/model/clam.go new file mode 100644 index 000000000..fcd440178 --- /dev/null +++ b/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"` +} diff --git a/backend/app/repo/clam.go b/backend/app/repo/clam.go new file mode 100644 index 000000000..215915556 --- /dev/null +++ b/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 +} diff --git a/backend/app/service/clam.go b/backend/app/service/clam.go new file mode 100644 index 000000000..a5bbb0c2b --- /dev/null +++ b/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 +} diff --git a/backend/app/service/cornjob.go b/backend/app/service/cronjob.go similarity index 100% rename from backend/app/service/cornjob.go rename to backend/app/service/cronjob.go diff --git a/backend/app/service/entry.go b/backend/app/service/entry.go index fea70a57d..2c72dee06 100644 --- a/backend/app/service/entry.go +++ b/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() diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index d1967295a..d374d451b 100644 --- a/backend/init/migration/migrate.go +++ b/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) diff --git a/backend/init/migration/migrations/v_1_10.go b/backend/init/migration/migrations/v_1_10.go index f89e93fac..ba5b21e66 100644 --- a/backend/init/migration/migrations/v_1_10.go +++ b/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 + }, +} diff --git a/backend/router/ro_toolbox.go b/backend/router/ro_toolbox.go index a35dd1b11..be3b80f6b 100644 --- a/backend/router/ro_toolbox.go +++ b/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) } } diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 14ba06543..da34f97a7 100644 --- a/cmd/server/docs/docs.go +++ b/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": { diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 8785d51e2..422f0270d 100644 --- a/cmd/server/docs/swagger.json +++ b/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": { diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index c6bc32b8c..66a8b0fce 100644 --- a/cmd/server/docs/swagger.yaml +++ b/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: diff --git a/frontend/src/api/interface/toolbox.ts b/frontend/src/api/interface/toolbox.ts index da368efab..9160bd03f 100644 --- a/frontend/src/api/interface/toolbox.ts +++ b/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; + } } diff --git a/frontend/src/api/modules/toolbox.ts b/frontend/src/api/modules/toolbox.ts index faed93ed4..bc893b759 100644 --- a/frontend/src/api/modules/toolbox.ts +++ b/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>(`/toolbox/clam/record/search`, param); +}; +export const searchClamFile = (name: string) => { + return http.post(`/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/clam/base`); +}; +export const updateClamBaseInfo = (operate: string) => { + return http.post(`/toolbox/clam/operate`, { Operation: operate }); +}; +export const searchClam = (param: ReqPage) => { + return http.post>(`/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 }); +}; diff --git a/frontend/src/components/error-message/error_code.vue b/frontend/src/components/error-message/error_code.vue index 00a93614f..8a345889e 100644 --- a/frontend/src/components/error-message/error_code.vue +++ b/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'; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 564c9a1c0..da484a754 100644 --- a/frontend/src/lang/modules/en.ts +++ b/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', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index ddea7087b..9ce2d4be8 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/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: '面板日誌', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8bce55d51..bd80f60eb 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/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: '面板日志', diff --git a/frontend/src/routers/modules/toolbox.ts b/frontend/src/routers/modules/toolbox.ts index 563987d2b..d77a80b78 100644 --- a/frontend/src/routers/modules/toolbox.ts +++ b/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', diff --git a/frontend/src/views/database/mysql/setting/index.vue b/frontend/src/views/database/mysql/setting/index.vue index accf8fe39..96e6b994f 100644 --- a/frontend/src/views/database/mysql/setting/index.vue +++ b/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) { diff --git a/frontend/src/views/toolbox/clam/index.vue b/frontend/src/views/toolbox/clam/index.vue new file mode 100644 index 000000000..282a3e4ae --- /dev/null +++ b/frontend/src/views/toolbox/clam/index.vue @@ -0,0 +1,246 @@ + + + diff --git a/frontend/src/views/toolbox/clam/operate/index.vue b/frontend/src/views/toolbox/clam/operate/index.vue new file mode 100644 index 000000000..b3a9eb420 --- /dev/null +++ b/frontend/src/views/toolbox/clam/operate/index.vue @@ -0,0 +1,133 @@ + + + diff --git a/frontend/src/views/toolbox/clam/record/index.vue b/frontend/src/views/toolbox/clam/record/index.vue new file mode 100644 index 000000000..1c6efb97f --- /dev/null +++ b/frontend/src/views/toolbox/clam/record/index.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/frontend/src/views/toolbox/clam/setting/index.vue b/frontend/src/views/toolbox/clam/setting/index.vue new file mode 100644 index 000000000..74ebb629c --- /dev/null +++ b/frontend/src/views/toolbox/clam/setting/index.vue @@ -0,0 +1,138 @@ + + + diff --git a/frontend/src/views/toolbox/clam/status/index.vue b/frontend/src/views/toolbox/clam/status/index.vue new file mode 100644 index 000000000..6b0d87b2a --- /dev/null +++ b/frontend/src/views/toolbox/clam/status/index.vue @@ -0,0 +1,129 @@ + + + + diff --git a/frontend/src/views/toolbox/index.vue b/frontend/src/views/toolbox/index.vue index cd9eddb77..dc6352212 100644 --- a/frontend/src/views/toolbox/index.vue +++ b/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',