feat: 病毒扫描增加病毒库刷新状态 (#5710)

This commit is contained in:
ssongliu 2024-07-08 15:47:23 +08:00 committed by GitHub
parent 0c2653d270
commit 577dfadb9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 400 additions and 18 deletions

View File

@ -190,12 +190,12 @@ func (b *BaseApi) LoadClamRecordLog(c *gin.Context) {
// @Summary Load clam file
// @Description 获取扫描文件
// @Accept json
// @Param request body dto.OperationWithName true "request"
// @Param request body dto.ClamFileReq 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
var req dto.ClamFileReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}

View File

@ -8,6 +8,10 @@ type ClamBaseInfo struct {
Version string `json:"version"`
IsActive bool `json:"isActive"`
IsExist bool `json:"isExist"`
FreshVersion string `json:"freshVersion"`
FreshIsActive bool `json:"freshIsActive"`
FreshIsExist bool `json:"freshIsExist"`
}
type ClamInfo struct {
@ -36,6 +40,11 @@ type ClamLogReq struct {
RecordName string `json:"recordName"`
}
type ClamFileReq struct {
Tail string `json:"tail"`
Name string `json:"name" validate:"required"`
}
type ClamLog struct {
Name string `json:"name"`
ScanDate string `json:"scanDate"`

View File

@ -26,6 +26,7 @@ import (
const (
clamServiceNameCentOs = "clamd@scan.service"
clamServiceNameUbuntu = "clamav-daemon.service"
freshClamService = "clamav-freshclam.service"
resultDir = "clamav"
)
@ -41,7 +42,7 @@ type IClamService interface {
Update(req dto.ClamUpdate) error
Delete(req dto.ClamDelete) error
HandleOnce(req dto.OperateByID) error
LoadFile(req dto.OperationWithName) (string, error)
LoadFile(req dto.ClamFileReq) (string, error)
UpdateFile(req dto.UpdateByNameAndFile) error
LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error)
CleanRecord(req dto.OperateByID) error
@ -56,6 +57,7 @@ func NewIClamService() IClamService {
func (f *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
var baseInfo dto.ClamBaseInfo
baseInfo.Version = "-"
baseInfo.FreshVersion = "-"
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
if exist1 {
f.serviceName = clamServiceNameCentOs
@ -68,6 +70,11 @@ func (f *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
baseInfo.IsExist = true
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
}
freshExist, _ := systemctl.IsExist(freshClamService)
if freshExist {
baseInfo.FreshIsExist = true
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService)
}
if baseInfo.IsActive {
version, err := cmd.Exec("clamdscan --version")
@ -80,6 +87,17 @@ func (f *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
}
}
if baseInfo.FreshIsActive {
version, err := cmd.Exec("freshclam --version")
if err != nil {
return baseInfo, nil
}
if strings.Contains(version, "/") {
baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
} else {
baseInfo.FreshVersion = strings.TrimPrefix(version, "ClamAV ")
}
}
return baseInfo, nil
}
@ -91,6 +109,12 @@ func (f *ClamService) Operate(operate string) error {
return fmt.Errorf("%s the %s failed, err: %s", operate, f.serviceName, stdout)
}
return nil
case "fresh-start", "fresh-restart", "fresh-stop":
stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService)
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)
}
@ -296,7 +320,7 @@ func (u *ClamService) CleanRecord(req dto.OperateByID) error {
return nil
}
func (u *ClamService) LoadFile(req dto.OperationWithName) (string, error) {
func (u *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
filePath := ""
switch req.Name {
case "clamd":
@ -306,6 +330,10 @@ func (u *ClamService) LoadFile(req dto.OperationWithName) (string, error) {
filePath = "/etc/clamd.d/scan.conf"
}
case "clamd-log":
filePath = u.loadLogPath("clamd-log")
if len(filePath) != 0 {
break
}
if u.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/clamav.log"
} else {
@ -318,6 +346,10 @@ func (u *ClamService) LoadFile(req dto.OperationWithName) (string, error) {
filePath = "/etc/freshclam.conf"
}
case "freshclam-log":
filePath = u.loadLogPath("freshclam-log")
if len(filePath) != 0 {
break
}
if u.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/freshclam.log"
} else {
@ -329,11 +361,18 @@ func (u *ClamService) LoadFile(req dto.OperationWithName) (string, error) {
if _, err := os.Stat(filePath); err != nil {
return "", buserr.New("ErrHttpReqNotFound")
}
content, err := os.ReadFile(filePath)
if err != nil {
return "", err
var tail string
if req.Tail != "0" {
tail = req.Tail
} else {
tail = "+1"
}
return string(content), nil
cmd := exec.Command("tail", "-n", tail, filePath)
stdout, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
}
return string(stdout), nil
}
func (u *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
@ -419,3 +458,42 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
}
return data
}
func (u *ClamService) loadLogPath(name string) string {
confPath := ""
if name == "clamd-log" {
if u.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/clamd.conf"
} else {
confPath = "/etc/clamd.d/scan.conf"
}
} else {
if u.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/freshclam.conf"
} else {
confPath = "/etc/freshclam.conf"
}
}
if _, err := os.Stat(confPath); err != nil {
return ""
}
content, err := os.ReadFile(confPath)
if err != nil {
return ""
}
lines := strings.Split(string(content), "\n")
if name == "clamd-log" {
for _, line := range lines {
if strings.HasPrefix(line, "LogFile ") {
return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ")
}
}
} else {
for _, line := range lines {
if strings.HasPrefix(line, "UpdateLogFile ") {
return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ")
}
}
}
return ""
}

View File

@ -11212,7 +11212,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperationWithName"
"$ref": "#/definitions/dto.ClamFileReq"
}
}
],
@ -12928,6 +12928,57 @@ const docTemplate = `{
}
}
},
"/websites/ca/download": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "下载 CA 证书文件",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Download CA file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteResourceReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_cas",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "download ca file [name]",
"formatZH": "下载 CA 证书文件 [name]",
"paramKeys": []
}
}
},
"/websites/ca/obtain": {
"post": {
"security": [
@ -15482,6 +15533,15 @@ const docTemplate = `{
"dto.ClamBaseInfo": {
"type": "object",
"properties": {
"freshIsActive": {
"type": "boolean"
},
"freshIsExist": {
"type": "boolean"
},
"freshVersion": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
@ -15533,6 +15593,20 @@ const docTemplate = `{
}
}
},
"dto.ClamFileReq": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"tail": {
"type": "string"
}
}
},
"dto.ClamLogReq": {
"type": "object",
"properties": {
@ -19778,6 +19852,9 @@ const docTemplate = `{
"group": {
"type": "string"
},
"isDetail": {
"type": "boolean"
},
"isDir": {
"type": "boolean"
},
@ -20610,6 +20687,9 @@ const docTemplate = `{
"path"
],
"properties": {
"isDetail": {
"type": "boolean"
},
"path": {
"type": "string"
}
@ -20765,6 +20845,9 @@ const docTemplate = `{
"expand": {
"type": "boolean"
},
"isDetail": {
"type": "boolean"
},
"page": {
"type": "integer"
},
@ -22265,6 +22348,9 @@ const docTemplate = `{
"additionalProperties": {
"type": "string"
}
},
"sni": {
"type": "boolean"
}
}
},
@ -22833,6 +22919,9 @@ const docTemplate = `{
"group": {
"type": "string"
},
"isDetail": {
"type": "boolean"
},
"isDir": {
"type": "boolean"
},

View File

@ -11205,7 +11205,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.OperationWithName"
"$ref": "#/definitions/dto.ClamFileReq"
}
}
],
@ -12921,6 +12921,57 @@
}
}
},
"/websites/ca/download": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "下载 CA 证书文件",
"consumes": [
"application/json"
],
"tags": [
"Website CA"
],
"summary": "Download CA file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteResourceReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "website_cas",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "download ca file [name]",
"formatZH": "下载 CA 证书文件 [name]",
"paramKeys": []
}
}
},
"/websites/ca/obtain": {
"post": {
"security": [
@ -15475,6 +15526,15 @@
"dto.ClamBaseInfo": {
"type": "object",
"properties": {
"freshIsActive": {
"type": "boolean"
},
"freshIsExist": {
"type": "boolean"
},
"freshVersion": {
"type": "string"
},
"isActive": {
"type": "boolean"
},
@ -15526,6 +15586,20 @@
}
}
},
"dto.ClamFileReq": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"tail": {
"type": "string"
}
}
},
"dto.ClamLogReq": {
"type": "object",
"properties": {
@ -19771,6 +19845,9 @@
"group": {
"type": "string"
},
"isDetail": {
"type": "boolean"
},
"isDir": {
"type": "boolean"
},
@ -20603,6 +20680,9 @@
"path"
],
"properties": {
"isDetail": {
"type": "boolean"
},
"path": {
"type": "string"
}
@ -20758,6 +20838,9 @@
"expand": {
"type": "boolean"
},
"isDetail": {
"type": "boolean"
},
"page": {
"type": "integer"
},
@ -22258,6 +22341,9 @@
"additionalProperties": {
"type": "string"
}
},
"sni": {
"type": "boolean"
}
}
},
@ -22826,6 +22912,9 @@
"group": {
"type": "string"
},
"isDetail": {
"type": "boolean"
},
"isDir": {
"type": "boolean"
},

