Commit 07d2f70

bryfry <bryon@fryer.io>
2025-09-17 12:14:44
optional callback id task-list
1 parent e6e7ac3
Changed files (2)
cmd/task_list.go
@@ -20,11 +20,11 @@ var (
 )
 
 var taskListCmd = &cobra.Command{
-	Use:     "task-list <callback_id>",
+	Use:     "task-list [callback_id]",
 	Aliases: []string{"tl"},
-	Short:   "List tasks for a specific agent",
-	Long:    "Display task history for a specific agent callback, equivalent to viewing the tasking tab for an agent in the UI.",
-	Args:    cobra.ExactArgs(1),
+	Short:   "List tasks for a specific agent or all agents",
+	Long:    "Display task history for a specific agent callback or all callbacks. When no callback_id is provided, shows tasks from all agents with a callback column.",
+	Args:    cobra.MaximumNArgs(1),
 	RunE:    runTaskList,
 }
 
@@ -40,40 +40,76 @@ func runTaskList(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	callbackID, err := strconv.Atoi(args[0])
-	if err != nil {
-		return fmt.Errorf("invalid callback ID: %s", args[0])
-	}
-
 	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer cancel()
 
-	tasks, err := client.GetCallbackTasks(ctx, callbackID)
-	if err != nil {
-		return fmt.Errorf("failed to get tasks for callback %d: %w", callbackID, err)
+	var tasks []mythic.Task
+	var err error
+	var showingAllCallbacks bool
+
+	// Determine if we're showing tasks for a specific callback or all callbacks
+	if len(args) == 0 {
+		// No callback_id provided, show tasks from all callbacks
+		showingAllCallbacks = true
+
+		// Set limit for all tasks query
+		limit := listTaskLimit
+		if listShowAll {
+			limit = 0
+		}
+
+		tasks, err = client.GetAllTasksWithResponses(ctx, limit)
+		if err != nil {
+			return fmt.Errorf("failed to get all tasks: %w", err)
+		}
+	} else {
+		// Specific callback_id provided
+		callbackID, parseErr := strconv.Atoi(args[0])
+		if parseErr != nil {
+			return fmt.Errorf("invalid callback ID: %s", args[0])
+		}
+
+		tasks, err = client.GetCallbackTasks(ctx, callbackID)
+		if err != nil {
+			return fmt.Errorf("failed to get tasks for callback %d: %w", callbackID, err)
+		}
+
+		// Apply limit from the end (keep newest tasks if limiting)
+		if listShowAll {
+			listTaskLimit = 0
+		}
+		if listTaskLimit > 0 && len(tasks) > listTaskLimit {
+			tasks = tasks[len(tasks)-listTaskLimit:]
+		}
 	}
 
 	if len(tasks) == 0 {
-		fmt.Printf("No tasks found for callback %d\n", callbackID)
+		if showingAllCallbacks {
+			fmt.Println("No tasks found")
+		} else {
+			fmt.Printf("No tasks found for callback %s\n", args[0])
+		}
 		return nil
 	}
 
-	// Apply limit from the end (keep newest tasks if limiting)
-	// If --all flag is set, show all tasks (equivalent to --limit 0)
-	if listShowAll {
-		listTaskLimit = 0
-	}
-	if listTaskLimit > 0 && len(tasks) > listTaskLimit {
-		tasks = tasks[len(tasks)-listTaskLimit:]
+	// Display header
+	if showingAllCallbacks {
+		fmt.Printf("All tasks (showing %d, newest first):\n\n", len(tasks))
+	} else {
+		fmt.Printf("Tasks for callback %s (showing %d, newest last):\n\n", args[0], len(tasks))
 	}
 
-	fmt.Printf("Tasks for callback %d (showing %d, newest last):\n\n", callbackID, len(tasks))
-
 	// Create a tab writer for formatted output
 	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
-	fmt.Fprintln(w, "TASK ID\tCOMMAND\tPARAMS\tSTATUS\tTIMESTAMP")
-	fmt.Fprintln(w, "-------\t-------\t------\t------\t---------")
+
+	if showingAllCallbacks {
+		fmt.Fprintln(w, "TASK ID\tCALLBACK\tCOMMAND\tPARAMS\tSTATUS\tTIMESTAMP")
+		fmt.Fprintln(w, "-------\t--------\t-------\t------\t------\t---------")
+	} else {
+		fmt.Fprintln(w, "TASK ID\tCOMMAND\tPARAMS\tSTATUS\tTIMESTAMP")
+		fmt.Fprintln(w, "-------\t-------\t------\t------\t---------")
+	}
 
 	for _, task := range tasks {
 		// Truncate params if too long
@@ -90,13 +126,24 @@ func runTaskList(cmd *cobra.Command, args []string) error {
 			}
 		}
 
-		fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
-			task.DisplayID,
-			task.Command,
-			params,
-			task.Status,
-			timestamp,
-		)
+		if showingAllCallbacks {
+			fmt.Fprintf(w, "%d\t%d\t%s\t%s\t%s\t%s\n",
+				task.DisplayID,
+				task.CallbackID,
+				task.Command,
+				params,
+				task.Status,
+				timestamp,
+			)
+		} else {
+			fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
+				task.DisplayID,
+				task.Command,
+				params,
+				task.Status,
+				timestamp,
+			)
+		}
 	}
 
 	w.Flush()
