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

pull/5851/head
ssongliu 4 months ago committed by GitHub
parent ca0c96cb12
commit 3c0dc7459c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

@ -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"`

@ -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"`
} }

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

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

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

@ -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
},
}

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

@ -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 {

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

@ -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": {

@ -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": {

@ -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:

@ -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 {

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

@ -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',

@ -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: '',

@ -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: '',

@ -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>

@ -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;
cleanData.value = false; await searchSnapshotPage(params)
data.value = res.data.items || []; .then((res) => {
paginationConfig.total = res.data.total; loading.value = false;
cleanData.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
}; };
onMounted(() => { onMounted(() => {

@ -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'),

@ -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>

Loading…
Cancel
Save