feat: 工具箱病毒扫描支持定时扫描 (#5847)

This commit is contained in:
ssongliu 2024-07-17 16:55:28 +08:00 committed by GitHub
parent ca0c96cb12
commit 3c0dc7459c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 747 additions and 26 deletions

View file

@ -51,16 +51,38 @@ func (b *BaseApi) UpdateClam(c *gin.Context) {
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, nil)
} }
// @Tags Clam
// @Summary Update clam status
// @Description 修改扫描规则状态
// @Accept json
// @Param request body dto.ClamUpdateStatus true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/status/update [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"clams","output_column":"name","output_value":"name"}],"formatZH":"修改扫描规则 [name] 状态为 [status]","formatEN":"change the status of clam [name] to [status]."}
func (b *BaseApi) UpdateClamStatus(c *gin.Context) {
var req dto.ClamUpdateStatus
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := clamService.UpdateStatus(req.ID, req.Status); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Clam // @Tags Clam
// @Summary Page clam // @Summary Page clam
// @Description 获取扫描规则列表分页 // @Description 获取扫描规则列表分页
// @Accept json // @Accept json
// @Param request body dto.SearchWithPage true "request" // @Param request body dto.SearchClamWithPage true "request"
// @Success 200 {object} dto.PageResult // @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /toolbox/clam/search [post] // @Router /toolbox/clam/search [post]
func (b *BaseApi) SearchClam(c *gin.Context) { func (b *BaseApi) SearchClam(c *gin.Context) {
var req dto.SearchWithPage var req dto.SearchClamWithPage
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }

View file

@ -4,6 +4,13 @@ import (
"time" "time"
) )
type SearchClamWithPage struct {
PageInfo
Info string `json:"info"`
OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"`
Order string `json:"order" validate:"required,oneof=null ascending descending"`
}
type ClamBaseInfo struct { type ClamBaseInfo struct {
Version string `json:"version"` Version string `json:"version"`
IsActive bool `json:"isActive"` IsActive bool `json:"isActive"`
@ -19,10 +26,12 @@ type ClamInfo struct {
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"`
Path string `json:"path"` Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"` InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"` InfectedDir string `json:"infectedDir"`
LastHandleDate string `json:"lastHandleDate"` LastHandleDate string `json:"lastHandleDate"`
Spec string `json:"spec"`
Description string `json:"description"` Description string `json:"description"`
} }
@ -56,9 +65,11 @@ type ClamLog struct {
type ClamCreate struct { type ClamCreate struct {
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"`
Path string `json:"path"` Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"` InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"` InfectedDir string `json:"infectedDir"`
Spec string `json:"spec"`
Description string `json:"description"` Description string `json:"description"`
} }
@ -69,9 +80,15 @@ type ClamUpdate struct {
Path string `json:"path"` Path string `json:"path"`
InfectedStrategy string `json:"infectedStrategy"` InfectedStrategy string `json:"infectedStrategy"`
InfectedDir string `json:"infectedDir"` InfectedDir string `json:"infectedDir"`
Spec string `json:"spec"`
Description string `json:"description"` Description string `json:"description"`
} }
type ClamUpdateStatus struct {
ID uint `json:"id"`
Status string `json:"status"`
}
type ClamDelete struct { type ClamDelete struct {
RemoveRecord bool `json:"removeRecord"` RemoveRecord bool `json:"removeRecord"`
RemoveInfected bool `json:"removeInfected"` RemoveInfected bool `json:"removeInfected"`

View file

@ -4,8 +4,11 @@ type Clam struct {
BaseModel BaseModel
Name string `gorm:"type:varchar(64);not null" json:"name"` Name string `gorm:"type:varchar(64);not null" json:"name"`
Status string `gorm:"type:varchar(64)" json:"status"`
Path string `gorm:"type:varchar(64);not null" json:"path"` Path string `gorm:"type:varchar(64);not null" json:"path"`
InfectedStrategy string `gorm:"type:varchar(64)" json:"infectedStrategy"` InfectedStrategy string `gorm:"type:varchar(64)" json:"infectedStrategy"`
InfectedDir string `gorm:"type:varchar(64)" json:"infectedDir"` InfectedDir string `gorm:"type:varchar(64)" json:"infectedDir"`
Spec string `gorm:"type:varchar(64)" json:"spec"`
EntryID int `gorm:"type:varchar(64)" json:"entryID"`
Description string `gorm:"type:varchar(64)" json:"description"` Description string `gorm:"type:varchar(64)" json:"description"`
} }

View file

@ -13,6 +13,7 @@ type IClamRepo interface {
Update(id uint, vars map[string]interface{}) error Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error Delete(opts ...DBOption) error
Get(opts ...DBOption) (model.Clam, error) Get(opts ...DBOption) (model.Clam, error)
List(opts ...DBOption) ([]model.Clam, error)
} }
func NewIClamRepo() IClamRepo { func NewIClamRepo() IClamRepo {
@ -29,6 +30,16 @@ func (u *ClamRepo) Get(opts ...DBOption) (model.Clam, error) {
return clam, err return clam, err
} }
func (u *ClamRepo) List(opts ...DBOption) ([]model.Clam, error) {
var clam []model.Clam
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&clam).Error
return clam, err
}
func (u *ClamRepo) Page(page, size int, opts ...DBOption) (int64, []model.Clam, error) { func (u *ClamRepo) Page(page, size int, opts ...DBOption) (int64, []model.Clam, error) {
var users []model.Clam var users []model.Clam
db := global.DB.Model(&model.Clam{}) db := global.DB.Model(&model.Clam{})

View file

@ -12,13 +12,16 @@ import (
"time" "time"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl" "github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"github.com/robfig/cron/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -37,9 +40,10 @@ type ClamService struct {
type IClamService interface { type IClamService interface {
LoadBaseInfo() (dto.ClamBaseInfo, error) LoadBaseInfo() (dto.ClamBaseInfo, error)
Operate(operate string) error Operate(operate string) error
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate) error Create(req dto.ClamCreate) error
Update(req dto.ClamUpdate) error Update(req dto.ClamUpdate) error
UpdateStatus(id uint, status string) error
Delete(req dto.ClamDelete) error Delete(req dto.ClamDelete) error
HandleOnce(req dto.OperateByID) error HandleOnce(req dto.OperateByID) error
LoadFile(req dto.ClamFileReq) (string, error) LoadFile(req dto.ClamFileReq) (string, error)
@ -75,8 +79,7 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
baseInfo.FreshIsExist = true baseInfo.FreshIsExist = true
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService) baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService)
} }
stdout, err := cmd.Exec("which clamdscan") if !cmd.Which("clamdscan") {
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) {
baseInfo.IsActive = false baseInfo.IsActive = false
} }
@ -122,8 +125,8 @@ func (c *ClamService) Operate(operate string) error {
} }
} }
func (c *ClamService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info)) total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
@ -164,6 +167,14 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" { if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" {
clam.InfectedDir = "" clam.InfectedDir = ""
} }
if len(req.Spec) != 0 {
entryID, err := xpack.StartClam(clam, false)
if err != nil {
return err
}
clam.EntryID = entryID
clam.Status = constant.StatusEnable
}
if err := clamRepo.Create(&clam); err != nil { if err := clamRepo.Create(&clam); err != nil {
return err return err
} }
@ -178,11 +189,36 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" { if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" {
req.InfectedDir = "" req.InfectedDir = ""
} }
var clamItem model.Clam
if err := copier.Copy(&clamItem, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
clamItem.EntryID = clam.EntryID
upMap := map[string]interface{}{} upMap := map[string]interface{}{}
if len(clam.Spec) != 0 && clam.EntryID != 0 {
global.Cron.Remove(cron.EntryID(clamItem.EntryID))
upMap["entry_id"] = 0
}
if len(req.Spec) == 0 {
upMap["status"] = ""
upMap["entry_id"] = 0
}
if len(req.Spec) != 0 && clam.Status != constant.StatusDisable {
newEntryID, err := xpack.StartClam(clamItem, true)
if err != nil {
return err
}
upMap["entry_id"] = newEntryID
}
if len(clam.Spec) == 0 && len(req.Spec) != 0 {
upMap["status"] = constant.StatusEnable
}
upMap["name"] = req.Name upMap["name"] = req.Name
upMap["path"] = req.Path upMap["path"] = req.Path
upMap["infected_dir"] = req.InfectedDir upMap["infected_dir"] = req.InfectedDir
upMap["infected_strategy"] = req.InfectedStrategy upMap["infected_strategy"] = req.InfectedStrategy
upMap["spec"] = req.Spec
upMap["description"] = req.Description upMap["description"] = req.Description
if err := clamRepo.Update(req.ID, upMap); err != nil { if err := clamRepo.Update(req.ID, upMap); err != nil {
return err return err
@ -190,6 +226,28 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
return nil return nil
} }
func (c *ClamService) UpdateStatus(id uint, status string) error {
clam, _ := clamRepo.Get(commonRepo.WithByID(id))
if clam.ID == 0 {
return constant.ErrRecordNotFound
}
var (
entryID int
err error
)
if status == constant.StatusEnable {
entryID, err = xpack.StartClam(clam, true)
if err != nil {
return err
}
} else {
global.Cron.Remove(cron.EntryID(clam.EntryID))
global.LOG.Infof("stop cronjob entryID: %v", clam.EntryID)
}
return clamRepo.Update(clam.ID, map[string]interface{}{"status": status, "entry_id": entryID})
}
func (c *ClamService) Delete(req dto.ClamDelete) error { func (c *ClamService) Delete(req dto.ClamDelete) error {
for _, id := range req.Ids { for _, id := range req.Ids {
clam, _ := clamRepo.Get(commonRepo.WithByID(id)) clam, _ := clamRepo.Get(commonRepo.WithByID(id))

View file

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

View file

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

View file

@ -56,6 +56,7 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo) toolboxRouter.POST("/clam/base", baseApi.LoadClamBaseInfo)
toolboxRouter.POST("/clam/operate", baseApi.OperateClam) toolboxRouter.POST("/clam/operate", baseApi.OperateClam)
toolboxRouter.POST("/clam/update", baseApi.UpdateClam) toolboxRouter.POST("/clam/update", baseApi.UpdateClam)
toolboxRouter.POST("/clam/status/update", baseApi.UpdateClamStatus)
toolboxRouter.POST("/clam/del", baseApi.DeleteClam) toolboxRouter.POST("/clam/del", baseApi.DeleteClam)
toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan) toolboxRouter.POST("/clam/handle", baseApi.HandleClamScan)
} }

View file

@ -203,8 +203,11 @@ func SudoHandleCmd() string {
} }
func Which(name string) bool { func Which(name string) bool {
_, err := exec.LookPath(name) stdout, err := Execf("which %s", name)
return err == nil if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) {
return false
}
return true
} }
func ExecShellWithTimeOut(cmdStr, workdir string, logger *log.Logger, timeout time.Duration) error { func ExecShellWithTimeOut(cmdStr, workdir string, logger *log.Logger, timeout time.Duration) error {

View file

@ -7,6 +7,10 @@ import (
"net" "net"
"net/http" "net/http"
"time" "time"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
) )
func RemoveTamper(website string) {} func RemoveTamper(website string) {}
@ -27,3 +31,7 @@ func LoadRequestTransport() *http.Transport {
func LoadGpuInfo() []interface{} { func LoadGpuInfo() []interface{} {
return nil return nil
} }
func StartClam(startClam model.Clam, isUpdate bool) (int, error) {
return 0, buserr.New(constant.ErrXpackNotFound)
}

View file

@ -11500,6 +11500,58 @@ const docTemplate = `{
} }
} }
}, },
"/toolbox/clam/status/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam status",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdateStatus"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id",
"status"
],
"formatEN": "change the status of clam [name] to [status].",
"formatZH": "修改扫描规则 [name] 状态为 [status]",
"paramKeys": []
}
}
},
"/toolbox/clam/update": { "/toolbox/clam/update": {
"post": { "post": {
"security": [ "security": [
@ -15570,6 +15622,12 @@ const docTemplate = `{
}, },
"path": { "path": {
"type": "string" "type": "string"
},
"spec": {
"type": "string"
},
"status": {
"type": "string"
} }
} }
}, },
@ -15665,6 +15723,20 @@ const docTemplate = `{
}, },
"path": { "path": {
"type": "string" "type": "string"
},
"spec": {
"type": "string"
}
}
},
"dto.ClamUpdateStatus": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
} }
} }
}, },
@ -18468,7 +18540,7 @@ const docTemplate = `{
"type": "string", "type": "string",
"enum": [ "enum": [
"name", "name",
"status", "state",
"created_at" "created_at"
] ]
}, },
@ -22601,7 +22673,8 @@ const docTemplate = `{
"primary_domain", "primary_domain",
"type", "type",
"status", "status",
"created_at" "created_at",
"expire_date"
] ]
}, },
"page": { "page": {
@ -22619,8 +22692,7 @@ const docTemplate = `{
"type": "object", "type": "object",
"required": [ "required": [
"id", "id",
"primaryDomain", "primaryDomain"
"webSiteGroupID"
], ],
"properties": { "properties": {
"IPV6": { "IPV6": {

View file

@ -11493,6 +11493,58 @@
} }
} }
}, },
"/toolbox/clam/status/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改扫描规则状态",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Update clam status",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamUpdateStatus"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "clams",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id",
"status"
],
"formatEN": "change the status of clam [name] to [status].",
"formatZH": "修改扫描规则 [name] 状态为 [status]",
"paramKeys": []
}
}
},
"/toolbox/clam/update": { "/toolbox/clam/update": {
"post": { "post": {
"security": [ "security": [
@ -15563,6 +15615,12 @@
}, },
"path": { "path": {
"type": "string" "type": "string"
},
"spec": {
"type": "string"
},
"status": {
"type": "string"
} }
} }
}, },
@ -15658,6 +15716,20 @@
}, },
"path": { "path": {
"type": "string" "type": "string"
},
"spec": {
"type": "string"
}
}
},
"dto.ClamUpdateStatus": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"status": {
"type": "string"
} }
} }
}, },
@ -18461,7 +18533,7 @@
"type": "string", "type": "string",
"enum": [ "enum": [
"name", "name",
"status", "state",
"created_at" "created_at"
] ]
}, },
@ -22594,7 +22666,8 @@
"primary_domain", "primary_domain",
"type", "type",
"status", "status",
"created_at" "created_at",
"expire_date"
] ]
}, },
"page": { "page": {
@ -22612,8 +22685,7 @@
"type": "object", "type": "object",
"required": [ "required": [
"id", "id",
"primaryDomain", "primaryDomain"
"webSiteGroupID"
], ],
"properties": { "properties": {
"IPV6": { "IPV6": {

View file

@ -243,6 +243,10 @@ definitions:
type: string type: string
path: path:
type: string type: string
spec:
type: string
status:
type: string
type: object type: object
dto.ClamDelete: dto.ClamDelete:
properties: properties:
@ -305,6 +309,15 @@ definitions:
type: string type: string
path: path:
type: string type: string
spec:
type: string
type: object
dto.ClamUpdateStatus:
properties:
id:
type: integer
status:
type: string
type: object type: object
dto.Clean: dto.Clean:
properties: properties:
@ -2198,7 +2211,7 @@ definitions:
orderBy: orderBy:
enum: enum:
- name - name
- status - state
- created_at - created_at
type: string type: string
page: page:
@ -4974,6 +4987,7 @@ definitions:
- type - type
- status - status
- created_at - created_at
- expire_date
type: string type: string
page: page:
type: integer type: integer
@ -5004,7 +5018,6 @@ definitions:
required: required:
- id - id
- primaryDomain - primaryDomain
- webSiteGroupID
type: object type: object
request.WebsiteUpdateDir: request.WebsiteUpdateDir:
properties: properties:
@ -12767,6 +12780,40 @@ paths:
summary: Page clam summary: Page clam
tags: tags:
- Clam - Clam
/toolbox/clam/status/update:
post:
consumes:
- application/json
description: 修改扫描规则状态
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamUpdateStatus'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update clam status
tags:
- Clam
x-panel-log:
BeforeFunctions:
- db: clams
input_column: id
input_value: id
isList: false
output_column: name
output_value: name
bodyKeys:
- id
- status
formatEN: change the status of clam [name] to [status].
formatZH: 修改扫描规则 [name] 状态为 [status]
paramKeys: []
/toolbox/clam/update: /toolbox/clam/update:
post: post:
consumes: consumes:

View file

@ -1,4 +1,5 @@
import { ReqPage } from '.'; import { ReqPage } from '.';
import { Cronjob } from './cronjob';
export namespace Toolbox { export namespace Toolbox {
export interface DeviceBaseInfo { export interface DeviceBaseInfo {
@ -129,10 +130,14 @@ export namespace Toolbox {
export interface ClamInfo { export interface ClamInfo {
id: number; id: number;
name: string; name: string;
status: string;
path: string; path: string;
infectedStrategy: string; infectedStrategy: string;
infectedDir: string; infectedDir: string;
lastHandleDate: string; lastHandleDate: string;
hasSpec: boolean;
spec: string;
specObj: Cronjob.SpecObj;
description: string; description: string;
} }
export interface ClamCreate { export interface ClamCreate {
@ -140,6 +145,8 @@ export namespace Toolbox {
path: string; path: string;
infectedStrategy: string; infectedStrategy: string;
infectedDir: string; infectedDir: string;
spec: string;
specObj: Cronjob.SpecObj;
description: string; description: string;
} }
export interface ClamUpdate { export interface ClamUpdate {
@ -148,6 +155,8 @@ export namespace Toolbox {
path: string; path: string;
infectedStrategy: string; infectedStrategy: string;
infectedDir: string; infectedDir: string;
spec: string;
specObj: Cronjob.SpecObj;
description: string; description: string;
} }
export interface ClamSearchLog extends ReqPage { export interface ClamSearchLog extends ReqPage {

View file

@ -138,6 +138,9 @@ export const createClam = (params: Toolbox.ClamCreate) => {
export const updateClam = (params: Toolbox.ClamUpdate) => { export const updateClam = (params: Toolbox.ClamUpdate) => {
return http.post(`/toolbox/clam/update`, params); return http.post(`/toolbox/clam/update`, params);
}; };
export const updateClamStatus = (id: number, status: string) => {
return http.post(`/toolbox/clam/status/update`, { id: id, status: status });
};
export const deleteClam = (params: { ids: number[]; removeRecord: boolean; removeInfected: boolean }) => { export const deleteClam = (params: { ids: number[]; removeRecord: boolean; removeInfected: boolean }) => {
return http.post(`/toolbox/clam/del`, params); return http.post(`/toolbox/clam/del`, params);
}; };

View file

@ -1082,6 +1082,13 @@ const message = {
}, },
clam: { clam: {
clam: 'Virus Scan', clam: 'Virus Scan',
cron: 'Scheduled scan',
cronHelper: 'Professional version supports scheduled scan feature',
specErr: 'Execution schedule format error, please check and retry!',
disableMsg:
'Stopping scheduled execution will prevent this scan task from running automatically. Do you want to continue?',
enableMsg:
'Enabling scheduled execution will allow this scan task to run automatically at regular intervals. Do you want to continue?',
showFresh: 'Show Virus Database Service', showFresh: 'Show Virus Database Service',
hideFresh: 'Hide Virus Database Service', hideFresh: 'Hide Virus Database Service',
clamHelper: clamHelper:
@ -1577,6 +1584,7 @@ const message = {
recoverDetail: 'Recover detail', recoverDetail: 'Recover detail',
createSnapshot: 'Create Snapshot', createSnapshot: 'Create Snapshot',
importSnapshot: 'Sync Snapshot', importSnapshot: 'Sync Snapshot',
importHelper: 'Snapshot directory:',
recover: 'Recover', recover: 'Recover',
lastRecoverAt: 'Last recovery time', lastRecoverAt: 'Last recovery time',
lastRollbackAt: 'Last rollback time', lastRollbackAt: 'Last rollback time',

View file

@ -1023,6 +1023,11 @@ const message = {
}, },
clam: { clam: {
clam: '病毒掃描', clam: '病毒掃描',
cron: '定時掃描',
cronHelper: '專業版支持定時掃描功能',
specErr: '執行周期格式錯誤請檢查後重試',
disableMsg: '停止定時執行會導致該掃描任務不再自動執行是否繼續',
enableMsg: '啟用定時執行會讓該掃描任務定期自動執行是否繼續',
showFresh: '顯示病毒庫服務', showFresh: '顯示病毒庫服務',
hideFresh: '隱藏病毒庫服務', hideFresh: '隱藏病毒庫服務',
clamHelper: clamHelper:
@ -1395,6 +1400,7 @@ const message = {
recoverDetail: '恢復詳情', recoverDetail: '恢復詳情',
createSnapshot: '創建快照', createSnapshot: '創建快照',
importSnapshot: '同步快照', importSnapshot: '同步快照',
importHelper: '快照文件目錄',
recover: '恢復', recover: '恢復',
lastRecoverAt: '上次恢復時間', lastRecoverAt: '上次恢復時間',
lastRollbackAt: '上次回滾時間', lastRollbackAt: '上次回滾時間',

View file

@ -1024,6 +1024,11 @@ const message = {
}, },
clam: { clam: {
clam: '病毒扫描', clam: '病毒扫描',
cron: '定时扫描',
cronHelper: '专业版支持定时扫描功能 ',
specErr: '执行周期格式错误请检查后重试',
disableMsg: '停止定时执行会导致该扫描任务不再自动执行是否继续',
enableMsg: '启用定时执行会让该扫描任务定期自动执行是否继续',
showFresh: '显示病毒库服务', showFresh: '显示病毒库服务',
hideFresh: '隐藏病毒库服务', hideFresh: '隐藏病毒库服务',
clamHelper: clamHelper:
@ -1397,6 +1402,7 @@ const message = {
recoverDetail: '恢复详情', recoverDetail: '恢复详情',
createSnapshot: '创建快照', createSnapshot: '创建快照',
importSnapshot: '同步快照', importSnapshot: '同步快照',
importHelper: '快照文件目录',
recover: '恢复', recover: '恢复',
lastRecoverAt: '上次恢复时间', lastRecoverAt: '上次恢复时间',
lastRollbackAt: '上次回滚时间', lastRollbackAt: '上次回滚时间',

View file

@ -16,6 +16,10 @@
:label="item.label" :label="item.label"
/> />
</el-select> </el-select>
<div v-if="form.from === 'LOCAL'">
<span class="import-help">{{ $t('setting.importHelper') }}</span>
<span @click="toFolder()" class="import-link-help">{{ backupPath }}</span>
</div>
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="names"> <el-form-item :label="$t('commons.table.name')" prop="names">
<el-select style="width: 100%" v-model="form.names" multiple clearable> <el-select style="width: 100%" v-model="form.names" multiple clearable>
@ -57,6 +61,7 @@ import { snapshotImport } from '@/api/modules/setting';
import { getBackupList, getFilesFromBackup } from '@/api/modules/setting'; import { getBackupList, getFilesFromBackup } from '@/api/modules/setting';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import router from '@/routers';
const drawerVisible = ref(false); const drawerVisible = ref(false);
const loading = ref(); const loading = ref();
@ -65,6 +70,7 @@ const formRef = ref();
const backupOptions = ref(); const backupOptions = ref();
const fileNames = ref(); const fileNames = ref();
const existNames = ref(); const existNames = ref();
const backupPath = ref('');
const form = reactive({ const form = reactive({
from: '', from: '',
@ -102,6 +108,9 @@ const checkDisable = (val: string) => {
} }
return false; return false;
}; };
const toFolder = async () => {
router.push({ path: '/hosts/files', query: { path: backupPath.value } });
};
const submitImport = async (formEl: FormInstance | undefined) => { const submitImport = async (formEl: FormInstance | undefined) => {
loading.value = true; loading.value = true;
@ -131,6 +140,10 @@ const loadBackups = async () => {
if (item.id !== 0) { if (item.id !== 0) {
backupOptions.value.push({ label: i18n.global.t('setting.' + item.type), value: item.type }); backupOptions.value.push({ label: i18n.global.t('setting.' + item.type), value: item.type });
} }
if (item.type === 'LOCAL') {
item.varsJson = JSON.parse(item.vars);
backupPath.value = item.varsJson['dir'] + '/system_snapshot';
}
} }
}) })
.catch(() => { .catch(() => {
@ -148,3 +161,18 @@ defineExpose({
acceptParams, acceptParams,
}); });
</script> </script>
<style lang="scss" scoped>
.import-help {
font-size: 12px;
color: #8f959e;
}
.import-link-help {
color: $primary-color;
cursor: pointer;
}
.import-link-help:hover {
opacity: 0.6;
}
</style>

View file

@ -405,10 +405,17 @@ const search = async () => {
page: paginationConfig.currentPage, page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize, pageSize: paginationConfig.pageSize,
}; };
const res = await searchSnapshotPage(params); loading.value = true;
await searchSnapshotPage(params)
.then((res) => {
loading.value = false;
cleanData.value = false; cleanData.value = false;
data.value = res.data.items || []; data.value = res.data.items || [];
paginationConfig.total = res.data.total; paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
}; };
onMounted(() => { onMounted(() => {

View file

@ -56,6 +56,7 @@
:label="$t('commons.table.name')" :label="$t('commons.table.name')"
:min-width="60" :min-width="60"
prop="name" prop="name"
sortable
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #default="{ row }">
@ -74,6 +75,47 @@
<el-button link type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button> <el-button link type="primary" @click="toFolder(row.path)">{{ row.path }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column
v-if="isProductPro"
:label="$t('commons.table.status')"
:min-width="70"
prop="status"
sortable
>
<template #default="{ row }">
<el-button
v-if="row.status === 'Enable'"
@click="onChangeStatus(row.id, 'disable')"
link
icon="VideoPlay"
type="success"
>
{{ $t('commons.status.enabled') }}
</el-button>
<el-button
v-if="row.status === 'Disable'"
icon="VideoPause"
link
type="danger"
@click="onChangeStatus(row.id, 'enable')"
>
{{ $t('commons.status.disabled') }}
</el-button>
<span v-if="row.status === ''">-</span>
</template>
</el-table-column>
<el-table-column
v-if="isProductPro"
:label="$t('cronjob.cronSpec')"
show-overflow-tooltip
:min-width="120"
>
<template #default="{ row }">
<span>
{{ row.spec !== '' ? transSpecToStr(row.spec) : '-' }}
</span>
</template>
</el-table-column>
<el-table-column <el-table-column
:label="$t('toolbox.clam.infectedDir')" :label="$t('toolbox.clam.infectedDir')"
:min-width="120" :min-width="120"
@ -138,17 +180,22 @@
import { onMounted, reactive, ref } from 'vue'; import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { deleteClam, handleClamScan, searchClam, updateClam } from '@/api/modules/toolbox'; import { deleteClam, handleClamScan, searchClam, updateClam, updateClamStatus } from '@/api/modules/toolbox';
import OperateDialog from '@/views/toolbox/clam/operate/index.vue'; import OperateDialog from '@/views/toolbox/clam/operate/index.vue';
import LogDialog from '@/views/toolbox/clam/record/index.vue'; import LogDialog from '@/views/toolbox/clam/record/index.vue';
import ClamStatus from '@/views/toolbox/clam/status/index.vue'; import ClamStatus from '@/views/toolbox/clam/status/index.vue';
import SettingDialog from '@/views/toolbox/clam/setting/index.vue'; import SettingDialog from '@/views/toolbox/clam/setting/index.vue';
import { Toolbox } from '@/api/interface/toolbox'; import { Toolbox } from '@/api/interface/toolbox';
import router from '@/routers'; import router from '@/routers';
import { transSpecToStr } from '../../cronjob/helper';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const loading = ref(); const loading = ref();
const selects = ref<any>([]); const selects = ref<any>([]);
const globalStore = GlobalStore();
const { isProductPro } = storeToRefs(globalStore);
const data = ref(); const data = ref();
const paginationConfig = reactive({ const paginationConfig = reactive({
cacheSizeKey: 'clam-page-size', cacheSizeKey: 'clam-page-size',
@ -176,12 +223,16 @@ const clamStatus = ref({
isRunning: true, isRunning: true,
}); });
const search = async () => { const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
loading.value = true; loading.value = true;
let params = { let params = {
info: searchName.value, info: searchName.value,
page: paginationConfig.currentPage, page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize, pageSize: paginationConfig.pageSize,
orderBy: paginationConfig.orderBy,
order: paginationConfig.order,
}; };
await searchClam(params) await searchClam(params)
.then((res) => { .then((res) => {
@ -218,6 +269,14 @@ const onOpenDialog = async (
title: string, title: string,
rowData: Partial<Toolbox.ClamInfo> = { rowData: Partial<Toolbox.ClamInfo> = {
infectedStrategy: 'none', infectedStrategy: 'none',
specObj: {
specType: 'perDay',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
},
}, },
) => { ) => {
let params = { let params = {
@ -272,6 +331,18 @@ const onSubmitDelete = async () => {
}); });
}; };
const onChangeStatus = async (id: number, status: string) => {
ElMessageBox.confirm(i18n.global.t('toolbox.clam.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
await updateClamStatus(id, itemStatus);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});
};
const buttons = [ const buttons = [
{ {
label: i18n.global.t('commons.button.handle'), label: i18n.global.t('commons.button.handle'),

View file

@ -50,6 +50,77 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="hasSpec">
<el-checkbox v-model="dialogData.rowData!.hasSpec" :label="$t('toolbox.clam.cron')" />
</el-form-item>
<el-form-item v-if="dialogData.rowData!.hasSpec && !isProductPro">
<span>{{ $t('toolbox.clam.cronHelper') }}</span>
<el-button link type="primary" @click="toUpload">
{{ $t('license.levelUpPro') }}
</el-button>
</el-form-item>
<el-form-item prop="spec" v-if="dialogData.rowData!.hasSpec && isProductPro">
<el-select
class="specTypeClass"
v-model="dialogData.rowData!.specObj.specType"
@change="changeSpecType()"
>
<el-option
v-for="item in specOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-select
v-if="dialogData.rowData!.specObj.specType === 'perWeek'"
class="specClass"
v-model="dialogData.rowData!.specObj.week"
>
<el-option
v-for="item in weekOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
<el-input
v-if="hasDay(dialogData.rowData!.specObj)"
class="specClass"
v-model.number="dialogData.rowData!.specObj.day"
>
<template #append>
<div class="append">{{ $t('cronjob.day') }}</div>
</template>
</el-input>
<el-input
v-if="hasHour(dialogData.rowData!.specObj)"
class="specClass"
v-model.number="dialogData.rowData!.specObj.hour"
>
<template #append>
<div class="append">{{ $t('commons.units.hour') }}</div>
</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specObj.specType !== 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.specObj.minute"
>
<template #append>
<div class="append">{{ $t('commons.units.minute') }}</div>
</template>
</el-input>
<el-input
v-if="dialogData.rowData!.specObj.specType === 'perNSecond'"
class="specClass"
v-model.number="dialogData.rowData!.specObj.second"
>
<template #append>
<div class="append">{{ $t('commons.units.second') }}</div>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description"> <el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" /> <el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" />
</el-form-item> </el-form-item>
@ -64,6 +135,7 @@
</el-button> </el-button>
</span> </span>
</template> </template>
<LicenseImport ref="licenseRef" />
</el-drawer> </el-drawer>
</template> </template>
@ -73,11 +145,18 @@ import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue'; import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
import LicenseImport from '@/components/license-import/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import { Toolbox } from '@/api/interface/toolbox'; import { Toolbox } from '@/api/interface/toolbox';
import { createClam, updateClam } from '@/api/modules/toolbox'; import { createClam, updateClam } from '@/api/modules/toolbox';
import { specOptions, transObjToSpec, transSpecToObj, weekOptions } from '../../../cronjob/helper';
import { storeToRefs } from 'pinia';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const licenseRef = ref();
const { isProductPro } = storeToRefs(globalStore);
interface DialogProps { interface DialogProps {
title: string; title: string;
rowData?: Toolbox.ClamInfo; rowData?: Toolbox.ClamInfo;
@ -92,6 +171,19 @@ const dialogData = ref<DialogProps>({
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
dialogData.value = params; dialogData.value = params;
if (dialogData.value.rowData?.spec) {
dialogData.value.rowData.hasSpec = true;
dialogData.value.rowData.specObj = transSpecToObj(dialogData.value.rowData.spec);
} else {
dialogData.value.rowData.specObj = {
specType: 'perDay',
week: 1,
day: 3,
hour: 1,
minute: 30,
second: 30,
};
}
title.value = i18n.global.t('commons.button.' + dialogData.value.title); title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisible.value = true; drawerVisible.value = true;
}; };
@ -101,9 +193,97 @@ const handleClose = () => {
drawerVisible.value = false; drawerVisible.value = false;
}; };
const verifySpec = (rule: any, value: any, callback: any) => {
let item = dialogData.value.rowData!.specObj;
if (
!Number.isInteger(item.day) ||
!Number.isInteger(item.hour) ||
!Number.isInteger(item.minute) ||
!Number.isInteger(item.second) ||
!Number.isInteger(item.week)
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
switch (item.specType) {
case 'perMonth':
if (
item.day < 0 ||
item.day > 31 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perNDay':
if (
item.day < 0 ||
item.day > 366 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perWeek':
if (
item.week < 0 ||
item.week > 6 ||
item.hour < 0 ||
item.hour > 23 ||
item.minute < 0 ||
item.minute > 59
) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perDay':
if (item.hour < 0 || item.hour > 23 || item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perNHour':
if (item.hour < 0 || item.hour > 8784 || item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perHour':
if (item.minute < 0 || item.minute > 59) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
case 'perNMinute':
if (item.minute < 0 || item.minute > 527040) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
case 'perNSecond':
if (item.second < 0 || item.second > 31622400) {
callback(new Error(i18n.global.t('cronjob.specErr')));
return;
}
break;
}
callback();
};
const rules = reactive({ const rules = reactive({
name: [Rules.simpleName], name: [Rules.simpleName],
path: [Rules.requiredInput, Rules.noSpace], path: [Rules.requiredInput, Rules.noSpace],
spec: [
{ validator: verifySpec, trigger: 'blur', required: true },
{ validator: verifySpec, trigger: 'change', required: true },
],
}); });
type FormInstance = InstanceType<typeof ElForm>; type FormInstance = InstanceType<typeof ElForm>;
@ -120,12 +300,62 @@ const loadDir = async (path: string) => {
const loadInfectedDir = async (path: string) => { const loadInfectedDir = async (path: string) => {
dialogData.value.rowData!.infectedDir = path; dialogData.value.rowData!.infectedDir = path;
}; };
const hasDay = (item: any) => {
return item.specType === 'perMonth' || item.specType === 'perNDay';
};
const hasHour = (item: any) => {
return item.specType !== 'perHour' && item.specType !== 'perNMinute' && item.specType !== 'perNSecond';
};
const toUpload = () => {
licenseRef.value.acceptParams();
};
const changeSpecType = () => {
let item = dialogData.value.rowData!.specObj;
switch (item.specType) {
case 'perMonth':
case 'perNDay':
item.day = 3;
item.hour = 1;
item.minute = 30;
break;
case 'perWeek':
item.week = 1;
item.hour = 1;
item.minute = 30;
break;
case 'perDay':
case 'perNHour':
item.hour = 2;
item.minute = 30;
break;
case 'perHour':
case 'perNMinute':
item.minute = 30;
break;
case 'perNSecond':
item.second = 30;
break;
}
};
const onSubmit = async (formEl: FormInstance | undefined) => { const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; if (!valid) return;
loading.value = true; loading.value = true;
let spec = '';
let item = dialogData.value.rowData.specObj;
if (dialogData.value.rowData!.hasSpec) {
spec = transObjToSpec(item.specType, item.week, item.day, item.hour, item.minute, item.second);
if (spec === '') {
MsgError(i18n.global.t('cronjob.cronSpecHelper'));
return;
}
}
dialogData.value.rowData.spec = spec;
if (dialogData.value.title === 'edit') { if (dialogData.value.title === 'edit') {
await updateClam(dialogData.value.rowData) await updateClam(dialogData.value.rowData)
.then(() => { .then(() => {
@ -158,3 +388,31 @@ defineExpose({
acceptParams, acceptParams,
}); });
</script> </script>
<style scoped lang="scss">
.specClass {
width: 20% !important;
margin-left: 20px;
.append {
width: 20px;
}
}
@media only screen and (max-width: 1000px) {
.specClass {
width: 100% !important;
margin-top: 20px;
margin-left: 0;
.append {
width: 43px;
}
}
}
.specTypeClass {
width: 22% !important;
}
@media only screen and (max-width: 1000px) {
.specTypeClass {
width: 100% !important;
}
}
</style>