@@ -111,18 +158,29 @@ func runTaskList(cmd *cobra.Command, args []string) error {
 				break
 			}
 
-			fmt.Printf("\nTask %d (%s):\n", task.DisplayID, task.Command)
-			fmt.Println(strings.Repeat("-", 30))
-
-			// Get full task with response
-			fullTask, err := client.GetTaskResponse(ctx, task.ID)
-			if err != nil {
-				fmt.Printf("Error getting response: %v\n", err)
-				continue
+			if showingAllCallbacks {
+				fmt.Printf("\nTask %d from callback %d (%s):\n", task.DisplayID, task.CallbackID, task.Command)
+			} else {
+				fmt.Printf("\nTask %d (%s):\n", task.DisplayID, task.Command)
 			}
+			fmt.Println(strings.Repeat("-", 30))
 
-			if fullTask.Response != "" {
-				fmt.Println(fullTask.Response)
+			// For all callbacks mode, task already includes response from GetAllTasksWithResponses
+			if showingAllCallbacks && task.Response != "" {
+				fmt.Println(task.Response)
+			} else if !showingAllCallbacks {
+				// For specific callback, get full task with response
+				fullTask, err := client.GetTaskResponse(ctx, task.ID)
+				if err != nil {
+					fmt.Printf("Error getting response: %v\n", err)
+					continue
+				}
+
+				if fullTask.Response != "" {
+					fmt.Println(fullTask.Response)
+				} else {
+					fmt.Println("(No response yet)")
+				}
 			} else {
 				fmt.Println("(No response yet)")
 			}
TODO.md
@@ -1,60 +1,9 @@
 - ✅ grep should also look at the command execution strings too, not just output
 - ✅ Renamed project from go-mythic to mysh (pronounced mɪʃh)
-- task-list --all (same as --limit 0)
+- ✅ task-list --all (same as --limit 0)
 - task-list without callback lists all tasks and shows callback column
 - task with --no-wait, doesn't wait for output
 - task [callback] arg make it accept lists, comma seperated, or ranges e.g. 1,2,3-5,10
 - invert cobra-cli nto functions (no global vars)
+- update output table formats to respect terminal width
 - forge payloads - equivalency with execute_assembly and inline_assembly
-
-## Code Quality Improvements
-
-### High Priority
-1. **Extract callback lookup logic to shared utility** - Remove duplication in task.go and interact.go
-2. **Standardize task polling with configurable timeouts** - Create reusable polling mechanism
-3. **Create constants file for magic numbers** - Replace hardcoded values (timeouts, status strings)
-4. **Add basic unit tests for client functions** - Test GraphQL operations and response handling
-
-### Medium Priority
-1. **Implement proper error types with context** - Create MythicError, CallbackNotFoundError, TaskTimeoutError
-2. **Add configuration management** - Centralized config with proper defaults
-3. **Create interfaces for better testability** - MythicClient interface for mocking
-4. **Improve logging and debugging** - Add structured logging with slog
-
-### Low Priority
-1. **Refactor large functions into smaller components** - Break down extractUUIDFromTaskResponse
-2. **Add comprehensive integration tests** - Test CLI commands with fixtures
-3. **Implement retry mechanisms for network operations** - Handle transient failures
-4. **Add performance monitoring/metrics** - Track operation success/failure rates
-
-## Code Duplication Issues
-- **Callback validation**: task.go:54-69 and interact.go:42-57 duplicate callback lookup
-- **Task polling**: Similar patterns in task.go:88-116 and interact.go:119-144
-- **Client creation**: Repeated client instantiation across all commands
-- **Config validation**: validateConfig() called in every command
-
-## Suggested Package Structure
-```
-pkg/
-├── mythic/          # Existing client
-├── utils/           # New shared utilities
-│   ├── callbacks.go # Callback lookup helpers
-│   ├── tasks.go     # Task polling utilities
-│   ├── constants.go # Shared constants
-│   └── validation.go # Common validation
-├── config/          # Configuration management
-│   └── config.go
-└── errors/          # Custom error types
-    └── errors.go
-```
-
-## Error Handling Improvements
-- Inconsistent timeout handling (some return nil, others error)
-- Missing error context for network/GraphQL failures
-- Raw output mode silently fails on timeout (task.go:118-124)
-
-## Testing Strategy
-- No Go tests currently exist
-- Reference Python tests in contrib/Mythic_Scripting/tests/
-- Need unit tests for client, integration tests for CLI
-- Missing coverage for base64 decoding, UUID extraction, error paths