Files
vikingowl a23eb6b92c style: gofmt drift from prior commits
Pure whitespace cleanup surfaced when 'make check' ran gofmt over the
tree. Mostly struct-field column alignment in internal/safety/banner.go
(SessionInfo) and the var(...) flag block in cmd/gnoma/main.go after
--dangerously-allow-anywhere was added without realignment. Verified
zero substantive changes via 'git diff --ignore-all-space
--ignore-blank-lines'.
2026-05-24 16:33:17 +02:00

158 lines
4.0 KiB
Go

package safety
import (
"os"
"path/filepath"
"sort"
"testing"
)
func TestScanCWDForSensitive_Matches(t *testing.T) {
dir := t.TempDir()
// Sensitive files we expect to flag.
sensitive := []string{
".env",
".env.local",
"id_rsa",
"private.pem",
"aws_credentials",
".netrc",
"vault.kdbx",
}
// Non-sensitive control files.
control := []string{
".envrc", // direnv config, not a credential
"main.go",
"README.md",
"secret_handler.go", // source code, not data
}
for _, f := range sensitive {
if err := os.WriteFile(filepath.Join(dir, f), []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
}
for _, f := range control {
if err := os.WriteFile(filepath.Join(dir, f), []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
}
// Sensitive directory.
if err := os.MkdirAll(filepath.Join(dir, ".ssh"), 0o700); err != nil {
t.Fatal(err)
}
matches := ScanCWDForSensitive(dir)
wantNames := append([]string{}, sensitive...)
wantNames = append(wantNames, ".ssh")
sort.Strings(wantNames)
gotNames := make([]string, 0, len(matches))
for _, m := range matches {
gotNames = append(gotNames, filepath.Base(m.Path))
}
sort.Strings(gotNames)
if len(gotNames) != len(wantNames) {
t.Errorf("matched %d files (%v), want %d (%v)", len(gotNames), gotNames, len(wantNames), wantNames)
}
for i, n := range wantNames {
if i >= len(gotNames) || gotNames[i] != n {
t.Errorf("match[%d] = %q, want %q (got=%v want=%v)", i, gotNames[i], n, gotNames, wantNames)
}
}
}
func TestScanCWDForSensitive_EmptyDir(t *testing.T) {
dir := t.TempDir()
matches := ScanCWDForSensitive(dir)
if len(matches) != 0 {
t.Errorf("empty dir matched %v, want none", matches)
}
}
func TestScanCWDForSensitive_PrecisionNoFalsePositives(t *testing.T) {
dir := t.TempDir()
// Files that look credential-y but conventionally hold no
// secrets — must NOT be flagged.
control := []string{
".envrc", // direnv config
"secret_handler.go", // source code
".env.example", // template
".env.sample", // template
".env.template", // template
".env.dist", // template
".env.default", // template
"env.local.example", // template
}
for _, name := range control {
if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
}
matches := ScanCWDForSensitive(dir)
if len(matches) != 0 {
names := make([]string, 0, len(matches))
for _, m := range matches {
names = append(names, filepath.Base(m.Path))
}
t.Errorf("precision regression: none of %v should flag, got %v", control, names)
}
}
func TestScanCWDForSensitive_RealEnvFilesStillMatch(t *testing.T) {
dir := t.TempDir()
// Real env files (non-template) must still be flagged.
real := []string{
".env",
".env.local",
".env.production",
".env.staging",
"env.local",
"env.local.production",
}
for _, name := range real {
if err := os.WriteFile(filepath.Join(dir, name), []byte("API_KEY=secret"), 0o600); err != nil {
t.Fatal(err)
}
}
matches := ScanCWDForSensitive(dir)
if len(matches) != len(real) {
got := make([]string, 0, len(matches))
for _, m := range matches {
got = append(got, filepath.Base(m.Path))
}
t.Errorf("expected %d real env files flagged, got %d (%v)", len(real), len(matches), got)
}
}
func TestScanCWDForSensitive_BoundedScan(t *testing.T) {
dir := t.TempDir()
// Populate just over the scan limit. The function should not panic
// or hang. Result count is at most scanLimit (matches may be 0 if
// the entries beyond the cap happen to be sensitive — that's OK,
// the bound is a safety knob, not a correctness one).
for i := 0; i < scanLimit+10; i++ {
if err := os.WriteFile(filepath.Join(dir, "file"+itoa(i)), []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
}
_ = ScanCWDForSensitive(dir) // mustn't panic
}
// itoa avoids importing strconv just for one use.
func itoa(n int) string {
if n == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
return string(buf[i:])
}