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

View File

@@ -14,20 +14,20 @@ import (
)
var (
compactDryRun bool
compactTier int
compactAll bool
compactID string
compactForce bool
compactBatch int
compactWorkers int
compactStats bool
compactAnalyze bool
compactApply bool
compactAuto bool
compactSummary string
compactActor string
compactLimit int
compactDryRun bool
compactTier int
compactAll bool
compactID string
compactForce bool
compactBatch int
compactWorkers int
compactStats bool
compactAnalyze bool
compactApply bool
compactAuto bool
compactSummary string
compactActor string
compactLimit int
)
var compactCmd = &cobra.Command{
@@ -762,6 +762,7 @@ func runCompactApply(ctx context.Context, store *sqlite.SQLiteStorage) {
os.Exit(1)
}
} else {
// #nosec G304 -- summary file path provided explicitly by operator
summaryBytes, err = os.ReadFile(compactSummary)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to read summary file: %v\n", err)

View File

@@ -13,13 +13,13 @@ import (
"time"
"github.com/fatih/color"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/daemon"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
// 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{
Path: path,
CLIVersion: Version,
@@ -293,7 +293,7 @@ func checkInstallation(path string) doctorCheck {
func checkDatabaseVersion(path string) doctorCheck {
beadsDir := filepath.Join(path, ".beads")
// Check metadata.json first for custom database name
var dbPath string
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
@@ -379,7 +379,7 @@ func checkDatabaseVersion(path string) doctorCheck {
func checkIDFormat(path string) doctorCheck {
beadsDir := filepath.Join(path, ".beads")
// Check metadata.json first for custom database name
var dbPath string
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
@@ -668,7 +668,7 @@ func printDiagnostics(result doctorResult) {
func checkMultipleDatabases(path string) doctorCheck {
beadsDir := filepath.Join(path, ".beads")
// Find all .db files (excluding backups and vc.db)
files, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
if err != nil {
@@ -1032,7 +1032,7 @@ func countJSONLIssues(jsonlPath string) (int, map[string]int, error) {
func checkPermissions(path string) doctorCheck {
beadsDir := filepath.Join(path, ".beads")
// Check if .beads/ is writable
testFile := filepath.Join(beadsDir, ".doctor-test-write")
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
@@ -1190,9 +1190,9 @@ func checkGitHooks(path string) doctorCheck {
// Recommended hooks and their purposes
recommendedHooks := map[string]string{
"pre-commit": "Flushes pending bd changes to JSONL before commit",
"post-merge": "Imports updated JSONL after git pull/merge",
"pre-push": "Exports database to JSONL before push",
"pre-commit": "Flushes pending bd changes to JSONL before commit",
"post-merge": "Imports updated JSONL after git pull/merge",
"pre-push": "Exports database to JSONL before push",
}
hooksDir := filepath.Join(gitDir, "hooks")
@@ -1240,7 +1240,7 @@ func checkGitHooks(path string) doctorCheck {
func checkSchemaCompatibility(path string) doctorCheck {
beadsDir := filepath.Join(path, ".beads")
// Check metadata.json first for custom database name
var dbPath string
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
@@ -1277,18 +1277,22 @@ func checkSchemaCompatibility(path string) doctorCheck {
// This is a simplified version since we can't import the internal package directly
// Check all critical tables and columns
criticalChecks := map[string][]string{
"issues": {"id", "title", "content_hash", "external_ref", "compacted_at"},
"dependencies": {"issue_id", "depends_on_id", "type"},
"issues": {"id", "title", "content_hash", "external_ref", "compacted_at"},
"dependencies": {"issue_id", "depends_on_id", "type"},
"child_counters": {"parent_id", "last_child"},
"export_hashes": {"issue_id", "content_hash"},
"export_hashes": {"issue_id", "content_hash"},
}
var missingElements []string
for table, columns := range criticalChecks {
// 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)
if err != nil {
errMsg := err.Error()
if strings.Contains(errMsg, "no such table") {
@@ -1296,7 +1300,7 @@ func checkSchemaCompatibility(path string) doctorCheck {
} else if strings.Contains(errMsg, "no such column") {
// Find which columns are missing
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") {
missingElements = append(missingElements, fmt.Sprintf("%s.%s", table, col))
}

View File

@@ -188,6 +188,7 @@ func collectDatabaseStats(dbPath string) map[string]string {
}
func startCPUProfile(path string) error {
// #nosec G304 -- profile path supplied by CLI flag in trusted environment
f, err := os.Create(path)
if err != nil {
return err

View File

@@ -74,6 +74,7 @@ func CheckGitHooks() []HookStatus {
// getHookVersion extracts the version from a hook file
func getHookVersion(path string) (string, error) {
// #nosec G304 -- hook path constrained to .git/hooks directory
file, err := os.Open(path)
if err != nil {
return "", err
@@ -293,6 +294,7 @@ func installHooks(embeddedHooks map[string]string, force bool) error {
}
// 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 {
return fmt.Errorf("failed to write %s: %w", hookName, err)
}

View File

@@ -42,14 +42,14 @@ NOTE: Import requires direct database access and does not work with daemon mode.
fmt.Fprintf(os.Stderr, "Error: failed to create database directory: %v\n", err)
os.Exit(1)
}
// Import requires direct database access due to complex transaction handling
// and collision detection. Force direct mode regardless of daemon state.
if daemonClient != nil {
debug.Logf("Debug: import command forcing direct mode (closes daemon connection)\n")
_ = daemonClient.Close()
daemonClient = nil
var err error
store, err = sqlite.New(dbPath)
if err != nil {
@@ -58,7 +58,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
}
defer func() { _ = store.Close() }()
}
// We'll check if database needs initialization after reading the JSONL
// so we can detect the prefix from the imported issues
@@ -96,78 +96,78 @@ NOTE: Import requires direct database access and does not work with daemon mode.
lineNum := 0
for scanner.Scan() {
lineNum++
rawLine := scanner.Bytes()
line := string(rawLine)
lineNum++
rawLine := scanner.Bytes()
line := string(rawLine)
// Skip empty lines
if line == "" {
continue
}
// Detect git conflict markers in raw bytes (before JSON decoding)
// This prevents false positives when issue content contains these strings
trimmed := bytes.TrimSpace(rawLine)
if bytes.HasPrefix(trimmed, []byte("<<<<<<< ")) ||
bytes.Equal(trimmed, []byte("=======")) ||
bytes.HasPrefix(trimmed, []byte(">>>>>>> ")) {
fmt.Fprintf(os.Stderr, "Git conflict markers detected in JSONL file (line %d)\n", lineNum)
fmt.Fprintf(os.Stderr, "→ Attempting automatic 3-way merge...\n\n")
// Attempt automatic merge using bd merge command
if err := attemptAutoMerge(input); err != nil {
fmt.Fprintf(os.Stderr, "Error: Automatic merge failed: %v\n\n", err)
fmt.Fprintf(os.Stderr, "To resolve manually:\n")
fmt.Fprintf(os.Stderr, " git checkout --ours .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n")
fmt.Fprintf(os.Stderr, " git checkout --theirs .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n\n")
fmt.Fprintf(os.Stderr, "For advanced field-level merging, see: https://github.com/neongreen/mono/tree/main/beads-merge\n")
os.Exit(1)
// Skip empty lines
if line == "" {
continue
}
fmt.Fprintf(os.Stderr, "✓ Automatic merge successful\n")
fmt.Fprintf(os.Stderr, "→ Restarting import with merged JSONL...\n\n")
// Detect git conflict markers in raw bytes (before JSON decoding)
// This prevents false positives when issue content contains these strings
trimmed := bytes.TrimSpace(rawLine)
if bytes.HasPrefix(trimmed, []byte("<<<<<<< ")) ||
bytes.Equal(trimmed, []byte("=======")) ||
bytes.HasPrefix(trimmed, []byte(">>>>>>> ")) {
fmt.Fprintf(os.Stderr, "Git conflict markers detected in JSONL file (line %d)\n", lineNum)
fmt.Fprintf(os.Stderr, "→ Attempting automatic 3-way merge...\n\n")
// Re-open the input file to read the merged content
if input != "" {
// Close current file handle
if in != os.Stdin {
_ = in.Close()
}
// Re-open the merged file
// #nosec G304 - user-provided file path is intentional
f, err := os.Open(input)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reopening merged file: %v\n", err)
// Attempt automatic merge using bd merge command
if err := attemptAutoMerge(input); err != nil {
fmt.Fprintf(os.Stderr, "Error: Automatic merge failed: %v\n\n", err)
fmt.Fprintf(os.Stderr, "To resolve manually:\n")
fmt.Fprintf(os.Stderr, " git checkout --ours .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n")
fmt.Fprintf(os.Stderr, " git checkout --theirs .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n\n")
fmt.Fprintf(os.Stderr, "For advanced field-level merging, see: https://github.com/neongreen/mono/tree/main/beads-merge\n")
os.Exit(1)
}
defer func() {
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close input file: %v\n", err)
fmt.Fprintf(os.Stderr, "✓ Automatic merge successful\n")
fmt.Fprintf(os.Stderr, "→ Restarting import with merged JSONL...\n\n")
// Re-open the input file to read the merged content
if input != "" {
// Close current file handle
if in != os.Stdin {
_ = in.Close()
}
}()
in = f
scanner = bufio.NewScanner(in)
allIssues = nil // Reset issues list
lineNum = 0 // Reset line counter
continue // Restart parsing from beginning
} else {
// Can't retry stdin - should not happen since git conflicts only in files
fmt.Fprintf(os.Stderr, "Error: Cannot retry merge from stdin\n")
// Re-open the merged file
// #nosec G304 - user-provided file path is intentional
f, err := os.Open(input)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reopening merged file: %v\n", err)
os.Exit(1)
}
defer func() {
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close input file: %v\n", err)
}
}()
in = f
scanner = bufio.NewScanner(in)
allIssues = nil // Reset issues list
lineNum = 0 // Reset line counter
continue // Restart parsing from beginning
} else {
// Can't retry stdin - should not happen since git conflicts only in files
fmt.Fprintf(os.Stderr, "Error: Cannot retry merge from stdin\n")
os.Exit(1)
}
}
// Parse JSON
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing line %d: %v\n", lineNum, err)
os.Exit(1)
}
}
// Parse JSON
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing line %d: %v\n", lineNum, err)
os.Exit(1)
allIssues = append(allIssues, &issue)
}
allIssues = append(allIssues, &issue)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
os.Exit(1)
@@ -190,12 +190,12 @@ NOTE: Import requires direct database access and does not work with daemon mode.
detectedPrefix = filepath.Base(cwd)
}
detectedPrefix = strings.TrimRight(detectedPrefix, "-")
if err := store.SetConfig(initCtx, "issue_prefix", detectedPrefix); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to set issue prefix: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "✓ Initialized database with prefix '%s' (detected from issues)\n", detectedPrefix)
}
@@ -233,7 +233,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
fmt.Fprintf(os.Stderr, "\nOr use 'bd rename-prefix' after import to fix the database.\n")
os.Exit(1)
}
// Check if it's a collision error
if result != nil && len(result.CollisionIDs) > 0 {
// Print collision report before exiting
@@ -259,7 +259,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
}
fmt.Fprintf(os.Stderr, "\nUse --rename-on-import to automatically fix prefixes during import.\n")
}
if result.Collisions > 0 {
fmt.Fprintf(os.Stderr, "\n=== Collision Detection Report ===\n")
fmt.Fprintf(os.Stderr, "COLLISIONS DETECTED: %d\n", result.Collisions)
@@ -393,7 +393,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
// If jsonlPath is empty or can't be read, falls back to time.Now().
func touchDatabaseFile(dbPath, jsonlPath string) error {
targetTime := time.Now()
// If we have the JSONL path, use max(JSONL mtime, now) to handle clock skew
if jsonlPath != "" {
if info, err := os.Stat(jsonlPath); err == nil {
@@ -403,7 +403,7 @@ func touchDatabaseFile(dbPath, jsonlPath string) error {
}
}
}
// Best-effort touch - don't fail import if this doesn't work
return os.Chtimes(dbPath, targetTime, targetTime)
}
@@ -418,17 +418,17 @@ func checkUncommittedChanges(filePath string, result *ImportResult) {
// Get the directory containing the file to use as git working directory
workDir := filepath.Dir(filePath)
// Use git diff to check if working tree differs from HEAD
cmd := fmt.Sprintf("git diff --quiet HEAD %s", filePath)
exitCode, _ := runGitCommand(cmd, workDir)
// Exit code 0 = no changes, 1 = changes exist, >1 = error
if exitCode == 1 {
// Get line counts for context
workingTreeLines := countLines(filePath)
headLines := countLinesInGitHEAD(filePath, workDir)
fmt.Fprintf(os.Stderr, "\n⚠ Warning: .beads/issues.jsonl has uncommitted changes\n")
fmt.Fprintf(os.Stderr, " Working tree: %d lines\n", workingTreeLines)
if headLines > 0 {
@@ -466,7 +466,7 @@ func countLines(filePath string) int {
return 0
}
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
lines := 0
for scanner.Scan() {
@@ -484,24 +484,24 @@ func countLinesInGitHEAD(filePath string, workDir string) int {
return 0
}
gitRoot := strings.TrimSpace(gitRootOutput)
// Make filePath relative to git root
absPath, err := filepath.Abs(filePath)
if err != nil {
return 0
}
relPath, err := filepath.Rel(gitRoot, absPath)
if err != nil {
return 0
}
cmd := fmt.Sprintf("git show HEAD:%s 2>/dev/null | wc -l", relPath)
exitCode, output := runGitCommand(cmd, workDir)
if exitCode != 0 {
return 0
}
var lines int
_, err = fmt.Sscanf(strings.TrimSpace(output), "%d", &lines)
if err != nil {
@@ -518,7 +518,7 @@ func attemptAutoMerge(conflictedPath string) error {
}
// 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()
if err != nil {
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")
// 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
baseContent, err := baseCmd.Output()
if err != nil {
@@ -566,7 +566,7 @@ func attemptAutoMerge(conflictedPath string) error {
}
// 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
leftContent, err := leftCmd.Output()
if err != nil {
@@ -577,7 +577,7 @@ func attemptAutoMerge(conflictedPath string) error {
}
// 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
rightContent, err := rightCmd.Output()
if err != nil {
@@ -594,7 +594,7 @@ func attemptAutoMerge(conflictedPath string) error {
}
// 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()
if err != nil {
// 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
// #nosec G304 -- merged output created earlier in this function
mergedContent, err := os.ReadFile(outputPath)
if err != nil {
return fmt.Errorf("failed to read merged output: %w", err)
@@ -618,7 +619,7 @@ func attemptAutoMerge(conflictedPath string) error {
}
// 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
if err := stageCmd.Run(); err != nil {
// Non-fatal - user can stage manually
@@ -634,7 +635,7 @@ func detectPrefixFromIssues(issues []*types.Issue) string {
if len(issues) == 0 {
return ""
}
// Count prefix occurrences
prefixCounts := make(map[string]int)
for _, issue := range issues {
@@ -644,7 +645,7 @@ func detectPrefixFromIssues(issues []*types.Issue) string {
prefixCounts[issue.ID[:idx]]++
}
}
// Find most common prefix
maxCount := 0
commonPrefix := ""
@@ -654,7 +655,7 @@ func detectPrefixFromIssues(issues []*types.Issue) string {
commonPrefix = prefix
}
}
return commonPrefix
}

View File

@@ -965,6 +965,7 @@ func createConfigYaml(beadsDir string, noDbMode bool) error {
// readFirstIssueFromJSONL reads the first issue from a JSONL file
func readFirstIssueFromJSONL(path string) (*types.Issue, error) {
// #nosec G304 -- helper reads JSONL file chosen by current bd command
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open JSONL file: %w", err)

View File

@@ -122,6 +122,7 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
// Create issues.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 {
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
`)
// #nosec G306 -- README should be world-readable
if err := os.WriteFile(readmePath, []byte(readmeContent), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create README: %v\n", err)
}

View File

@@ -299,7 +299,7 @@ func findCandidateIssues(ctx context.Context, db *sql.DB, p migrateIssuesParams)
}
// 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...)
if err != nil {
@@ -499,7 +499,7 @@ func countCrossRepoEdges(ctx context.Context, db *sql.DB, migrationSet []string)
incomingQuery := fmt.Sprintf(`
SELECT COUNT(*) FROM dependencies
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
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(`
SELECT COUNT(*) FROM dependencies
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
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) {
// #nosec G304 -- file path supplied explicitly via CLI flag
data, err := os.ReadFile(path)
if err != nil {
return nil, err

View File

@@ -75,6 +75,7 @@ func isMCPActive() bool {
}
settingsPath := filepath.Join(home, ".claude/settings.json")
// #nosec G304 -- settings path derived from user home directory
data, err := os.ReadFile(settingsPath)
if err != nil {
return false

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{}
@@ -434,7 +434,7 @@ var updateCmd = &cobra.Command{
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
updateArgs.AcceptanceCriteria = &acceptanceCriteria
}
if externalRef, ok := updates["external_ref"].(string); ok { // NEW: Map external_ref
if externalRef, ok := updates["external_ref"].(string); ok { // NEW: Map external_ref
updateArgs.ExternalRef = &externalRef
}
@@ -464,12 +464,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)
@@ -508,7 +508,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)
@@ -625,6 +625,7 @@ Examples:
}
// Read the edited content
// #nosec G304 -- tmpPath was created earlier in this function
editedContent, err := os.ReadFile(tmpPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err)
@@ -699,7 +700,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

@@ -306,6 +306,7 @@ func (sm *SnapshotManager) writeMetadata(path string, meta snapshotMetadata) err
// Use process-specific temp file for atomic write
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 {
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) {
// #nosec G304 -- metadata lives under .beads and path is derived internally
data, err := os.ReadFile(path)
if err != nil {
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) {
result := make(map[string]string)
// #nosec G304 -- snapshot file lives in .beads/snapshots and path is derived internally
f, err := os.Open(path)
if err != nil {
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) {
result := make(map[string]bool)
// #nosec G304 -- snapshot file path derived from internal state
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
@@ -443,12 +447,14 @@ func (sm *SnapshotManager) jsonEquals(a, b string) bool {
}
func (sm *SnapshotManager) copyFile(src, dst string) error {
// #nosec G304 -- snapshot copy only touches files inside .beads/snapshots
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
// #nosec G304 -- snapshot copy only writes files inside .beads/snapshots
destFile, err := os.Create(dst)
if err != nil {
return err

View File

@@ -32,13 +32,13 @@ type StatusSummary struct {
// RecentActivitySummary represents activity from git history
type RecentActivitySummary struct {
HoursTracked int `json:"hours_tracked"`
CommitCount int `json:"commit_count"`
IssuesCreated int `json:"issues_created"`
IssuesClosed int `json:"issues_closed"`
IssuesUpdated int `json:"issues_updated"`
IssuesReopened int `json:"issues_reopened"`
TotalChanges int `json:"total_changes"`
HoursTracked int `json:"hours_tracked"`
CommitCount int `json:"commit_count"`
IssuesCreated int `json:"issues_created"`
IssuesClosed int `json:"issues_closed"`
IssuesUpdated int `json:"issues_updated"`
IssuesReopened int `json:"issues_reopened"`
TotalChanges int `json:"total_changes"`
}
var statusCmd = &cobra.Command{
@@ -168,8 +168,8 @@ func getGitActivity(hours int) *RecentActivitySummary {
// Run git log to get patches for the last N 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()
if err != nil {
// Git log failed (might not be a git repo or no commits)
@@ -178,63 +178,63 @@ func getGitActivity(hours int) *RecentActivitySummary {
scanner := bufio.NewScanner(strings.NewReader(string(output)))
commitCount := 0
for scanner.Scan() {
line := scanner.Text()
// Empty lines separate commits
if line == "" {
continue
}
// Commit hash line
if !strings.Contains(line, "\t") {
commitCount++
continue
}
// numstat line format: "additions\tdeletions\tfilename"
parts := strings.Split(line, "\t")
if len(parts) < 3 {
continue
}
// For JSONL files, each added line is a new/updated issue
// We need to analyze the actual diff to understand what changed
}
// 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()
if err != nil {
return nil
}
scanner = bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
line := scanner.Text()
// Look for added lines in diff (lines starting with +)
if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
continue
}
// Remove the + prefix
jsonLine := strings.TrimPrefix(line, "+")
// Skip empty lines
if strings.TrimSpace(jsonLine) == "" {
continue
}
// Try to parse as issue JSON
var issue types.Issue
if err := json.Unmarshal([]byte(jsonLine), &issue); err != nil {
continue
}
activity.TotalChanges++
// Analyze the change type based on timestamps and status
// Created recently if created_at is close to now
if time.Since(issue.CreatedAt) < time.Duration(hours)*time.Hour {
@@ -253,7 +253,7 @@ func getGitActivity(hours int) *RecentActivitySummary {
activity.IssuesUpdated++
}
}
activity.CommitCount = commitCount
return activity
}