Annotate gosec-safe file accesses

This commit is contained in:
Codex Agent
2025-11-17 10:12:46 -07:00
parent 7b63b5a30b
commit bf9b2c83fb
14 changed files with 182 additions and 158 deletions
+1
View File
@@ -762,6 +762,7 @@ func runCompactApply(ctx context.Context, store *sqlite.SQLiteStorage) {
os.Exit(1) os.Exit(1)
} }
} else { } else {
// #nosec G304 -- summary file path provided explicitly by operator
summaryBytes, err = os.ReadFile(compactSummary) summaryBytes, err = os.ReadFile(compactSummary)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to read summary file: %v\n", err) fmt.Fprintf(os.Stderr, "Error: failed to read summary file: %v\n", err)
+9 -5
View File
@@ -13,13 +13,13 @@ import (
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor" "github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/internal/beads" "github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile" "github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/daemon" "github.com/steveyegge/beads/internal/daemon"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
) )
// Status constants for doctor checks // Status constants for doctor checks
@@ -148,7 +148,7 @@ func applyFixes(result doctorResult) {
} }
} }
func runDiagnostics(path string) doctorResult{ func runDiagnostics(path string) doctorResult {
result := doctorResult{ result := doctorResult{
Path: path, Path: path,
CLIVersion: Version, CLIVersion: Version,
@@ -1286,7 +1286,11 @@ func checkSchemaCompatibility(path string) doctorCheck {
var missingElements []string var missingElements []string
for table, columns := range criticalChecks { for table, columns := range criticalChecks {
// Try to query all columns // Try to query all columns
query := fmt.Sprintf("SELECT %s FROM %s LIMIT 0", strings.Join(columns, ", "), table) query := fmt.Sprintf(
"SELECT %s FROM %s LIMIT 0",
strings.Join(columns, ", "),
table,
) // #nosec G201 -- table/column names sourced from hardcoded map
_, err := db.Exec(query) _, err := db.Exec(query)
if err != nil { if err != nil {
@@ -1296,7 +1300,7 @@ func checkSchemaCompatibility(path string) doctorCheck {
} else if strings.Contains(errMsg, "no such column") { } else if strings.Contains(errMsg, "no such column") {
// Find which columns are missing // Find which columns are missing
for _, col := range columns { for _, col := range columns {
colQuery := fmt.Sprintf("SELECT %s FROM %s LIMIT 0", col, table) colQuery := fmt.Sprintf("SELECT %s FROM %s LIMIT 0", col, table) // #nosec G201 -- names come from static schema definition
if _, colErr := db.Exec(colQuery); colErr != nil && strings.Contains(colErr.Error(), "no such column") { if _, colErr := db.Exec(colQuery); colErr != nil && strings.Contains(colErr.Error(), "no such column") {
missingElements = append(missingElements, fmt.Sprintf("%s.%s", table, col)) missingElements = append(missingElements, fmt.Sprintf("%s.%s", table, col))
} }
+1
View File
@@ -188,6 +188,7 @@ func collectDatabaseStats(dbPath string) map[string]string {
} }
func startCPUProfile(path string) error { func startCPUProfile(path string) error {
// #nosec G304 -- profile path supplied by CLI flag in trusted environment
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {
return err return err
+2
View File
@@ -74,6 +74,7 @@ func CheckGitHooks() []HookStatus {
// getHookVersion extracts the version from a hook file // getHookVersion extracts the version from a hook file
func getHookVersion(path string) (string, error) { func getHookVersion(path string) (string, error) {
// #nosec G304 -- hook path constrained to .git/hooks directory
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return "", err return "", err
@@ -293,6 +294,7 @@ func installHooks(embeddedHooks map[string]string, force bool) error {
} }
// Write hook file // Write hook file
// #nosec G306 -- git hooks must be executable for Git to run them
if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil { if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil {
return fmt.Errorf("failed to write %s: %w", hookName, err) return fmt.Errorf("failed to write %s: %w", hookName, err)
} }
+7 -6
View File
@@ -518,7 +518,7 @@ func attemptAutoMerge(conflictedPath string) error {
} }
// Get git repository root // Get git repository root
gitRootCmd := exec.Command("git", "rev-parse", "--show-toplevel") gitRootCmd := exec.Command("git", "rev-parse", "--show-toplevel") // #nosec G204 -- fixed git invocation for repo root discovery
gitRootOutput, err := gitRootCmd.Output() gitRootOutput, err := gitRootCmd.Output()
if err != nil { if err != nil {
return fmt.Errorf("not in a git repository: %w", err) return fmt.Errorf("not in a git repository: %w", err)
@@ -553,7 +553,7 @@ func attemptAutoMerge(conflictedPath string) error {
outputPath := filepath.Join(tmpDir, "merged.jsonl") outputPath := filepath.Join(tmpDir, "merged.jsonl")
// Extract base version (merge-base) // Extract base version (merge-base)
baseCmd := exec.Command("git", "show", fmt.Sprintf(":1:%s", relPath)) baseCmd := exec.Command("git", "show", fmt.Sprintf(":1:%s", relPath)) // #nosec G204 -- relPath limited to files tracked in current repo
baseCmd.Dir = gitRoot baseCmd.Dir = gitRoot
baseContent, err := baseCmd.Output() baseContent, err := baseCmd.Output()
if err != nil { if err != nil {
@@ -566,7 +566,7 @@ func attemptAutoMerge(conflictedPath string) error {
} }
// Extract left version (ours/HEAD) // Extract left version (ours/HEAD)
leftCmd := exec.Command("git", "show", fmt.Sprintf(":2:%s", relPath)) leftCmd := exec.Command("git", "show", fmt.Sprintf(":2:%s", relPath)) // #nosec G204 -- relPath limited to files tracked in current repo
leftCmd.Dir = gitRoot leftCmd.Dir = gitRoot
leftContent, err := leftCmd.Output() leftContent, err := leftCmd.Output()
if err != nil { if err != nil {
@@ -577,7 +577,7 @@ func attemptAutoMerge(conflictedPath string) error {
} }
// Extract right version (theirs/MERGE_HEAD) // Extract right version (theirs/MERGE_HEAD)
rightCmd := exec.Command("git", "show", fmt.Sprintf(":3:%s", relPath)) rightCmd := exec.Command("git", "show", fmt.Sprintf(":3:%s", relPath)) // #nosec G204 -- relPath limited to files tracked in current repo
rightCmd.Dir = gitRoot rightCmd.Dir = gitRoot
rightContent, err := rightCmd.Output() rightContent, err := rightCmd.Output()
if err != nil { if err != nil {
@@ -594,7 +594,7 @@ func attemptAutoMerge(conflictedPath string) error {
} }
// Invoke bd merge command // Invoke bd merge command
mergeCmd := exec.Command(exe, "merge", outputPath, basePath, leftPath, rightPath) mergeCmd := exec.Command(exe, "merge", outputPath, basePath, leftPath, rightPath) // #nosec G204 -- executes current bd binary for deterministic merge
mergeOutput, err := mergeCmd.CombinedOutput() mergeOutput, err := mergeCmd.CombinedOutput()
if err != nil { if err != nil {
// Check exit code - bd merge returns 1 if there are conflicts, 2 for errors // Check exit code - bd merge returns 1 if there are conflicts, 2 for errors
@@ -608,6 +608,7 @@ func attemptAutoMerge(conflictedPath string) error {
} }
// Merge succeeded - copy merged result back to original file // Merge succeeded - copy merged result back to original file
// #nosec G304 -- merged output created earlier in this function
mergedContent, err := os.ReadFile(outputPath) mergedContent, err := os.ReadFile(outputPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to read merged output: %w", err) return fmt.Errorf("failed to read merged output: %w", err)
@@ -618,7 +619,7 @@ func attemptAutoMerge(conflictedPath string) error {
} }
// Stage the resolved file // Stage the resolved file
stageCmd := exec.Command("git", "add", relPath) stageCmd := exec.Command("git", "add", relPath) // #nosec G204 -- relPath constrained to file within current repo
stageCmd.Dir = gitRoot stageCmd.Dir = gitRoot
if err := stageCmd.Run(); err != nil { if err := stageCmd.Run(); err != nil {
// Non-fatal - user can stage manually // Non-fatal - user can stage manually
+1
View File
@@ -965,6 +965,7 @@ func createConfigYaml(beadsDir string, noDbMode bool) error {
// readFirstIssueFromJSONL reads the first issue from a JSONL file // readFirstIssueFromJSONL reads the first issue from a JSONL file
func readFirstIssueFromJSONL(path string) (*types.Issue, error) { func readFirstIssueFromJSONL(path string) (*types.Issue, error) {
// #nosec G304 -- helper reads JSONL file chosen by current bd command
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open JSONL file: %w", err) return nil, fmt.Errorf("failed to open JSONL file: %w", err)
+2
View File
@@ -122,6 +122,7 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
// Create issues.jsonl // Create issues.jsonl
jsonlPath := filepath.Join(beadsDir, "beads.jsonl") jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
// #nosec G306 -- planning repo JSONL must be shareable across collaborators
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil { if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
return fmt.Errorf("failed to create issues.jsonl: %w", err) return fmt.Errorf("failed to create issues.jsonl: %w", err)
} }
@@ -144,6 +145,7 @@ Issues here are automatically created when working on forked repositories.
Created by: bd init --contributor Created by: bd init --contributor
`) `)
// #nosec G306 -- README should be world-readable
if err := os.WriteFile(readmePath, []byte(readmeContent), 0644); err != nil { if err := os.WriteFile(readmePath, []byte(readmeContent), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create README: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: failed to create README: %v\n", err)
} }
+4 -3
View File
@@ -299,7 +299,7 @@ func findCandidateIssues(ctx context.Context, db *sql.DB, p migrateIssuesParams)
} }
// Build query // Build query
query := "SELECT id FROM issues WHERE " + strings.Join(conditions, " AND ") query := "SELECT id FROM issues WHERE " + strings.Join(conditions, " AND ") // #nosec G202 -- query fragments are constant strings with parameter placeholders
rows, err := db.QueryContext(ctx, query, args...) rows, err := db.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
@@ -499,7 +499,7 @@ func countCrossRepoEdges(ctx context.Context, db *sql.DB, migrationSet []string)
incomingQuery := fmt.Sprintf(` incomingQuery := fmt.Sprintf(`
SELECT COUNT(*) FROM dependencies SELECT COUNT(*) FROM dependencies
WHERE depends_on_id IN (%s) WHERE depends_on_id IN (%s)
AND issue_id NOT IN (%s)`, inClause, inClause) AND issue_id NOT IN (%s)`, inClause, inClause) // #nosec G201 -- inClause generated from sanitized placeholders
var incoming int var incoming int
if err := db.QueryRowContext(ctx, incomingQuery, append(args, args...)...).Scan(&incoming); err != nil { if err := db.QueryRowContext(ctx, incomingQuery, append(args, args...)...).Scan(&incoming); err != nil {
@@ -510,7 +510,7 @@ func countCrossRepoEdges(ctx context.Context, db *sql.DB, migrationSet []string)
outgoingQuery := fmt.Sprintf(` outgoingQuery := fmt.Sprintf(`
SELECT COUNT(*) FROM dependencies SELECT COUNT(*) FROM dependencies
WHERE issue_id IN (%s) WHERE issue_id IN (%s)
AND depends_on_id NOT IN (%s)`, inClause, inClause) AND depends_on_id NOT IN (%s)`, inClause, inClause) // #nosec G201 -- inClause generated from sanitized placeholders
var outgoing int var outgoing int
if err := db.QueryRowContext(ctx, outgoingQuery, append(args, args...)...).Scan(&outgoing); err != nil { if err := db.QueryRowContext(ctx, outgoingQuery, append(args, args...)...).Scan(&outgoing); err != nil {
@@ -665,6 +665,7 @@ func executeMigration(ctx context.Context, db *sql.DB, migrationSet []string, to
} }
func loadIDsFromFile(path string) ([]string, error) { func loadIDsFromFile(path string) ([]string, error) {
// #nosec G304 -- file path supplied explicitly via CLI flag
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
+1
View File
@@ -75,6 +75,7 @@ func isMCPActive() bool {
} }
settingsPath := filepath.Join(home, ".claude/settings.json") settingsPath := filepath.Join(home, ".claude/settings.json")
// #nosec G304 -- settings path derived from user home directory
data, err := os.ReadFile(settingsPath) data, err := os.ReadFile(settingsPath)
if err != nil { if err != nil {
return false return false
+1
View File
@@ -625,6 +625,7 @@ Examples:
} }
// Read the edited content // Read the edited content
// #nosec G304 -- tmpPath was created earlier in this function
editedContent, err := os.ReadFile(tmpPath) editedContent, err := os.ReadFile(tmpPath)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err) fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err)
+6
View File
@@ -306,6 +306,7 @@ func (sm *SnapshotManager) writeMetadata(path string, meta snapshotMetadata) err
// Use process-specific temp file for atomic write // Use process-specific temp file for atomic write
tempPath := fmt.Sprintf("%s.%d.tmp", path, os.Getpid()) tempPath := fmt.Sprintf("%s.%d.tmp", path, os.Getpid())
// #nosec G306 -- metadata is shared across repo users and must stay readable
if err := os.WriteFile(tempPath, data, 0644); err != nil { if err := os.WriteFile(tempPath, data, 0644); err != nil {
return fmt.Errorf("failed to write metadata temp file: %w", err) return fmt.Errorf("failed to write metadata temp file: %w", err)
} }
@@ -315,6 +316,7 @@ func (sm *SnapshotManager) writeMetadata(path string, meta snapshotMetadata) err
} }
func (sm *SnapshotManager) readMetadata(path string) (*snapshotMetadata, error) { func (sm *SnapshotManager) readMetadata(path string) (*snapshotMetadata, error) {
// #nosec G304 -- metadata lives under .beads and path is derived internally
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -360,6 +362,7 @@ func (sm *SnapshotManager) validateMetadata(meta *snapshotMetadata, currentCommi
func (sm *SnapshotManager) buildIDToLineMap(path string) (map[string]string, error) { func (sm *SnapshotManager) buildIDToLineMap(path string) (map[string]string, error) {
result := make(map[string]string) result := make(map[string]string)
// #nosec G304 -- snapshot file lives in .beads/snapshots and path is derived internally
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -397,6 +400,7 @@ func (sm *SnapshotManager) buildIDToLineMap(path string) (map[string]string, err
func (sm *SnapshotManager) buildIDSet(path string) (map[string]bool, error) { func (sm *SnapshotManager) buildIDSet(path string) (map[string]bool, error) {
result := make(map[string]bool) result := make(map[string]bool)
// #nosec G304 -- snapshot file path derived from internal state
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -443,12 +447,14 @@ func (sm *SnapshotManager) jsonEquals(a, b string) bool {
} }
func (sm *SnapshotManager) copyFile(src, dst string) error { func (sm *SnapshotManager) copyFile(src, dst string) error {
// #nosec G304 -- snapshot copy only touches files inside .beads/snapshots
sourceFile, err := os.Open(src) sourceFile, err := os.Open(src)
if err != nil { if err != nil {
return err return err
} }
defer sourceFile.Close() defer sourceFile.Close()
// #nosec G304 -- snapshot copy only writes files inside .beads/snapshots
destFile, err := os.Create(dst) destFile, err := os.Create(dst)
if err != nil { if err != nil {
return err return err
+2 -2
View File
@@ -168,7 +168,7 @@ func getGitActivity(hours int) *RecentActivitySummary {
// Run git log to get patches for the last N hours // Run git log to get patches for the last N hours
since := fmt.Sprintf("%d hours ago", hours) since := fmt.Sprintf("%d hours ago", hours)
cmd := exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/beads.jsonl") cmd := exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/beads.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
@@ -204,7 +204,7 @@ func getGitActivity(hours int) *RecentActivitySummary {
} }
// Get detailed diff to analyze changes // Get detailed diff to analyze changes
cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/beads.jsonl") cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/beads.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
output, err = cmd.Output() output, err = cmd.Output()
if err != nil { if err != nil {
return nil return nil
+3 -2
View File
@@ -118,7 +118,7 @@ func Merge3Way(outputPath, basePath, leftPath, rightPath string, debug bool) err
} }
// Open output file for writing // Open output file for writing
outFile, err := os.Create(outputPath) outFile, err := os.Create(outputPath) // #nosec G304 -- outputPath provided by CLI flag but sanitized earlier
if err != nil { if err != nil {
return fmt.Errorf("error creating output file: %w", err) return fmt.Errorf("error creating output file: %w", err)
} }
@@ -150,7 +150,8 @@ func Merge3Way(outputPath, basePath, leftPath, rightPath string, debug bool) err
if err := outFile.Sync(); err != nil { if err := outFile.Sync(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to sync output file: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: failed to sync output file: %v\n", err)
} }
if content, err := os.ReadFile(outputPath); err == nil { // #nosec G304 -- debug output reads file created earlier in same function // #nosec G304 -- debug output reads file created earlier in same function
if content, err := os.ReadFile(outputPath); err == nil {
lines := 0 lines := 0
fmt.Fprintf(os.Stderr, "Output file preview (first 10 lines):\n") fmt.Fprintf(os.Stderr, "Output file preview (first 10 lines):\n")
for _, line := range splitLines(string(content)) { for _, line := range splitLines(string(content)) {
+2
View File
@@ -403,6 +403,7 @@ func exportToJSONL(ctx context.Context, store storage.Storage, path string) erro
} }
// Write to JSONL file // Write to JSONL file
// #nosec G304 -- fixture exports to deterministic file controlled by tests
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to create JSONL file: %w", err) return fmt.Errorf("failed to create JSONL file: %w", err)
@@ -422,6 +423,7 @@ func exportToJSONL(ctx context.Context, store storage.Storage, path string) erro
// importFromJSONL imports issues from a JSONL file // importFromJSONL imports issues from a JSONL file
func importFromJSONL(ctx context.Context, store storage.Storage, path string) error { func importFromJSONL(ctx context.Context, store storage.Storage, path string) error {
// Read JSONL file // Read JSONL file
// #nosec G304 -- fixture imports from deterministic file created earlier in test
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to read JSONL file: %w", err) return fmt.Errorf("failed to read JSONL file: %w", err)