fix: Suppress gosec warnings with nolint comments

- Add nolint:gosec comments for safe file operations
- G304: File reads from validated/secure paths
- G306/G302: JSONL/error files need 0644 for sharing/debugging
- G204: Subprocess launches with validated arguments
- G104: Deferred file close errors are non-critical
- G115: Safe integer conversions in backoff
- G201: SQL placeholders for IN clause expansion

All warnings are for intentional behavior that is safe in context.

Amp-Thread-ID: https://ampcode.com/threads/T-d78f2780-4709-497f-97b0-035ca8c809e1
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-11-02 08:09:58 -08:00
parent 20b21fda42
commit 15affbe11e
14 changed files with 123 additions and 83 deletions

View File

@@ -517,6 +517,7 @@ func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error)
}
// Set appropriate file permissions (0644: rw-r--r--)
// nolint:gosec // G302: JSONL needs to be readable by other tools
if err := os.Chmod(jsonlPath, 0644); err != nil {
// Non-fatal - file is already written
if os.Getenv("BD_DEBUG") != "" {

View File

@@ -84,34 +84,34 @@ Use --health to check daemon health and metrics.`,
if os.Getenv("BD_DAEMON_FOREGROUND") != "1" {
// Check if daemon is already running
if isRunning, pid := isDaemonRunning(pidFile); isRunning {
// Check if running daemon has compatible version
socketPath := getSocketPathForPID(pidFile, global)
if client, err := rpc.TryConnectWithTimeout(socketPath, 1*time.Second); err == nil && client != nil {
health, healthErr := client.Health()
_ = client.Close()
// If we can check version and it's compatible, exit
if healthErr == nil && health.Compatible {
fmt.Fprintf(os.Stderr, "Error: daemon already running (PID %d, version %s)\n", pid, health.Version)
// Check if running daemon has compatible version
socketPath := getSocketPathForPID(pidFile, global)
if client, err := rpc.TryConnectWithTimeout(socketPath, 1*time.Second); err == nil && client != nil {
health, healthErr := client.Health()
_ = client.Close()
// If we can check version and it's compatible, exit
if healthErr == nil && health.Compatible {
fmt.Fprintf(os.Stderr, "Error: daemon already running (PID %d, version %s)\n", pid, health.Version)
fmt.Fprintf(os.Stderr, "Use 'bd daemon --stop%s' to stop it first\n", boolToFlag(global, " --global"))
os.Exit(1)
}
// Version mismatch - auto-stop old daemon
if healthErr == nil && !health.Compatible {
fmt.Fprintf(os.Stderr, "Warning: daemon version mismatch (daemon: %s, client: %s)\n", health.Version, Version)
fmt.Fprintf(os.Stderr, "Stopping old daemon and starting new one...\n")
stopDaemon(pidFile)
// Continue with daemon startup
}
} else {
// Can't check version - assume incompatible
fmt.Fprintf(os.Stderr, "Error: daemon already running (PID %d)\n", pid)
fmt.Fprintf(os.Stderr, "Use 'bd daemon --stop%s' to stop it first\n", boolToFlag(global, " --global"))
os.Exit(1)
}
// Version mismatch - auto-stop old daemon
if healthErr == nil && !health.Compatible {
fmt.Fprintf(os.Stderr, "Warning: daemon version mismatch (daemon: %s, client: %s)\n", health.Version, Version)
fmt.Fprintf(os.Stderr, "Stopping old daemon and starting new one...\n")
stopDaemon(pidFile)
// Continue with daemon startup
}
} else {
// Can't check version - assume incompatible
fmt.Fprintf(os.Stderr, "Error: daemon already running (PID %d)\n", pid)
fmt.Fprintf(os.Stderr, "Use 'bd daemon --stop%s' to stop it first\n", boolToFlag(global, " --global"))
os.Exit(1)
}
}
}
// Global daemon doesn't support auto-commit/auto-push (no sync loop)
if global && (autoCommit || autoPush) {
@@ -231,15 +231,16 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p
errMsg += fmt.Sprintf("\nBeads requires a single canonical database: %s\n", beads.CanonicalDatabaseName)
errMsg += "Run 'bd init' to migrate legacy databases or manually remove old databases\n"
errMsg += "Or run 'bd doctor' for more diagnostics"
log.log(errMsg)
// Write error to file so user can see it without checking logs
errFile := filepath.Join(beadsDir, "daemon-error")
// nolint:gosec // G306: Error file needs to be readable for debugging
if err := os.WriteFile(errFile, []byte(errMsg), 0644); err != nil {
log.log("Warning: could not write daemon-error file: %v", err)
}
os.Exit(1)
}
}
@@ -286,18 +287,18 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p
log.log("Error: failed to read database version: %v", err)
os.Exit(1)
}
if dbVersion != "" && dbVersion != Version {
log.log("Warning: Database schema version mismatch")
log.log(" Database version: %s", dbVersion)
log.log(" Daemon version: %s", Version)
log.log(" Auto-upgrading database to daemon version...")
// Auto-upgrade database to daemon version
// The daemon operates on its own database, so it should always use its own version
if err := store.SetMetadata(versionCtx, "bd_version", Version); err != nil {
log.log("Error: failed to update database version: %v", err)
// Allow override via environment variable for emergencies
if os.Getenv("BEADS_IGNORE_VERSION_MISMATCH") != "1" {
os.Exit(1)

View File

@@ -212,6 +212,7 @@ func isDaemonHealthy(socketPath string) bool {
}
func acquireStartLock(lockPath, socketPath string) bool {
// nolint:gosec // G304: lockPath is derived from secure beads directory
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")
@@ -340,6 +341,7 @@ func getPIDFileForSocket(socketPath string) string {
// readPIDFromFile reads a PID from a file
func readPIDFromFile(path string) (int, error) {
// nolint:gosec // G304: path is derived from secure beads directory
data, err := os.ReadFile(path)
if err != nil {
return 0, err

View File

@@ -242,6 +242,7 @@ func detectTestPollution(issues []*types.Issue) []pollutionResult {
func backupPollutedIssues(polluted []pollutionResult, path string) error {
// Create backup file
// nolint:gosec // G304: path is provided by user as explicit backup location
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create backup file: %w", err)

View File

@@ -106,13 +106,13 @@ Output to stdout by default, or use -o flag for file output.`,
}
store, err = sqlite.New(dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
os.Exit(1)
fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
os.Exit(1)
}
defer func() { _ = store.Close() }()
}
}
// Build filter
// Build filter
filter := types.IssueFilter{}
if statusFilter != "" {
status := types.Status(statusFilter)
@@ -215,10 +215,10 @@ Output to stdout by default, or use -o flag for file output.`,
// Ensure cleanup on failure
defer func() {
if tempFile != nil {
_ = tempFile.Close()
_ = os.Remove(tempPath) // Clean up temp file if we haven't renamed it
}
if tempFile != nil {
_ = tempFile.Close()
_ = os.Remove(tempPath) // Clean up temp file if we haven't renamed it
}
}()
out = tempFile
@@ -232,7 +232,7 @@ Output to stdout by default, or use -o flag for file output.`,
// DISABLED: timestamp-only deduplication causes data loss (bd-160)
// The export_hashes table gets out of sync with JSONL after git operations,
// causing exports to skip issues that aren't actually in the file.
//
//
// skip, err := shouldSkipExport(ctx, issue)
// if err != nil {
// fmt.Fprintf(os.Stderr, "Warning: failed to check if %s should skip: %v\n", issue.ID, err)
@@ -242,12 +242,12 @@ Output to stdout by default, or use -o flag for file output.`,
// skippedCount++
// continue
// }
if err := encoder.Encode(issue); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding issue %s: %v\n", issue.ID, err)
os.Exit(1)
}
// DISABLED: export hash tracking (bd-160)
// contentHash, err := computeIssueContentHash(issue)
// if err != nil {
@@ -255,10 +255,10 @@ Output to stdout by default, or use -o flag for file output.`,
// } else if err := store.SetExportHash(ctx, issue.ID, contentHash); err != nil {
// fmt.Fprintf(os.Stderr, "Warning: failed to save export hash for %s: %v\n", issue.ID, err)
// }
exportedIDs = append(exportedIDs, issue.ID)
}
// Report skipped issues if any (helps debugging bd-159)
if skippedCount > 0 && (output == "" || output == findJSONLPath()) {
fmt.Fprintf(os.Stderr, "Skipped %d issue(s) with timestamp-only changes\n", skippedCount)
@@ -275,8 +275,9 @@ Output to stdout by default, or use -o flag for file output.`,
// Clear auto-flush state since we just manually exported
// This cancels any pending auto-flush timer and marks DB as clean
clearAutoFlushState()
// Store JSONL file hash for integrity validation (bd-160)
// nolint:gosec // G304: finalPath is validated JSONL export path
jsonlData, err := os.ReadFile(finalPath)
if err == nil {
hasher := sha256.New()
@@ -298,9 +299,9 @@ Output to stdout by default, or use -o flag for file output.`,
// Atomically replace the target file
if err := os.Rename(tempPath, finalPath); err != nil {
_ = os.Remove(tempPath) // Clean up on failure
fmt.Fprintf(os.Stderr, "Error replacing output file: %v\n", err)
os.Exit(1)
_ = os.Remove(tempPath) // Clean up on failure
fmt.Fprintf(os.Stderr, "Error replacing output file: %v\n", err)
os.Exit(1)
}
// Set appropriate file permissions (0600: rw-------)
@@ -312,10 +313,10 @@ Output to stdout by default, or use -o flag for file output.`,
// Output statistics if JSON format requested
if jsonOutput {
stats := map[string]interface{}{
"success": true,
"exported": len(exportedIDs),
"skipped": skippedCount,
"total_issues": len(issues),
"success": true,
"exported": len(exportedIDs),
"skipped": skippedCount,
"total_issues": len(issues),
}
if output != "" {
stats["output_file"] = output

View File

@@ -108,7 +108,8 @@ With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite
// Create empty issues.jsonl file
jsonlPath := filepath.Join(localBeadsDir, "issues.jsonl")
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
// nolint:gosec // G306: JSONL file needs to be readable by other tools
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to create issues.jsonl: %v\n", err)
os.Exit(1)
}

View File

@@ -153,16 +153,16 @@ var rootCmd = &cobra.Command{
// Initialize database path
if dbPath == "" {
// Use public API to find database (same logic as extensions)
if foundDB := beads.FindDatabasePath(); foundDB != "" {
dbPath = foundDB
} else {
// No database found - error out instead of falling back to ~/.beads
fmt.Fprintf(os.Stderr, "Error: no beads database found\n")
fmt.Fprintf(os.Stderr, "Hint: run 'bd init' to create a database in the current directory\n")
fmt.Fprintf(os.Stderr, " or set BEADS_DB environment variable to specify a database\n")
os.Exit(1)
}
// Use public API to find database (same logic as extensions)
if foundDB := beads.FindDatabasePath(); foundDB != "" {
dbPath = foundDB
} else {
// No database found - error out instead of falling back to ~/.beads
fmt.Fprintf(os.Stderr, "Error: no beads database found\n")
fmt.Fprintf(os.Stderr, "Hint: run 'bd init' to create a database in the current directory\n")
fmt.Fprintf(os.Stderr, " or set BEADS_DB environment variable to specify a database\n")
os.Exit(1)
}
}
// Set actor from flag, viper (env), or default
@@ -343,6 +343,7 @@ var rootCmd = &cobra.Command{
// Check for daemon-error file to provide better error message
if beadsDir := filepath.Dir(socketPath); beadsDir != "" {
errFile := filepath.Join(beadsDir, "daemon-error")
// nolint:gosec // G304: errFile is derived from secure beads directory
if errMsg, readErr := os.ReadFile(errFile); readErr == nil && len(errMsg) > 0 {
fmt.Fprintf(os.Stderr, "\n%s\n", string(errMsg))
daemonStatus.Detail = string(errMsg)

View File

@@ -373,15 +373,18 @@ func saveMappingFile(path string, mapping map[string]string) error {
return err
}
// nolint:gosec // G306: JSONL file needs to be readable by other tools
return os.WriteFile(path, data, 0644)
}
// copyFile copies a file from src to dst
func copyFile(src, dst string) error {
// nolint:gosec // G304: src is validated migration backup path
data, err := os.ReadFile(src)
if err != nil {
return err
}
// nolint:gosec // G306: JSONL file needs to be readable by other tools
return os.WriteFile(dst, data, 0644)
}

View File

@@ -75,6 +75,7 @@ func initializeNoDbMode() error {
// loadIssuesFromJSONL reads all issues from a JSONL file
func loadIssuesFromJSONL(path string) ([]*types.Issue, error) {
// nolint:gosec // G304: path is validated JSONL file from findJSONLPath
file, err := os.Open(path)
if err != nil {
return nil, err

View File

@@ -22,7 +22,7 @@ var showCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
jsonOutput, _ := cmd.Flags().GetBool("json")
ctx := context.Background()
// Resolve partial IDs first
var resolvedIDs []string
if daemonClient != nil {
@@ -45,7 +45,7 @@ var showCmd = &cobra.Command{
os.Exit(1)
}
}
// If daemon is running, use RPC
if daemonClient != nil {
allDetails := []interface{}{}
@@ -381,7 +381,7 @@ var updateCmd = &cobra.Command{
}
ctx := context.Background()
// Resolve partial IDs first
var resolvedIDs []string
if daemonClient != nil {
@@ -402,7 +402,7 @@ var updateCmd = &cobra.Command{
os.Exit(1)
}
}
// If daemon is running, use RPC
if daemonClient != nil {
updatedIssues := []*types.Issue{}
@@ -461,12 +461,12 @@ var updateCmd = &cobra.Command{
// Direct mode
updatedIssues := []*types.Issue{}
for _, id := range resolvedIDs {
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
continue
}
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
continue
}
if jsonOutput {
if jsonOutput {
issue, _ := store.GetIssue(ctx, id)
if issue != nil {
updatedIssues = append(updatedIssues, issue)
@@ -505,7 +505,7 @@ Examples:
Run: func(cmd *cobra.Command, args []string) {
id := args[0]
ctx := context.Background()
// Resolve partial ID if in direct mode
if daemonClient == nil {
fullID, err := utils.ResolvePartialID(ctx, store, id)
@@ -604,11 +604,11 @@ Examples:
// Write current value to temp file
if _, err := tmpFile.WriteString(currentValue); err != nil {
tmpFile.Close()
_ = tmpFile.Close() // nolint:gosec // G104: Error already handled above
fmt.Fprintf(os.Stderr, "Error writing to temp file: %v\n", err)
os.Exit(1)
}
tmpFile.Close()
_ = tmpFile.Close() // nolint:gosec // G104: Defer close errors are non-critical
// Open the editor
editorCmd := exec.Command(editor, tmpPath)
@@ -622,6 +622,7 @@ Examples:
}
// Read the edited content
// nolint:gosec // G304: tmpPath is securely created temp file
editedContent, err := os.ReadFile(tmpPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err)
@@ -696,7 +697,7 @@ var closeCmd = &cobra.Command{
jsonOutput, _ := cmd.Flags().GetBool("json")
ctx := context.Background()
// Resolve partial IDs first
var resolvedIDs []string
if daemonClient != nil {

View File

@@ -374,6 +374,7 @@ func validateGitConflicts(ctx context.Context, fix bool) checkResult {
// Check JSONL file for conflict markers
jsonlPath := findJSONLPath()
// nolint:gosec // G304: jsonlPath is validated JSONL file from findJSONLPath
data, err := os.ReadFile(jsonlPath)
if err != nil {
if os.IsNotExist(err) {