package safety import ( "os" "path/filepath" "sort" "strings" ) // Match represents a sensitive file found in the cwd's top level. type Match struct { Path string // path relative to cwd, e.g. ".env" or ".ssh" Reason string // short label, e.g. "env file", "private key" } // sensitivePatterns is the rule table. Each entry has a check that // runs against a single dirent (with d.Name() and d.IsDir() readily // available) plus a label for reporting. var sensitivePatterns = []struct { Label string Match func(name string, isDir bool) bool }{ {"env file", func(name string, isDir bool) bool { if isDir { return false } low := strings.ToLower(name) // Match `.env`, `.env.foo`, `env.local`, but NOT `.envrc` // (envrc is direnv config, not credential storage) and NOT // conventional templates like `.env.example`, `.env.sample`, // `.env.template`, `.env.dist`, `.env.default` (which hold // variable LISTS, no values). if low == ".env" { return true } if !strings.HasPrefix(low, ".env.") && !strings.HasPrefix(low, "env.local") { return false } if isEnvTemplate(low) { return false } return true }}, {"private key", func(name string, isDir bool) bool { if isDir { return false } low := strings.ToLower(name) if strings.HasSuffix(low, ".pem") || strings.HasSuffix(low, ".key") || strings.HasSuffix(low, ".crt") || strings.HasSuffix(low, ".p12") || strings.HasSuffix(low, ".pfx") { return true } // SSH private-key default names. if name == "id_rsa" || name == "id_ed25519" || name == "id_ecdsa" || name == "id_dsa" { return true } return false }}, {"credentials file", func(name string, isDir bool) bool { if isDir { return false } low := strings.ToLower(name) // Match credential-y filenames without being too aggressive. // "credentials" as a substring is fine (e.g. ".aws_credentials") // but we'd rather not flag every "secret-something.go" source // file. Restrict "secret" matches to filenames that look like // data, not source. if strings.Contains(low, "credentials") { return true } if strings.HasSuffix(low, ".secret") || strings.HasSuffix(low, ".secrets") { return true } return false }}, {"shell secrets", func(name string, isDir bool) bool { if isDir { return false } return name == ".netrc" || name == ".pgpass" }}, {"password vault", func(name string, isDir bool) bool { if isDir { return false } low := strings.ToLower(name) return strings.HasSuffix(low, ".kdbx") || strings.HasSuffix(low, ".kbdx") }}, {"credentials directory", func(name string, isDir bool) bool { if !isDir { return false } switch name { case ".ssh", ".aws", ".kube", ".gcloud", ".azure", ".docker": return true } return false }}, } // envTemplateSuffixes lists conventional .env template suffixes that // hold variable names without values — `.env.example`, `.env.sample`, // etc. Skipped during the sensitive scan to keep the banner honest; // real credential files (.env, .env.production, .env.local) still // match. var envTemplateSuffixes = []string{ ".example", ".sample", ".template", ".dist", ".default", } func isEnvTemplate(low string) bool { for _, suf := range envTemplateSuffixes { if strings.HasSuffix(low, suf) { return true } } return false } // scanLimit caps the number of dir entries inspected. Prevents a // pathological case (cwd handed a giant temp dir, /tmp with thousands // of files, etc.) from making the safety scan slow. const scanLimit = 1000 // ScanCWDForSensitive walks the cwd's top level (no recursion) and // returns sensitive matches. Conservative by design: only matches the // rules in sensitivePatterns. Bounded to scanLimit entries to keep // the safety check fast even in pathological directories. // // Results are sorted by path for deterministic ordering — both the // banner and the tests rely on this. func ScanCWDForSensitive(cwd string) []Match { entries, err := os.ReadDir(cwd) if err != nil { return nil } var matches []Match for i, entry := range entries { if i >= scanLimit { break } name := entry.Name() isDir := entry.IsDir() for _, p := range sensitivePatterns { if p.Match(name, isDir) { matches = append(matches, Match{ Path: filepath.Join(cwd, name), Reason: p.Label, }) break } } } sort.Slice(matches, func(i, j int) bool { return matches[i].Path < matches[j].Path }) return matches }