Files
osmedeus/internal/functions/sudo_functions.go
T
j3ssie bd1434739e feat: add sudo authentication support with keepalive and variable renames
- Add sudo_auth() function for TTY prompts and credential management with optional 4-minute keepalive loop
- Add --sudo-aware CLI flag to detect workflows with sudo commands and offer authentication guidance
- Add sudo step scanner to detect sudo usage across all step types (bash, parallel, foreach) and nested structures
- Add parse_url_file() function to batch-process URLs with format directives, supporting bare IPs and CIDR notation
- Add portscan test data with realistic nmap JSONL samples
- Rename {{Workspace}} to {{TargetSpace}} in function examples for clarity
- Add sudo E2E tests covering tip message, flag acceptance, and non-sudo workflows
2026-02-16 23:46:33 +07:00

130 lines
3.1 KiB
Go

package functions
import (
"context"
"io"
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/dop251/goja"
"github.com/j3ssie/osmedeus/v5/internal/logger"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"go.uber.org/zap"
)
var (
sudoKeepaliveMu sync.Mutex
sudoKeepaliveCancel context.CancelFunc
sudoKeepaliveActive bool
)
// AuthenticateSudo validates or refreshes the sudo credential cache.
// An empty password triggers a TTY prompt (stdin passthrough).
// A non-empty password is piped via stdin. The password is never logged.
func AuthenticateSudo(password string) bool {
// #nosec G204 -- sudo -S -v is a fixed command used intentionally
// to validate/refresh sudo credentials. No user input is interpolated
// into the command itself.
cmd := exec.Command("sudo", "-S", "-v")
if password == "" {
// TTY prompt: let the user type the password interactively
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
} else {
// Pipe the password via stdin
pipe, err := cmd.StdinPipe()
if err != nil {
logger.Get().Warn("sudo_auth: failed to create stdin pipe", zap.Error(err))
return false
}
go func() {
defer func() { _ = pipe.Close() }()
_, _ = io.WriteString(pipe, password+"\n")
}()
}
if err := cmd.Run(); err != nil {
logger.Get().Warn("sudo_auth: authentication failed", zap.Error(err))
return false
}
return true
}
// StartSudoKeepalive spawns a background goroutine that refreshes
// sudo credentials every 4 minutes. It is a singleton — calling it
// multiple times is safe; only the first call has any effect.
func StartSudoKeepalive(password string) {
sudoKeepaliveMu.Lock()
defer sudoKeepaliveMu.Unlock()
if sudoKeepaliveActive {
return
}
ctx, cancel := context.WithCancel(context.Background())
sudoKeepaliveCancel = cancel
sudoKeepaliveActive = true
go func() {
ticker := time.NewTicker(4 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
_ = AuthenticateSudo(password)
}
}
}()
}
// StopSudoKeepalive cancels the keepalive goroutine.
// Safe to call multiple times or when no keepalive is running.
func StopSudoKeepalive() {
sudoKeepaliveMu.Lock()
defer sudoKeepaliveMu.Unlock()
if sudoKeepaliveCancel != nil {
sudoKeepaliveCancel()
sudoKeepaliveCancel = nil
}
sudoKeepaliveActive = false
}
// sudoAuth is the Goja wrapper exposed as sudo_auth() in workflow YAML.
//
// Usage:
//
// sudo_auth() -> bool (TTY prompt, no keepalive)
// sudo_auth(password) -> bool (pipe password, no keepalive)
// sudo_auth(password, keepalive) -> bool (pipe password, start keepalive if true)
func (vf *vmFunc) sudoAuth(call goja.FunctionCall) goja.Value {
logger.Get().Debug("Calling " + terminal.HiGreen("sudoAuth"))
password := ""
if len(call.Arguments) > 0 {
p := strings.TrimSpace(call.Argument(0).String())
if p != "undefined" {
password = p
}
}
keepalive := false
if len(call.Arguments) > 1 {
keepalive = call.Argument(1).ToBoolean()
}
ok := AuthenticateSudo(password)
if ok && keepalive {
StartSudoKeepalive(password)
}
return vf.vm.ToValue(ok)
}