fix: 正确识别 Agent 安装与审计失败
- 下载脚本失败时立即终止安装并提示 WAF 拦截 - Agent 服务未运行时不再计为安装成功 - 审计列表兼容后端 status 成功失败字段
This commit is contained in:
1 parent
7553ef54de
commit
827d9a31cd
3 files changed
+75
-25
No files matched your search
@@ -180,6 +180,34 @@ func shellQuote(value string) string {
|
|||||||
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
|
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// agentInstallCommand 先完整下载脚本再执行,确保 curl 失败能正确传递退出状态。 // Logan.Li
|
||||||
|
func agentInstallCommand(serverURL, token string) string {
|
||||||
|
scriptURL := shellQuote(serverURL + "/agent/install.sh")
|
||||||
|
serverArg := shellQuote(serverURL)
|
||||||
|
tokenArg := shellQuote(token)
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`tmp=$(mktemp /tmp/aiops-agent-install.XXXXXX) && trap 'rm -f "$tmp"' EXIT && curl -fsSL -o "$tmp" %s && if [ "$(id -u)" -eq 0 ]; then bash "$tmp" --server %s --token %s; else sudo -n bash "$tmp" --server %s --token %s; fi`,
|
||||||
|
scriptURL, serverArg, tokenArg, serverArg, tokenArg,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// agentInstallErrorMessage 将常见安装失败转换为可操作的中文提示。 // Logan.Li
|
||||||
|
func agentInstallErrorMessage(serverURL, output string) string {
|
||||||
|
lowerOutput := strings.ToLower(output)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(lowerOutput, "403") || strings.Contains(lowerOutput, "forbidden"):
|
||||||
|
return "域名代理或 WAF 拒绝 Agent 下载(HTTP 403),请为 /agent/* 关闭人机验证或加入机器访问白名单"
|
||||||
|
case strings.Contains(lowerOutput, "sudo: a password is required") || strings.Contains(lowerOutput, "需要密码"):
|
||||||
|
return "目标主机 SSH 用户没有免密 sudo 权限,请使用 root 账号或配置 passwordless sudo"
|
||||||
|
case strings.Contains(lowerOutput, "curl") && (strings.Contains(lowerOutput, "not found") || strings.Contains(lowerOutput, "未找到")):
|
||||||
|
return "目标主机未安装 curl"
|
||||||
|
case strings.Contains(lowerOutput, "下载失败") || strings.Contains(lowerOutput, "failed to connect") || strings.Contains(lowerOutput, "connection refused"):
|
||||||
|
return "目标主机无法下载安装脚本,请检查后端地址 " + serverURL + " 是否可达"
|
||||||
|
default:
|
||||||
|
return "Agent 安装命令执行失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// detectAgentVersion SSH 到目标主机读取已安装 Agent 的真实版本号 // Logan.Li
|
// detectAgentVersion SSH 到目标主机读取已安装 Agent 的真实版本号 // Logan.Li
|
||||||
func detectAgentVersion(client *ssh.Client) string {
|
func detectAgentVersion(client *ssh.Client) string {
|
||||||
verOut, _ := runSSHCommand(client, "/usr/local/bin/aiops-agent version 2>/dev/null || /usr/local/bin/aiops-agent -v 2>/dev/null || echo ''")
|
verOut, _ := runSSHCommand(client, "/usr/local/bin/aiops-agent version 2>/dev/null || /usr/local/bin/aiops-agent -v 2>/dev/null || echo ''")
|
||||||
@@ -298,25 +326,13 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
|||||||
token = c.Query("token")
|
token = c.Query("token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 构造安装命令:root 用户直接 bash,非 root 用 sudo -n(无密码才执行,避免挂死)// Logan.Li
|
// 4. 先下载脚本再执行,curl/WAF 失败时不会被管道末端 bash 的退出码掩盖。 // Logan.Li
|
||||||
installCmd := fmt.Sprintf(
|
installCmd := agentInstallCommand(serverURL, token)
|
||||||
`if [ "$(id -u)" -eq 0 ]; then curl -fsSL %s/agent/install.sh | bash -s -- --server %s --token %s; else curl -fsSL %s/agent/install.sh | sudo -n bash -s -- --server %s --token %s; fi`,
|
|
||||||
shellQuote(serverURL), shellQuote(serverURL), shellQuote(token),
|
|
||||||
shellQuote(serverURL), shellQuote(serverURL), shellQuote(token),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 5. 执行安装
|
// 5. 执行安装
|
||||||
installOut, err := runSSHCommand(client, installCmd)
|
installOut, err := runSSHCommand(client, installCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := "安装命令执行失败"
|
msg := agentInstallErrorMessage(serverURL, installOut)
|
||||||
lowerOut := strings.ToLower(installOut)
|
|
||||||
if strings.Contains(lowerOut, "sudo: a password is required") || strings.Contains(lowerOut, "需要密码") {
|
|
||||||
msg = "目标主机 SSH 用户没有免密 sudo 权限,请使用 root 账号或配置 passwordless sudo"
|
|
||||||
} else if strings.Contains(lowerOut, "curl") && (strings.Contains(lowerOut, "not found") || strings.Contains(lowerOut, "未找到")) {
|
|
||||||
msg = "目标主机未安装 curl"
|
|
||||||
} else if strings.Contains(lowerOut, "下载失败") || strings.Contains(lowerOut, "failed to connect") || strings.Contains(lowerOut, "connection refused") {
|
|
||||||
msg = "目标主机无法下载安装脚本,请检查后端地址 " + serverURL + " 是否可达"
|
|
||||||
}
|
|
||||||
h.recordAgentAudit(ac, "install", resource, resourceID, 0,
|
h.recordAgentAudit(ac, "install", resource, resourceID, 0,
|
||||||
fmt.Sprintf("server=%s arch=%s", serverURL, arch), installOut, msg)
|
fmt.Sprintf("server=%s arch=%s", serverURL, arch), installOut, msg)
|
||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
@@ -330,6 +346,13 @@ func (h *AgentInstallHandler) InstallAgent(c *gin.Context) {
|
|||||||
// 5. 验证安装是否成功
|
// 5. 验证安装是否成功
|
||||||
verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'")
|
verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'")
|
||||||
installed := contains(verifyOut, "active") || contains(verifyOut, "running")
|
installed := contains(verifyOut, "active") || contains(verifyOut, "running")
|
||||||
|
if !installed {
|
||||||
|
msg := "安装脚本已执行,但 Agent 服务未运行"
|
||||||
|
h.recordAgentAudit(ac, "install", resource, resourceID, 0,
|
||||||
|
fmt.Sprintf("server=%s arch=%s", serverURL, arch), installOut+"\n"+verifyOut, msg)
|
||||||
|
c.JSON(http.StatusOK, model.Response{Code: 500, Message: msg, Data: gin.H{"output": installOut, "verify": verifyOut}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var agentVer string
|
var agentVer string
|
||||||
if installed {
|
if installed {
|
||||||
@@ -384,11 +407,7 @@ func (h *AgentInstallHandler) installOne(server model.Server, serverURL, token s
|
|||||||
|
|
||||||
// 不强制 server.Status==running:只要 SSH 可达即可安装(DB 状态可能滞后于实际开机状态) // Logan.Li
|
// 不强制 server.Status==running:只要 SSH 可达即可安装(DB 状态可能滞后于实际开机状态) // Logan.Li
|
||||||
|
|
||||||
installCmd := fmt.Sprintf(
|
installCmd := agentInstallCommand(serverURL, token)
|
||||||
`if [ "$(id -u)" -eq 0 ]; then curl -fsSL %s/agent/install.sh | bash -s -- --server %s --token %s; else curl -fsSL %s/agent/install.sh | sudo -n bash -s -- --server %s --token %s; fi`,
|
|
||||||
shellQuote(serverURL), shellQuote(serverURL), shellQuote(token),
|
|
||||||
shellQuote(serverURL), shellQuote(serverURL), shellQuote(token),
|
|
||||||
)
|
|
||||||
|
|
||||||
installOut, err := runSSHCommand(client, installCmd)
|
installOut, err := runSSHCommand(client, installCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -397,6 +416,9 @@ func (h *AgentInstallHandler) installOne(server model.Server, serverURL, token s
|
|||||||
|
|
||||||
verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'")
|
verifyOut, _ := runSSHCommand(client, "systemctl is-active aiops-agent 2>/dev/null || echo 'not_found'")
|
||||||
installed := contains(verifyOut, "active") || contains(verifyOut, "running")
|
installed := contains(verifyOut, "active") || contains(verifyOut, "running")
|
||||||
|
if !installed {
|
||||||
|
return false, installOut, verifyOut, fmt.Errorf("安装脚本已执行,但 Agent 服务未运行")
|
||||||
|
}
|
||||||
|
|
||||||
if installed {
|
if installed {
|
||||||
// 只更新在线状态;版本号仅在能读到真实版本时才写入,未上报时不写假版本 // Logan.Li
|
// 只更新在线状态;版本号仅在能读到真实版本时才写入,未上报时不写假版本 // Logan.Li
|
||||||
@@ -473,8 +495,13 @@ func (h *AgentInstallHandler) BatchInstallAgent(c *gin.Context) {
|
|||||||
rid := strconv.Itoa(int(server.ID))
|
rid := strconv.Itoa(int(server.ID))
|
||||||
reqDetail := fmt.Sprintf("server=%s host=%s", serverURL, res)
|
reqDetail := fmt.Sprintf("server=%s host=%s", serverURL, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[BatchInstall] %s (%s) 安装失败: %v", server.Hostname, server.IP, err)
|
errMessage := agentInstallErrorMessage(serverURL, installOut)
|
||||||
h.recordAgentAudit(ac, "install", res, rid, 0, reqDetail, installOut, truncateAuditText(err.Error(), 500))
|
if errMessage == "Agent 安装命令执行失败" {
|
||||||
|
errMessage = err.Error()
|
||||||
|
}
|
||||||
|
log.Printf("[BatchInstall] %s (%s) 安装失败: %s", server.Hostname, server.IP, errMessage)
|
||||||
|
respDetail := fmt.Sprintf("=== 安装输出 ===\n%s\n=== 服务状态 ===\n%s", installOut, verifyOut)
|
||||||
|
h.recordAgentAudit(ac, "install", res, rid, 0, reqDetail, respDetail, truncateAuditText(errMessage, 500))
|
||||||
} else {
|
} else {
|
||||||
successCount++
|
successCount++
|
||||||
respDetail := fmt.Sprintf("=== 安装输出 ===\n%s\n=== 服务状态 ===\n%s", installOut, verifyOut)
|
respDetail := fmt.Sprintf("=== 安装输出 ===\n%s\n=== 服务状态 ===\n%s", installOut, verifyOut)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
// TestValidateInstallServerURL 验证 Agent 公网地址仅接受安全的 HTTP(S) origin。 // Logan.Li
|
// TestValidateInstallServerURL 验证 Agent 公网地址仅接受安全的 HTTP(S) origin。 // Logan.Li
|
||||||
func TestValidateInstallServerURL(t *testing.T) {
|
func TestValidateInstallServerURL(t *testing.T) {
|
||||||
@@ -36,3 +39,22 @@ func TestValidateInstallServerURL(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAgentInstallCommand 验证下载失败不会被管道末端命令掩盖。 // Logan.Li
|
||||||
|
func TestAgentInstallCommand(t *testing.T) {
|
||||||
|
command := agentInstallCommand("https://aiops.example.com", "token-value")
|
||||||
|
if strings.Contains(command, "| bash") {
|
||||||
|
t.Fatalf("安装命令不应继续使用 curl 管道: %s", command)
|
||||||
|
}
|
||||||
|
if !strings.Contains(command, "curl -fsSL -o") || !strings.Contains(command, "https://aiops.example.com/agent/install.sh") {
|
||||||
|
t.Fatalf("安装命令缺少脚本下载步骤: %s", command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAgentInstallErrorMessage 验证 WAF 拒绝能返回明确提示。 // Logan.Li
|
||||||
|
func TestAgentInstallErrorMessage(t *testing.T) {
|
||||||
|
message := agentInstallErrorMessage("https://aiops.example.com", "curl: (22) The requested URL returned error: 403")
|
||||||
|
if !strings.Contains(message, "WAF") || !strings.Contains(message, "/agent/*") {
|
||||||
|
t.Fatalf("WAF 错误提示不完整: %s", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,8 +33,9 @@ function normalizeLog(raw: JsonData): OperationLog {
|
|||||||
resource: raw.resource ?? '',
|
resource: raw.resource ?? '',
|
||||||
ip: raw.ip ?? '',
|
ip: raw.ip ?? '',
|
||||||
userAgent: raw.user_agent ?? raw.userAgent ?? '',
|
userAgent: raw.user_agent ?? raw.userAgent ?? '',
|
||||||
result: raw.result === 'failure' ? 'failure' : 'success',
|
// 后端审计日志使用 status=1/0 表示成功/失败,同时兼容旧 result 字段。 // Logan.Li
|
||||||
message: raw.message ?? raw.detail,
|
result: raw.result === 'failure' || raw.status === 0 || raw.status === 'failed' ? 'failure' : 'success',
|
||||||
|
message: raw.message ?? raw.detail ?? raw.error,
|
||||||
createdAt: raw.createdAt ?? raw.created_at ?? '',
|
createdAt: raw.createdAt ?? raw.created_at ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user