From 89b8356806c7f33e4889c81e67b83f4640924d22 Mon Sep 17 00:00:00 2001 From: Wangjunqi123 Date: Wed, 15 Jan 2025 14:02:18 +0800 Subject: [PATCH] cmd/server: custom script --- .../network/controller/agentcontroller/rpm.go | 20 +- .../network/controller/pluginapi/script.go | 11 +- cmd/server/app/network/controller/script.go | 335 ++++++++++++++---- cmd/server/app/network/httpserver.go | 14 +- cmd/server/app/service/auth/casbin.go | 2 + .../app/service/internal/dao/scriptdao.go | 193 ++++++++-- .../app/service/script/dangerouscommands.go | 54 +++ cmd/server/app/service/script/script.go | 148 +++++++- docs/api/customScriptAPI.jsonc | 180 ++++++++++ pkg/dbmanager/db.go | 7 +- pkg/global/commandscheck.go | 67 ++++ 11 files changed, 916 insertions(+), 115 deletions(-) create mode 100644 cmd/server/app/service/script/dangerouscommands.go create mode 100644 docs/api/customScriptAPI.jsonc create mode 100644 pkg/global/commandscheck.go diff --git a/cmd/server/app/network/controller/agentcontroller/rpm.go b/cmd/server/app/network/controller/agentcontroller/rpm.go index 67e930b4..e78a5e2d 100644 --- a/cmd/server/app/network/controller/agentcontroller/rpm.go +++ b/cmd/server/app/network/controller/agentcontroller/rpm.go @@ -8,6 +8,7 @@ package agentcontroller import ( + "fmt" "net/http" "strconv" "strings" @@ -15,6 +16,7 @@ import ( "gitee.com/openeuler/PilotGo/cmd/server/app/agentmanager" "gitee.com/openeuler/PilotGo/cmd/server/app/network/jwt" "gitee.com/openeuler/PilotGo/cmd/server/app/service/auditlog" + "gitee.com/openeuler/PilotGo/pkg/global" "gitee.com/openeuler/PilotGo/pkg/utils" "gitee.com/openeuler/PilotGo/sdk/logger" "gitee.com/openeuler/PilotGo/sdk/response" @@ -139,7 +141,7 @@ func InstallRpmHandler(c *gin.Context) { if err != nil { auditlog.UpdateMessage(log_s, "agentuuid:"+uuid+err.Error()) auditlog.UpdateStatus(log_s, auditlog.StatusFailed) - logger.Error("%v",err.Error()) + logger.Error("%v", err.Error()) StatusCodes = append(StatusCodes, strconv.Itoa(http.StatusBadRequest)) continue } @@ -166,9 +168,14 @@ func InstallRpmHandler(c *gin.Context) { } status := auditlog.BatchActionStatus(StatusCodes) if err := auditlog.UpdateStatus(log, status); err != nil { - logger.Error("%v",err.Error()) + logger.Error("%v", err.Error()) } + global.SendRemindMsg( + global.MachineSendMsg, + fmt.Sprintf("用户 %s 执行 %s 软件包安装, machines: %v", u.Username, rpm.RPM, rpm.UUIDs), + ) + switch strings.Split(status, ",")[2] { case "0.00": response.Fail(c, nil, "软件包安装失败") @@ -239,7 +246,7 @@ func RemoveRpmHandler(c *gin.Context) { if err != nil { auditlog.UpdateMessage(log_s, "agentuuid:"+uuid+err.Error()) auditlog.UpdateStatus(log_s, auditlog.StatusFailed) - logger.Error("%v",err.Error()) + logger.Error("%v", err.Error()) StatusCodes = append(StatusCodes, strconv.Itoa(http.StatusBadRequest)) continue } @@ -267,9 +274,14 @@ func RemoveRpmHandler(c *gin.Context) { status := auditlog.BatchActionStatus(StatusCodes) if err := auditlog.UpdateStatus(log, status); err != nil { - logger.Error("%v",err.Error()) + logger.Error("%v", err.Error()) } + global.SendRemindMsg( + global.MachineSendMsg, + fmt.Sprintf("用户 %s 执行 %s 软件包卸载, machines: %v", u.Username, rpm.RPM, rpm.UUIDs), + ) + switch strings.Split(status, ",")[2] { case "0.00": response.Fail(c, nil, "软件包卸载失败") diff --git a/cmd/server/app/network/controller/pluginapi/script.go b/cmd/server/app/network/controller/pluginapi/script.go index e71c3a2a..e7e0ff1e 100644 --- a/cmd/server/app/network/controller/pluginapi/script.go +++ b/cmd/server/app/network/controller/pluginapi/script.go @@ -16,10 +16,11 @@ import ( "time" "gitee.com/openeuler/PilotGo/cmd/server/app/agentmanager" - "gitee.com/openeuler/PilotGo/cmd/server/app/network/controller" "gitee.com/openeuler/PilotGo/cmd/server/app/network/jwt" "gitee.com/openeuler/PilotGo/cmd/server/app/service/batch" "gitee.com/openeuler/PilotGo/cmd/server/app/service/plugin" + "gitee.com/openeuler/PilotGo/cmd/server/app/service/script" + "gitee.com/openeuler/PilotGo/pkg/global" "gitee.com/openeuler/PilotGo/sdk/common" "gitee.com/openeuler/PilotGo/sdk/logger" "gitee.com/openeuler/PilotGo/sdk/plugin/client" @@ -135,7 +136,13 @@ func RunScriptHandler(c *gin.Context) { logger.Debug("run script on agents :%v", d.Batch.MachineUUIDs) // Enabled according to the needs of the plugin - positions, matchedCommands := controller.FindDangerousCommandsPos(d.Script) + cmds, err := script.GetDangerousCommandsInBlackList() + if err != nil { + logger.Error("run script error(dangerous commands list): %s", err.Error()) + response.Fail(c, nil, "internal error occurred while retrieving dangerous commands") + return + } + positions, matchedCommands := global.FindDangerousCommandsPos(d.Script, cmds) if len(positions) > 0 { logger.Debug("Matched Commands: %v", matchedCommands) str := strings.Join(matchedCommands, "\n") diff --git a/cmd/server/app/network/controller/script.go b/cmd/server/app/network/controller/script.go index 5d7bc6fa..376b1e6d 100644 --- a/cmd/server/app/network/controller/script.go +++ b/cmd/server/app/network/controller/script.go @@ -8,84 +8,289 @@ package controller import ( - "regexp" + "fmt" + "strconv" + "strings" + "gitee.com/openeuler/PilotGo/cmd/server/app/network/jwt" + "gitee.com/openeuler/PilotGo/cmd/server/app/service/auditlog" scriptservice "gitee.com/openeuler/PilotGo/cmd/server/app/service/script" + "gitee.com/openeuler/PilotGo/pkg/global" + "gitee.com/openeuler/PilotGo/sdk/common" + "gitee.com/openeuler/PilotGo/sdk/logger" "gitee.com/openeuler/PilotGo/sdk/response" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) // 存储脚本文件 func AddScriptHandler(c *gin.Context) { - var script scriptservice.Script - err := scriptservice.AddScript(&script) + script := &scriptservice.Script{} + if err := c.ShouldBindJSON(script); err != nil { + logger.Error("fail to create script(bind): %s", err.Error()) + response.Fail(c, nil, fmt.Sprintf("脚本文件添加失败: %s", err.Error())) + return + } + + u, err := jwt.ParseUser(c) + if err != nil { + response.Fail(c, nil, "user token error:"+err.Error()) + return + } + log := &auditlog.AuditLog{ + LogUUID: uuid.New().String(), + ParentUUID: "", + Module: auditlog.ModuleMachine, + Status: auditlog.StatusOK, + UserID: u.ID, + Action: "创建脚本", + } + auditlog.Add(log) + + cmds, err := scriptservice.GetDangerousCommandsInBlackList() + if err != nil { + logger.Error("fail to create script(dangerous commands list): %s", err.Error()) + response.Fail(c, nil, "internal error occurred while retrieving dangerous commands") + return + } + positions, matchedCommands := global.FindDangerousCommandsPos(script.Content, cmds) + if len(positions) > 0 { + logger.Error("Matched Commands: %v", matchedCommands) + response.Fail(c, nil, "Dangerous commands detected in script: "+strings.Join(matchedCommands, "\n")) + return + } + + if err := scriptservice.AddScript(script); err != nil { + logger.Error("fail to create script: %s", err.Error()) + response.Fail(c, nil, fmt.Sprintf("脚本文件添加失败: %s", err.Error())) + return + } + + global.SendRemindMsg( + global.ServerSendMsg, + fmt.Sprintf("用户 %s 创建脚本 %s", u.Username, script.Name), + ) + + response.Success(c, nil, "成功") +} + +func UpdateScriptHandler(c *gin.Context) { + script := &scriptservice.Script{} + if err := c.ShouldBindJSON(script); err != nil { + logger.Error("fail to edit script(bind): %s", err.Error()) + response.Fail(c, nil, fmt.Sprintf("脚本文件添加失败: %s", err.Error())) + return + } + + u, err := jwt.ParseUser(c) + if err != nil { + response.Fail(c, nil, "user token error:"+err.Error()) + return + } + log := &auditlog.AuditLog{ + LogUUID: uuid.New().String(), + ParentUUID: "", + Module: auditlog.ModuleMachine, + Status: auditlog.StatusOK, + UserID: u.ID, + Action: "更新脚本", + } + auditlog.Add(log) + + cmds, err := scriptservice.GetDangerousCommandsInBlackList() if err != nil { - response.Fail(c, gin.H{"error": err.Error()}, "脚本文件添加失败") + logger.Error("fail to edit script(dangerous commands list): %s", err.Error()) + response.Fail(c, nil, "internal error occurred while retrieving dangerous commands") return } - response.Success(c, nil, "脚本文件添加成功") + positions, matchedCommands := global.FindDangerousCommandsPos(script.Content, cmds) + if len(positions) > 0 { + logger.Error("Matched Commands: %v", matchedCommands) + response.Fail(c, nil, "Dangerous commands detected in script: "+strings.Join(matchedCommands, "\n")) + return + } + + if err := scriptservice.UpdateScript(script); err != nil { + logger.Error("fail to edit script: %s", err.Error()) + response.Fail(c, nil, fmt.Sprintf("脚本文件添加失败: %s", err.Error())) + return + } + + global.SendRemindMsg( + global.ServerSendMsg, + fmt.Sprintf("用户 %s 更新脚本 %s", u.Username, script.Name), + ) + + response.Success(c, nil, "成功") } -// 高危命令检测 -func FindDangerousCommandsPos(content string) ([][]int, []string) { - var positions [][]int - var matchedCommands []string - - for _, pattern := range DangerousCommandsList { - re, err := regexp.Compile(pattern) - if err != nil { - // TOODO: info remind - continue - } - matches := re.FindAllStringIndex(content, -1) - for _, match := range matches { - start, end := match[0], match[1]-1 - positions = append(positions, []int{start, end}) - matchedCommands = append(matchedCommands, content[start:end+1]) - } - } - return positions, matchedCommands +func DeleteScriptHandler(c *gin.Context) { + req_body := struct { + ScriptID uint `json:"script_id"` + Version string `json:"version"` + }{} + if err := c.ShouldBindJSON(&req_body); err != nil { + logger.Error("fail to delete script(bind): %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + + u, err := jwt.ParseUser(c) + if err != nil { + response.Fail(c, nil, "user token error:"+err.Error()) + return + } + log := &auditlog.AuditLog{ + LogUUID: uuid.New().String(), + ParentUUID: "", + Module: auditlog.ModuleMachine, + Status: auditlog.StatusOK, + UserID: u.ID, + Action: "删除脚本", + } + auditlog.Add(log) + + if err := scriptservice.DeleteScript(req_body.ScriptID, req_body.Version); err != nil { + logger.Error("fail to delete script: %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + + var script_name string + script, err := scriptservice.GetScriptByID(req_body.ScriptID) + if err != nil { + logger.Error("fail to get script by id: %s", err.Error()) + script_name = "" + } else { + script_name = script.Name + } + global.SendRemindMsg( + global.MachineSendMsg, + fmt.Sprintf("用户 %s 删除脚本 %s %s", u.Username, script_name, req_body.Version), + ) + + response.Success(c, nil, "成功") } -var DangerousCommandsList = []string{ - `.*rm\s+-[r,f,rf].*`, - `.*lvremove\s+-f.*`, - `.*poweroff.*`, - `.*shutdown\s+-[f,F,h,k,n,r,t,C].*`, - `.*pvremove\s+-f.*`, - `.*vgremove\s+-f.*`, - `.*exportfs\s+-[a,u].*`, - `.*umount.nfs+.*.+-[r,f,rf].*`, - `.*mv+.*.+/dev/null.*`, - `.*reboot.*`, - `.*rmmod\s+-[a,s,v,f,w].*`, - `.*dpkg-divert+.*.+-remove.*`, - `.*dd.*`, - `.*mkfs.*`, - `.*vmo.*`, - `.*init.*`, - `.*halt.*`, - `.*fasthalt.*`, - `.*fastboot.*`, - `.*startsrc.*`, - `.*stopsrc.*`, - `.*chkconfig.*`, - `.*off.*`, - `.*refresh.*`, - `.*umount.*`, - `.*rmdev.*`, - `.*chdev.*`, - `.*extendvg.*`, - `.*reducevg.*`, - `.*importvg.*`, - `.*exportvg.*`, - `.*mklv.*`, - `.*rmlv.*`, - `.*rmfs.*`, - `.*chfs.*`, - `.*installp.*`, - `.*instfix.*`, - `.*crontab.*`, - `.*cfgmgr.*`, - `.*mknod.*`, +func RunScriptHandler(c *gin.Context) { + body := &scriptservice.RunScriptMeta{} + if err := c.ShouldBindJSON(body); err != nil { + logger.Error("fail to run script(bind): %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + + u, err := jwt.ParseUser(c) + if err != nil { + response.Fail(c, nil, "user token error:"+err.Error()) + return + } + log := &auditlog.AuditLog{ + LogUUID: uuid.New().String(), + ParentUUID: "", + Module: auditlog.ModuleMachine, + Status: auditlog.StatusOK, + UserID: u.ID, + Action: "执行脚本", + } + auditlog.Add(log) + + batch := &common.Batch{} + if body.BatchID < 1 && len(body.MachineUUIDs) == 0 { + logger.Error("fail to run script, batchid and machine_uuids are both empty") + response.Fail(c, nil, "目标类型错误") + return + } + if body.BatchID >= 1 { + batch.BatchIds = append(batch.BatchIds, int(body.BatchID)) + } else { + batch.MachineUUIDs = append(batch.MachineUUIDs, body.MachineUUIDs...) + } + + result, err := scriptservice.RunScript(body, batch) + if err != nil { + logger.Error("fail to run script: %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + + var script_name string + script, err := scriptservice.GetScriptByID(body.ScriptID) + if err != nil { + logger.Error("fail to get script by id: %s", err.Error()) + script_name = "" + } else { + script_name = script.Name + } + global.SendRemindMsg( + global.MachineSendMsg, + fmt.Sprintf("用户 %s 执行脚本 %s %s, batch: %v, machines: %v", u.Username, script_name, body.Version, body.BatchID, body.MachineUUIDs), + ) + + response.Success(c, result, "成功") +} + +func GetScriptListHandler(c *gin.Context) { + query := &response.PaginationQ{} + err := c.ShouldBindQuery(query) + if err != nil { + logger.Error("fail to get script list(bind): %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + + scripts, total, err := scriptservice.ScriptList(query) + if err != nil { + logger.Error("fail to get script list: %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + response.DataPagination(c, scripts, total, query) +} + +func GetScriptHistoryVersionHandler(c *gin.Context) { + scriptid := c.Query("script_id") + + id, err := strconv.Atoi(scriptid) + if err != nil { + logger.Error("fail to get script history version: %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + + scripts, err := scriptservice.ScriptHistoryVersion(uint(id)) + if err != nil { + logger.Error("fail to get script history version: %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + response.Success(c, scripts, "成功") +} + +func UpdateCommandsBlackListHandler(c *gin.Context) { + body := &struct { + WhiteList []uint `json:"white_list"` + }{} + if err := c.ShouldBindJSON(body); err != nil { + logger.Error("fail to update script blacklist(bind): %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + + if err := scriptservice.UpdateCommandsBlackList(body.WhiteList); err != nil { + logger.Error("fail to update script blacklist: %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + response.Success(c, nil, "成功") +} + +func GetDangerousCommandsList(c *gin.Context) { + commands, err := scriptservice.GetDangerousCommandsList() + if err != nil { + logger.Error("fail to get dangerous commands list: %s", err.Error()) + response.Fail(c, nil, err.Error()) + return + } + response.Success(c, commands, "成功") } diff --git a/cmd/server/app/network/httpserver.go b/cmd/server/app/network/httpserver.go index deca26dd..1170124b 100644 --- a/cmd/server/app/network/httpserver.go +++ b/cmd/server/app/network/httpserver.go @@ -127,7 +127,7 @@ func SetupRouter() *gin.Engine { } func registerAPIs(router *gin.Engine) { - router.GET("/event", controller.PushAlarmHandler) + router.GET("/event", middleware.TokenCheckMiddleware, controller.PushAlarmHandler) api := router.Group("/api/v1") @@ -177,6 +177,11 @@ func registerAPIs(router *gin.Engine) { system.POST("/modifydepart", middleware.NeedPermission("dept_change", "button"), controller.ModifyMachineDepartHandler) system.POST("/deletemachine", middleware.NeedPermission("machine_delete", "button"), controller.DeleteMachineHandler) } + { + script := authenApi.Group("/script_auth") + script.POST("/run", middleware.NeedPermission("run_script", "button"), controller.RunScriptHandler) + script.PUT("/update_blacklist", middleware.NeedPermission("update_script_blacklist", "button"), controller.UpdateCommandsBlackListHandler) + } } tokenApi := api.Group("") // web页面显示 @@ -249,7 +254,12 @@ func registerAPIs(router *gin.Engine) { { // script manager script := system.Group("/script") - script.POST("/save", controller.AddScriptHandler) + script.POST("/create", controller.AddScriptHandler) + script.PUT("/update", controller.UpdateScriptHandler) + script.DELETE("/delete", controller.DeleteScriptHandler) + script.GET("/list_all", controller.GetScriptListHandler) + script.GET("/list_history", controller.GetScriptHistoryVersionHandler) + script.GET("/blacklist", controller.GetDangerousCommandsList) } } diff --git a/cmd/server/app/service/auth/casbin.go b/cmd/server/app/service/auth/casbin.go index 9d813bf1..1ffc53aa 100644 --- a/cmd/server/app/service/auth/casbin.go +++ b/cmd/server/app/service/auth/casbin.go @@ -108,6 +108,8 @@ var ( "dept_delete", "dept_update", "machine_delete", + "run_script", + "update_script_blacklist", } MenuList = []string{ diff --git a/cmd/server/app/service/internal/dao/scriptdao.go b/cmd/server/app/service/internal/dao/scriptdao.go index 7df041fa..4f1dcdf6 100644 --- a/cmd/server/app/service/internal/dao/scriptdao.go +++ b/cmd/server/app/service/internal/dao/scriptdao.go @@ -9,56 +9,201 @@ package dao import ( "fmt" + "math/rand" "time" "gitee.com/openeuler/PilotGo/pkg/dbmanager/mysqlmanager" + "gitee.com/openeuler/PilotGo/pkg/global" + "gitee.com/openeuler/PilotGo/sdk/response" + "github.com/pkg/errors" ) type Script struct { + ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` + Name string `json:"name"` + Content string `json:"content"` + Description string `json:"description"` + UpdatedAt time.Time + HistoryVersion []HistoryVersion `gorm:"foreignKey:ScriptID" json:"history_version"` + Deleted int `json:"deleted"` //deleted为1的时候表示删除,一般表示为0 +} + +type HistoryVersion struct { ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` - Name string `json:"name"` + ScriptID uint `json:"scriptid"` + Version string `gorm:"unique" json:"version"` Content string `json:"content"` Description string `json:"description"` UpdatedAt time.Time - Version string `gorm:"unique" json:"version"` - Deleted int `json:"deleted"` //deleted为1的时候表示删除,一般表示为0 + Script Script //`gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` } // 添加脚本文件 -func AddScript(s Script) error { - version := s.Version - if len(version) == 0 { - return fmt.Errorf("版本号不能为空") +func AddScript(_script Script) error { + _script.UpdatedAt = time.Now() + if err := mysqlmanager.MySQL().Save(&_script).Error; err != nil { + return err } - return mysqlmanager.MySQL().Save(&s).Error + return nil +} + +func UpdateScript(_script Script) error { + now := time.Now() + + old_script := Script{} + if err := mysqlmanager.MySQL().Where("id=?", _script.ID).Find(&old_script).Error; err != nil { + return nil + } + + err := mysqlmanager.MySQL().Model(&Script{}).Where("id=?", _script.ID).Updates(Script{ + Name: _script.Name, + Content: _script.Content, + Description: _script.Description, + UpdatedAt: now, + }).Error + if err != nil { + return err + } + + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + vcode := fmt.Sprintf("%06v", rnd.Int31n(1000000)) + version := now.Format("2006-01-02 15:04:05") + "-" + vcode + history := HistoryVersion{ + ScriptID: _script.ID, + Version: version, + Content: old_script.Content, + Description: old_script.Description, + UpdatedAt: now, + } + if err := mysqlmanager.MySQL().Save(&history).Error; err != nil { + return err + } + return nil } // 根据脚本版本号查询文件是否存在 func IsVersionExist(scriptversion string) (bool, error) { - var script Script - err := mysqlmanager.MySQL().Where("version=?", scriptversion).Find(&script).Error - return script.Deleted == 0, err + var historyscript HistoryVersion + err := mysqlmanager.MySQL().Where("version=?", scriptversion).Find(&historyscript).Error + return len(historyscript.Version) > 0, err } -// 根据版本号删除文件(将标志位变为1) -func DeleteScript(scriptversion string) error { - var script Script - VersionExistBool, err := IsVersionExist(scriptversion) +func DeleteScript(id uint, version string) error { + if id >= 1 && version == "" { + if err := mysqlmanager.MySQL().Model(&HistoryVersion{}).Where("script_id = ?", id).Unscoped().Delete(HistoryVersion{}).Error; err != nil { + return err + } + if err := mysqlmanager.MySQL().Model(&Script{}).Where("id=?", id).Unscoped().Delete(Script{}).Error; err != nil { + return err + } + return nil + } + + VersionExistBool, err := IsVersionExist(version) if err != nil { return err } - if VersionExistBool { - if err := mysqlmanager.MySQL().Model(&script).Where("version=?", scriptversion).Update("deleted", 1).Error; err != nil { + if !VersionExistBool { + return errors.Errorf("version %s not exist", version) + } + if err := mysqlmanager.MySQL().Model(&HistoryVersion{}).Where("version=?", version).Delete(HistoryVersion{}).Error; err != nil { + return err + } + return nil +} + +func ShowScriptContent(id uint) (string, error) { + var script Script + if err := mysqlmanager.MySQL().Where("id=?", id).Find(&script).Error; err != nil { + return "", err + } + return script.Content, nil +} + +func ShowScript(id uint) (*Script, error) { + var script *Script + if err := mysqlmanager.MySQL().Where("id=?", id).Find(&script).Error; err != nil { + return nil, err + } + return script, nil +} + +func ShowScriptWithVersion(id uint, version string) (string, error) { + var historyscript HistoryVersion + if err := mysqlmanager.MySQL().Where("script_id=? and version=?", id, version).Find(&historyscript).Error; err != nil { + return "", err + } + return historyscript.Content, nil +} + +func ScriptList(query *response.PaginationQ) ([]*Script, int, error) { + scripts := make([]*Script, 0) + if err := mysqlmanager.MySQL().Order("id desc").Limit(query.PageSize).Offset((query.Page - 1) * query.PageSize).Find(&scripts).Error; err != nil { + return nil, 0, err + } + + for _, script := range scripts { + script.UpdatedAt = script.UpdatedAt.Local() + } + + var total int64 + if err := mysqlmanager.MySQL().Model(&Script{}).Count(&total).Error; err != nil { + return nil, 0, err + } + return scripts, int(total), nil +} + +func GetScriptHistoryVersion(scriptid uint) ([]*HistoryVersion, error) { + history_scripts := make([]*HistoryVersion, 0) + if err := mysqlmanager.MySQL().Where("script_id=?", scriptid).Find(&history_scripts).Error; err != nil { + return nil, err + } + + for _, script := range history_scripts { + script.UpdatedAt = script.UpdatedAt.Local() + } + return history_scripts, nil +} + +type DangerousCommands struct { + ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` + Command string `gorm:"unique" json:"command"` + Active bool `json:"active"` +} + +func CreateDangerousCommands() error { + var total int64 + if err := mysqlmanager.MySQL().Model(&DangerousCommands{}).Count(&total).Error; err != nil { + return err + } + + if total == 0 { + for _, command := range global.DangerousCommandsList { + command_db := DangerousCommands{ + Command: command, + Active: true, + } + if err := mysqlmanager.MySQL().Create(&command_db).Error; err != nil { + return err + } + } + } + return nil +} + +func UpdateCommandsBlackList(_whitelist []uint) error { + for _, id := range _whitelist { + if err := mysqlmanager.MySQL().Model(&DangerousCommands{}).Where("id=?", id).Update("active", false).Error; err != nil { return err } - return nil } - return fmt.Errorf("脚本不存在") + return nil } -// 根据版本号查询脚本文件内容 -func ShowScript(scriptversion string) (string, error) { - var script Script - err := mysqlmanager.MySQL().Where("version=?", scriptversion).Find(&script).Error - return script.Content, err +func GetDangerousCommandsList() ([]*DangerousCommands, error) { + commands := make([]*DangerousCommands, 0) + if err := mysqlmanager.MySQL().Order("id").Find(&commands).Error; err != nil { + return nil, err + } + return commands, nil } diff --git a/cmd/server/app/service/script/dangerouscommands.go b/cmd/server/app/service/script/dangerouscommands.go new file mode 100644 index 00000000..d7d67c09 --- /dev/null +++ b/cmd/server/app/service/script/dangerouscommands.go @@ -0,0 +1,54 @@ +package script + +import "gitee.com/openeuler/PilotGo/cmd/server/app/service/internal/dao" + +type DangerousCommands dao.DangerousCommands + +type CommandsWithKey struct { + ID uint `json:"id"` + Key uint `json:"key"` + Command string `json:"command"` + Active bool `json:"active"` +} + +func CreateDangerousCommands() error { + return dao.CreateDangerousCommands() +} + +func UpdateCommandsBlackList(_whitelist []uint) error { + return dao.UpdateCommandsBlackList(_whitelist) +} + +func GetDangerousCommandsList() ([]*CommandsWithKey, error) { + commands, err := dao.GetDangerousCommandsList() + if err != nil { + return nil, err + } + + _commands := []*CommandsWithKey{} + for _, c := range commands { + _c := &CommandsWithKey{ + ID: c.ID, + Key: c.ID, + Command: c.Command, + Active: c.Active, + } + _commands = append(_commands, _c) + } + return _commands, nil +} + +func GetDangerousCommandsInBlackList() ([]string, error) { + commands, err := dao.GetDangerousCommandsList() + if err != nil { + return nil, err + } + + _commands := make([]string, 0) + for _, c := range commands { + if c.Active { + _commands = append(_commands, c.Command) + } + } + return _commands, nil +} diff --git a/cmd/server/app/service/script/script.go b/cmd/server/app/service/script/script.go index fb8d54f0..7dc75754 100644 --- a/cmd/server/app/service/script/script.go +++ b/cmd/server/app/service/script/script.go @@ -8,16 +8,31 @@ package script import ( - "errors" - "fmt" - "math/rand" - "time" + "strings" + "github.com/pkg/errors" + + "gitee.com/openeuler/PilotGo/cmd/server/app/agentmanager" + batchservice "gitee.com/openeuler/PilotGo/cmd/server/app/service/batch" "gitee.com/openeuler/PilotGo/cmd/server/app/service/internal/dao" + "gitee.com/openeuler/PilotGo/pkg/global" + "gitee.com/openeuler/PilotGo/sdk/common" + "gitee.com/openeuler/PilotGo/sdk/logger" + "gitee.com/openeuler/PilotGo/sdk/response" ) type Script = dao.Script +type HistoryVersion = dao.HistoryVersion + +type RunScriptMeta struct { + BatchID uint `json:"batch_id"` + MachineUUIDs []string `json:"machine_uuids"` + ScriptID uint `json:"script_id"` + Version string `json:"version"` + Params []string `json:"params"` +} + // 存储脚本文件 func AddScript(script *dao.Script) error { if len(script.Name) == 0 { @@ -29,20 +44,119 @@ func AddScript(script *dao.Script) error { if len(script.Description) == 0 { return errors.New("请输入脚本描述") } - rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - vcode := fmt.Sprintf("%06v", rnd.Int31n(1000000)) - version := time.Now().Format("2006-01-02 15:04:05") + "-" + vcode - sc := dao.Script{ - Name: script.Name, - Content: script.Content, - Description: script.Description, - UpdatedAt: time.Time{}, - Version: version, - Deleted: 0, - } - err := dao.AddScript(sc) - if err != nil { + + if err := dao.AddScript(*script); err != nil { + return errors.New("脚本文件添加失败") + } + return nil +} + +func UpdateScript(script *dao.Script) error { + if len(script.Name) == 0 { + return errors.New("请输入脚本文件名字") + } + if len(script.Content) == 0 { + return errors.New("请输入脚本内容") + } + if len(script.Description) == 0 { + return errors.New("请输入脚本描述") + } + + if err := dao.UpdateScript(*script); err != nil { return errors.New("脚本文件添加失败") } return nil } + +func DeleteScript(script_id uint, version string) error { + if script_id < 1 && version == "" { + return errors.New("script id or version abnormal") + } + + if err := dao.DeleteScript(script_id, version); err != nil { + return err + } + + return nil +} + +func ScriptList(query *response.PaginationQ) ([]*dao.Script, int, error) { + if query.PageSize == 0 && query.Page == 0 { + return nil, 0, errors.Errorf("pagesize: %d, page: %d", query.PageSize, query.Page) + } + + scripts, total, err := dao.ScriptList(query) + if err != nil { + return nil, 0, err + } + return scripts, total, nil +} + +func ScriptHistoryVersion(scriptid uint) ([]*dao.HistoryVersion, error) { + if scriptid < 1 { + return nil, errors.Errorf("script id abnormal, id: %d", scriptid) + } + + scripts, err := dao.GetScriptHistoryVersion(scriptid) + if err != nil { + return nil, err + } + return scripts, nil +} + +func RunScript(runscriptmeta *RunScriptMeta, batch *common.Batch) ([]batchservice.R, error) { + var err error + + script_content := "" + if runscriptmeta.Version == "" { + script_content, err = dao.ShowScriptContent(runscriptmeta.ScriptID) + if err != nil { + return nil, err + } + } else { + script_content, err = dao.ShowScriptWithVersion(runscriptmeta.ScriptID, runscriptmeta.Version) + if err != nil { + return nil, err + } + } + + cmds, err := GetDangerousCommandsInBlackList() + if err != nil { + return nil, errors.Errorf("run script error(dangerous commands list): %s", err.Error()) + } + positions, matchedCommands := global.FindDangerousCommandsPos(script_content, cmds) + if len(positions) > 0 { + return nil, errors.New("Dangerous commands detected in script: " + strings.Join(matchedCommands, "\n")) + } + + f := func(uuid string) batchservice.R { + agent := agentmanager.GetAgent(uuid) + if agent != nil { + data, err := agent.RunScript(script_content, runscriptmeta.Params) + if err != nil { + logger.Error("run script error, agent:%s, command:%s", uuid, script_content) + } + logger.Debug("run script on agent result:%v", data) + re := common.CmdResult{ + MachineUUID: uuid, + MachineIP: agent.IP, + RetCode: data.RetCode, + Stdout: data.Stdout, + Stderr: data.Stderr, + } + return re + } + return common.CmdResult{} + } + + result := batchservice.BatchProcess(batch, f, script_content, runscriptmeta.Params) + return result, nil +} + +func GetScriptByID(id uint) (*dao.Script, error) { + script, err := dao.ShowScript(id) + if err != nil { + return nil, err + } + return script, nil +} diff --git a/docs/api/customScriptAPI.jsonc b/docs/api/customScriptAPI.jsonc new file mode 100644 index 00000000..19b2c62d --- /dev/null +++ b/docs/api/customScriptAPI.jsonc @@ -0,0 +1,180 @@ +{ + "/api/v1/script/create": { + "POST": { + "summary": "创建脚本", + "queryParam": null, + "requestBody": { + "name": "unset-web-proxy", + "content": "#!/bin/bash\nunset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy\necho \"third edit\"", + "description": "取消网络代理环境变量" + }, + "response": {} + } + }, + "/api/v1/script/update": { + "PUT": { + "summary": "更新脚本", + "queryParam": null, + "requestBody": { + "id": 4, + "name": "", + "content": "", + "description": "" + }, + "response": {} + } + }, + "/api/v1/script/delete": { + "DELETE": { + "summary": "删除脚本", + "queryParam": null, + "requestBody": { + "script_id": 4, + "version": "2025-01-03 14:09:23-109164" + }, + "response": {} + } + }, + "/api/v1/script_auth/run": { + "POST": { + "summary": "运行脚本", + "queryParam": null, + "requestBody": { + "batch_id": 13, + "machine_uuids": [ + ], + "script_id": 4, + "version": "2025-01-03 14:20:32-201623", + "params": [ + "" + ] + }, + "response": { + "code": 200, + "data": [ + { + "machine_uuid": "2dab460c-3075-4e09-90a3-ab031ff823f2", + "machine_ip": "192.168.75.134", + "retcode": 0, + "stdout": "second edit", + "stderr": "" + }, + { + "machine_uuid": "", + "machine_ip": "", + "retcode": 0, + "stdout": "", + "stderr": "" + } + ], + "msg": "成功" + } + } + }, + "/api/v1/script/list_all": { + "GET": { + "summary": "获取脚本列表", + "queryParam": { + "page": 1, + "size": 10 + }, + "requestBody": null, + "response": { + "code": 200, + "data": [ + { + "id": 4, + "name": "unset-web-proxy", + "content": "#!/bin/bash\nunset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy\necho \"third edit\"", + "description": "取消网络代理环境变量", + "UpdatedAt": "2025-01-03T14:20:32.299+08:00", + "history_version": null, + "deleted": 0 + } + ], + "ok": true, + "page": 1, + "size": 10, + "total": 2 + } + } + }, + "/api/v1/script/list_history": { + "GET": { + "summary": "获取指定脚本的历史版本", + "queryParam": { + "script_id": 1 + }, + "requestBody": null, + "response": { + "code": 200, + "data": [ + { + "id": 6, + "scriptid": 4, + "version": "2025-01-03 14:08:55-930328", + "content": "#!/bin/bash\nunset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy", + "description": "取消网络代理环境变量", + "UpdatedAt": "2025-01-03T14:08:55.922+08:00", + "Script": { + "id": 0, + "name": "", + "content": "", + "description": "", + "UpdatedAt": "0001-01-01T00:00:00Z", + "history_version": null, + "deleted": 0 + } + } + ], + "msg": "成功" + } + } + }, + "/api/v1/script/blacklist": { + "GET": { + "summary": "获取命令黑名单列表", + "queryParam": null, + "requestBody": null, + "response": { + "code": 200, + "data": [ + { + "id": 1, + "command": ".*rm\\s+-[r,f,rf].*", + "active": false + }, + { + "id": 2, + "command": ".*lvremove\\s+-f.*", + "active": false + }, + { + "id": 3, + "command": ".*poweroff.*", + "active": false + } + ], + "msg": "成功" + } + } + }, + "/api/v1/script_auth/update_blacklist": { + "PUT": { + "summary": "修改命令黑名单", + "queryParam": null, + "requestBody": { + "white_list": [ + 1, + 2, + 3 + ] + }, + "response": { + "code": 200, + "data": null, + "msg": "成功" + } + } + } +} \ No newline at end of file diff --git a/pkg/dbmanager/db.go b/pkg/dbmanager/db.go index 3c5e31f2..28e90086 100644 --- a/pkg/dbmanager/db.go +++ b/pkg/dbmanager/db.go @@ -56,7 +56,7 @@ func MysqldbInit(conf *options.MysqlDBInfo) error { mysqlmanager.MySQL().AutoMigrate(&auditlog.AuditLog{}) mysqlmanager.MySQL().AutoMigrate(&configmanage.ConfigFiles{}) mysqlmanager.MySQL().AutoMigrate(&configmanage.HistoryConfigFiles{}) - mysqlmanager.MySQL().AutoMigrate(&script.Script{}) + mysqlmanager.MySQL().AutoMigrate(&script.Script{}, &script.HistoryVersion{}, &script.DangerousCommands{}) mysqlmanager.MySQL().AutoMigrate(&configfile.ConfigFile{}) mysqlmanager.MySQL().AutoMigrate(&user.User{}) mysqlmanager.MySQL().AutoMigrate(&role.Role{}) @@ -71,5 +71,10 @@ func MysqldbInit(conf *options.MysqlDBInfo) error { // 创建公司组织 mysqlmanager.MySQL().AutoMigrate(&depart.DepartNode{}) + // 创建高危命令黑名单 + if err := script.CreateDangerousCommands(); err != nil { + return err + } + return depart.CreateOrganization() } diff --git a/pkg/global/commandscheck.go b/pkg/global/commandscheck.go new file mode 100644 index 00000000..f6beb056 --- /dev/null +++ b/pkg/global/commandscheck.go @@ -0,0 +1,67 @@ +package global + +import "regexp" + +// 高危命令检测 +func FindDangerousCommandsPos(content string, dangerous_commands []string) ([][]int, []string) { + var positions [][]int + var matchedCommands []string + + for _, pattern := range dangerous_commands { + re, err := regexp.Compile(pattern) + if err != nil { + // TODO: info remind + continue + } + matches := re.FindAllStringIndex(content, -1) + for _, match := range matches { + start, end := match[0], match[1]-1 + positions = append(positions, []int{start, end}) + matchedCommands = append(matchedCommands, content[start:end+1]) + } + } + return positions, matchedCommands +} + +var DangerousCommandsList = []string{ + `.*rm\s+-[r,f,rf].*`, + `.*lvremove\s+-f.*`, + `.*poweroff.*`, + `.*shutdown\s+-[f,F,h,k,n,r,t,C].*`, + `.*pvremove\s+-f.*`, + `.*vgremove\s+-f.*`, + `.*exportfs\s+-[a,u].*`, + `.*umount.nfs+.*.+-[r,f,rf].*`, + `.*mv+.*.+/dev/null.*`, + `.*reboot.*`, + `.*rmmod\s+-[a,s,v,f,w].*`, + `.*dpkg-divert+.*.+-remove.*`, + `.*dd.*`, + `.*mkfs.*`, + `.*vmo.*`, + `.*init.*`, + `.*halt.*`, + `.*fasthalt.*`, + `.*fastboot.*`, + `.*startsrc.*`, + `.*stopsrc.*`, + `.*chkconfig.*`, + `.*off.*`, + `.*refresh.*`, + `.*umount.*`, + `.*rmdev.*`, + `.*chdev.*`, + `.*extendvg.*`, + `.*reducevg.*`, + `.*importvg.*`, + `.*exportvg.*`, + `.*mklv.*`, + `.*rmlv.*`, + `.*rmfs.*`, + `.*chfs.*`, + `.*installp.*`, + `.*instfix.*`, + `.*crontab.*`, + `.*cfgmgr.*`, + `.*mknod.*`, +} \ No newline at end of file -- Gitee