Files
MyGoNavi/internal/app/methods_jvm_diagnostic.go
Syngnat ec2eefc9d2 🐛 fix(jvm): 加固诊断命令策略与输出脱敏
在服务端阻断只读连接中的高风险和多行诊断命令,并对诊断事件与错误消息统一脱敏,避免凭证、Authorization 和 PEM 片段泄漏。
2026-04-28 09:42:41 +08:00

297 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/jvm"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
var newJVMDiagnosticTransport = jvm.NewDiagnosticTransport
var emitJVMDiagnosticRuntimeEvent = runtime.EventsEmit
const diagnosticChunkEvent = "jvm:diagnostic:chunk"
type diagnosticChunkEventPayload struct {
TabID string `json:"tabId"`
Chunk jvm.DiagnosticEventChunk `json:"chunk"`
}
func swapJVMDiagnosticTransportFactory(factory func(mode string) (jvm.DiagnosticTransport, error)) func() {
prev := newJVMDiagnosticTransport
newJVMDiagnosticTransport = factory
return func() { newJVMDiagnosticTransport = prev }
}
func resolveJVMDiagnosticTransport(cfg connection.ConnectionConfig) (connection.ConnectionConfig, jvm.DiagnosticTransport, error) {
normalized, err := jvm.NormalizeConnectionConfig(cfg)
if err != nil {
return connection.ConnectionConfig{}, nil, err
}
diagCfg, err := jvm.NormalizeDiagnosticConfig(normalized)
if err != nil {
return connection.ConnectionConfig{}, nil, err
}
if !diagCfg.Enabled {
return connection.ConnectionConfig{}, nil, errors.New("当前连接未启用 JVM 诊断增强模式")
}
normalized.JVM.Diagnostic = diagCfg
transport, err := newJVMDiagnosticTransport(diagCfg.Transport)
if err != nil {
return connection.ConnectionConfig{}, nil, err
}
return normalized, transport, nil
}
func (a *App) JVMProbeDiagnosticCapabilities(cfg connection.ConnectionConfig) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
items, err := transport.ProbeCapabilities(a.ctx, normalized)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: items}
}
func (a *App) JVMStartDiagnosticSession(cfg connection.ConnectionConfig, req jvm.DiagnosticSessionRequest) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
handle, err := transport.StartSession(a.ctx, normalized, req)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: handle}
}
func (a *App) JVMExecuteDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, req jvm.DiagnosticCommandRequest) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
redactor := jvm.NewDiagnosticOutputRedactor()
req.SessionID = strings.TrimSpace(req.SessionID)
req.CommandID = strings.TrimSpace(req.CommandID)
req.Command = strings.TrimSpace(req.Command)
req.Source = strings.TrimSpace(req.Source)
req.Reason = strings.TrimSpace(req.Reason)
if req.SessionID == "" {
return connection.QueryResult{Success: false, Message: "诊断会话 ID 不能为空,请先创建会话"}
}
if req.Command == "" {
return connection.QueryResult{Success: false, Message: "诊断命令不能为空"}
}
if req.CommandID == "" {
req.CommandID = fmt.Sprintf("diag-%d", time.Now().UnixNano())
}
if req.Source == "" {
req.Source = "manual"
}
commandType, err := jvm.ValidateDiagnosticExecutionPolicy(normalized, req.Command)
if err != nil {
message := redactor.RedactContent(req.SessionID, req.CommandID, err.Error())
return connection.QueryResult{Success: false, Message: message}
}
riskLevel := diagnosticRiskLevel(commandType)
auditStore := jvm.NewDiagnosticAuditStore(filepath.Join(a.auditRootDir(), "jvm_diag_audit.jsonl"))
var auditWarnings []string
if err := auditStore.Append(jvm.DiagnosticAuditRecord{
ConnectionID: normalized.ID,
SessionID: req.SessionID,
CommandID: req.CommandID,
Transport: normalized.JVM.Diagnostic.Transport,
Command: req.Command,
CommandType: commandType,
Source: req.Source,
Reason: req.Reason,
RiskLevel: riskLevel,
Status: "running",
}); err != nil {
return connection.QueryResult{Success: false, Message: "诊断审计记录写入失败,已阻止命令执行: " + err.Error()}
}
terminalSeen := false
appendTerminalAudit := func(status string) {
if terminalSeen {
return
}
terminalSeen = true
if err := auditStore.Append(jvm.DiagnosticAuditRecord{
ConnectionID: normalized.ID,
SessionID: req.SessionID,
CommandID: req.CommandID,
Transport: normalized.JVM.Diagnostic.Transport,
Command: req.Command,
CommandType: commandType,
Source: req.Source,
Reason: req.Reason,
RiskLevel: riskLevel,
Status: status,
}); err != nil {
auditWarnings = append(auditWarnings, "审计记录写入失败: "+err.Error())
}
}
if binder, ok := transport.(interface{ SetEventSink(jvm.DiagnosticEventSink) }); ok {
binder.SetEventSink(func(chunk jvm.DiagnosticEventChunk) {
if chunk.Timestamp == 0 {
chunk.Timestamp = time.Now().UnixMilli()
}
chunk.SessionID = req.SessionID
chunk.CommandID = req.CommandID
chunk = redactor.RedactChunk(chunk)
a.emitDiagnosticChunk(tabID, chunk)
if isDiagnosticTerminalPhase(chunk.Phase) {
appendTerminalAudit(chunk.Phase)
}
})
}
if err := transport.ExecuteCommand(a.ctx, normalized, req); err != nil {
phase := "failed"
if strings.Contains(strings.ToLower(err.Error()), "canceled") {
phase = "canceled"
}
redactedError := redactor.RedactContent(req.SessionID, req.CommandID, err.Error())
if !terminalSeen {
chunk := jvm.DiagnosticEventChunk{
SessionID: req.SessionID,
CommandID: req.CommandID,
Event: "diagnostic",
Phase: phase,
Content: redactedError,
Timestamp: time.Now().UnixMilli(),
}
a.emitDiagnosticChunk(tabID, chunk)
appendTerminalAudit(phase)
}
return connection.QueryResult{Success: false, Message: joinDiagnosticMessages(redactedError, auditWarnings)}
}
if !terminalSeen {
chunk := jvm.DiagnosticEventChunk{
SessionID: req.SessionID,
CommandID: req.CommandID,
Event: "diagnostic",
Phase: "completed",
Content: "诊断命令执行完成",
Timestamp: time.Now().UnixMilli(),
}
a.emitDiagnosticChunk(tabID, chunk)
appendTerminalAudit("completed")
}
return connection.QueryResult{
Success: true,
Message: joinDiagnosticMessages("", auditWarnings),
Data: map[string]any{
"sessionId": req.SessionID,
"commandId": req.CommandID,
"status": "accepted",
},
}
}
func (a *App) JVMCancelDiagnosticCommand(cfg connection.ConnectionConfig, tabID string, sessionID string, commandID string) connection.QueryResult {
normalized, transport, err := resolveJVMDiagnosticTransport(cfg)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
sessionID = strings.TrimSpace(sessionID)
commandID = strings.TrimSpace(commandID)
if sessionID == "" || commandID == "" {
return connection.QueryResult{Success: false, Message: "取消命令缺少 sessionId 或 commandId"}
}
if err := transport.CancelCommand(a.ctx, normalized, sessionID, commandID); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
a.emitDiagnosticChunk(tabID, jvm.DiagnosticEventChunk{
SessionID: sessionID,
CommandID: commandID,
Event: "diagnostic",
Phase: "canceling",
Content: "已发送取消请求,等待诊断桥接端结束命令",
Timestamp: time.Now().UnixMilli(),
})
return connection.QueryResult{
Success: true,
Data: map[string]any{
"sessionId": sessionID,
"commandId": commandID,
"status": "cancel-requested",
},
}
}
func (a *App) JVMListDiagnosticAuditRecords(connectionID string, limit int) connection.QueryResult {
records, err := jvm.NewDiagnosticAuditStore(filepath.Join(a.auditRootDir(), "jvm_diag_audit.jsonl")).List(connectionID, limit)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Data: records}
}
func (a *App) emitDiagnosticChunk(tabID string, chunk jvm.DiagnosticEventChunk) {
if a.ctx == nil {
return
}
emitJVMDiagnosticRuntimeEvent(a.ctx, diagnosticChunkEvent, diagnosticChunkEventPayload{
TabID: strings.TrimSpace(tabID),
Chunk: chunk,
})
}
func diagnosticRiskLevel(commandType string) string {
switch strings.TrimSpace(commandType) {
case jvm.DiagnosticCommandCategoryObserve:
return "low"
case jvm.DiagnosticCommandCategoryTrace:
return "medium"
default:
return "high"
}
}
func isDiagnosticTerminalPhase(phase string) bool {
switch strings.ToLower(strings.TrimSpace(phase)) {
case "completed", "failed", "canceled":
return true
default:
return false
}
}
func joinDiagnosticMessages(primary string, warnings []string) string {
items := make([]string, 0, 1+len(warnings))
if strings.TrimSpace(primary) != "" {
items = append(items, strings.TrimSpace(primary))
}
for _, warning := range warnings {
if strings.TrimSpace(warning) == "" {
continue
}
items = append(items, strings.TrimSpace(warning))
}
return strings.Join(items, "")
}