feat: capture SSH exec commands (PLAN.md 4.4)
Bots often send commands via `ssh user@host <command>` (exec request) rather than requesting an interactive shell. These were previously rejected silently. Now exec commands are captured, stored on the session record, and displayed in the web UI session detail page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -231,14 +231,24 @@ func (s *Server) handleSession(channel ssh.Channel, requests <-chan *ssh.Request
|
||||
s.notifier.Notify(context.Background(), notify.EventSessionStarted, sessionInfo)
|
||||
defer s.notifier.CleanupSession(sessionID)
|
||||
|
||||
// Handle session requests (pty-req, shell, etc.)
|
||||
// Handle session requests (pty-req, shell, exec, etc.)
|
||||
execCh := make(chan string, 1)
|
||||
go func() {
|
||||
defer close(execCh)
|
||||
for req := range requests {
|
||||
switch req.Type {
|
||||
case "pty-req", "shell":
|
||||
if req.WantReply {
|
||||
req.Reply(true, nil)
|
||||
}
|
||||
case "exec":
|
||||
if req.WantReply {
|
||||
req.Reply(true, nil)
|
||||
}
|
||||
var payload struct{ Command string }
|
||||
if err := ssh.Unmarshal(req.Payload, &payload); err == nil {
|
||||
execCh <- payload.Command
|
||||
}
|
||||
default:
|
||||
if req.WantReply {
|
||||
req.Reply(false, nil)
|
||||
@@ -247,6 +257,29 @@ func (s *Server) handleSession(channel ssh.Channel, requests <-chan *ssh.Request
|
||||
}
|
||||
}()
|
||||
|
||||
// Check for exec request before proceeding to interactive shell.
|
||||
select {
|
||||
case cmd, ok := <-execCh:
|
||||
if ok && cmd != "" {
|
||||
s.logger.Info("exec command received",
|
||||
"remote_addr", conn.RemoteAddr(),
|
||||
"user", conn.User(),
|
||||
"session_id", sessionID,
|
||||
"command", cmd,
|
||||
)
|
||||
if err := s.store.SetExecCommand(context.Background(), sessionID, cmd); err != nil {
|
||||
s.logger.Error("failed to set exec command", "err", err, "session_id", sessionID)
|
||||
}
|
||||
s.metrics.ExecCommandsTotal.Inc()
|
||||
// Send exit-status 0 and close channel.
|
||||
exitPayload := make([]byte, 4) // uint32(0)
|
||||
_, _ = channel.SendRequest("exit-status", false, exitPayload)
|
||||
return
|
||||
}
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
// No exec request within timeout — proceed with interactive shell.
|
||||
}
|
||||
|
||||
// Build session context.
|
||||
var shellCfg map[string]any
|
||||
if s.cfg.Shell.Shells != nil {
|
||||
|
||||
Reference in New Issue
Block a user