mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-20 06:35:48 +02:00
- Add worker eval command for distributed function execution with Redis hooks registration - Add worker set command to update worker fields (alias, public-ip, ssh-enabled, ssh-keys-path) - Enhance worker status with JSON output, search filtering, and column selection (--columns, --exclude-columns, --search) - Add --keep-setting flag to install base/validate commands to preserve osm-settings.yaml after base installation - Fix binary installation in Nix: replace CopyInstalledBinaryToFolder with SymlinkInstalledBinaryToFolder - Add --clean-ws flag to db clean command for removing workspace data - Add HooksEnabled field to Run records when creating runs from CLI and API - Add comprehensive test coverage for hook execution (pre/post hooks, execution order, failure handling) - Add test coverage for worker commands (eval, set, status with JSON) and db clean operations - Improve usage documentation for worker subcommands and db operations
513 lines
14 KiB
Go
513 lines
14 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/j3ssie/osmedeus/v5/internal/core"
|
|
"github.com/j3ssie/osmedeus/v5/internal/runner"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- stripTimeoutPrefix tests ---
|
|
|
|
func TestStripTimeoutPrefix_BasicTimeout(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
assert.Equal(t, 30*time.Second, r.duration)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_WithKillAfterFlag(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout -k 10 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
assert.Equal(t, 30*time.Second, r.duration)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_WithKillAfterEquals(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout --kill-after=10s 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_WithSignalFlag(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout -s SIGKILL 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_WithSignalEquals(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout --signal=TERM 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_WithForeground(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout --foreground 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_WithPreserveStatus(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout --preserve-status 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_WithVerbose(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout -v 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_MultipleFlags(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout -k 10 -s SIGKILL --foreground 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_ShortKillAttached(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout -k5s 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_NoCommandAfterDuration(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout 30")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "", r.command)
|
|
assert.Equal(t, 30*time.Second, r.duration)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_NotTimeout(t *testing.T) {
|
|
r := stripTimeoutPrefix("nuclei -t templates")
|
|
assert.False(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_EmptyString(t *testing.T) {
|
|
r := stripTimeoutPrefix("")
|
|
assert.False(t, r.stripped)
|
|
assert.Equal(t, "", r.command)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_FullPathTimeout(t *testing.T) {
|
|
r := stripTimeoutPrefix("/usr/bin/timeout 30 nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
assert.Equal(t, 30*time.Second, r.duration)
|
|
}
|
|
|
|
func TestStripTimeoutPrefix_DurationWithSuffix(t *testing.T) {
|
|
r := stripTimeoutPrefix("timeout 2h nuclei -t templates")
|
|
assert.True(t, r.stripped)
|
|
assert.Equal(t, "nuclei -t templates", r.command)
|
|
assert.Equal(t, 2*time.Hour, r.duration)
|
|
}
|
|
|
|
// --- parseTimeoutDuration tests ---
|
|
|
|
func TestParseTimeoutDuration_Seconds(t *testing.T) {
|
|
assert.Equal(t, 30*time.Second, parseTimeoutDuration("30"))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_SecondsWithSuffix(t *testing.T) {
|
|
assert.Equal(t, 30*time.Second, parseTimeoutDuration("30s"))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_Minutes(t *testing.T) {
|
|
assert.Equal(t, 5*time.Minute, parseTimeoutDuration("5m"))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_Hours(t *testing.T) {
|
|
assert.Equal(t, 2*time.Hour, parseTimeoutDuration("2h"))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_Days(t *testing.T) {
|
|
assert.Equal(t, 24*time.Hour, parseTimeoutDuration("1d"))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_Float(t *testing.T) {
|
|
assert.Equal(t, time.Duration(1.5*float64(time.Second)), parseTimeoutDuration("1.5"))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_FloatWithSuffix(t *testing.T) {
|
|
assert.Equal(t, time.Duration(2.5*float64(time.Minute)), parseTimeoutDuration("2.5m"))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_Empty(t *testing.T) {
|
|
assert.Equal(t, time.Duration(0), parseTimeoutDuration(""))
|
|
}
|
|
|
|
func TestParseTimeoutDuration_Invalid(t *testing.T) {
|
|
assert.Equal(t, time.Duration(0), parseTimeoutDuration("abc"))
|
|
}
|
|
|
|
// --- prependBinariesPath tests ---
|
|
|
|
func TestPrependBinariesPath_Normal(t *testing.T) {
|
|
cmd, ok := prependBinariesPath("nuclei -t templates", "/opt/tools")
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "/opt/tools/nuclei -t templates", cmd)
|
|
}
|
|
|
|
func TestPrependBinariesPath_AlreadyHasPath(t *testing.T) {
|
|
cmd, ok := prependBinariesPath("/usr/bin/nuclei -t templates", "/opt/tools")
|
|
assert.False(t, ok)
|
|
assert.Equal(t, "/usr/bin/nuclei -t templates", cmd)
|
|
}
|
|
|
|
func TestPrependBinariesPath_RelativePath(t *testing.T) {
|
|
cmd, ok := prependBinariesPath("./nuclei -t templates", "/opt/tools")
|
|
assert.False(t, ok)
|
|
assert.Equal(t, "./nuclei -t templates", cmd)
|
|
}
|
|
|
|
func TestPrependBinariesPath_EmptyBinariesPath(t *testing.T) {
|
|
cmd, ok := prependBinariesPath("nuclei -t templates", "")
|
|
assert.False(t, ok)
|
|
assert.Equal(t, "nuclei -t templates", cmd)
|
|
}
|
|
|
|
func TestPrependBinariesPath_EmptyCommand(t *testing.T) {
|
|
cmd, ok := prependBinariesPath("", "/opt/tools")
|
|
assert.False(t, ok)
|
|
assert.Equal(t, "", cmd)
|
|
}
|
|
|
|
func TestPrependBinariesPath_TrailingSlash(t *testing.T) {
|
|
cmd, ok := prependBinariesPath("nuclei -t templates", "/opt/tools/")
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "/opt/tools/nuclei -t templates", cmd)
|
|
}
|
|
|
|
func TestPrependBinariesPath_SingleWord(t *testing.T) {
|
|
cmd, ok := prependBinariesPath("nuclei", "/opt/tools")
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "/opt/tools/nuclei", cmd)
|
|
}
|
|
|
|
// --- exitCodeError tests ---
|
|
|
|
func TestExitCodeError_ErrorMessage(t *testing.T) {
|
|
err := newExitCodeErrorf(127, "command not found: %s", "nuclei")
|
|
assert.Equal(t, "command not found: nuclei", err.Error())
|
|
assert.Equal(t, 127, err.code)
|
|
}
|
|
|
|
func TestExitCodeError_ErrorsAs(t *testing.T) {
|
|
err := newExitCodeErrorf(127, "command not found")
|
|
var ecErr *exitCodeError
|
|
require.True(t, errors.As(err, &ecErr))
|
|
assert.Equal(t, 127, ecErr.code)
|
|
}
|
|
|
|
func TestExitCodeError_NotExitCodeError(t *testing.T) {
|
|
err := errors.New("some other error")
|
|
var ecErr *exitCodeError
|
|
assert.False(t, errors.As(err, &ecErr))
|
|
}
|
|
|
|
func TestNewExitCodeError(t *testing.T) {
|
|
err := newExitCodeError(1, "failed")
|
|
assert.Equal(t, 1, err.code)
|
|
assert.Equal(t, "failed", err.Error())
|
|
}
|
|
|
|
// --- mockRunner for integration tests ---
|
|
|
|
type mockFallbackRunner struct {
|
|
// responses maps command strings to their mock results
|
|
responses map[string]*mockResponse
|
|
}
|
|
|
|
type mockResponse struct {
|
|
output string
|
|
exitCode int
|
|
}
|
|
|
|
func newMockFallbackRunner() *mockFallbackRunner {
|
|
return &mockFallbackRunner{
|
|
responses: make(map[string]*mockResponse),
|
|
}
|
|
}
|
|
|
|
func (m *mockFallbackRunner) addResponse(command string, output string, exitCode int) {
|
|
m.responses[command] = &mockResponse{output: output, exitCode: exitCode}
|
|
}
|
|
|
|
func (m *mockFallbackRunner) Execute(_ context.Context, command string) (*runner.CommandResult, error) {
|
|
if resp, ok := m.responses[command]; ok {
|
|
return &runner.CommandResult{
|
|
Output: resp.output,
|
|
ExitCode: resp.exitCode,
|
|
}, nil
|
|
}
|
|
// Default: command not found
|
|
return &runner.CommandResult{
|
|
Output: "sh: command not found",
|
|
ExitCode: 127,
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockFallbackRunner) Setup(_ context.Context) error { return nil }
|
|
func (m *mockFallbackRunner) Cleanup(_ context.Context) error { return nil }
|
|
func (m *mockFallbackRunner) Type() core.RunnerType { return core.RunnerTypeHost }
|
|
func (m *mockFallbackRunner) IsRemote() bool { return false }
|
|
func (m *mockFallbackRunner) SetPIDCallbacks(_, _ runner.PIDCallback) {}
|
|
func (m *mockFallbackRunner) CopyFromRemote(_ context.Context, _, _ string) error {
|
|
return nil
|
|
}
|
|
|
|
// --- Integration tests: BashExecutor fallback chain ---
|
|
|
|
func TestBashExecutor_Fallback_StripTimeout(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// Original command with timeout fails (127)
|
|
// Stripped command succeeds
|
|
mock.addResponse("nuclei -t templates", "found 5 results", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"timeout 30 nuclei -t templates",
|
|
0,
|
|
"",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "found 5 results", output)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_PrependBinariesPath(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// Original command fails (127)
|
|
// Prepended command succeeds
|
|
mock.addResponse("/opt/tools/nuclei -t templates", "found 3 results", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"nuclei -t templates",
|
|
0,
|
|
"/opt/tools",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "found 3 results", output)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_StripTimeoutThenPrependBinaries(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// Original "timeout 30 nuclei -t templates" → 127
|
|
// Stripped "nuclei -t templates" → still 127
|
|
// Prepended "/opt/tools/nuclei -t templates" → success
|
|
mock.addResponse("/opt/tools/nuclei -t templates", "found 7 results", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"timeout 30 nuclei -t templates",
|
|
0,
|
|
"/opt/tools",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "found 7 results", output)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_NoFallbackOnNon127(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// Command fails with exit code 1 (not 127)
|
|
mock.addResponse("nuclei -t templates", "error output", 1)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
_, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"nuclei -t templates",
|
|
0,
|
|
"/opt/tools",
|
|
)
|
|
|
|
require.Error(t, err)
|
|
var ecErr *exitCodeError
|
|
require.True(t, errors.As(err, &ecErr))
|
|
assert.Equal(t, 1, ecErr.code)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_SuccessNoFallback(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// Command succeeds on first try
|
|
mock.addResponse("nuclei -t templates", "all good", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"nuclei -t templates",
|
|
0,
|
|
"/opt/tools",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "all good", output)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_AllFallbacksFail(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// All commands return 127 (everything is command not found)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
_, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"timeout 30 nuclei -t templates",
|
|
0,
|
|
"/opt/tools",
|
|
)
|
|
|
|
require.Error(t, err)
|
|
var ecErr *exitCodeError
|
|
require.True(t, errors.As(err, &ecErr))
|
|
assert.Equal(t, 127, ecErr.code)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_CancelledContextNoRetry(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// All responses return 127
|
|
// But we expect no fallback because context is cancelled
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // cancel immediately
|
|
|
|
_, err := exec.executeCommandWithFallback(
|
|
ctx,
|
|
"timeout 30 nuclei -t templates",
|
|
0,
|
|
"/opt/tools",
|
|
)
|
|
|
|
// Should fail due to context cancellation, not 127 fallback
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_TimeoutFlagsWithEquals(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// timeout with --kill-after= and --signal= flags
|
|
mock.addResponse("nuclei -t templates", "found results", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"timeout --kill-after=10s --signal=TERM 30 nuclei -t templates",
|
|
0,
|
|
"",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "found results", output)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_BinaryWithPathNoFallback2(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// Binary already has a path prefix — fallback 2 (prepend) should not apply
|
|
// Fallback 1 (strip timeout) should work
|
|
mock.addResponse("/usr/local/bin/nuclei -t templates", "found", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"timeout 30 /usr/local/bin/nuclei -t templates",
|
|
0,
|
|
"/opt/tools",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "found", output)
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_SequentialUsesWithFallback(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// First command needs fallback (strip timeout)
|
|
mock.addResponse("nuclei -t templates", "result1", 0)
|
|
// Second command succeeds directly
|
|
mock.addResponse("echo done", "done", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeSequential(
|
|
context.Background(),
|
|
[]string{"timeout 30 nuclei -t templates", "echo done"},
|
|
0,
|
|
"",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, output, "result1")
|
|
assert.Contains(t, output, "done")
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_ParallelUsesWithFallback(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
mock.addResponse("nuclei -t templates", "result1", 0)
|
|
mock.addResponse("echo done", "done", 0)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
output, err := exec.executeParallel(
|
|
context.Background(),
|
|
[]string{"timeout 30 nuclei -t templates", "echo done"},
|
|
0,
|
|
"",
|
|
)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, output, "result1")
|
|
assert.Contains(t, output, "done")
|
|
}
|
|
|
|
func TestBashExecutor_Fallback_StripTimeoutNonZeroExitAfterStrip(t *testing.T) {
|
|
mock := newMockFallbackRunner()
|
|
// After stripping timeout, command fails with non-127 exit code
|
|
mock.addResponse("nuclei -t templates", "some error", 2)
|
|
|
|
exec := NewBashExecutor(nil)
|
|
exec.SetRunner(mock)
|
|
|
|
_, err := exec.executeCommandWithFallback(
|
|
context.Background(),
|
|
"timeout 30 nuclei -t templates",
|
|
time.Duration(0),
|
|
"/opt/tools",
|
|
)
|
|
|
|
// Should return the exit code 2 error, not try fallback 2
|
|
require.Error(t, err)
|
|
var ecErr *exitCodeError
|
|
require.True(t, errors.As(err, &ecErr))
|
|
assert.Equal(t, 2, ecErr.code)
|
|
}
|