Files
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

305 lines
8.6 KiB
Go

package source
import (
"slices"
"strings"
"testing"
"time"
"github.com/pranshuparmar/witr/pkg/model"
)
func TestDetectShellWindowsCaseInsensitive(t *testing.T) {
// Windows reports the shell with mixed casing (e.g. "Explorer.EXE"). Shell
// detection must be case-insensitive, otherwise desktop-launched apps fall
// through to SourceUnknown and pick up a spurious no-supervisor warning.
for _, shell := range []string{"Explorer.EXE", "cmd.EXE", "PowerShell.exe"} {
ancestry := []model.Process{
{PID: 100, Command: shell},
{PID: 200, PPID: 100, Command: "Claude.exe"},
}
if got := Detect(ancestry).Type; got != model.SourceShell {
t.Errorf("Detect with %q ancestor = %v; want SourceShell", shell, got)
}
if slices.Contains(Warnings(ancestry, 0), "No known supervisor or service manager detected") {
t.Errorf("%q ancestor should not raise the no-supervisor warning", shell)
}
}
}
func TestDetectWindowsSystemKernel(t *testing.T) {
// A process rooted at the Windows System process (PID 4) is a kernel/system
// process, not an unsupervised one. It must resolve to an init source and
// must not raise the no-supervisor warning (the Memory Compression false
// positive).
ancestry := []model.Process{
{PID: 4, Command: "System"},
{PID: 4108, PPID: 4, Command: "Memory Compression"},
}
if got := Detect(ancestry).Type; got != model.SourceInit {
t.Errorf("Detect with System (pid 4) root = %v; want SourceInit", got)
}
if slices.Contains(Warnings(ancestry, 0), "No known supervisor or service manager detected") {
t.Errorf("System-rooted process should not raise the no-supervisor warning")
}
}
func TestWarningsDetectsLDPreload(t *testing.T) {
p := []model.Process{
{PID: 999999, Command: "pm2", Cmdline: "pm2"},
{
PID: 123,
Command: "bash",
StartedAt: time.Now(),
User: "bob",
WorkingDir: "/home/bob",
Env: []string{"LD_PRELOAD=/tmp/libhack.so"},
},
}
warnings := Warnings(p, 0)
if !slices.Contains(warnings, "Process sets LD_PRELOAD (potential library injection)") {
t.Fatalf("expected LD_PRELOAD warning, got: %v", warnings)
}
}
func TestWarningsDetectsDYLDVars(t *testing.T) {
p := []model.Process{
{PID: 999999, Command: "pm2", Cmdline: "pm2"},
{
PID: 123,
Command: "zsh",
StartedAt: time.Now(),
User: "bob",
WorkingDir: "/home/bob",
Env: []string{
"DYLD_LIBRARY_PATH=/tmp",
"DYLD_INSERT_LIBRARIES=/tmp/inject.dylib",
},
},
}
warnings := Warnings(p, 0)
want := "Process sets DYLD_* variables (potential library injection): DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH"
if !slices.Contains(warnings, want) {
t.Fatalf("expected DYLD warning %q, got: %v", want, warnings)
}
}
func TestWarningsIgnoresEmptyPreloadVars(t *testing.T) {
p := []model.Process{
{PID: 999999, Command: "pm2", Cmdline: "pm2"},
{
PID: 123,
Command: "zsh",
StartedAt: time.Now(),
User: "bob",
WorkingDir: "/home/bob",
Env: []string{
"LD_PRELOAD=",
"DYLD_INSERT_LIBRARIES=",
},
},
}
warnings := Warnings(p, 0)
if slices.Contains(warnings, "Process sets LD_PRELOAD (potential library injection)") {
t.Fatalf("did not expect LD_PRELOAD warning, got: %v", warnings)
}
if slices.Contains(warnings, "Process sets DYLD_* variables (potential library injection): DYLD_INSERT_LIBRARIES") {
t.Fatalf("did not expect DYLD warning, got: %v", warnings)
}
}
// checks if the order of env vars warnings are deterministic
func FuzzEnvSuspiciousWarningsDeterministic(f *testing.F) {
f.Add("LD_PRELOAD=/tmp/lib.so")
f.Add("DYLD_LIBRARY_PATH=/tmp\nDYLD_INSERT_LIBRARIES=/tmp/inject.dylib")
f.Add("DYLD_LIBRARY_PATH=\nLD_PRELOAD=")
f.Add("")
f.Fuzz(func(t *testing.T, input string) {
parts := strings.Split(input, "\n")
if len(parts) > 50 {
parts = parts[:50]
}
for i := range parts {
if len(parts[i]) > 200 {
parts[i] = parts[i][:200]
}
}
w1 := envSuspiciousWarnings(parts)
w2 := envSuspiciousWarnings(parts)
if !slices.Equal(w1, w2) {
t.Fatalf("expected deterministic output, got %v vs %v", w1, w2)
}
})
}
func TestEnvSuspiciousWarnings(t *testing.T) {
tests := []struct {
name string
env []string
want []string
}{
{
name: "LD_PRELOAD",
env: []string{"LD_PRELOAD=/tmp/libhack.so"},
want: []string{"Process sets LD_PRELOAD (potential library injection)"},
},
{
name: "DYLD keys sorted and deduped",
env: []string{
"DYLD_LIBRARY_PATH=/tmp",
"DYLD_INSERT_LIBRARIES=/tmp/inject.dylib",
"DYLD_LIBRARY_PATH=/tmp", // dup
},
want: []string{
"Process sets DYLD_* variables (potential library injection): DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH",
},
},
{
name: "ignores empty values (current behavior)",
env: []string{"LD_PRELOAD=", "DYLD_INSERT_LIBRARIES="},
want: nil,
},
{
name: "value with '=' still counts",
env: []string{"LD_PRELOAD=a=b"},
want: []string{"Process sets LD_PRELOAD (potential library injection)"},
},
{
name: "no '=' ignored",
env: []string{"LD_PRELOAD"},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := envSuspiciousWarnings(tt.env)
if !slices.Equal(got, tt.want) {
t.Fatalf("got %v, want %v", got, tt.want)
}
})
}
}
// attempts to cause a panic when checking for env vars
func FuzzWarningsNoPanic(f *testing.F) {
f.Add("LD_PRELOAD=/tmp/lib.so")
f.Add("DYLD_INSERT_LIBRARIES=/tmp/inject.dylib")
f.Add("NOT_AN_ENV")
f.Fuzz(func(t *testing.T, entry string) {
if len(entry) > 2000 {
entry = entry[:2000]
}
p := []model.Process{
{
PID: 123,
Command: "test",
Cmdline: "test",
StartedAt: time.Now(),
User: "bob",
WorkingDir: "/home/bob",
Env: []string{entry},
},
}
_ = Warnings(p, 0)
})
}
func TestWarningsDetectsDeletedExecutable(t *testing.T) {
p := []model.Process{
{
PID: 123,
Command: "nginx",
StartedAt: time.Now(),
ExeDeleted: true,
},
}
warnings := Warnings(p, 0)
want := "Process is running from a deleted binary (potential library injection or pending update)"
if !slices.Contains(warnings, want) {
t.Fatalf("expected deleted binary warning, got: %v", warnings)
}
}
func TestEnrichSocketInfo(t *testing.T) {
tests := []struct {
state string
wantExplanation string
wantWorkaround string // empty means no workaround should be set
}{
{
state: "TIME_WAIT",
wantExplanation: "The local OS is holding the port in a protocol-wait state to ensure all packets are received.",
wantWorkaround: "Wait ~60s for the OS to release it, or enable SO_REUSEADDR in your code.",
},
{
state: "CLOSE_WAIT",
wantExplanation: "The remote end has closed the connection, but the local application hasn't responded.",
wantWorkaround: "This usually indicates a resource leak in the application. Restart the process.",
},
{
state: "LISTEN",
wantExplanation: "The process is actively waiting for incoming connections.",
wantWorkaround: "", // no workaround for a healthy listener
},
{
state: "ESTABLISHED",
wantExplanation: "The connection is active and data can be transferred.",
wantWorkaround: "",
},
{
state: "FIN_WAIT_1",
wantExplanation: "The connection is in the process of being closed.",
wantWorkaround: "",
},
{
state: "FIN_WAIT_2",
wantExplanation: "The connection is in the process of being closed.",
wantWorkaround: "",
},
}
for _, tt := range tests {
si := &model.SocketInfo{State: tt.state}
EnrichSocketInfo(si)
if si.Explanation != tt.wantExplanation {
t.Errorf("state %s: got explanation %q, want %q", tt.state, si.Explanation, tt.wantExplanation)
}
if si.Workaround != tt.wantWorkaround {
t.Errorf("state %s: got workaround %q, want %q", tt.state, si.Workaround, tt.wantWorkaround)
}
}
}
// TestEnrichSocketInfoUnknownState ensures unknown states pass through
// without panic and without populating either string. Forward-compatible
// states added by newer kernels (e.g. NEW_SYN_RECV) must not corrupt the
// record.
func TestEnrichSocketInfoUnknownState(t *testing.T) {
si := &model.SocketInfo{State: "MYSTERY"}
EnrichSocketInfo(si)
if si.Explanation != "" || si.Workaround != "" {
t.Errorf("unknown state should leave fields blank, got explanation=%q workaround=%q",
si.Explanation, si.Workaround)
}
}
// TestEnrichSocketInfoNilSafe ensures EnrichSocketInfo is safe to call on a
// nil pointer (some callers pass *SocketInfo conditionally).
func TestEnrichSocketInfoNilSafe(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("EnrichSocketInfo(nil) panicked: %v", r)
}
}()
EnrichSocketInfo(nil)
}