mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-31 20:29:38 +02:00
- Add auto-generated _<variable>_ path-friendly variables for foreach loops that sanitize unsafe filesystem characters (/, :, etc.) and deterministically truncate long values - Refactor asset and vulnerability import/merge logic to preserve existing non-empty fields instead of full overwrite on conflict - Add mergeAssetFields() and mergeVulnFields() helper functions for consistent field-level merge behavior across all import methods - Add comprehensive unit tests for merge functions and path-friendly variable behavior in foreach loops - Add E2E test module (test-foreach-path-friendly) validating sanitization, directory creation, truncation, and variable coexistence
303 lines
7.8 KiB
Go
303 lines
7.8 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/j3ssie/osmedeus/v5/internal/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHostRunner_Execute(t *testing.T) {
|
|
ctx := context.Background()
|
|
runner := NewHostRunner("")
|
|
|
|
err := runner.Setup(ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = runner.Cleanup(ctx) }()
|
|
|
|
result, err := runner.Execute(ctx, "echo hello")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, result.ExitCode)
|
|
assert.Contains(t, result.Output, "hello")
|
|
}
|
|
|
|
func TestHostRunner_Type(t *testing.T) {
|
|
runner := NewHostRunner("")
|
|
assert.Equal(t, core.RunnerTypeHost, runner.Type())
|
|
assert.False(t, runner.IsRemote())
|
|
}
|
|
|
|
func TestHostRunner_ExitCode(t *testing.T) {
|
|
ctx := context.Background()
|
|
runner := NewHostRunner("")
|
|
|
|
result, err := runner.Execute(ctx, "exit 1")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, result.ExitCode)
|
|
}
|
|
|
|
func TestHostRunner_WithBinariesPath(t *testing.T) {
|
|
ctx := context.Background()
|
|
runner := NewHostRunner("/tmp/test-binaries")
|
|
|
|
result, err := runner.Execute(ctx, "echo $PATH")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, result.ExitCode)
|
|
assert.Contains(t, result.Output, "/tmp/test-binaries")
|
|
}
|
|
|
|
// Integration test - requires Docker
|
|
func TestDockerRunner_Execute_Integration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
config := &core.RunnerConfig{
|
|
Image: "alpine:latest",
|
|
Persistent: false,
|
|
}
|
|
|
|
runner, err := NewDockerRunner(config, "")
|
|
require.NoError(t, err)
|
|
|
|
err = runner.Setup(ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = runner.Cleanup(ctx) }()
|
|
|
|
result, err := runner.Execute(ctx, "echo hello from docker")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, result.ExitCode)
|
|
assert.Contains(t, result.Output, "hello from docker")
|
|
}
|
|
|
|
func TestDockerRunner_Type(t *testing.T) {
|
|
config := &core.RunnerConfig{
|
|
Image: "alpine:latest",
|
|
}
|
|
runner, err := NewDockerRunner(config, "")
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, core.RunnerTypeDocker, runner.Type())
|
|
assert.True(t, runner.IsRemote())
|
|
}
|
|
|
|
func TestDockerRunner_RequiresImage(t *testing.T) {
|
|
config := &core.RunnerConfig{}
|
|
_, err := NewDockerRunner(config, "")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "image")
|
|
}
|
|
|
|
// Integration test - requires SSH server (linuxserver/openssh-server)
|
|
func TestSSHRunner_Execute_Integration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
conn, err := net.DialTimeout("tcp", "localhost:2222", 500*time.Millisecond)
|
|
if err != nil {
|
|
t.Skip("skipping integration test: SSH server not available on localhost:2222")
|
|
}
|
|
_ = conn.Close()
|
|
|
|
ctx := context.Background()
|
|
config := &core.RunnerConfig{
|
|
Host: "localhost",
|
|
Port: 2222,
|
|
User: "testuser",
|
|
Password: "testpass",
|
|
}
|
|
|
|
runner, err := NewSSHRunner(config, "")
|
|
require.NoError(t, err)
|
|
|
|
err = runner.Setup(ctx)
|
|
require.NoError(t, err)
|
|
defer func() { _ = runner.Cleanup(ctx) }()
|
|
|
|
result, err := runner.Execute(ctx, "echo hello from ssh")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, result.ExitCode)
|
|
assert.Contains(t, result.Output, "hello from ssh")
|
|
}
|
|
|
|
func TestSSHRunner_Type(t *testing.T) {
|
|
config := &core.RunnerConfig{
|
|
Host: "localhost",
|
|
User: "test",
|
|
}
|
|
runner, err := NewSSHRunner(config, "")
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, core.RunnerTypeSSH, runner.Type())
|
|
assert.True(t, runner.IsRemote())
|
|
}
|
|
|
|
func TestSSHRunner_RequiresHost(t *testing.T) {
|
|
config := &core.RunnerConfig{
|
|
User: "test",
|
|
}
|
|
_, err := NewSSHRunner(config, "")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "host")
|
|
}
|
|
|
|
func TestSSHRunner_RequiresUser(t *testing.T) {
|
|
config := &core.RunnerConfig{
|
|
Host: "localhost",
|
|
}
|
|
_, err := NewSSHRunner(config, "")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user")
|
|
}
|
|
|
|
func TestNewRunner_Host(t *testing.T) {
|
|
workflow := &core.Workflow{
|
|
Name: "test",
|
|
Kind: core.KindModule,
|
|
Runner: core.RunnerTypeHost,
|
|
}
|
|
|
|
runner, err := NewRunner(workflow, "")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, core.RunnerTypeHost, runner.Type())
|
|
}
|
|
|
|
func TestNewRunner_DefaultsToHost(t *testing.T) {
|
|
workflow := &core.Workflow{
|
|
Name: "test",
|
|
Kind: core.KindModule,
|
|
}
|
|
|
|
runner, err := NewRunner(workflow, "")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, core.RunnerTypeHost, runner.Type())
|
|
}
|
|
|
|
// ============================================================================
|
|
// LimitedBuffer tests
|
|
// ============================================================================
|
|
|
|
func TestLimitedBuffer_WritesUnderLimit(t *testing.T) {
|
|
buf := NewLimitedBuffer(100)
|
|
|
|
n, err := buf.Write([]byte("hello"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 5, n)
|
|
assert.Equal(t, 5, buf.Len())
|
|
assert.False(t, buf.Overflow())
|
|
assert.Equal(t, "hello", string(buf.Bytes()))
|
|
}
|
|
|
|
func TestLimitedBuffer_TruncatesAtLimit(t *testing.T) {
|
|
buf := NewLimitedBuffer(10)
|
|
|
|
n, err := buf.Write([]byte("hello world!")) // 12 bytes > 10 limit
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 12, n) // reports full length written
|
|
assert.Equal(t, 10, buf.Len())
|
|
assert.True(t, buf.Overflow())
|
|
assert.Equal(t, "hello worl", string(buf.Bytes()))
|
|
}
|
|
|
|
func TestLimitedBuffer_DiscardsAfterFull(t *testing.T) {
|
|
buf := NewLimitedBuffer(5)
|
|
|
|
_, _ = buf.Write([]byte("hello"))
|
|
assert.Equal(t, 5, buf.Len())
|
|
assert.False(t, buf.Overflow())
|
|
|
|
// Further writes are silently discarded
|
|
n, err := buf.Write([]byte(" world"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 6, n) // reports full length
|
|
assert.Equal(t, 5, buf.Len())
|
|
assert.True(t, buf.Overflow())
|
|
assert.Equal(t, "hello", string(buf.Bytes()))
|
|
}
|
|
|
|
func TestLimitedBuffer_MultipleWrites(t *testing.T) {
|
|
buf := NewLimitedBuffer(10)
|
|
|
|
_, _ = buf.Write([]byte("aaa")) // 3 bytes, total 3
|
|
_, _ = buf.Write([]byte("bbb")) // 3 bytes, total 6
|
|
_, _ = buf.Write([]byte("ccc")) // 3 bytes, total 9
|
|
_, _ = buf.Write([]byte("dddd")) // 4 bytes, only 1 fits -> total 10
|
|
|
|
assert.Equal(t, 10, buf.Len())
|
|
assert.True(t, buf.Overflow())
|
|
assert.Equal(t, "aaabbbcccd", string(buf.Bytes()))
|
|
}
|
|
|
|
func TestLimitedBuffer_ZeroSize(t *testing.T) {
|
|
buf := NewLimitedBuffer(0)
|
|
|
|
n, err := buf.Write([]byte("anything"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 8, n)
|
|
assert.Equal(t, 0, buf.Len())
|
|
assert.True(t, buf.Overflow())
|
|
}
|
|
|
|
func TestCombineOutput_Normal(t *testing.T) {
|
|
stdout := NewLimitedBuffer(100)
|
|
stderr := NewLimitedBuffer(100)
|
|
|
|
_, _ = stdout.Write([]byte("out"))
|
|
_, _ = stderr.Write([]byte("err"))
|
|
|
|
result := combineOutput(stdout, stderr)
|
|
assert.Equal(t, "outerr", result)
|
|
}
|
|
|
|
func TestCombineOutput_Empty(t *testing.T) {
|
|
stdout := NewLimitedBuffer(100)
|
|
stderr := NewLimitedBuffer(100)
|
|
|
|
result := combineOutput(stdout, stderr)
|
|
assert.Equal(t, "", result)
|
|
}
|
|
|
|
func TestCombineOutput_Truncated(t *testing.T) {
|
|
stdout := NewLimitedBuffer(5)
|
|
stderr := NewLimitedBuffer(5)
|
|
|
|
_, _ = stdout.Write([]byte("long output that overflows"))
|
|
_, _ = stderr.Write([]byte("err"))
|
|
|
|
result := combineOutput(stdout, stderr)
|
|
assert.True(t, stdout.Overflow())
|
|
assert.Contains(t, result, "[output truncated]")
|
|
}
|
|
|
|
func TestHostRunner_LargeOutput_Bounded(t *testing.T) {
|
|
ctx := context.Background()
|
|
runner := NewHostRunner("")
|
|
|
|
// Generate output larger than MaxOutputSize limit
|
|
// Use printf to generate ~20KB of output (well under limit, but proves buffer works)
|
|
result, err := runner.Execute(ctx, "printf '%0.s-' {1..20000}")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, result.ExitCode)
|
|
assert.True(t, len(result.Output) > 0)
|
|
assert.True(t, len(result.Output) <= MaxOutputSize+20) // +20 for "[output truncated]\n"
|
|
|
|
// Verify that very large output gets truncated
|
|
// Generate output of ~11MB (> 10MB limit)
|
|
bigResult, err := runner.Execute(ctx, "head -c 11000000 /dev/zero | tr '\\0' 'A'")
|
|
require.NoError(t, err)
|
|
assert.True(t, len(bigResult.Output) <= MaxOutputSize+20)
|
|
if len(bigResult.Output) > MaxOutputSize {
|
|
assert.Contains(t, bigResult.Output, "[output truncated]")
|
|
}
|
|
}
|