View File

@ -218,6 +218,12 @@ definitions:
type: object
dto.ClamBaseInfo:
properties:
freshIsActive:
type: boolean
freshIsExist:
type: boolean
freshVersion:
type: string
isActive:
type: boolean
isExist:
@ -251,6 +257,15 @@ definitions:
required:
- ids
type: object
dto.ClamFileReq:
properties:
name:
type: string
tail:
type: string
required:
- name
type: object
dto.ClamLogReq:
properties:
clamName:
@ -3119,6 +3134,8 @@ definitions:
type: string
group:
type: string
isDetail:
type: boolean
isDir:
type: boolean
isHidden:
@ -3668,6 +3685,8 @@ definitions:
type: object
request.FileContentReq:
properties:
isDetail:
type: boolean
path:
type: string
required:
@ -3773,6 +3792,8 @@ definitions:
type: boolean
expand:
type: boolean
isDetail:
type: boolean
page:
type: integer
pageSize:
@ -4780,6 +4801,8 @@ definitions:
additionalProperties:
type: string
type: object
sni:
type: boolean
required:
- id
- match
@ -5167,6 +5190,8 @@ definitions:
type: string
group:
type: string
isDetail:
type: boolean
isDir:
type: boolean
isHidden:
@ -12558,7 +12583,7 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/dto.OperationWithName'
$ref: '#/definitions/dto.ClamFileReq'
responses:
"200":
description: OK
@ -13665,6 +13690,39 @@ paths:
formatEN: Delete website ca [name]
formatZH: 删除网站 ca [name]
paramKeys: []
/websites/ca/download:
post:
consumes:
- application/json
description: 下载 CA 证书文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteResourceReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Download CA file
tags:
- Website CA
x-panel-log:
BeforeFunctions:
- db: website_cas
input_column: id
input_value: id
isList: false
output_column: name
output_value: name
bodyKeys:
- id
formatEN: download ca file [name]
formatZH: 下载 CA 证书文件 [name]
paramKeys: []
/websites/ca/obtain:
post:
consumes:

