From 54d27aca56e5db9f158c1da2d9415e1eefdf9064 Mon Sep 17 00:00:00 2001 From: Jtekkk Date: Fri, 10 Jul 2026 01:24:20 +0000 Subject: [PATCH] Add unit tests for util package helpers The util package's generic helpers were largely untested (~5% statement coverage). This adds table-driven and round-trip tests covering: - generics.go: Contains, Keys - resource_ids.go: RemoveElement (including input-immutability) - files.go: ByteCountBinary, DeflateBuf (deflate round-trip), CopyFile, ReadFileFromTarGz, ChmodR - cryptography.go: secure RNG (Intn, Int63n, Shuffle, Float64) range, distribution and panic behavior; PreludeEncrypt/PreludeDecrypt and RC4EncryptUnsafe round-trips - leaky: LeakyBuf Get/Put pooling, overflow, and wrong-size panic Raises util coverage to ~90% and util/leaky to 100%. No production code changes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011UEBr6PgAvPt2hp6yzBXEk --- util/cryptography_test.go | 198 ++++++++++++++++++++++++++++++ util/files_test.go | 241 +++++++++++++++++++++++++++++++++++++ util/generics_test.go | 68 +++++++++++ util/leaky/leakbuf_test.go | 66 ++++++++++ util/resource_ids_test.go | 76 ++++++++++++ 5 files changed, 649 insertions(+) create mode 100644 util/cryptography_test.go create mode 100644 util/files_test.go create mode 100644 util/generics_test.go create mode 100644 util/leaky/leakbuf_test.go create mode 100644 util/resource_ids_test.go diff --git a/util/cryptography_test.go b/util/cryptography_test.go new file mode 100644 index 0000000000..9de5a08485 --- /dev/null +++ b/util/cryptography_test.go @@ -0,0 +1,198 @@ +package util + +/* + Sliver Implant Framework + Copyright (C) 2021 Bishop Fox + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "bytes" + "crypto/aes" + "testing" +) + +func TestIntnRange(t *testing.T) { + for _, n := range []int{1, 2, 7, 100, 1024} { + for i := 0; i < 2000; i++ { + v := Intn(n) + if v < 0 || v >= n { + t.Fatalf("Intn(%d) returned %d, out of range [0, %d)", n, v, n) + } + } + } +} + +func TestIntnCoversRange(t *testing.T) { + // Over enough samples every bucket in a small range should be hit. + const n = 6 + seen := make([]bool, n) + for i := 0; i < 10000; i++ { + seen[Intn(n)] = true + } + for bucket, hit := range seen { + if !hit { + t.Fatalf("Intn(%d) never returned %d over 10000 samples", n, bucket) + } + } +} + +func TestIntnSingleValue(t *testing.T) { + for i := 0; i < 100; i++ { + if v := Intn(1); v != 0 { + t.Fatalf("Intn(1) = %d, want 0", v) + } + } +} + +func TestIntnPanicsOnNonPositive(t *testing.T) { + for _, n := range []int{0, -1, -100} { + assertPanics(t, func() { Intn(n) }) + } +} + +func TestInt63nRange(t *testing.T) { + for _, n := range []int64{1, 3, 255, 1 << 40} { + for i := 0; i < 2000; i++ { + v := Int63n(n) + if v < 0 || v >= n { + t.Fatalf("Int63n(%d) returned %d, out of range [0, %d)", n, v, n) + } + } + } +} + +func TestInt63nPanicsOnNonPositive(t *testing.T) { + for _, n := range []int64{0, -1, -100} { + assertPanics(t, func() { Int63n(n) }) + } +} + +func TestFloat64Range(t *testing.T) { + for i := 0; i < 10000; i++ { + v := Float64() + if v < 0.0 || v >= 1.0 { + t.Fatalf("Float64() returned %v, out of range [0.0, 1.0)", v) + } + } +} + +func TestShuffleIsPermutation(t *testing.T) { + const n = 500 + data := make([]int, n) + for i := range data { + data[i] = i + } + Shuffle(n, func(i, j int) { data[i], data[j] = data[j], data[i] }) + + seen := make([]bool, n) + for _, v := range data { + if v < 0 || v >= n { + t.Fatalf("shuffle produced an out-of-range value: %d", v) + } + if seen[v] { + t.Fatalf("shuffle produced a duplicate value: %d", v) + } + seen[v] = true + } +} + +func TestShuffleZeroAndOneAreNoOps(t *testing.T) { + for _, n := range []int{0, 1} { + swaps := 0 + Shuffle(n, func(i, j int) { swaps++ }) + if swaps != 0 { + t.Fatalf("Shuffle(%d) performed %d swaps, want 0", n, swaps) + } + } +} + +func TestShufflePanicsOnNegative(t *testing.T) { + assertPanics(t, func() { Shuffle(-1, func(i, j int) {}) }) +} + +func TestPreludeEncryptDecryptRoundTrip(t *testing.T) { + key := []byte("0123456789abcdef0123456789abcdef") // 32 bytes -> AES-256 + iv := []byte("fedcba9876543210") // 16 bytes + + plaintexts := [][]byte{ + []byte(""), + []byte("a"), + []byte("exactly-16-bytes"), + []byte("a longer message that spans multiple AES blocks for good measure"), + } + + for _, plaintext := range plaintexts { + // With an explicit IV. + cipherText := PreludeEncrypt(plaintext, key, iv) + if len(cipherText) < aes.BlockSize { + t.Fatalf("ciphertext shorter than one block for input %q", plaintext) + } + decrypted := PreludeDecrypt(cipherText, key) + if !bytes.Equal(decrypted, plaintext) { + t.Fatalf("explicit-IV round-trip mismatch: got %q, want %q", decrypted, plaintext) + } + + // With a randomly generated IV (nil IV argument). + cipherText = PreludeEncrypt(plaintext, key, nil) + decrypted = PreludeDecrypt(cipherText, key) + if !bytes.Equal(decrypted, plaintext) { + t.Fatalf("random-IV round-trip mismatch: got %q, want %q", decrypted, plaintext) + } + } +} + +func TestPreludeEncryptRandomIVIsUnique(t *testing.T) { + key := []byte("0123456789abcdef") + plaintext := []byte("same plaintext every time") + first := PreludeEncrypt(plaintext, key, nil) + second := PreludeEncrypt(plaintext, key, nil) + if bytes.Equal(first, second) { + t.Fatalf("expected distinct ciphertexts from independently random IVs") + } +} + +func TestRC4EncryptUnsafeRoundTrip(t *testing.T) { + key := []byte("super-secret-key") + plaintext := []byte("shellcode obfuscation round-trip check") + + cipherText := RC4EncryptUnsafe(plaintext, key) + if bytes.Equal(cipherText, plaintext) { + t.Fatalf("RC4 output should differ from the plaintext") + } + + // RC4 is symmetric: applying the same keystream again restores the input. + restored := RC4EncryptUnsafe(cipherText, key) + if !bytes.Equal(restored, plaintext) { + t.Fatalf("RC4 round-trip mismatch: got %q, want %q", restored, plaintext) + } +} + +func TestRC4EncryptUnsafeEmptyKey(t *testing.T) { + // An empty key is invalid for RC4; the helper returns an empty slice. + if out := RC4EncryptUnsafe([]byte("data"), []byte{}); len(out) != 0 { + t.Fatalf("expected empty output for an invalid (empty) key, got %d bytes", len(out)) + } +} + +func assertPanics(t *testing.T, fn func()) { + t.Helper() + defer func() { + if recover() == nil { + t.Fatalf("expected the function to panic, but it did not") + } + }() + fn() +} diff --git a/util/files_test.go b/util/files_test.go new file mode 100644 index 0000000000..ac84a6ba7d --- /dev/null +++ b/util/files_test.go @@ -0,0 +1,241 @@ +package util + +/* + Sliver Implant Framework + Copyright (C) 2019 Bishop Fox + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "archive/tar" + "bytes" + "io" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/klauspost/compress/flate" + "github.com/klauspost/compress/gzip" +) + +func TestByteCountBinary(t *testing.T) { + tests := []struct { + bytes int64 + expected string + }{ + {0, "0 B"}, + {512, "512 B"}, + {1023, "1023 B"}, + {1024, "1.0 KiB"}, + {1536, "1.5 KiB"}, + {1024 * 1024, "1.0 MiB"}, + {1024 * 1024 * 1024, "1.0 GiB"}, + {1024 * 1024 * 1024 * 1024, "1.0 TiB"}, + {1024 * 1024 * 1024 * 1024 * 1024, "1.0 PiB"}, + {1024 * 1024 * 1024 * 1024 * 1024 * 1024, "1.0 EiB"}, + } + for _, test := range tests { + if got := ByteCountBinary(test.bytes); got != test.expected { + t.Errorf("ByteCountBinary(%d) = %q, want %q", test.bytes, got, test.expected) + } + } +} + +func TestDeflateBuf(t *testing.T) { + // Highly compressible input so we can also assert the output is smaller. + original := bytes.Repeat([]byte("sliver adversary emulation framework\n"), 512) + + compressed := DeflateBuf(original) + if len(compressed) == 0 { + t.Fatalf("DeflateBuf returned an empty buffer") + } + if len(compressed) >= len(original) { + t.Fatalf("DeflateBuf did not compress: %d >= %d", len(compressed), len(original)) + } + + reader := flate.NewReader(bytes.NewReader(compressed)) + defer reader.Close() + inflated, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("failed to inflate DeflateBuf output: %v", err) + } + if !bytes.Equal(inflated, original) { + t.Fatalf("round-trip mismatch: inflated data does not equal the original") + } +} + +func TestDeflateBufEmpty(t *testing.T) { + compressed := DeflateBuf([]byte{}) + reader := flate.NewReader(bytes.NewReader(compressed)) + defer reader.Close() + inflated, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("failed to inflate empty DeflateBuf output: %v", err) + } + if len(inflated) != 0 { + t.Fatalf("expected empty inflated output, got %d bytes", len(inflated)) + } +} + +func TestCopyFile(t *testing.T) { + dir := t.TempDir() + src := filepath.Join(dir, "src.bin") + dst := filepath.Join(dir, "dst.bin") + content := []byte("the quick brown fox jumps over the lazy dog") + + if err := os.WriteFile(src, content, 0o600); err != nil { + t.Fatalf("failed to write source file: %v", err) + } + + if err := CopyFile(src, dst); err != nil { + t.Fatalf("CopyFile returned an error: %v", err) + } + + copied, err := os.ReadFile(dst) + if err != nil { + t.Fatalf("failed to read destination file: %v", err) + } + if !bytes.Equal(copied, content) { + t.Fatalf("copied content mismatch: got %q, want %q", copied, content) + } +} + +func TestCopyFileMissingSource(t *testing.T) { + dir := t.TempDir() + src := filepath.Join(dir, "does-not-exist.bin") + dst := filepath.Join(dir, "dst.bin") + + if err := CopyFile(src, dst); err == nil { + t.Fatalf("expected an error copying a nonexistent source file") + } +} + +func TestReadFileFromTarGz(t *testing.T) { + dir := t.TempDir() + archivePath := filepath.Join(dir, "archive.tar.gz") + wantName := "nested/data.txt" + wantContent := []byte("payload contents inside the archive") + + writeTarGz(t, archivePath, map[string][]byte{ + wantName: wantContent, + "nested/other.txt": []byte("some other file"), + }) + + got, err := ReadFileFromTarGz(archivePath, wantName) + if err != nil { + t.Fatalf("ReadFileFromTarGz returned an error: %v", err) + } + if !bytes.Equal(got, wantContent) { + t.Fatalf("content mismatch: got %q, want %q", got, wantContent) + } +} + +func TestReadFileFromTarGzMissingEntry(t *testing.T) { + dir := t.TempDir() + archivePath := filepath.Join(dir, "archive.tar.gz") + writeTarGz(t, archivePath, map[string][]byte{"present.txt": []byte("here")}) + + got, err := ReadFileFromTarGz(archivePath, "absent.txt") + if err != nil { + t.Fatalf("ReadFileFromTarGz returned an error for a missing entry: %v", err) + } + if got != nil { + t.Fatalf("expected nil for a missing entry, got %q", got) + } +} + +func TestReadFileFromTarGzMissingArchive(t *testing.T) { + if _, err := ReadFileFromTarGz(filepath.Join(t.TempDir(), "nope.tar.gz"), "any"); err == nil { + t.Fatalf("expected an error opening a nonexistent archive") + } +} + +func TestChmodR(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("file mode bits are not meaningful on Windows") + } + + root := t.TempDir() + subDir := filepath.Join(root, "sub") + if err := os.Mkdir(subDir, 0o755); err != nil { + t.Fatalf("failed to create sub directory: %v", err) + } + filePath := filepath.Join(subDir, "file.txt") + if err := os.WriteFile(filePath, []byte("data"), 0o644); err != nil { + t.Fatalf("failed to create file: %v", err) + } + + const filePerm = os.FileMode(0o600) + const dirPerm = os.FileMode(0o700) + if err := ChmodR(root, filePerm, dirPerm); err != nil { + t.Fatalf("ChmodR returned an error: %v", err) + } + + for _, dir := range []string{root, subDir} { + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("failed to stat %s: %v", dir, err) + } + if info.Mode().Perm() != dirPerm { + t.Fatalf("directory %s has perm %o, want %o", dir, info.Mode().Perm(), dirPerm) + } + } + + info, err := os.Stat(filePath) + if err != nil { + t.Fatalf("failed to stat file: %v", err) + } + if info.Mode().Perm() != filePerm { + t.Fatalf("file has perm %o, want %o", info.Mode().Perm(), filePerm) + } +} + +// writeTarGz builds a gzip-compressed tar archive at path containing the given +// name -> content entries. +func writeTarGz(t *testing.T, path string, entries map[string][]byte) { + t.Helper() + + f, err := os.Create(path) + if err != nil { + t.Fatalf("failed to create archive: %v", err) + } + defer f.Close() + + gzw := gzip.NewWriter(f) + tw := tar.NewWriter(gzw) + + for name, content := range entries { + header := &tar.Header{ + Name: name, + Mode: 0o600, + Size: int64(len(content)), + Typeflag: tar.TypeReg, + } + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("failed to write tar header: %v", err) + } + if _, err := tw.Write(content); err != nil { + t.Fatalf("failed to write tar body: %v", err) + } + } + + if err := tw.Close(); err != nil { + t.Fatalf("failed to close tar writer: %v", err) + } + if err := gzw.Close(); err != nil { + t.Fatalf("failed to close gzip writer: %v", err) + } +} diff --git a/util/generics_test.go b/util/generics_test.go new file mode 100644 index 0000000000..ab736bf5a1 --- /dev/null +++ b/util/generics_test.go @@ -0,0 +1,68 @@ +package util + +/* + Sliver Implant Framework + Copyright (C) 2019 Bishop Fox + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "sort" + "testing" +) + +func TestContains(t *testing.T) { + ints := []int{1, 2, 3, 4} + if !Contains(ints, 3) { + t.Fatalf("expected slice to contain 3") + } + if Contains(ints, 5) { + t.Fatalf("expected slice not to contain 5") + } + + strs := []string{"alpha", "beta", "gamma"} + if !Contains(strs, "beta") { + t.Fatalf("expected slice to contain \"beta\"") + } + if Contains(strs, "delta") { + t.Fatalf("expected slice not to contain \"delta\"") + } + + // An empty slice contains nothing. + if Contains([]string{}, "anything") { + t.Fatalf("expected empty slice to contain nothing") + } +} + +func TestKeys(t *testing.T) { + m := map[string]int{"a": 1, "b": 2, "c": 3} + keys := Keys(m) + if len(keys) != len(m) { + t.Fatalf("expected %d keys, got %d", len(m), len(keys)) + } + sort.Strings(keys) + expected := []string{"a", "b", "c"} + for i := range expected { + if keys[i] != expected[i] { + t.Fatalf("expected key %q at index %d, got %q", expected[i], i, keys[i]) + } + } + + // The keys of an empty map is an empty (non-nil) slice. + empty := Keys(map[int]string{}) + if len(empty) != 0 { + t.Fatalf("expected no keys for an empty map, got %d", len(empty)) + } +} diff --git a/util/leaky/leakbuf_test.go b/util/leaky/leakbuf_test.go new file mode 100644 index 0000000000..dee0ce5529 --- /dev/null +++ b/util/leaky/leakbuf_test.go @@ -0,0 +1,66 @@ +package leaky + +import "testing" + +func TestGetReturnsCorrectlySizedBuffer(t *testing.T) { + const bufSize = 4096 + lb := NewLeakyBuf(2, bufSize) + + b := lb.Get() + if len(b) != bufSize { + t.Fatalf("Get returned a buffer of size %d, want %d", len(b), bufSize) + } +} + +func TestPutThenGetReusesBuffer(t *testing.T) { + const bufSize = 8 + lb := NewLeakyBuf(1, bufSize) + + original := lb.Get() + original[0] = 0x42 + lb.Put(original) + + reused := lb.Get() + if &reused[0] != &original[0] { + t.Fatalf("expected Get to return the pooled buffer after Put") + } + if reused[0] != 0x42 { + t.Fatalf("expected pooled buffer to retain its contents") + } +} + +func TestPutBeyondCapacityDoesNotBlock(t *testing.T) { + const bufSize = 8 + lb := NewLeakyBuf(1, bufSize) + + // Putting more buffers than the pool can hold must silently drop the + // overflow rather than block. + lb.Put(make([]byte, bufSize)) + lb.Put(make([]byte, bufSize)) + lb.Put(make([]byte, bufSize)) +} + +func TestPutWrongSizePanics(t *testing.T) { + const bufSize = 8 + lb := NewLeakyBuf(1, bufSize) + + defer func() { + if recover() == nil { + t.Fatalf("expected Put with a wrong-sized buffer to panic") + } + }() + lb.Put(make([]byte, bufSize+1)) +} + +func TestGetOnEmptyPoolAllocates(t *testing.T) { + const bufSize = 16 + lb := NewLeakyBuf(4, bufSize) + + // Nothing has been Put yet, so each Get must allocate a fresh buffer of + // the configured size. + for i := 0; i < 3; i++ { + if b := lb.Get(); len(b) != bufSize { + t.Fatalf("Get on an empty pool returned size %d, want %d", len(b), bufSize) + } + } +} diff --git a/util/resource_ids_test.go b/util/resource_ids_test.go new file mode 100644 index 0000000000..fad8741eec --- /dev/null +++ b/util/resource_ids_test.go @@ -0,0 +1,76 @@ +package util + +/* + Sliver Implant Framework + Copyright (C) 2019 Bishop Fox + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "reflect" + "testing" +) + +func TestRemoveElement(t *testing.T) { + tests := []struct { + name string + slice []uint64 + value uint64 + expected []uint64 + }{ + { + name: "removes every occurrence", + slice: []uint64{1, 2, 3, 2, 4, 2}, + value: 2, + expected: []uint64{1, 3, 4}, + }, + { + name: "value not present leaves slice unchanged", + slice: []uint64{1, 2, 3}, + value: 9, + expected: []uint64{1, 2, 3}, + }, + { + name: "single matching element yields empty slice", + slice: []uint64{7}, + value: 7, + expected: []uint64{}, + }, + { + name: "empty input yields empty slice", + slice: []uint64{}, + value: 1, + expected: []uint64{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := RemoveElement(test.slice, test.value) + if !reflect.DeepEqual(got, test.expected) { + t.Fatalf("RemoveElement(%v, %d) = %v, want %v", test.slice, test.value, got, test.expected) + } + }) + } +} + +func TestRemoveElementDoesNotMutateInput(t *testing.T) { + original := []uint64{1, 2, 3, 2} + snapshot := append([]uint64{}, original...) + RemoveElement(original, 2) + if !reflect.DeepEqual(original, snapshot) { + t.Fatalf("RemoveElement mutated its input: got %v, want %v", original, snapshot) + } +}