feat: 完善日志审计功能

- 实现文件系统日志(FilesystemLog)记录文件管理器操作
- 实现操作日志(OperationLog)记录用户操作行为
- 实现数据库SQL日志(DatabaseSQLLog)模型和API
- 实现SSH会话命令记录(SessionCommand)含命令输出和风险等级
- 添加IP提取服务支持X-Real-IP和X-Forwarded-For
- 添加日志自动清理功能
- 修复ProFormSwitch required验证问题
- 修复设置页面默认值问题
- 修复文件上传错误检测逻辑
- 修复资产树key前缀问题
- 添加VNC/RDP设置默认值
- 修复文件管理标题翻译
This commit is contained in:
2026-04-19 06:57:42 +08:00
parent a2a1613384
commit 1f7c491048
42 changed files with 1214 additions and 130 deletions
+7 -6
View File
@@ -30,7 +30,8 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
}
// 存储登录失败次数信息
loginFailCountKey := c.RealIP() + loginAccount.Username
clientIP := service.PropertyService.GetClientIP(c)
loginFailCountKey := clientIP + loginAccount.Username
v, ok := cache.LoginFailedKeyManager.Get(loginFailCountKey)
if !ok {
v = 1
@@ -49,7 +50,7 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
count++
cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration)
// 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入的账号或密码不正确", count)
@@ -63,7 +64,7 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
count++
cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration)
// 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入的账号或密码不正确", count)
@@ -78,7 +79,7 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
count++
cache.LoginFailedKeyManager.Set(loginFailCountKey, count, cache.LoginLockExpiration)
// 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "双因素认证授权码不正确"); err != nil {
if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "双因素认证授权码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入双因素认证授权码不正确", count)
@@ -86,12 +87,12 @@ func (api AccountApi) LoginEndpoint(c echo.Context) error {
}
}
token, err := api.LoginSuccess(loginAccount, user, c.RealIP())
token, err := api.LoginSuccess(loginAccount, user, clientIP)
if err != nil {
return err
}
// 保存登录日志
if err := service.UserService.SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil {
if err := service.UserService.SaveLoginLog(clientIP, c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil {
return err
}
+23
View File
@@ -21,6 +21,21 @@ import (
type AssetApi struct{}
func recordOperationLog(c echo.Context, action, content, status, errorMessage string) {
account, _ := GetCurrentAccount(c)
clientIP := service.PropertyService.GetClientIP(c)
_ = service.OperationLogService.Record(context.TODO(), service.OperationLogParams{
AccountId: account.ID,
AccountName: account.Username,
Action: action,
Content: content,
IP: clientIP,
Status: status,
ErrorMessage: errorMessage,
UserAgent: c.Request().UserAgent(),
})
}
func (assetApi AssetApi) AssetCreateEndpoint(c echo.Context) error {
m := maps.Map{}
if err := c.Bind(&m); err != nil {
@@ -30,10 +45,13 @@ func (assetApi AssetApi) AssetCreateEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
m["owner"] = account.ID
assetName, _ := m["name"].(string)
if _, err := service.AssetService.Create(context.TODO(), m); err != nil {
recordOperationLog(c, "asset-add", "创建资产: "+assetName, "failed", err.Error())
return err
}
recordOperationLog(c, "asset-add", "创建资产: "+assetName, "success", "")
return Success(c, nil)
}
@@ -167,9 +185,12 @@ func (assetApi AssetApi) AssetUpdateEndpoint(c echo.Context) error {
if err := c.Bind(&m); err != nil {
return err
}
assetName, _ := m["name"].(string)
if err := service.AssetService.UpdateById(id, m); err != nil {
recordOperationLog(c, "asset-edit", "更新资产: "+assetName, "failed", err.Error())
return err
}
recordOperationLog(c, "asset-edit", "更新资产: "+assetName, "success", "")
return Success(c, nil)
}
@@ -178,10 +199,12 @@ func (assetApi AssetApi) AssetDeleteEndpoint(c echo.Context) error {
split := strings.Split(id, ",")
for i := range split {
if err := service.AssetService.DeleteById(split[i]); err != nil {
recordOperationLog(c, "asset-del", "删除资产: "+id, "failed", err.Error())
return err
}
}
recordOperationLog(c, "asset-del", "删除资产: "+id, "success", "")
return Success(c, nil)
}
+2 -2
View File
@@ -127,7 +127,7 @@ func buildAssetTree(assets []model.Asset, groups []model.AssetGroup, groupId str
node := maps.Map{
"id": g.ID,
"name": g.Name,
"key": g.ID,
"key": "group_" + g.ID,
"title": g.Name,
"value": g.ID,
}
@@ -144,7 +144,7 @@ func buildAssetTree(assets []model.Asset, groups []model.AssetGroup, groupId str
nodes = append(nodes, maps.Map{
"id": a.ID,
"name": a.Name,
"key": a.ID,
"key": "asset_" + a.ID,
"title": a.Name,
"value": a.ID,
"isLeaf": true,
+45
View File
@@ -1,10 +1,15 @@
package api
import (
"context"
"fmt"
"next-terminal/server/common"
"next-terminal/server/common/maps"
"next-terminal/server/global/session"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"os"
"path"
"strconv"
@@ -17,6 +22,28 @@ import (
type FileSystemApi struct{}
func (api FileSystemApi) recordFilesystemLog(ctx context.Context, sessionId, action, fileName string) {
sess, err := repository.SessionRepository.FindById(ctx, sessionId)
if err != nil {
log.Error("Failed to find session for filesystem log", log.NamedError("err", err))
return
}
filesystemLog := &model.FilesystemLog{
ID: utils.UUID(),
AssetId: sess.AssetId,
SessionId: sessionId,
UserId: sess.Creator,
Action: action,
FileName: fileName,
Created: common.NowJsonTime(),
}
if err := repository.FilesystemLogRepository.Create(ctx, filesystemLog); err != nil {
log.Error("Failed to create filesystem log", log.NamedError("err", err))
}
}
func (api FileSystemApi) LsEndpoint(c echo.Context) error {
sessionId := c.Param("id")
dir := c.QueryParam("dir")
@@ -84,6 +111,8 @@ func (api FileSystemApi) MkdirEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to create directory: "+err.Error())
}
go api.recordFilesystemLog(context.TODO(), sessionId, "mkdir", dir)
return Success(c, nil)
}
@@ -110,6 +139,8 @@ func (api FileSystemApi) TouchEndpoint(c echo.Context) error {
}
f.Close()
go api.recordFilesystemLog(context.TODO(), sessionId, "touch", filename)
return Success(c, nil)
}
@@ -150,6 +181,8 @@ func (api FileSystemApi) RmEndpoint(c echo.Context) error {
}
}
go api.recordFilesystemLog(context.TODO(), sessionId, "rm", filename)
return Success(c, nil)
}
@@ -200,6 +233,8 @@ func (api FileSystemApi) RenameEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to rename: "+err.Error())
}
go api.recordFilesystemLog(context.TODO(), sessionId, "rename", oldName+" -> "+newName)
return Success(c, nil)
}
@@ -238,6 +273,8 @@ func (api FileSystemApi) EditEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to write file: "+err.Error())
}
go api.recordFilesystemLog(context.TODO(), sessionId, "edit", req.Filename)
return Success(c, nil)
}
@@ -269,6 +306,8 @@ func (api FileSystemApi) ReadEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to read file: "+err.Error())
}
go api.recordFilesystemLog(context.TODO(), sessionId, "read", filename)
return Success(c, maps.Map{
"content": content,
"path": filename,
@@ -322,6 +361,8 @@ func (api FileSystemApi) ChmodEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to chmod: "+err.Error())
}
go api.recordFilesystemLog(context.TODO(), sessionId, "chmod", filename+" ("+modeStr+")")
return Success(c, nil)
}
@@ -369,6 +410,8 @@ func (api FileSystemApi) DownloadEndpoint(c echo.Context) error {
return Fail(c, -1, "failed to stat file: "+err.Error())
}
go api.recordFilesystemLog(context.TODO(), sessionId, "download", filename)
c.Response().Header().Set("Content-Disposition", "attachment; filename="+path.Base(filename))
c.Response().Header().Set("Content-Type", "application/octet-stream")
c.Response().Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
@@ -425,6 +468,8 @@ func (api FileSystemApi) UploadEndpoint(c echo.Context) error {
}
}
go api.recordFilesystemLog(context.TODO(), sessionId, "upload", destPath)
return Success(c, maps.Map{
"path": destPath,
"size": file.Size,
+17 -30
View File
@@ -65,10 +65,10 @@ func (api PortalApi) AssetsTreeEndpoint(c echo.Context) error {
tree := make([]maps.Map, 0)
for _, g := range groups {
node := maps.Map{
"key": g.ID,
"key": "group_" + g.ID,
"title": g.Name,
"isLeaf": false,
"children": buildPortalAssetChildren(items, g.ID, keyword),
"children": []interface{}{},
}
tree = append(tree, node)
}
@@ -77,7 +77,7 @@ func (api PortalApi) AssetsTreeEndpoint(c echo.Context) error {
continue
}
node := maps.Map{
"key": a.ID,
"key": "asset_" + a.ID,
"title": a.Name,
"isLeaf": true,
"extra": maps.Map{
@@ -93,29 +93,6 @@ func (api PortalApi) AssetsTreeEndpoint(c echo.Context) error {
return Success(c, tree)
}
func buildPortalAssetChildren(assets []model.Asset, groupId, keyword string) []maps.Map {
children := make([]maps.Map, 0)
for _, a := range assets {
if keyword != "" && !containsKeyword(a.Name, keyword) {
continue
}
node := maps.Map{
"key": a.ID,
"title": a.Name,
"isLeaf": true,
"extra": maps.Map{
"protocol": a.Protocol,
"logo": "",
"status": "unknown",
"network": a.IP,
"wolEnabled": false,
},
}
children = append(children, node)
}
return children
}
func containsKeyword(name, keyword string) bool {
if keyword == "" {
return true
@@ -139,7 +116,7 @@ func (api PortalApi) WebsitesTreeEndpoint(c echo.Context) error {
continue
}
node := maps.Map{
"key": w.ID,
"key": "website_" + w.ID,
"title": w.Name,
"isLeaf": true,
"extra": maps.Map{
@@ -159,7 +136,7 @@ func (api PortalApi) AssetsGroupTreeEndpoint(c echo.Context) error {
tree := make([]maps.Map, 0)
for _, g := range groups {
node := maps.Map{
"key": g.ID,
"key": "group_" + g.ID,
"title": g.Name,
"isLeaf": false,
"children": []interface{}{},
@@ -186,7 +163,8 @@ func (api PortalApi) CreateSessionEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
s, err := service.SessionService.Create(c.RealIP(), assetId, nt.Native, account)
clientIP := service.PropertyService.GetClientIP(c)
s, err := service.SessionService.Create(clientIP, assetId, nt.Native, account)
if err != nil {
return err
}
@@ -201,7 +179,16 @@ func (api PortalApi) CreateSessionEndpoint(c echo.Context) error {
"id": s.ID,
"protocol": s.Protocol,
"assetName": assetName,
"strategy": maps.Map{},
"strategy": maps.Map{
"upload": s.Upload == "1",
"download": s.Download == "1",
"delete": s.Delete == "1",
"rename": s.Rename == "1",
"edit": s.Edit == "1",
"copy": s.Copy == "1",
"paste": s.Paste == "1",
"fileSystem": s.FileSystem == "1",
},
"url": "",
"watermark": maps.Map{},
"readonly": false,
+2 -1
View File
@@ -202,7 +202,8 @@ func (api SessionApi) SessionCreateEndpoint(c echo.Context) error {
user, _ := GetCurrentAccount(c)
s, err := service.SessionService.Create(c.RealIP(), assetId, mode, user)
clientIP := service.PropertyService.GetClientIP(c)
s, err := service.SessionService.Create(clientIP, assetId, mode, user)
if err != nil {
return err
}
+150 -10
View File
@@ -2,7 +2,11 @@ package api
import (
"context"
"strconv"
"next-terminal/server/common/maps"
"next-terminal/server/model"
"next-terminal/server/repository"
"github.com/labstack/echo/v4"
)
@@ -10,18 +14,37 @@ import (
type SessionCommandApi struct{}
func (api SessionCommandApi) AllEndpoint(c echo.Context) error {
return Success(c, []interface{}{})
sessionId := c.QueryParam("sessionId")
items, err := repository.SessionCommandRepository.FindBySessionId(context.TODO(), sessionId)
if err != nil {
return err
}
return Success(c, items)
}
func (api SessionCommandApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
sessionId := c.QueryParam("sessionId")
items, total, err := repository.SessionCommandRepository.FindByPage(context.TODO(), pageIndex, pageSize, sessionId)
if err != nil {
return err
}
return Success(c, maps.Map{
"total": 0,
"items": []interface{}{},
"total": total,
"items": items,
})
}
func (api SessionCommandApi) GetEndpoint(c echo.Context) error {
return Success(c, nil)
id := c.Param("id")
var item model.SessionCommand
err := repository.SessionCommandRepository.GetDB(context.TODO()).Where("id = ?", id).First(&item).Error
if err != nil {
return err
}
return Success(c, item)
}
type OperationLogApi struct{}
@@ -31,17 +54,55 @@ func (api OperationLogApi) AllEndpoint(c echo.Context) error {
}
func (api OperationLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
accountName := c.QueryParam("accountName")
action := c.QueryParam("action")
status := c.QueryParam("status")
order := c.QueryParam("order")
field := c.QueryParam("field")
items, total, err := repository.OperationLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, accountName, action, status, order, field)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
result = append(result, maps.Map{
"id": item.ID,
"accountId": item.AccountId,
"accountName": item.AccountName,
"action": item.Action,
"content": item.Content,
"ip": item.IP,
"region": item.Region,
"userAgent": item.UserAgent,
"status": item.Status,
"errorMessage": item.ErrorMessage,
"remark": item.Remark,
"createdAt": item.Created,
})
}
return Success(c, maps.Map{
"total": 0,
"items": []interface{}{},
"total": total,
"items": result,
})
}
func (api OperationLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.OperationLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil)
}
func (api OperationLogApi) DeleteEndpoint(c echo.Context) error {
id := c.Param("id")
if err := repository.OperationLogRepository.DeleteById(context.TODO(), id); err != nil {
return err
}
return Success(c, nil)
}
@@ -111,17 +172,57 @@ func (api FilesystemLogApi) AllEndpoint(c echo.Context) error {
}
func (api FilesystemLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
action := c.QueryParam("action")
items, total, err := repository.FilesystemLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, action)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
var assetName, userName string
if item.AssetId != "" {
asset, _ := repository.AssetRepository.FindById(context.TODO(), item.AssetId)
assetName = asset.Name
}
if item.UserId != "" {
user, _ := repository.UserRepository.FindById(context.TODO(), item.UserId)
userName = user.Username
}
result = append(result, maps.Map{
"id": item.ID,
"assetId": item.AssetId,
"sessionId": item.SessionId,
"userId": item.UserId,
"action": item.Action,
"fileName": item.FileName,
"createdAt": item.Created,
"assetName": assetName,
"userName": userName,
})
}
return Success(c, maps.Map{
"total": 0,
"items": []interface{}{},
"total": total,
"items": result,
})
}
func (api FilesystemLogApi) DeleteEndpoint(c echo.Context) error {
id := c.Param("id")
if err := repository.FilesystemLogRepository.DeleteById(context.TODO(), id); err != nil {
return err
}
return Success(c, nil)
}
func (api FilesystemLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.FilesystemLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil)
}
@@ -161,17 +262,56 @@ func (api AccessLogApi) AllEndpoint(c echo.Context) error {
}
func (api AccessLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
domain := c.QueryParam("domain")
websiteId := c.QueryParam("websiteId")
accountId := c.QueryParam("accountId")
items, total, err := repository.AccessLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, domain, websiteId, accountId)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
result = append(result, maps.Map{
"id": item.ID,
"domain": item.Domain,
"websiteId": item.WebsiteId,
"accountId": item.AccountId,
"method": item.Method,
"uri": item.Uri,
"statusCode": item.StatusCode,
"responseSize": item.ResponseSize,
"clientIp": item.ClientIp,
"region": item.Region,
"userAgent": item.UserAgent,
"referer": item.Referer,
"requestTime": item.RequestTime,
"responseTime": item.ResponseTime,
"createdAt": item.Created,
})
}
return Success(c, maps.Map{
"total": 0,
"items": []interface{}{},
"total": total,
"items": result,
})
}
func (api AccessLogApi) DeleteEndpoint(c echo.Context) error {
id := c.Param("id")
if err := repository.AccessLogRepository.DeleteById(context.TODO(), id); err != nil {
return err
}
return Success(c, nil)
}
func (api AccessLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.AccessLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil)
}
+48 -2
View File
@@ -537,13 +537,59 @@ func (api DbWorkOrderApi) DeleteEndpoint(c echo.Context) error {
type DatabaseSQLLogApi struct{}
func (api DatabaseSQLLogApi) PagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
assetId := c.QueryParam("assetId")
userId := c.QueryParam("userId")
status := c.QueryParam("status")
source := c.QueryParam("source")
order := c.QueryParam("order")
field := c.QueryParam("field")
items, total, err := repository.DatabaseSQLLogRepository.FindByPage(context.TODO(), pageIndex, pageSize, assetId, userId, status, source, order, field)
if err != nil {
return err
}
result := make([]maps.Map, 0)
for _, item := range items {
var assetName, userName string
if item.AssetId != "" {
asset, _ := repository.DatabaseAssetRepository.FindById(context.TODO(), item.AssetId)
assetName = asset.Name
}
if item.UserId != "" {
user, _ := repository.UserRepository.FindById(context.TODO(), item.UserId)
userName = user.Username
}
result = append(result, maps.Map{
"id": item.ID,
"assetId": item.AssetId,
"assetName": assetName,
"database": item.Database,
"userId": item.UserId,
"userName": userName,
"clientIp": item.ClientIP,
"sql": item.SQL,
"durationMs": item.DurationMs,
"rowsAffected": item.RowsAffected,
"status": item.Status,
"errorMessage": item.ErrorMessage,
"source": item.Source,
"createdAt": item.Created,
})
}
return Success(c, maps.Map{
"total": 0,
"items": []interface{}{},
"total": total,
"items": result,
})
}
func (api DatabaseSQLLogApi) ClearEndpoint(c echo.Context) error {
if err := repository.DatabaseSQLLogRepository.DeleteAll(context.TODO()); err != nil {
return err
}
return Success(c, nil)
}
+2 -2
View File
@@ -188,7 +188,7 @@ func (api WebTerminalApi) SshEndpoint(c echo.Context) error {
if err != nil {
service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
} else {
_ = termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, ""))
_ = termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, msg.Content))
}
}
@@ -358,7 +358,7 @@ func (api WebTerminalApi) AccessTerminalEndpoint(c echo.Context) error {
termHandler.Write([]byte(msg.Content))
case Ping:
termHandler.SendRequest()
termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, ""))
termHandler.SendMessageToWebSocket(dto.NewMessage(Ping, msg.Content))
}
}
return nil
+117 -3
View File
@@ -3,18 +3,27 @@ package api
import (
"bytes"
"context"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/gorilla/websocket"
"next-terminal/server/common"
"next-terminal/server/common/term"
"next-terminal/server/dto"
"next-terminal/server/global/session"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
const maxOutputLength = 10000
type TermHandler struct {
sessionId string
userId string
assetId string
isRecording bool
webSocket *websocket.Conn
nextTerminal *term.NextTerminal
@@ -24,6 +33,10 @@ type TermHandler struct {
tick *time.Ticker
mutex sync.Mutex
buf bytes.Buffer
commandBuf bytes.Buffer
outputBuf bytes.Buffer
lastCommand *model.SessionCommand
outputMutex sync.Mutex
}
func NewTermHandler(userId, assetId, sessionId string, isRecording bool, ws *websocket.Conn, nextTerminal *term.NextTerminal) *TermHandler {
@@ -32,6 +45,8 @@ func NewTermHandler(userId, assetId, sessionId string, isRecording bool, ws *web
return &TermHandler{
sessionId: sessionId,
userId: userId,
assetId: assetId,
isRecording: isRecording,
webSocket: ws,
nextTerminal: nextTerminal,
@@ -48,9 +63,9 @@ func (r *TermHandler) Start() {
}
func (r *TermHandler) Stop() {
// 会话结束时记录最后一个命令
r.tick.Stop()
r.cancel()
r.saveLastCommandOutput()
}
func (r *TermHandler) readFormTunnel() {
@@ -86,8 +101,8 @@ func (r *TermHandler) writeToWebsocket() {
if r.isRecording && r.nextTerminal.Recorder != nil {
_ = r.nextTerminal.Recorder.WriteData(s)
}
// 监控
SendObData(r.sessionId, s)
r.collectOutput(s)
r.buf.Reset()
case data := <-r.dataChan:
if data != utf8.RuneError {
@@ -101,12 +116,111 @@ func (r *TermHandler) writeToWebsocket() {
}
}
func (r *TermHandler) collectOutput(output string) {
r.outputMutex.Lock()
defer r.outputMutex.Unlock()
if r.outputBuf.Len()+len(output) <= maxOutputLength {
r.outputBuf.WriteString(output)
}
}
func (r *TermHandler) getAndClearOutput() string {
r.outputMutex.Lock()
defer r.outputMutex.Unlock()
output := r.outputBuf.String()
r.outputBuf.Reset()
return output
}
func (r *TermHandler) saveLastCommandOutput() {
r.outputMutex.Lock()
defer r.outputMutex.Unlock()
if r.lastCommand != nil && r.outputBuf.Len() > 0 {
output := cleanOutput(r.outputBuf.String())
if len(output) > maxOutputLength {
output = output[:maxOutputLength]
}
r.lastCommand.Output = output
_ = repository.SessionCommandRepository.Create(context.TODO(), r.lastCommand)
r.lastCommand = nil
r.outputBuf.Reset()
}
}
func cleanOutput(output string) string {
output = strings.ReplaceAll(output, "\x1b[0m", "")
output = strings.ReplaceAll(output, "\x1b[?2004h", "")
output = strings.ReplaceAll(output, "\x1b[?2004l", "")
var result strings.Builder
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line != "" {
result.WriteString(line)
result.WriteString("\n")
}
}
return strings.TrimSpace(result.String())
}
func (r *TermHandler) Write(input []byte) error {
// 正常的字符输入
for _, b := range input {
switch b {
case 13: // Enter key
command := strings.TrimSpace(r.commandBuf.String())
if command != "" {
r.saveLastCommandOutput()
r.lastCommand = &model.SessionCommand{
ID: utils.UUID(),
SessionId: r.sessionId,
RiskLevel: detectRiskLevel(command),
Command: command,
Output: "",
Created: common.NowJsonTime(),
}
}
r.commandBuf.Reset()
case 127, 8: // Backspace, Delete
if r.commandBuf.Len() > 0 {
r.commandBuf.Truncate(r.commandBuf.Len() - 1)
}
case 27: // Escape sequence (arrow keys, etc.)
default:
if b >= 32 && b < 127 { // Printable ASCII
r.commandBuf.WriteByte(b)
}
}
}
_, err := r.nextTerminal.Write(input)
return err
}
func detectRiskLevel(command string) int {
highRiskPatterns := []string{
"rm -rf /", "rm -rf /*", "mkfs", "dd if=", "> /dev/sd",
"chmod -R 777", "chown -R", ":(){ :|:& };:",
}
mediumRiskPatterns := []string{
"rm -rf", "rm -r", "passwd", "useradd", "userdel",
"chmod 777", "chmod -R", "chown", "wget", "curl",
"apt-get", "yum install", "dnf install", "pacman",
"systemctl stop", "systemctl disable", "service stop",
"iptables -F", "ufw disable", "setenforce 0",
}
lowerCmd := strings.ToLower(command)
for _, pattern := range highRiskPatterns {
if strings.Contains(lowerCmd, strings.ToLower(pattern)) {
return 2
}
}
for _, pattern := range mediumRiskPatterns {
if strings.Contains(lowerCmd, strings.ToLower(pattern)) {
return 1
}
}
return 0
}
func (r *TermHandler) WindowChange(h int, w int) error {
return r.nextTerminal.WindowChange(h, w)
}
+8
View File
@@ -25,9 +25,11 @@ func (userApi UserApi) CreateEndpoint(c echo.Context) (err error) {
}
if err := service.UserService.CreateUser(item); err != nil {
recordOperationLog(c, "user-add", "创建用户: "+item.Username, "failed", err.Error())
return err
}
recordOperationLog(c, "user-add", "创建用户: "+item.Username, "success", "")
return Success(c, item)
}
@@ -68,9 +70,11 @@ func (userApi UserApi) UpdateEndpoint(c echo.Context) error {
}
if err := service.UserService.UpdateUser(id, item); err != nil {
recordOperationLog(c, "user-edit", "更新用户: "+item.Username, "failed", err.Error())
return err
}
recordOperationLog(c, "user-edit", "更新用户: "+item.Username, "success", "")
return Success(c, nil)
}
@@ -83,9 +87,11 @@ func (userApi UserApi) UpdateStatusEndpoint(c echo.Context) error {
}
if err := service.UserService.UpdateStatusById(id, status); err != nil {
recordOperationLog(c, "user-edit", "更新用户状态", "failed", err.Error())
return err
}
recordOperationLog(c, "user-edit", "更新用户状态", "success", "")
return Success(c, nil)
}
@@ -102,10 +108,12 @@ func (userApi UserApi) DeleteEndpoint(c echo.Context) error {
return Fail(c, -1, "不允许删除自身账户")
}
if err := service.UserService.DeleteUserById(userId); err != nil {
recordOperationLog(c, "user-del", "删除用户: "+ids, "failed", err.Error())
return err
}
}
recordOperationLog(c, "user-del", "删除用户: "+ids, "success", "")
return Success(c, nil)
}
+1 -1
View File
@@ -169,7 +169,7 @@ func buildWebsiteGroupTree(groups []model.WebsiteGroup, parentId string) []maps.
"id": g.ID,
"name": g.Name,
"title": g.Name,
"key": g.ID,
"key": "wgroup_" + g.ID,
"value": g.ID,
}
children := buildWebsiteGroupTree(groups, g.ID)