// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

// Security-focused (pentest) tests for the ss builtin.
// Each test exercises a class of attack and verifies the implementation
// behaves safely (no crash, no hang, exit 0 or 1 only).

package ss_test

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/DataDog/rshell/builtins/testutil"
)

// runScriptTimeout runs a script with a hard context deadline.
func runScriptTimeout(t *testing.T, script string, timeout time.Duration) (string, string, int) {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()
	return testutil.RunScriptCtx(ctx, t, script, "")
}

// --- GTFOBins: -F/--filter file-read vector ---

func TestSSPentestGTFObinsFilterShort(t *testing.T) {
	_, stderr, code := cmdRun(t, "ss -F /etc/passwd")
	assert.Equal(t, 1, code, "GTFOBins -F must be rejected")
	assert.Contains(t, stderr, "ss:")
}

func TestSSPentestGTFObinsFilterLong(t *testing.T) {
	_, stderr, code := cmdRun(t, "ss --filter /etc/passwd")
	assert.Equal(t, 1, code, "GTFOBins --filter must be rejected")
	assert.Contains(t, stderr, "ss:")
}

// --- Flag injection ---

func TestSSPentestUnknownLongFlag(t *testing.T) {
	_, stderr, code := cmdRun(t, "ss --no-such-flag")
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ss:")
}

func TestSSPentestUnknownShortFlag(t *testing.T) {
	_, _, code := cmdRun(t, "ss -Z")
	assert.Equal(t, 1, code)
}

func TestSSPentestProcessesFlag(t *testing.T) {
	// -p would reveal PID/process info; must be rejected.
	_, stderr, code := cmdRun(t, "ss -p")
	assert.Equal(t, 1, code, "-p (process info disclosure) must be rejected")
	assert.Contains(t, stderr, "ss:")
}

func TestSSPentestKillFlag(t *testing.T) {
	// -K would forcibly close sockets; must be rejected.
	_, stderr, code := cmdRun(t, "ss -K")
	assert.Equal(t, 1, code, "-K (socket kill) must be rejected")
	assert.Contains(t, stderr, "ss:")
}

func TestSSPentestEventsFlag(t *testing.T) {
	// -E would produce an infinite stream of socket events; must be rejected.
	_, stderr, code := runScriptTimeout(t, "ss -E", 3*time.Second)
	assert.Equal(t, 1, code, "-E (infinite event stream) must be rejected")
	assert.Contains(t, stderr, "ss:")
}

func TestSSPentestNetNamespaceFlag(t *testing.T) {
	// -N would switch network namespaces (privilege escalation); must be rejected.
	_, stderr, code := cmdRun(t, "ss -N ns0")
	assert.Equal(t, 1, code, "-N (namespace switching) must be rejected")
	assert.Contains(t, stderr, "ss:")
}

func TestSSPentestResolveFlag(t *testing.T) {
	// -r would perform DNS lookups (network side-effect); must be rejected.
	_, _, code := cmdRun(t, "ss -r")
	assert.Equal(t, 1, code, "-r (DNS resolve) must be rejected")
}

func TestSSPentestBPFFlag(t *testing.T) {
	// -b (BPF filter) must be rejected.
	_, _, code := cmdRun(t, "ss -b")
	assert.Equal(t, 1, code, "-b (BPF) must be rejected")
}

// --- Flag via word expansion ---

func TestSSPentestFlagViaWordExpansion(t *testing.T) {
	// Verify that flag injection via variable expansion still hits the flag parser.
	_, stderr, code := cmdRun(t, `flag="-F"; ss $flag /etc/passwd; echo "exit:$?"`)
	// The echo exit:$? means the overall script exits 0, but ss itself must fail.
	assert.Equal(t, 0, code, "script itself succeeds via echo")
	assert.Contains(t, stderr, "ss:")
}

// --- Double-dash end-of-flags separator ---

func TestSSPentestDoubleDashEndOfFlags(t *testing.T) {
	// -- stops flag parsing; any remaining args are positional.
	// ss does not accept file arguments, so extra positionals are silently ignored.
	_, _, code := cmdRun(t, "ss -- -H")
	// Should succeed (no error from flag parser; -H treated as positional, ignored).
	assert.Equal(t, 0, code)
}

// --- Context cancellation ---

func TestSSPentestCancelledContext(t *testing.T) {
	// A pre-cancelled context should cause ss to exit quickly (code 1) without panic.
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cancel immediately
	_, _, code := testutil.RunScriptCtx(ctx, t, "ss -an", "")
	// Code may be 0 or 1 — depends on timing — but must not panic.
	assert.True(t, code == 0 || code == 1, "unexpected code %d", code)
}

func TestSSPentestShortTimeout(t *testing.T) {
	// 5-second timeout: ss must complete well within this limit on any platform.
	_, _, code := runScriptTimeout(t, "ss -ans", 5*time.Second)
	assert.True(t, code == 0 || code == 1, "unexpected code %d after short timeout", code)
}

// --- Valid flag combinations must not crash ---

func TestSSPentestAllFlagCombinations(t *testing.T) {
	cmds := []string{
		"ss -t",
		"ss -u",
		"ss -x",
		"ss -l",
		"ss -a",
		"ss -n",
		"ss -4",
		"ss -6",
		"ss -s",
		"ss -H",
		"ss -o",
		"ss -e",
		"ss -tln",
		"ss -uln",
		"ss -tan",
		"ss -4tn",
		"ss -6tn",
		"ss -taH",
		"ss -lH",
	}
	for _, cmd := range cmds {
		cmd := cmd
		t.Run(cmd, func(t *testing.T) {
			t.Parallel()
			_, _, code := cmdRun(t, cmd)
			assert.True(t, code == 0 || code == 1,
				"%s: unexpected exit code %d", cmd, code)
		})
	}
}

// --- Help flag behavior ---

func TestSSPentestHelpDoesNotLeakSensitiveInfo(t *testing.T) {
	stdout, stderr, code := cmdRun(t, "ss -h")
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	// Help output must not reveal paths, PIDs, or internal structures.
	assert.NotContains(t, stdout, "/proc/net")
	assert.NotContains(t, stdout, "pid")
}

// --- Extra positional arguments are harmless ---

func TestSSPentestExtraPositionalArgs(t *testing.T) {
	// ss does not read files; extra arguments should be silently ignored.
	_, _, code := cmdRun(t, "ss -an extra_arg_1 extra_arg_2")
	assert.True(t, code == 0 || code == 1, "unexpected code %d", code)
}

// --- Summary must not panic on empty entry list ---

func TestSSPentestSummaryEmptyEntries(t *testing.T) {
	// Run summary with filters that produce 0 entries.
	// -4 -6 together would include everything, but -t with an empty /proc/net
	// (by denying access) can produce 0 entries.
	// Just verify no crash.
	_, _, code := cmdRun(t, "ss -s -t -u -x")
	assert.True(t, code == 0 || code == 1)
}