View File

@ -121,6 +121,10 @@ export namespace Toolbox {
version: string;
isActive: boolean;
isExist: boolean;
freshVersion: string;
freshIsExist: boolean;
freshIsActive: boolean;
}
export interface ClamInfo {
id: number;

View File

@ -117,8 +117,8 @@ export const searchClamRecord = (param: Toolbox.ClamSearchLog) => {
export const getClamRecordLog = (param: Toolbox.ClamRecordReq) => {
return http.post<string>(`/toolbox/clam/record/log`, param);
};
export const searchClamFile = (name: string) => {
return http.post<string>(`/toolbox/clam/file/search`, { name: name });
export const searchClamFile = (name: string, tail: string) => {
return http.post<string>(`/toolbox/clam/file/search`, { name: name, tail: tail });
};
export const updateClamFile = (name: string, file: string) => {
return http.post(`/toolbox/clam/file/update`, { name: name, file: file });

View File

@ -1074,6 +1074,8 @@ const message = {
},
clam: {
clam: 'Virus Scan',
showFresh: 'Show Virus Database Service',
hideFresh: 'Hide Virus Database Service',
clamHelper:
'The minimum recommended configuration for ClamAV is: 3 GiB of RAM or more, single-core CPU with 2.0 GHz or higher, and at least 5 GiB of available hard disk space.',
noClam: 'ClamAV service not detected, please refer to the official documentation for installation!',

View File

@ -1016,6 +1016,8 @@ const message = {
},
clam: {
clam: '病毒掃描',
showFresh: '顯示病毒庫服務',
hideFresh: '隱藏病毒庫服務',
clamHelper:
'ClamAV 的最低建議配置為3 GiB 以上的 RAM2.0 GHz 以上的單核 CPU以及至少 5 GiB 的可用硬盤空間',
noClam: '未檢測到 ClamAV 服務請參考官方文檔進行安裝',

View File

@ -1017,6 +1017,8 @@ const message = {
},
clam: {
clam: '病毒扫描',
showFresh: '显示病毒库服务',
hideFresh: '隐藏病毒库服务',
clamHelper:
'ClamAV 的最低建议配置为3 GiB 以上的 RAM2.0 GHz 以上的单核 CPU以及至少 5 GiB 的可用硬盘空间',
doc: '帮助文档',

View File

@ -29,6 +29,15 @@
<template #main>
<div>
<el-select style="width: 20%" @change="search" v-model.number="tail">
<template #prefix>{{ $t('toolbox.clam.scanResult') }}</template>
<el-option :value="0" :label="$t('commons.table.all')" />
<el-option :value="10" :label="10" />
<el-option :value="100" :label="100" />
<el-option :value="200" :label="200" />
<el-option :value="500" :label="500" />
<el-option :value="1000" :label="1000" />
</el-select>
<codemirror
:autofocus="true"
:placeholder="$t('commons.msg.noneData')"
@ -75,13 +84,14 @@ const handleReady = (payload) => {
};
const activeName = ref('clamd');
const tail = ref(0);
const content = ref();
const confirmRef = ref();
const loadHeight = () => {
let height = globalStore.openMenuTabs ? '405px' : '375px';
let height = globalStore.openMenuTabs ? '425px' : '395px';
if (canUpdate()) {
height = globalStore.openMenuTabs ? '363px' : '333px';
height = globalStore.openMenuTabs ? '383px' : '353px';
}
return height;
};
@ -93,7 +103,7 @@ const canUpdate = () => {
const search = async (itemName: string) => {
loading.value = true;
activeName.value = itemName;
await searchClamFile(activeName.value)
await searchClamFile(activeName.value, tail.value + '')
.then((res) => {
loading.value = false;
content.value = res.data;

View File

@ -3,14 +3,14 @@
<div class="app-status tool-status" v-if="data.isExist">
<el-card>
<div>
<el-tag effect="dark" type="success">ClamAV</el-tag>
<el-tag class="w-17" effect="dark" type="success">ClamAV</el-tag>
<el-tag round class="status-content" v-if="data.isActive" type="success">
{{ $t('commons.status.running') }}
</el-tag>
<el-tag round class="status-content" v-if="!data.isActive" type="info">
{{ $t('commons.status.stopped') }}
</el-tag>
<el-tag class="status-content">{{ $t('app.version') }}:{{ data.version }}</el-tag>
<el-tag class="status-content w-24">{{ $t('app.version') }}:{{ data.version }}</el-tag>
<span class="buttons">
<el-button type="primary" v-if="!data.isActive" link @click="onOperate('start')">
{{ $t('app.start') }}
@ -26,6 +26,35 @@
<el-button type="primary" link @click="setting">
{{ $t('commons.button.set') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" v-if="showFresh" link @click="changeShow(false)">
{{ $t('toolbox.clam.hideFresh') }}
</el-button>
<el-button type="primary" v-if="!showFresh" link @click="changeShow(true)">
{{ $t('toolbox.clam.showFresh') }}
</el-button>
</span>
</div>
<div class="mt-4" v-if="showFresh">
<el-tag class="w-16" effect="dark" type="success">FreshClam</el-tag>
<el-tag round class="status-content" v-if="data.freshIsActive" type="success">
{{ $t('commons.status.running') }}
</el-tag>
<el-tag round class="status-content" v-if="!data.freshIsActive" type="info">
{{ $t('commons.status.stopped') }}
</el-tag>
<el-tag class="status-content w-24">{{ $t('app.version') }}:{{ data.freshVersion }}</el-tag>
<span class="buttons">
<el-button type="primary" v-if="!data.freshIsActive" link @click="onOperate('fresh-start')">
{{ $t('app.start') }}
</el-button>
<el-button type="primary" v-if="data.freshIsActive" link @click="onOperate('fresh-stop')">
{{ $t('app.stop') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" link @click="onOperate('fresh-restart')">
{{ $t('app.restart') }}
</el-button>
</span>
</div>
</el-card>
@ -59,8 +88,13 @@ const data = ref({
isExist: false,
isActive: false,
version: '',
freshIsExist: false,
freshIsActive: false,
freshVersion: '',
});
const loading = ref(false);
const showFresh = ref(localStorage.getItem('clam-fresh-show') !== 'hide');
const em = defineEmits(['setting', 'getStatus', 'update:loading', 'update:maskShow']);
@ -72,6 +106,11 @@ const toDoc = async () => {
window.open('https://1panel.cn/docs/user_manual/toolbox/clam/', '_blank', 'noopener,noreferrer');
};
const changeShow = (val: boolean) => {
showFresh.value = val;
localStorage.setItem('clam-fresh-show', showFresh.value ? 'show' : 'hide');
};
const onOperate = async (operation: string) => {
em('update:maskShow', false);
ElMessageBox.confirm(