├── .dockerignore ├── .github └── workflows │ ├── go.yml │ └── publish.yml ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── entropy.png ├── go.mod ├── go.sum ├── main.go ├── main_test.go └── testdata ├── .gitignore ├── dangling ├── folder ├── fileA.py └── fileB.go └── random.js /.dockerignore: -------------------------------------------------------------------------------- 1 | testdata/* 2 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: ["master"] 9 | pull_request: 10 | branches: ["master"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: "1.22" 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | - name: Test 27 | run: go test -v ./... 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | homebrew: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Bump Homebrew formula 13 | uses: dawidd6/action-homebrew-bump-formula@v3 14 | with: 15 | token: ${{secrets.GH_API_TOKEN}} 16 | formula: entropy 17 | tap: EwenQuim/homebrew-repo 18 | user_name: entropy-releaser 19 | user_email: entropy-releaser@mail.com 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "nuxt.isNuxtApp": false 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 as builder 2 | 3 | ARG TARGETARCH 4 | 5 | WORKDIR /go/src 6 | 7 | COPY go.mod go.sum . 8 | 9 | RUN go mod download 10 | 11 | COPY . . 12 | 13 | RUN mkdir /data 14 | 15 | RUN GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "-s -w" -o entropy . 16 | 17 | # Path: Dockerfile 18 | FROM scratch 19 | 20 | WORKDIR /bin 21 | 22 | COPY --from=builder /go/src/entropy /bin 23 | COPY --from=builder /data /data 24 | 25 | ENTRYPOINT [ "entropy" ] 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ewen Quimerc'h 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-buildx: 2 | docker buildx build --platform linux/amd64,linux/arm64 --tag ewenquim/entropy:latest --push . 3 | 4 | docker-run: 5 | docker run --rm -v $(pwd):/data ewenquim/entropy /data 6 | 7 | docker-push: 8 | docker push ewenquim/entropy:latest 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Entropy logo](./entropy.png) 2 | 3 | > Paranoïd about having secrets leaked in your huge codebase? Entropy is here to help you find them! 4 | 5 | # Entropy 6 | 7 | [![Go Reference](https://pkg.go.dev/badge/github.com/EwenQuim/entropy.svg)](https://pkg.go.dev/github.com/EwenQuim/entropy) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/EwenQuim/entropy)](https://goreportcard.com/report/github.com/EwenQuim/entropy) 9 | 10 | Entropy is a CLI tool that will **scan your codebase for high entropy lines**, which are often secrets. 11 | 12 | ## Installation 13 | 14 | ### From source with Go (preferred) 15 | 16 | ```bash 17 | go install github.com/EwenQuim/entropy@latest 18 | entropy 19 | 20 | # More options 21 | entropy -h 22 | entropy -top 20 -ext go,py,js 23 | entropy -top 5 -ignore-ext min.js,pdf,png,jpg,jpeg,zip,mp4,gif my-folder my-file1 my-file2 24 | ``` 25 | 26 | or in one line 27 | 28 | ```bash 29 | go run github.com/EwenQuim/entropy@latest 30 | ``` 31 | 32 | ### With brew 33 | 34 | ```bash 35 | brew install ewenquim/repo/entropy 36 | entropy 37 | 38 | # More options 39 | entropy -h 40 | entropy -top 20 -ext go,py,js 41 | entropy -top 5 -ignore-ext min.js,_test.go,pdf,png,jpg my-folder my-file1 my-file2 42 | ``` 43 | 44 | ### With docker 45 | 46 | ```bash 47 | docker run --rm -v $(pwd):/data ewenquim/entropy /data 48 | 49 | # More options 50 | docker run --rm -v $(pwd):/data ewenquim/entropy -h 51 | docker run --rm -v $(pwd):/data ewenquim/entropy -top 20 -ext go,py,js /data 52 | docker run --rm -v $(pwd):/data ewenquim/entropy -top 5 /data/my-folder /data/my-file 53 | ``` 54 | 55 | The docker image is available on [Docker Hub](https://hub.docker.com/r/ewenquim/entropy). 56 | 57 | The `-v` option is used to mount the current directory into the container. The `/data` directory is the default directory where the tool will look for files. **Don't forget to add /data at the end of the command**, otherwise the tool will search inside the container, not your local filesystem. 58 | 59 | ## My other projects 60 | 61 | - [**Fuego**](https://github.com/go-fuego/fuego): A Go framework that generates OpenAPI documentation from your codebase. 62 | - [**Renpy-Graphviz**](https://github.com/EwenQuim/renpy-graphviz): A tool to generate a graph of the Ren'Py game engine's screens and labels. 63 | -------------------------------------------------------------------------------- /entropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EwenQuim/entropy/aa854363a87fb9e4902d74407e6155c3135833c9/entropy.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/EwenQuim/entropy 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require golang.org/x/term v0.30.0 8 | 9 | require golang.org/x/sys v0.31.0 // indirect 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 2 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 3 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 4 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "math" 8 | "os" 9 | "regexp" 10 | "slices" 11 | "strings" 12 | "sync" 13 | "unicode/utf8" 14 | 15 | "golang.org/x/term" 16 | ) 17 | 18 | const ( 19 | minCharactersDefault = 8 20 | resultCountDefault = 10 21 | exploreHiddenDefault = false 22 | extensionsToIgnoreDefault = ".pyc,yarn.lock,go.mod,go.sum,go.work.sum,package-lock.json,.wasm,.pdf" 23 | ) 24 | 25 | // CLI options. Will be initialized by flags 26 | var ( 27 | minCharacters int // Minimum number of characters to consider computing entropy 28 | resultCount int // Number of results to display 29 | exploreHidden bool // Ignore hidden files and folders 30 | extensions []string // List of file extensions to include. Empty string means all files 31 | extensionsToIgnore []string // List of file extensions to ignore. Empty string means all files 32 | discrete bool // Discrete mode, don't show the line, only the entropy and file 33 | includeBinaryFiles bool // Include binary files in search. 34 | disableAdvancedMode bool // Advanced mode : filters more than just entropy 35 | ) 36 | 37 | type Entropy struct { 38 | Entropy float64 // Entropy of the line 39 | File string // File where the line is found 40 | LineNum int // Line number in the file 41 | Line string // Line with high entropy 42 | } 43 | 44 | func NewEntropies(n int) *Entropies { 45 | return &Entropies{ 46 | Entropies: make([]Entropy, n), 47 | maxLength: n, 48 | } 49 | } 50 | 51 | // Entropies should be created with NewEntropies(n). 52 | // It should not be written to manually, instead use Entropies.Add 53 | type Entropies struct { 54 | mu sync.Mutex 55 | Entropies []Entropy // Ordered list of entropies with highest entropy first, with length fixed at creation 56 | maxLength int 57 | } 58 | 59 | var mediaBase64Regex = regexp.MustCompile(`(audio|video|image|font)\/[-+.\w]+;base64`) 60 | 61 | // Add assumes that es contains an ordered list of entropies of length es.maxLength. 62 | // It preserves ordering, and inserts an additional value e, if it has high enough entropy. 63 | // In that case, the entry with lowest entropy is rejected. 64 | func (es *Entropies) Add(e Entropy) { 65 | // This condition is to avoid acquiring the lock (slow) if the entropy is not high enough. 66 | // Not goroutine safe, but another check is made after acquiring the lock. 67 | if es.Entropies[es.maxLength-1].Entropy >= e.Entropy { 68 | return 69 | } 70 | 71 | if !disableAdvancedMode { 72 | line := strings.ToLower(e.Line) 73 | line = strings.ReplaceAll(line, "'", "") 74 | line = strings.ReplaceAll(line, "\"", "") 75 | if mediaBase64Regex.MatchString(line) || 76 | strings.HasPrefix(line, "http") || 77 | strings.Contains(line, "abcdefghijklmnopqrstuvwxyz") || 78 | strings.Contains(line, "aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz") { 79 | return 80 | } 81 | 82 | } 83 | 84 | es.mu.Lock() 85 | defer es.mu.Unlock() 86 | 87 | if es.Entropies[len(es.Entropies)-1].Entropy >= e.Entropy { 88 | return 89 | } 90 | 91 | i, _ := slices.BinarySearchFunc(es.Entropies, e, func(a, b Entropy) int { 92 | if b.Entropy > a.Entropy { 93 | return 1 94 | } 95 | if a.Entropy > b.Entropy { 96 | return -1 97 | } 98 | return 0 99 | }) 100 | 101 | copy(es.Entropies[i+1:], es.Entropies[i:]) 102 | es.Entropies[i] = e 103 | } 104 | 105 | func main() { 106 | minCharactersFlag := flag.Int("min", minCharactersDefault, "Minimum number of characters in the line to consider computing entropy") 107 | resultCountFlag := flag.Int("top", resultCountDefault, "Number of results to display") 108 | exploreHiddenFlag := flag.Bool("include-hidden", exploreHiddenDefault, "Search in hidden files and folders (.git, .env...). Slows down the search.") 109 | extensionsFlag := flag.String("ext", "", "Search only in files with these extensions. Comma separated list, e.g. -ext go,py,js (default all files)") 110 | extensionsToIgnoreFlag := flag.String("ignore-ext", "", "Ignore files with these suffixes. Comma separated list, e.g. -ignore-ext min.css,_test.go,pdf,Test.php. Adds ignored extensions to the default ones.") 111 | noDefaultExtensionsToIgnore := flag.Bool("ignore-ext-no-defaults", false, "Remove the default ignored extensions (default "+extensionsToIgnoreDefault+")") 112 | discreteFlag := flag.Bool("discrete", false, "Only show the entropy and file, not the line containing the possible secret") 113 | binaryFilesFlag := flag.Bool("binaries", false, "Include binary files in search. Slows down the search and creates many false positives. A file is considered binary if the first line is not valid utf8.") 114 | disableAdvancedModeFlag := flag.Bool("dumb", false, "Just dumb entropy. Disable filters that removes alphabets, urls, base64 encoded images and other false positives.") 115 | 116 | flag.CommandLine.Usage = func() { 117 | fmt.Fprintf(flag.CommandLine.Output(), "%s [flags] file1 file2 file3 ...\n", os.Args[0]) 118 | fmt.Fprintf(flag.CommandLine.Output(), "Example: %s -top 10 -ext go,py,js,yaml,json .\n", os.Args[0]) 119 | fmt.Fprintln(flag.CommandLine.Output(), "Finds the highest entropy strings in files. The higher the entropy, the more random the string is. Useful for finding secrets (and alphabets, it seems).") 120 | fmt.Fprintln(flag.CommandLine.Output(), "Please support me on GitHub: https://github.com/EwenQuim") 121 | flag.PrintDefaults() 122 | } 123 | flag.Parse() 124 | 125 | // Apply flags 126 | minCharacters = *minCharactersFlag 127 | resultCount = *resultCountFlag 128 | exploreHidden = *exploreHiddenFlag 129 | discrete = *discreteFlag 130 | includeBinaryFiles = *binaryFilesFlag 131 | disableAdvancedMode = *disableAdvancedModeFlag 132 | extensions = strings.Split(*extensionsFlag, ",") 133 | extensionsToIgnoreString := *extensionsToIgnoreFlag + "," + extensionsToIgnoreDefault 134 | if *noDefaultExtensionsToIgnore { 135 | extensionsToIgnoreString = *extensionsToIgnoreFlag 136 | } 137 | extensionsToIgnore = strings.Split(extensionsToIgnoreString, ",") 138 | extensions = removeEmptyStrings(extensions) 139 | extensionsToIgnore = removeEmptyStrings(extensionsToIgnore) 140 | 141 | // Read file names from cli 142 | fileNames := flag.Args() 143 | if len(fileNames) == 0 { 144 | fmt.Println("No files provided, defaults to current folder.") 145 | fileNames = []string{"."} 146 | } 147 | entropies := NewEntropies(resultCount) 148 | for _, fileName := range fileNames { 149 | err := readFile(entropies, fileName) 150 | if err != nil { 151 | fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", fileName, err) 152 | } 153 | } 154 | 155 | redMark := "\033[31m" 156 | resetMark := "\033[0m" 157 | if !term.IsTerminal(int(os.Stdout.Fd())) { 158 | // If not a terminal, remove color 159 | redMark = "" 160 | resetMark = "" 161 | } 162 | 163 | for _, entropy := range entropies.Entropies { 164 | if entropy == (Entropy{}) { 165 | return 166 | } 167 | if discrete { 168 | entropy.Line = "" 169 | } 170 | fmt.Printf("%.3f: %s%s:%d%s %s\n", entropy.Entropy, redMark, entropy.File, entropy.LineNum, resetMark, entropy.Line) 171 | } 172 | } 173 | 174 | func readFile(entropies *Entropies, fileName string) error { 175 | fileInfo, err := os.Stat(fileName) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | if isFileHidden(fileInfo.Name()) && !exploreHidden { 181 | return nil 182 | } 183 | 184 | if fileInfo.IsDir() { 185 | dir, err := os.ReadDir(fileName) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | var wg sync.WaitGroup 191 | for i, file := range dir { 192 | wg.Add(1) 193 | go func(i int, file os.DirEntry) { 194 | defer wg.Done() 195 | err := readFile(entropies, fileName+"/"+file.Name()) 196 | if err != nil { 197 | fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", file.Name(), err) 198 | } 199 | }(i, file) 200 | } 201 | 202 | wg.Wait() 203 | } 204 | 205 | if !isFileIncluded(fileInfo.Name()) { 206 | return nil 207 | } 208 | 209 | file, err := os.Open(fileName) 210 | if err != nil { 211 | return err 212 | } 213 | defer file.Close() 214 | 215 | i := 0 216 | scanner := bufio.NewScanner(file) 217 | for scanner.Scan() { 218 | i++ 219 | line := strings.TrimSpace(scanner.Text()) 220 | 221 | if i == 1 && !includeBinaryFiles && !utf8.ValidString(line) { 222 | break 223 | } 224 | 225 | for _, field := range strings.Fields(line) { 226 | if len(field) < minCharacters { 227 | continue 228 | } 229 | 230 | entropies.Add(Entropy{ 231 | Entropy: entropy(field), 232 | File: fileName, 233 | LineNum: i, 234 | Line: field, 235 | }) 236 | } 237 | } 238 | 239 | return nil 240 | } 241 | 242 | func entropy(text string) float64 { 243 | uniqueCharacters := make(map[rune]int64, len(text)) 244 | for _, r := range text { 245 | uniqueCharacters[r]++ 246 | } 247 | 248 | entropy := 0.0 249 | for character := range uniqueCharacters { 250 | res := float64(uniqueCharacters[character]) / float64(len(text)) 251 | if res == 0 { 252 | continue 253 | } 254 | 255 | entropy -= res * math.Log2(res) 256 | } 257 | 258 | return entropy 259 | } 260 | 261 | func isFileHidden(filename string) bool { 262 | if filename == "." { 263 | return false 264 | } 265 | filename = strings.TrimPrefix(filename, "./") 266 | 267 | return strings.HasPrefix(filename, ".") || filename == "node_modules" 268 | } 269 | 270 | // isFileIncluded returns true if the file should be included in the search 271 | func isFileIncluded(filename string) bool { 272 | for _, ext := range extensionsToIgnore { 273 | if strings.HasSuffix(filename, ext) { 274 | return false 275 | } 276 | } 277 | 278 | if len(extensions) == 0 { 279 | return true 280 | } 281 | 282 | for _, ext := range extensions { 283 | if strings.HasSuffix(filename, ext) { 284 | return true 285 | } 286 | } 287 | 288 | return false 289 | } 290 | 291 | func removeEmptyStrings(slice []string) []string { 292 | slices.Sort(slice) 293 | slice = slices.Compact(slice) 294 | 295 | if len(slice) > 0 && slice[0] == "" { 296 | return slice[1:] 297 | } 298 | 299 | return slice 300 | } 301 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkFile(b *testing.B) { 9 | entropies := NewEntropies(10) 10 | for range b.N { 11 | err := readFile(entropies, "testdata") 12 | if err != nil { 13 | b.Errorf("expected nil, got %v", err) 14 | } 15 | b.Logf("Entropies: %v", entropies) 16 | } 17 | } 18 | 19 | func TestEntropy(t *testing.T) { 20 | t.Run("empty", func(t *testing.T) { 21 | Expect(t, entropy(""), 0.0) 22 | }) 23 | 24 | t.Run("single character", func(t *testing.T) { 25 | Expect(t, entropy("a"), 0.0) 26 | }) 27 | 28 | t.Run("two same characters", func(t *testing.T) { 29 | Expect(t, entropy("aa"), 0.0) 30 | }) 31 | 32 | t.Run("three different characters", func(t *testing.T) { 33 | ExpectFloat(t, entropy("abc"), 1.5849625007211563) 34 | }) 35 | 36 | t.Run("three same characters", func(t *testing.T) { 37 | Expect(t, entropy("aaa"), 0.0) 38 | }) 39 | 40 | t.Run("four different characters", func(t *testing.T) { 41 | Expect(t, entropy("abcd"), 2.0) 42 | }) 43 | 44 | t.Run("four same characters", func(t *testing.T) { 45 | Expect(t, entropy("aabb"), 1.0) 46 | }) 47 | 48 | t.Run("12 characters", func(t *testing.T) { 49 | ExpectFloat(t, entropy("aabbccddeeff"), 2.584962500721156) 50 | }) 51 | } 52 | 53 | func TestReadFile(t *testing.T) { 54 | t.Run("random.js", func(t *testing.T) { 55 | res := NewEntropies(10) 56 | err := readFile(res, "testdata/random.js") 57 | if err != nil { 58 | t.Errorf("expected nil, got %v", err) 59 | } 60 | 61 | ExpectFloat(t, res.Entropies[0].Entropy, 5.53614242151549) 62 | Expect(t, res.Entropies[0].LineNum, 7) // The token is hidden here 63 | ExpectFloat(t, res.Entropies[4].Entropy, 3.321928094887362) 64 | }) 65 | 66 | t.Run("testdata/folder", func(t *testing.T) { 67 | res := NewEntropies(10) 68 | err := readFile(res, "testdata/folder") 69 | if err != nil { 70 | t.Errorf("expected nil, got %v", err) 71 | } 72 | 73 | ExpectFloat(t, res.Entropies[0].Entropy, 3.7667029194153567) 74 | Expect(t, res.Entropies[0].LineNum, 7) // The token is hidden here 75 | ExpectFloat(t, res.Entropies[6].Entropy, 2.8553885422075336) 76 | }) 77 | 78 | t.Run("dangling symlink in testdata folder", func(t *testing.T) { 79 | entropies := NewEntropies(10) 80 | err := readFile(entropies, "testdata") 81 | if err != nil { 82 | t.Errorf("expected nil, got %v", err) 83 | } 84 | 85 | Expect(t, len(entropies.Entropies), 10) 86 | }) 87 | } 88 | 89 | func TestIsFileIncluded(t *testing.T) { 90 | t.Run("empty", func(t *testing.T) { 91 | extensions = []string{} 92 | Expect(t, isFileIncluded("main.go"), true) 93 | Expect(t, isFileIncluded("main.py"), true) 94 | }) 95 | 96 | t.Run("one element included", func(t *testing.T) { 97 | extensions = []string{"go"} 98 | Expect(t, isFileIncluded("main.py"), false) 99 | Expect(t, isFileIncluded("main.go"), true) 100 | }) 101 | 102 | t.Run("one element excluded", func(t *testing.T) { 103 | extensions = []string{} 104 | extensionsToIgnore = []string{"go"} 105 | Expect(t, isFileIncluded("main.go"), false) 106 | Expect(t, isFileIncluded("main.py"), true) 107 | }) 108 | 109 | t.Run("multiple elements", func(t *testing.T) { 110 | extensions = []string{"go", "py"} 111 | extensionsToIgnore = []string{"pdf"} 112 | Expect(t, isFileIncluded("main.go"), true) 113 | Expect(t, isFileIncluded("main.py"), true) 114 | Expect(t, isFileIncluded("main.pdf"), false) 115 | }) 116 | } 117 | 118 | func TestIsFileHidden(t *testing.T) { 119 | Expect(t, isFileHidden("."), false) 120 | Expect(t, isFileHidden("main.go"), false) 121 | Expect(t, isFileHidden("main.py"), false) 122 | Expect(t, isFileHidden("node_modules"), true) 123 | Expect(t, isFileHidden("./.git"), true) 124 | Expect(t, isFileHidden("src"), false) 125 | Expect(t, isFileHidden("./src"), false) 126 | Expect(t, isFileHidden(".git"), true) 127 | Expect(t, isFileHidden(".env"), true) 128 | } 129 | 130 | func TestEntropies(t *testing.T) { 131 | t.Run("synchronous", func(t *testing.T) { 132 | res := NewEntropies(5) 133 | for _, i := range []float64{1, 3, 5, 7, 2, 4, 6, 8} { 134 | res.Add(Entropy{Entropy: i}) 135 | } 136 | 137 | Expect(t, res.Entropies[0].Entropy, 8) 138 | Expect(t, res.Entropies[1].Entropy, 7) 139 | Expect(t, res.Entropies[2].Entropy, 6) 140 | Expect(t, res.Entropies[3].Entropy, 5) 141 | Expect(t, res.Entropies[4].Entropy, 4) 142 | }) 143 | 144 | t.Run("asynchronous (add from multiple parallel goroutines)", func(t *testing.T) { 145 | res := NewEntropies(5) 146 | var wg sync.WaitGroup 147 | for _, i := range []float64{1, 3, 5, 7, 2, 4, 6, 8} { 148 | wg.Add(1) 149 | go func(i float64) { 150 | res.Add(Entropy{Entropy: i}) 151 | wg.Done() 152 | }(i) 153 | } 154 | wg.Wait() 155 | Expect(t, res.Entropies[0].Entropy, 8) 156 | Expect(t, res.Entropies[1].Entropy, 7) 157 | Expect(t, res.Entropies[2].Entropy, 6) 158 | Expect(t, res.Entropies[3].Entropy, 5) 159 | Expect(t, res.Entropies[4].Entropy, 4) 160 | }) 161 | } 162 | 163 | func Expect[T comparable](t *testing.T, got, expected T) { 164 | t.Helper() 165 | if got != expected { 166 | t.Errorf("expected %v, got %v", expected, got) 167 | } 168 | } 169 | 170 | func ExpectFloat(t *testing.T, got, expected float64) { 171 | t.Helper() 172 | 173 | gotInt := int(got * 10000) 174 | expectedInt := int(expected * 10000) 175 | if gotInt != expectedInt { 176 | t.Errorf("expected %d, got %d", expectedInt, gotInt) 177 | } 178 | } 179 | 180 | func TestRemoveEmptyStrings(t *testing.T) { 181 | t.Run("empty", func(t *testing.T) { 182 | Expect(t, len(removeEmptyStrings([]string{})), 0) 183 | }) 184 | 185 | t.Run("single empty string", func(t *testing.T) { 186 | Expect(t, len(removeEmptyStrings([]string{""})), 0) 187 | }) 188 | 189 | t.Run("no empty strings", func(t *testing.T) { 190 | Expect(t, len(removeEmptyStrings([]string{"a", "b", "c"})), 3) 191 | }) 192 | 193 | t.Run("one empty string", func(t *testing.T) { 194 | Expect(t, len(removeEmptyStrings([]string{"a", "", "c"})), 2) 195 | }) 196 | 197 | t.Run("multiple consecutive empty strings", func(t *testing.T) { 198 | Expect(t, len(removeEmptyStrings([]string{"a", "", "", "", "c"})), 2) 199 | }) 200 | 201 | t.Run("multiple non-consecutive empty strings", func(t *testing.T) { 202 | Expect(t, len(removeEmptyStrings([]string{"", "a", "", "", "", "c", ""})), 2) 203 | }) 204 | 205 | t.Run("all empty strings", func(t *testing.T) { 206 | Expect(t, len(removeEmptyStrings([]string{"", "", "", ""})), 0) 207 | }) 208 | 209 | } 210 | -------------------------------------------------------------------------------- /testdata/.gitignore: -------------------------------------------------------------------------------- 1 | # repositories entropy is tested against 2 | kubernetes 3 | git 4 | cli 5 | etcd 6 | -------------------------------------------------------------------------------- /testdata/dangling: -------------------------------------------------------------------------------- 1 | nonExistantFile -------------------------------------------------------------------------------- /testdata/folder/fileA.py: -------------------------------------------------------------------------------- 1 | def hiMom(): 2 | print("Hi Mom!") 3 | 4 | SUPER_SECRET = "dqizuydièqddQ9ZDYçDBzqdqj" 5 | -------------------------------------------------------------------------------- /testdata/folder/fileB.go: -------------------------------------------------------------------------------- 1 | package folder 2 | 3 | func Hello() { 4 | println("Hello, World!") 5 | } 6 | 7 | const SECRET = "x!çQDSDG9ZQdiiqbzdzqkdbqdzquid" 8 | -------------------------------------------------------------------------------- /testdata/random.js: -------------------------------------------------------------------------------- 1 | function doThings() { 2 | console.log("Doing innocent things"); 3 | return Math.random(); 4 | } 5 | 6 | const DangerousToken = 7 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkV3ZW4iLCJpYXQiOjE1MTYyMzkwMjJ9.BPqjdIG7zp-j9CUEZ-EcYQXplbhzo-2QNMG_QwcAgfk"; 8 | --------------------------------------------------------------------------------