mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-24 14:59:59 +02:00
201 lines
4.7 KiB
Go
201 lines
4.7 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"sync"
|
|
|
|
"github.com/cloudwego/eino/adk/filesystem"
|
|
"github.com/cloudwego/eino/schema"
|
|
)
|
|
|
|
// ConfigureShellCmdForAgentExecute 与 exec 工具一致:非交互 stdin、pager/TERM 环境、独立进程组。
|
|
func ConfigureShellCmdForAgentExecute(cmd *exec.Cmd) {
|
|
if cmd == nil {
|
|
return
|
|
}
|
|
applyDefaultTerminalEnv(cmd)
|
|
attachNonInteractiveStdin(cmd)
|
|
_ = prepareShellCmdSession(cmd)
|
|
}
|
|
|
|
// TerminateShellCmdTree 尽力终止 shell 及其子进程组(与 exec/execute 超时取消一致)。
|
|
func TerminateShellCmdTree(cmd *exec.Cmd) {
|
|
terminateCmdTree(cmd)
|
|
}
|
|
|
|
// EinoStreamingShell 为 Eino ADK execute 工具提供流式 shell,行为与 exec 对齐:
|
|
// 并发读取 stdout/stderr(定长块,非按行),避免官方 local.ExecuteStreaming 先排空 stdout
|
|
// 导致 stderr 错误(如 sudo 密码提示)长时间不可见、UI 一直显示「执行中」。
|
|
type EinoStreamingShell struct{}
|
|
|
|
// NewEinoStreamingShell 创建 execute 流式 shell 实现。
|
|
func NewEinoStreamingShell() *EinoStreamingShell {
|
|
return &EinoStreamingShell{}
|
|
}
|
|
|
|
// ExecuteStreaming 实现 filesystem.StreamingShell。
|
|
func (s *EinoStreamingShell) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) {
|
|
if input == nil || input.Command == "" {
|
|
return nil, fmt.Errorf("command is required")
|
|
}
|
|
|
|
sr, w := schema.Pipe[*filesystem.ExecuteResponse](100)
|
|
if input.RunInBackendGround {
|
|
go runShellInBackground(ctx, input.Command, w)
|
|
return sr, nil
|
|
}
|
|
go streamShellForeground(ctx, input.Command, w)
|
|
return sr, nil
|
|
}
|
|
|
|
func runShellInBackground(ctx context.Context, command string, w *schema.StreamWriter[*filesystem.ExecuteResponse]) {
|
|
defer w.Close()
|
|
|
|
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
|
ConfigureShellCmdForAgentExecute(cmd)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
_ = w.Send(nil, fmt.Errorf("failed to create stdout pipe: %w", err))
|
|
return
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
_ = stdout.Close()
|
|
_ = w.Send(nil, fmt.Errorf("failed to create stderr pipe: %w", err))
|
|
return
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
_ = stdout.Close()
|
|
_ = stderr.Close()
|
|
_ = w.Send(nil, fmt.Errorf("failed to start command: %w", err))
|
|
return
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
drainShellPipes(stdout, stderr)
|
|
_ = cmd.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-ctx.Done():
|
|
TerminateShellCmdTree(cmd)
|
|
}
|
|
|
|
exitCode := 0
|
|
_ = w.Send(&filesystem.ExecuteResponse{
|
|
Output: "command started in background\n",
|
|
ExitCode: &exitCode,
|
|
}, nil)
|
|
}
|
|
|
|
func drainShellPipes(stdout, stderr io.Reader) {
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = io.Copy(io.Discard, stdout)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = io.Copy(io.Discard, stderr)
|
|
}()
|
|
wg.Wait()
|
|
}
|
|
|
|
func streamShellForeground(ctx context.Context, command string, w *schema.StreamWriter[*filesystem.ExecuteResponse]) {
|
|
defer w.Close()
|
|
|
|
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
|
ConfigureShellCmdForAgentExecute(cmd)
|
|
|
|
stdoutPipe, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
_ = w.Send(nil, fmt.Errorf("failed to create stdout pipe: %w", err))
|
|
return
|
|
}
|
|
stderrPipe, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
_ = stdoutPipe.Close()
|
|
_ = w.Send(nil, fmt.Errorf("failed to create stderr pipe: %w", err))
|
|
return
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
_ = stdoutPipe.Close()
|
|
_ = stderrPipe.Close()
|
|
_ = w.Send(nil, fmt.Errorf("failed to start command: %w", err))
|
|
return
|
|
}
|
|
|
|
stopWatch := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
TerminateShellCmdTree(cmd)
|
|
case <-stopWatch:
|
|
}
|
|
}()
|
|
defer close(stopWatch)
|
|
|
|
chunks := make(chan string, 64)
|
|
var wg sync.WaitGroup
|
|
readFn := func(r io.Reader) {
|
|
defer wg.Done()
|
|
buf := make([]byte, 8192)
|
|
for {
|
|
n, readErr := r.Read(buf)
|
|
if n > 0 {
|
|
chunks <- string(buf[:n])
|
|
}
|
|
if readErr != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
wg.Add(2)
|
|
go readFn(stdoutPipe)
|
|
go readFn(stderrPipe)
|
|
go func() {
|
|
wg.Wait()
|
|
close(chunks)
|
|
}()
|
|
|
|
hadOutput := false
|
|
for chunk := range chunks {
|
|
if chunk == "" {
|
|
continue
|
|
}
|
|
hadOutput = true
|
|
if w.Send(&filesystem.ExecuteResponse{Output: chunk}, nil) {
|
|
TerminateShellCmdTree(cmd)
|
|
return
|
|
}
|
|
}
|
|
|
|
waitErr := cmd.Wait()
|
|
if waitErr == nil {
|
|
exitCode := 0
|
|
_ = w.Send(&filesystem.ExecuteResponse{ExitCode: &exitCode}, nil)
|
|
return
|
|
}
|
|
|
|
var exitError *exec.ExitError
|
|
if errors.As(waitErr, &exitError) {
|
|
exitCode := exitError.ExitCode()
|
|
resp := &filesystem.ExecuteResponse{ExitCode: &exitCode}
|
|
if !hadOutput {
|
|
resp.Output = FormatCommandFailureResult(exitCode, "")
|
|
}
|
|
_ = w.Send(resp, nil)
|
|
return
|
|
}
|
|
_ = w.Send(nil, fmt.Errorf("command failed: %w", waitErr))
|
|
}
|