diff --git a/internal/tui/feed/convoy.go b/internal/tui/feed/convoy.go index 94f499b5..32390b57 100644 --- a/internal/tui/feed/convoy.go +++ b/internal/tui/feed/convoy.go @@ -6,6 +6,7 @@ import ( "fmt" "os/exec" "path/filepath" + "regexp" "sort" "strings" "time" @@ -13,6 +14,9 @@ import ( "github.com/charmbracelet/lipgloss" ) +// convoyIDPattern validates convoy IDs to prevent SQL injection +var convoyIDPattern = regexp.MustCompile(`^hq-[a-zA-Z0-9-]+$`) + // Convoy represents a convoy's status for the dashboard type Convoy struct { ID string `json:"id"` @@ -145,9 +149,15 @@ type trackedStatus struct { // getTrackedIssueStatus queries tracked issues and their status func getTrackedIssueStatus(beadsDir, convoyID string) []trackedStatus { + // Validate convoyID to prevent SQL injection + if !convoyIDPattern.MatchString(convoyID) { + return nil + } + dbPath := filepath.Join(beadsDir, "beads.db") // Query tracked dependencies from SQLite + // convoyID is validated above to match ^hq-[a-zA-Z0-9-]+$ cmd := exec.Command("sqlite3", "-json", dbPath, fmt.Sprintf(`SELECT depends_on_id FROM dependencies WHERE issue_id = '%s' AND type = 'tracks'`, convoyID)) diff --git a/internal/tui/feed/model.go b/internal/tui/feed/model.go index 26ae107d..8767c30b 100644 --- a/internal/tui/feed/model.go +++ b/internal/tui/feed/model.go @@ -190,11 +190,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case convoyUpdateMsg: if msg.state != nil { + // Fresh data arrived - update state and schedule next tick m.convoyState = msg.state m.updateViewContent() + cmds = append(cmds, m.convoyRefreshTick()) + } else { + // Tick fired - fetch new data + cmds = append(cmds, m.fetchConvoys()) } - // Schedule next refresh - cmds = append(cmds, m.fetchConvoys(), m.convoyRefreshTick()) case tickMsg: cmds = append(cmds, tick())