Address gosec security warnings (bd-102)
- Enable gosec linter in .golangci.yml - Tighten file permissions: 0755→0750 for directories, 0644→0600 for configs - Git hooks remain 0700 (executable, user-only access) - Add #nosec comments for safe cases with justifications: - G204: Safe subprocess launches (git show, bd daemon) - G304: File inclusions with controlled paths - G201: SQL formatting with controlled column names - G115: Integer conversions with controlled values All gosec warnings resolved (20→0). All tests passing. Amp-Thread-ID: https://ampcode.com/threads/T-d7166b9e-cbbe-4c7b-9e48-3df36b20f0d0 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -88,7 +88,7 @@ func checkGitForIssues() (int, string) {
|
||||
for _, relPath := range candidates {
|
||||
// Use ToSlash for git path compatibility on Windows
|
||||
gitPath := filepath.ToSlash(relPath)
|
||||
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", gitPath))
|
||||
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", gitPath)) // #nosec G204 - git command with safe args
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(output) > 0 {
|
||||
lines := bytes.Count(output, []byte("\n"))
|
||||
@@ -139,7 +139,7 @@ func findGitRoot() string {
|
||||
func importFromGit(ctx context.Context, dbFilePath string, store storage.Storage, jsonlPath string) error {
|
||||
// Get content from git (use ToSlash for Windows compatibility)
|
||||
gitPath := filepath.ToSlash(jsonlPath)
|
||||
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", gitPath))
|
||||
cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", gitPath)) // #nosec G204 - git command with safe args
|
||||
jsonlData, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read from git: %w", err)
|
||||
|
||||
@@ -114,7 +114,7 @@ Examples:
|
||||
commentText, _ := cmd.Flags().GetString("file")
|
||||
if commentText != "" {
|
||||
// Read from file
|
||||
data, err := os.ReadFile(commentText)
|
||||
data, err := os.ReadFile(commentText) // #nosec G304 - user-provided file path is intentional
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -533,7 +533,7 @@ func migrateToGlobalDaemon() {
|
||||
binPath = os.Args[0]
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, "daemon", "--global")
|
||||
cmd := exec.Command(binPath, "daemon", "--global") // #nosec G204 - bd daemon command from trusted binary
|
||||
devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
|
||||
if err == nil {
|
||||
cmd.Stdout = devNull
|
||||
@@ -643,7 +643,7 @@ func startDaemon(interval time.Duration, autoCommit, autoPush bool, logFile, pid
|
||||
args = append(args, "--global")
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, args...)
|
||||
cmd := exec.Command(exe, args...) // #nosec G204 - bd daemon command from trusted binary
|
||||
cmd.Env = append(os.Environ(), "BD_DAEMON_FOREGROUND=1")
|
||||
configureDaemonProcess(cmd)
|
||||
|
||||
@@ -671,6 +671,7 @@ func startDaemon(interval time.Duration, autoCommit, autoPush bool, logFile, pid
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// #nosec G304 - controlled path from config
|
||||
if data, err := os.ReadFile(pidFile); err == nil {
|
||||
if pid, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil && pid == expectedPID {
|
||||
fmt.Printf("Daemon started (PID %d)\n", expectedPID)
|
||||
@@ -791,7 +792,7 @@ func exportToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPat
|
||||
// We need to implement direct import logic here
|
||||
func importToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPath string) error {
|
||||
// Read JSONL file
|
||||
file, err := os.Open(jsonlPath)
|
||||
file, err := os.Open(jsonlPath) // #nosec G304 - controlled path from config
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open JSONL: %w", err)
|
||||
}
|
||||
@@ -950,6 +951,7 @@ func setupDaemonLock(pidFile string, dbPath string, log daemonLogger) (io.Closer
|
||||
}
|
||||
|
||||
myPID := os.Getpid()
|
||||
// #nosec G304 - controlled path from config
|
||||
if data, err := os.ReadFile(pidFile); err == nil {
|
||||
if pid, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil && pid == myPID {
|
||||
// PID file is correct, continue
|
||||
|
||||
@@ -45,6 +45,7 @@ func acquireDaemonLock(beadsDir string, dbPath string) (*DaemonLock, error) {
|
||||
lockPath := filepath.Join(beadsDir, "daemon.lock")
|
||||
|
||||
// Open or create the lock file
|
||||
// #nosec G304 - controlled path from config
|
||||
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open lock file: %w", err)
|
||||
@@ -88,6 +89,7 @@ func tryDaemonLock(beadsDir string) (running bool, pid int) {
|
||||
lockPath := filepath.Join(beadsDir, "daemon.lock")
|
||||
|
||||
// Open lock file with read-write access (required for LockFileEx on Windows)
|
||||
// #nosec G304 - controlled path from config
|
||||
f, err := os.OpenFile(lockPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
// No lock file - could be old daemon without lock support
|
||||
@@ -134,6 +136,7 @@ func tryDaemonLock(beadsDir string) (running bool, pid int) {
|
||||
func readDaemonLockInfo(beadsDir string) (*DaemonLockInfo, error) {
|
||||
lockPath := filepath.Join(beadsDir, "daemon.lock")
|
||||
|
||||
// #nosec G304 - controlled path from config
|
||||
data, err := os.ReadFile(lockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -182,6 +185,7 @@ func validateDaemonLock(beadsDir string, expectedDB string) error {
|
||||
// This is used for backward compatibility with pre-lock daemons.
|
||||
func checkPIDFile(beadsDir string) (running bool, pid int) {
|
||||
pidFile := filepath.Join(beadsDir, "daemon.pid")
|
||||
// #nosec G304 - controlled path from config
|
||||
data, err := os.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
return false, 0
|
||||
|
||||
@@ -257,6 +257,7 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
|
||||
|
||||
if jsonOutput {
|
||||
// JSON mode: read entire file
|
||||
// #nosec G304 - controlled path from daemon discovery
|
||||
content, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
outputJSON(map[string]string{"error": err.Error()})
|
||||
@@ -283,6 +284,7 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
|
||||
}
|
||||
|
||||
func tailLines(filePath string, n int) error {
|
||||
// #nosec G304 - controlled path from daemon discovery
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -312,6 +314,7 @@ func tailLines(filePath string, n int) error {
|
||||
}
|
||||
|
||||
func tailFollow(filePath string) {
|
||||
// #nosec G304 - controlled path from daemon discovery
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error opening log file: %v\n", err)
|
||||
|
||||
@@ -310,6 +310,7 @@ func removeIssueFromJSONL(issueID string) error {
|
||||
}
|
||||
|
||||
// Read all issues except the deleted one
|
||||
// #nosec G304 - controlled path from config
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -345,6 +346,7 @@ func removeIssueFromJSONL(issueID string) error {
|
||||
|
||||
// Write to temp file atomically
|
||||
temp := fmt.Sprintf("%s.tmp.%d", path, os.Getpid())
|
||||
// #nosec G304 - controlled path from config
|
||||
out, err := os.OpenFile(temp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
@@ -602,6 +604,7 @@ func updateTextReferencesInIssues(ctx context.Context, deletedIDs []string, conn
|
||||
|
||||
// readIssueIDsFromFile reads issue IDs from a file (one per line)
|
||||
func readIssueIDsFromFile(filename string) ([]string, error) {
|
||||
// #nosec G304 - user-provided file path is intentional
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -69,6 +69,7 @@ func shouldSkipExport(ctx context.Context, store storage.Storage, issue *types.I
|
||||
|
||||
// countIssuesInJSONL counts the number of issues in a JSONL file
|
||||
func countIssuesInJSONL(path string) (int, error) {
|
||||
// #nosec G304 - controlled path from config
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -39,6 +39,7 @@ Behavior:
|
||||
// Open input
|
||||
in := os.Stdin
|
||||
if input != "" {
|
||||
// #nosec G304 - user-provided file path is intentional
|
||||
f, err := os.Open(input)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error opening input file: %v\n", err)
|
||||
|
||||
@@ -272,11 +272,13 @@ func hooksInstalled() bool {
|
||||
}
|
||||
|
||||
// Verify they're bd hooks by checking for signature comment
|
||||
// #nosec G304 - controlled path from git directory
|
||||
preCommitContent, err := os.ReadFile(preCommit)
|
||||
if err != nil || !strings.Contains(string(preCommitContent), "bd (beads) pre-commit hook") {
|
||||
return false
|
||||
}
|
||||
|
||||
// #nosec G304 - controlled path from git directory
|
||||
postMergeContent, err := os.ReadFile(postMerge)
|
||||
if err != nil || !strings.Contains(string(postMergeContent), "bd (beads) post-merge hook") {
|
||||
return false
|
||||
@@ -290,7 +292,7 @@ func installGitHooks() error {
|
||||
hooksDir := filepath.Join(".git", "hooks")
|
||||
|
||||
// Ensure hooks directory exists
|
||||
if err := os.MkdirAll(hooksDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(hooksDir, 0750); err != nil {
|
||||
return fmt.Errorf("failed to create hooks directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -375,6 +377,7 @@ exit 0
|
||||
for _, hookPath := range []string{preCommitPath, postMergePath} {
|
||||
if _, err := os.Stat(hookPath); err == nil {
|
||||
// Read existing hook to check if it's already a bd hook
|
||||
// #nosec G304 - controlled path from git directory
|
||||
content, err := os.ReadFile(hookPath)
|
||||
if err == nil && strings.Contains(string(content), "bd (beads)") {
|
||||
// Already a bd hook, skip backup
|
||||
@@ -389,13 +392,15 @@ exit 0
|
||||
}
|
||||
}
|
||||
|
||||
// Write pre-commit hook
|
||||
if err := os.WriteFile(preCommitPath, []byte(preCommitContent), 0755); err != nil {
|
||||
// Write pre-commit hook (executable scripts need 0700)
|
||||
// #nosec G306 - git hooks must be executable
|
||||
if err := os.WriteFile(preCommitPath, []byte(preCommitContent), 0700); err != nil {
|
||||
return fmt.Errorf("failed to write pre-commit hook: %w", err)
|
||||
}
|
||||
|
||||
// Write post-merge hook
|
||||
if err := os.WriteFile(postMergePath, []byte(postMergeContent), 0755); err != nil {
|
||||
// Write post-merge hook (executable scripts need 0700)
|
||||
// #nosec G306 - git hooks must be executable
|
||||
if err := os.WriteFile(postMergePath, []byte(postMergeContent), 0700); err != nil {
|
||||
return fmt.Errorf("failed to write post-merge hook: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -601,7 +601,7 @@ func restartDaemonForVersionMismatch() bool {
|
||||
}
|
||||
|
||||
args := []string{"daemon"}
|
||||
cmd := exec.Command(exe, args...)
|
||||
cmd := exec.Command(exe, args...) // #nosec G204 - bd daemon command from trusted binary
|
||||
cmd.Env = append(os.Environ(), "BD_DAEMON_FOREGROUND=1")
|
||||
|
||||
// Set working directory to database directory so daemon finds correct DB
|
||||
@@ -696,6 +696,7 @@ func isDaemonHealthy(socketPath string) bool {
|
||||
}
|
||||
|
||||
func acquireStartLock(lockPath, socketPath string) bool {
|
||||
// #nosec G304 - controlled path from config
|
||||
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
debugLog("another process is starting daemon, waiting for readiness")
|
||||
@@ -776,7 +777,7 @@ func startDaemonProcess(socketPath string, isGlobal bool) bool {
|
||||
args = append(args, "--global")
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, args...)
|
||||
cmd := exec.Command(binPath, args...) // #nosec G204 - bd daemon command from trusted binary
|
||||
setupDaemonIO(cmd)
|
||||
|
||||
if !isGlobal && dbPath != "" {
|
||||
@@ -824,6 +825,7 @@ func getPIDFileForSocket(socketPath string) string {
|
||||
|
||||
// readPIDFromFile reads a PID from a file
|
||||
func readPIDFromFile(path string) (int, error) {
|
||||
// #nosec G304 - controlled path from config
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -879,7 +881,7 @@ func canRetryDaemonStart() bool {
|
||||
}
|
||||
|
||||
// Exponential backoff: 5s, 10s, 20s, 40s, 80s, 120s (capped at 120s)
|
||||
backoff := time.Duration(5*(1<<uint(daemonStartFailures-1))) * time.Second
|
||||
backoff := time.Duration(5*(1<<uint(daemonStartFailures-1))) * time.Second // #nosec G115 - controlled value, no overflow risk
|
||||
if backoff > 120*time.Second {
|
||||
backoff = 120 * time.Second
|
||||
}
|
||||
@@ -943,7 +945,7 @@ func findJSONLPath() string {
|
||||
// Ensure the directory exists (important for new databases)
|
||||
// This is the only difference from the public API - we create the directory
|
||||
dbDir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dbDir, 0750); err != nil {
|
||||
// If we can't create the directory, return discovered path anyway
|
||||
// (the subsequent write will fail with a clearer error)
|
||||
return jsonlPath
|
||||
@@ -1237,6 +1239,7 @@ func flushToJSONL() {
|
||||
// Read existing JSONL into a map (skip for full export - we'll rebuild from scratch)
|
||||
issueMap := make(map[string]*types.Issue)
|
||||
if !fullExport {
|
||||
// #nosec G304 - controlled path from config
|
||||
if existingFile, err := os.Open(jsonlPath); err == nil {
|
||||
scanner := bufio.NewScanner(existingFile)
|
||||
lineNum := 0
|
||||
@@ -1295,6 +1298,7 @@ func flushToJSONL() {
|
||||
// Write to temp file first, then rename (atomic)
|
||||
// Use PID in filename to avoid collisions between concurrent bd commands (bd-306)
|
||||
tempPath := fmt.Sprintf("%s.tmp.%d", jsonlPath, os.Getpid())
|
||||
// #nosec G304 - controlled path from config
|
||||
f, err := os.Create(tempPath)
|
||||
if err != nil {
|
||||
recordFailure(fmt.Errorf("failed to create temp file: %w", err))
|
||||
@@ -1331,6 +1335,7 @@ func flushToJSONL() {
|
||||
}
|
||||
|
||||
// Store hash of exported JSONL (fixes bd-84: enables hash-based auto-import)
|
||||
// #nosec G304 - controlled path from config
|
||||
jsonlData, err := os.ReadFile(jsonlPath)
|
||||
if err == nil {
|
||||
hasher := sha256.New()
|
||||
@@ -2270,7 +2275,7 @@ Examples:
|
||||
tmpFile.Close()
|
||||
|
||||
// Open the editor
|
||||
editorCmd := exec.Command(editor, tmpPath)
|
||||
editorCmd := exec.Command(editor, tmpPath) // #nosec G204 - user-provided editor command is intentional
|
||||
editorCmd.Stdin = os.Stdin
|
||||
editorCmd.Stdout = os.Stdout
|
||||
editorCmd.Stderr = os.Stderr
|
||||
@@ -2281,6 +2286,7 @@ Examples:
|
||||
}
|
||||
|
||||
// Read the edited content
|
||||
// #nosec G304 - controlled temp file path
|
||||
editedContent, err := os.ReadFile(tmpPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err)
|
||||
|
||||
@@ -154,6 +154,7 @@ func gitCheckout(ref string) error {
|
||||
|
||||
// readIssueFromJSONL reads a specific issue from JSONL file
|
||||
func readIssueFromJSONL(jsonlPath, issueID string) (*types.Issue, error) {
|
||||
// #nosec G304 - controlled path from config
|
||||
file, err := os.Open(jsonlPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open JSONL: %w", err)
|
||||
|
||||
@@ -466,7 +466,7 @@ func importFromJSONL(ctx context.Context, jsonlPath string, renameOnImport bool)
|
||||
}
|
||||
|
||||
// Run import command with --resolve-collisions to automatically handle conflicts
|
||||
cmd := exec.CommandContext(ctx, exe, args...)
|
||||
cmd := exec.CommandContext(ctx, exe, args...) // #nosec G204 - bd import command from trusted binary
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("import failed: %w\n%s", err, output)
|
||||
|
||||
Reference in New Issue
Block a user