Merge pull request #119 from lanrenwo/dev

新增审计日志的搜索、导出功能
This commit is contained in:
bjdgyc 2022-07-25 20:02:21 +08:00 committed by GitHub
commit 6722541fa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 305 additions and 8 deletions

View File

@ -5,6 +5,7 @@ import (
"strconv"
"github.com/bjdgyc/anylink/dbdata"
"github.com/gocarina/gocsv"
)
func SetAuditList(w http.ResponseWriter, r *http.Request) {
@ -14,15 +15,13 @@ func SetAuditList(w http.ResponseWriter, r *http.Request) {
if page < 1 {
page = 1
}
var datas []dbdata.AccessAudit
count := dbdata.CountAll(&dbdata.AccessAudit{})
err := dbdata.Find(&datas, dbdata.PageSize, page)
session := dbdata.GetAuditSession(r.FormValue("search"))
count, err := dbdata.FindAndCount(session, &datas, dbdata.PageSize, page)
if err != nil && !dbdata.CheckErrNotFound(err) {
RespError(w, RespInternalErr, err)
return
}
data := map[string]interface{}{
"count": count,
"page_size": dbdata.PageSize,
@ -31,3 +30,24 @@ func SetAuditList(w http.ResponseWriter, r *http.Request) {
RespSucess(w, data)
}
func SetAuditExport(w http.ResponseWriter, r *http.Request) {
var datas []dbdata.AccessAudit
maxNum := 1000000
session := dbdata.GetAuditSession(r.FormValue("search"))
count, err := dbdata.FindAndCount(session, &datas, maxNum, 0)
if err != nil && !dbdata.CheckErrNotFound(err) {
RespError(w, RespInternalErr, err)
return
}
if count == 0 {
RespError(w, RespParamErr, "你导出的总数量为0条请调整搜索条件再导出")
return
}
if count > int64(maxNum) {
RespError(w, RespParamErr, "你导出的数据量超过100万条请调整搜索条件再导出")
return
}
gocsv.Marshal(datas, w)
}

View File

@ -39,6 +39,7 @@ func StartAdmin() {
r.HandleFunc("/set/other/smtp", SetOtherSmtp)
r.HandleFunc("/set/other/smtp/edit", SetOtherSmtpEdit)
r.HandleFunc("/set/audit/list", SetAuditList)
r.HandleFunc("/set/audit/export", SetAuditExport)
r.HandleFunc("/user/list", UserList)
r.HandleFunc("/user/detail", UserDetail)

51
server/dbdata/audit.go Normal file
View File

@ -0,0 +1,51 @@
package dbdata
import (
"encoding/json"
"xorm.io/xorm"
)
type SearchCon struct {
Username string `json:"username"`
Src string `json:"src"`
Dst string `json:"dst"`
DstPort string `json:"dst_port"`
AccessProto string `json:"access_proto"`
Date []string `json:"date"`
Info string `json:"info"`
}
func GetAuditSession(search string) *xorm.Session {
session := xdb.Where("1=1")
if search == "" {
return session
}
var searchData SearchCon
err := json.Unmarshal([]byte(search), &searchData)
if err != nil {
return session
}
if searchData.Username != "" {
session.And("username = ?", searchData.Username)
}
if searchData.Src != "" {
session.And("src = ?", searchData.Src)
}
if searchData.Dst != "" {
session.And("dst = ?", searchData.Dst)
}
if searchData.DstPort != "" {
session.And("dst_port = ?", searchData.DstPort)
}
if searchData.AccessProto != "" {
session.And("access_proto = ?", searchData.AccessProto)
}
if len(searchData.Date) > 0 && searchData.Date[0] != "" {
session.And("created_at BETWEEN ? AND ?", searchData.Date[0], searchData.Date[1])
}
if searchData.Info != "" {
session.And("info LIKE ?", "%"+searchData.Info+"%")
}
return session
}

View File

@ -0,0 +1,43 @@
package dbdata
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSearchAudit(t *testing.T) {
ast := assert.New(t)
preIpData()
defer closeIpdata()
currDateVal := "2022-07-24 00:00:00"
CreatedAt, _ := time.Parse("2006-01-02 15:04:05", currDateVal)
dataTest := AccessAudit{
Username: "Test",
Protocol: 6,
Src: "10.10.1.5",
SrcPort: 0,
Dst: "172.217.160.68",
DstPort: 80,
AccessProto: 4,
Info: "www.google.com",
CreatedAt: CreatedAt,
}
err := Add(dataTest)
ast.Nil(err)
var datas []AccessAudit
searchFormat := `{"username": "%s", "src":"%s", "dst": "%s", "dst_port":"%d","access_proto":"%d","info":"%s","date":["%s","%s"]}`
search := fmt.Sprintf(searchFormat, dataTest.Username, dataTest.Src, dataTest.Dst, dataTest.DstPort, dataTest.AccessProto, dataTest.Info, currDateVal, currDateVal)
session := GetAuditSession(search)
count, _ := FindAndCount(session, &datas, PageSize, 0)
ast.Equal(count, int64(1))
ast.Equal(datas[0].Username, dataTest.Username)
ast.Equal(datas[0].Dst, dataTest.Dst)
}

View File

@ -3,6 +3,8 @@ package dbdata
import (
"errors"
"reflect"
"xorm.io/xorm"
)
const PageSize = 10
@ -82,3 +84,12 @@ func Prefix(fieldName string, prefix string, data interface{}, limit, page int)
start := (page - 1) * limit
return where.Limit(limit, start).Find(data)
}
func FindAndCount(session *xorm.Session, data interface{}, limit, page int) (int64, error) {
if limit == 0 {
return session.FindAndCount(data)
}
start := (page - 1) * limit
totalCount, err := session.Limit(limit, start).FindAndCount(data)
return totalCount, err
}

View File

@ -7,6 +7,7 @@ require (
github.com/go-ldap/ldap v3.0.3+incompatible // indirect
github.com/go-ldap/ldap/v3 v3.4.3 // indirect
github.com/go-sql-driver/mysql v1.6.0
github.com/gocarina/gocsv v0.0.0-20220712153207-8b2118da4570 // indirect
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/google/gopacket v1.1.19
github.com/gorilla/mux v1.8.0

View File

@ -1,10 +1,68 @@
<template>
<div>
<el-card>
<el-form :model="searchForm" :rules="rules" ref="searchForm" :inline="true" class="form-inner-error">
<el-form-item label="用户名:" prop="username">
<el-input size="small" v-model="searchForm.username" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="源IP地址:" prop="src">
<el-input size="small" v-model="searchForm.src" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="目的IP地址:" prop="dst">
<el-input size="small" v-model="searchForm.dst" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="目的端口:" prop="dst_port">
<el-input size="small" v-model="searchForm.dst_port" style="width: 80px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item label="访问协议:">
<el-select size="small" v-model="searchForm.access_proto" style="width: 100px">
<el-option v-for="(item,index) in access_proto" :key="index" :label="item.text" :value="item.value">
</el-option>
</el-select>
</el-form-item>
<div>
<el-form-item label="日期范围:">
<el-date-picker
v-model="searchForm.date"
type="datetimerange"
size="small"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期">
>
</el-date-picker>
</el-form-item>
<el-form-item label="详情:">
<el-input size="small" v-model="searchForm.info" placeholder="请输入关键字" style="width: 200px" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item>
<el-button
size="small"
type="primary"
icon="el-icon-search"
@click="handleSearch">搜索
</el-button>
<el-button
size="small"
icon="el-icon-refresh"
@click="rest">重置搜索
</el-button>
<el-button
size="small"
icon="el-icon-download"
@click="handleExport">导出
</el-button>
</el-form-item>
</div>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
v-loading="loading"
element-loading-text="玩命搜索中"
element-loading-spinner="el-icon-loading"
border>
<el-table-column
@ -102,8 +160,9 @@ export default {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['基础信息', '审计日志'])
},
mounted() {
mounted() {
this.getData(1)
this.setSearchData()
},
data() {
return {
@ -111,19 +170,64 @@ export default {
count: 10,
nowIndex: 0,
accessProtoArr:["", "UDP", "TCP", "HTTPS", "HTTP"],
defSearchForm: {username:'', src:'', dst:'', dst_port:'', access_proto:'', info:'', date:["",""]},
searchForm: {},
access_proto: [
{ text: '请选择', value: '' },
{ text: 'UDP', value: '1' },
{ text: 'TCP', value: '2' },
{ text: 'HTTPS', value: '3' },
{ text: 'HTTP', value: '4' },
],
loading: false,
rules: {
username: [
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
],
src: [
{ message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
],
dst: [
{ message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' },
],
},
}
},
methods: {
setSearchData() {
this.searchForm = JSON.parse(JSON.stringify(this.defSearchForm));
},
handleSearch() {
this.$refs["searchForm"].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
this.getData(1)
})
},
searchEnterFun(e) {
var keyCode = window.event ? e.keyCode : e.which;
if (keyCode == 13) {
this.handleSearch()
}
},
getData(p) {
this.loading = true
if (! this.searchForm.date) {
this.searchForm.date = ["", ""];
}
axios.get('/set/audit/list', {
params: {
page: p,
search: this.searchForm,
}
}).then(resp => {
var data = resp.data.data
console.log(data);
this.tableData = data.datas;
this.count = data.count
this.loading = false
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
@ -148,6 +252,45 @@ export default {
console.log(error);
});
},
handleExport() {
if (! this.searchForm.date) {
this.searchForm.date = ["", ""];
}
const exporting = this.$loading({
lock: true,
text: '玩命导出中,请稍等片刻...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
axios.get('/set/audit/export', {
params: {
search: this.searchForm,
}
}).then(resp => {
var rdata = resp.data
if (rdata.code && rdata.code != 0) {
exporting.close();
this.$message.error(rdata.msg);
return ;
}
exporting.close();
this.$message.success("成功导出CSV文件")
let csvData = 'data:text/csv;charset=utf-8,\uFEFF' + rdata
this.createDownLoadClick(csvData, `anylink_audit_log_` + Date.parse(new Date()) + `.csv`)
}).catch(error => {
exporting.close();
this.$message.error('哦,请求出错');
console.log(error);
});
},
createDownLoadClick(content, fileName) {
const link = document.createElement('a')
link.href = encodeURI(content)
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
protoFormat(row) {
var access_proto = row.access_proto
if (row.access_proto == 0) {
@ -158,6 +301,23 @@ export default {
}
return this.accessProtoArr[access_proto]
},
rest() {
console.log("rest");
this.setSearchData();
this.handleSearch();
},
validateIP(rule, value, callback) {
if (value === '' || typeof value === 'undefined' || value == null) {
callback()
} else {
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
if ((!reg.test(value)) && value !== '') {
callback(new Error('请输入正确的IP地址'))
} else {
callback()
}
}
},
},
}
</script>

View File

@ -12,7 +12,7 @@
</el-form-item>
<el-form-item label="用户名:">
<el-input size="small" v-model="searchData" placeholder="请输入内容"></el-input>
<el-input size="small" v-model="searchData" placeholder="请输入内容" @keydown.enter.native="searchEnterFun"></el-input>
</el-form-item>
<el-form-item>
@ -25,7 +25,7 @@
<el-button
size="small"
icon="el-icon-refresh"
@click="searchData=''">重置搜索
@click="reset">重置搜索
</el-button>
</el-form-item>
</el-form>
@ -401,7 +401,17 @@ export default {
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
searchEnterFun(e) {
var keyCode = window.event ? e.keyCode : e.which;
if (keyCode == 13) {
this.handleSearch()
}
},
reset() {
this.searchData = "";
this.handleSearch();
},
},
}
</script>