Files
beads/cmd/bd/doctor/gitignore.go
Steve Yegge ae5a4ac6ea Add security tests for WriteFile permissions in doctor command
Resolves bd-ee1: Add security tests for WriteFile permissions in doctor command

Added comprehensive security tests for the FixGitignore function to verify:
- Files are created with 0600 permissions (secure, owner-only read/write)
- Existing files with insecure permissions are fixed
- Read-only files can be updated (permissions fixed first)
- File ownership is correct
- Permissions are enforced even on systems that respect umask

Also improved FixGitignore implementation to:
- Handle read-only files by fixing permissions before writing
- Explicitly set permissions after write to ensure 0600 regardless of umask
- Maintain secure permissions throughout the operation

Tests verify the gosec G306 security concern is properly addressed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:19:14 -08:00

118 lines
2.6 KiB
Go

package doctor
import (
"os"
"path/filepath"
"strings"
)
// GitignoreTemplate is the canonical .beads/.gitignore content
const GitignoreTemplate = `# SQLite databases
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
# Legacy database files
db.sqlite
bd.db
# Merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.base.meta.json
beads.left.jsonl
beads.left.meta.json
beads.right.jsonl
beads.right.meta.json
# Keep JSONL exports and config (source of truth for git)
!issues.jsonl
!metadata.json
!config.json
`
// requiredPatterns are patterns that MUST be in .beads/.gitignore
var requiredPatterns = []string{
"beads.base.jsonl",
"beads.left.jsonl",
"beads.right.jsonl",
"beads.base.meta.json",
"beads.left.meta.json",
"beads.right.meta.json",
"*.db?*",
}
// CheckGitignore checks if .beads/.gitignore is up to date
func CheckGitignore() DoctorCheck {
gitignorePath := filepath.Join(".beads", ".gitignore")
// Check if file exists
content, err := os.ReadFile(gitignorePath) // #nosec G304 -- path is hardcoded
if err != nil {
return DoctorCheck{
Name: "Gitignore",
Status: "warning",
Message: ".beads/.gitignore not found",
Fix: "Run: bd init (safe to re-run) or bd doctor --fix",
}
}
// Check for required patterns
contentStr := string(content)
var missing []string
for _, pattern := range requiredPatterns {
if !strings.Contains(contentStr, pattern) {
missing = append(missing, pattern)
}
}
if len(missing) > 0 {
return DoctorCheck{
Name: "Gitignore",
Status: "warning",
Message: "Outdated .beads/.gitignore (missing merge artifact patterns)",
Detail: "Missing: " + strings.Join(missing, ", "),
Fix: "Run: bd doctor --fix or bd init (safe to re-run)",
}
}
return DoctorCheck{
Name: "Gitignore",
Status: "ok",
Message: "Up to date",
}
}
// FixGitignore updates .beads/.gitignore to the current template
func FixGitignore() error {
gitignorePath := filepath.Join(".beads", ".gitignore")
// If file exists and is read-only, fix permissions first
if info, err := os.Stat(gitignorePath); err == nil {
if info.Mode().Perm()&0200 == 0 { // No write permission for owner
if err := os.Chmod(gitignorePath, 0600); err != nil {
return err
}
}
}
// Write canonical template with secure file permissions
if err := os.WriteFile(gitignorePath, []byte(GitignoreTemplate), 0600); err != nil {
return err
}
// Ensure permissions are set correctly (some systems respect umask)
if err := os.Chmod(gitignorePath, 0600); err != nil {
return err
}
return nil
}