Commit de61f63

bryfry <bryon@fryer.io>
2025-09-18 10:02:22
add copy task functionalitiy
1 parent f17d5bd
cmd/callbacks.go
@@ -6,6 +6,7 @@ import (
 	"mysh/pkg/mythic"
 	"os"
 	"sort"
+	"strconv"
 	"strings"
 	"time"
 
@@ -22,13 +23,15 @@ var (
 	sortByLast        bool
 	sortByDescription bool
 	sortReverse       bool
+	showCommands      bool
 )
 
 var callbacksCmd = &cobra.Command{
-	Use:     "callbacks",
+	Use:     "callbacks [callback_id]",
 	Aliases: []string{"cb", "callback"},
-	Short:   "List active callbacks",
-	Long:    "Display all active callbacks from the Mythic server, equivalent to the Active Callbacks view in the UI.",
+	Short:   "List active callbacks or show detailed callback information",
+	Long:    "Display all active callbacks from the Mythic server, or show detailed information about a specific callback including available commands.",
+	Args:    cobra.MaximumNArgs(1),
 	RunE:    runCallbacks,
 }
 
@@ -46,6 +49,9 @@ func init() {
 
 	// Reverse sort flag
 	callbacksCmd.Flags().BoolVar(&sortReverse, "rev", false, "Reverse sort order")
+
+	// Commands flag for detailed view
+	callbacksCmd.Flags().BoolVar(&showCommands, "commands", false, "Show available commands in detailed callback view")
 }
 
 // formatTimeSince formats a timestamp into a human-readable "time ago" format
@@ -182,6 +188,16 @@ func runCallbacks(cmd *cobra.Command, args []string) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer cancel()
 
+	// Check if a specific callback ID was provided
+	if len(args) == 1 {
+		callbackID, err := strconv.Atoi(args[0])
+		if err != nil {
+			return fmt.Errorf("invalid callback ID: %s", args[0])
+		}
+		return showDetailedCallback(ctx, client, callbackID)
+	}
+
+	// Show table of all active callbacks
 	callbacks, err := client.GetActiveCallbacks(ctx)
 	if err != nil {
 		return fmt.Errorf("failed to get active callbacks: %w", err)
@@ -194,12 +210,10 @@ func runCallbacks(cmd *cobra.Command, args []string) error {
 
 	// Sort callbacks based on flags
 	sortField := getSortField()
-	// For ID sort, default to newest (highest) last, so reverse=true by default
+	// For ID sort, default to ascending order with highest ID last (reverse=false)
 	// For other sorts, use the --rev flag
 	shouldReverse := sortReverse
-	if sortField == "id" && !sortReverse {
-		shouldReverse = true // Default for ID is newest last (reverse order)
-	}
+	// Note: No special logic needed for ID sort - ascending is the natural order
 
 	sortCallbacks(callbacks, sortField, shouldReverse)
 
@@ -262,3 +276,219 @@ func runCallbacks(cmd *cobra.Command, args []string) error {
 	t.Render()
 	return nil
 }
+
+// wrapDescription wraps text to fit within the specified width, adding proper indentation for continuation lines
+func wrapDescription(text string, maxWidth int, indent string) string {
+	if text == "" {
+		return ""
+	}
+
+	// If the text fits on one line, return as-is
+	if len(text) <= maxWidth {
+		return text
+	}
+
+	words := strings.Fields(text)
+	if len(words) == 0 {
+		return text
+	}
+
+	var lines []string
+	var currentLine []string
+	currentLength := 0
+
+	for _, word := range words {
+		// Check if adding this word would exceed the line width
+		wordLen := len(word)
+		if currentLength > 0 {
+			wordLen += 1 // space before word
+		}
+
+		if currentLength == 0 || currentLength + wordLen <= maxWidth {
+			// Word fits on current line
+			currentLine = append(currentLine, word)
+			if currentLength == 0 {
+				currentLength = len(word)
+			} else {
+				currentLength += 1 + len(word) // space + word
+			}
+		} else {
+			// Word doesn't fit, finish current line and start new one
+			if len(currentLine) > 0 {
+				lines = append(lines, strings.Join(currentLine, " "))
+			}
+
+			// Handle very long single words that exceed maxWidth
+			if len(word) > maxWidth {
+				// Split the word if it's longer than maxWidth
+				for len(word) > maxWidth {
+					lines = append(lines, word[:maxWidth])
+					word = word[maxWidth:]
+				}
+				if len(word) > 0 {
+					currentLine = []string{word}
+					currentLength = len(word)
+				} else {
+					currentLine = []string{}
+					currentLength = 0
+				}
+			} else {
+				// Start new line with this word
+				currentLine = []string{word}
+				currentLength = len(word)
+			}
+		}
+	}
+
+	// Add the final line
+	if len(currentLine) > 0 {
+		lines = append(lines, strings.Join(currentLine, " "))
+	}
+
+	// Join lines with appropriate indentation
+	result := lines[0]
+	for i := 1; i < len(lines); i++ {
+		result += "\n" + indent + lines[i]
+	}
+
+	return result
+}
+
+func showDetailedCallback(ctx context.Context, client *mythic.Client, callbackID int) error {
+	callback, err := client.GetDetailedCallback(ctx, callbackID)
+	if err != nil {
+		return err
+	}
+
+	// Display detailed callback information
+	fmt.Printf("Callback %d\n", callback.DisplayID)
+	fmt.Printf("\n")
+
+	// Agent Overview Section
+	fmt.Printf("Agent Overview\n")
+	fmt.Printf("  ID:              %d\n", callback.DisplayID)
+	fmt.Printf("  Status:          %s\n", getStatusString(callback.Active))
+	fmt.Printf("  Agent Type:      %s\n", callback.Payload.PayloadType.Name)
+	fmt.Printf("  Operator:        %s\n", callback.Operator.Username)
+	fmt.Printf("  Last Checkin:    %s\n", formatTimeSince(callback.LastCheckin))
+	if callback.Description != "" {
+		fmt.Printf("  Description:     %s\n", callback.Description)
+	}
+	fmt.Printf("\n")
+
+	// System Information Section
+	fmt.Printf("System Information\n")
+	fmt.Printf("  Hostname:        %s\n", callback.Host)
+	fmt.Printf("  Username:        %s\n", callback.User)
+	fmt.Printf("  Process:         %s (PID: %d)\n", callback.ProcessName, callback.PID)
+	if callback.OS != "" {
+		fmt.Printf("  Operating System: %s\n", callback.OS)
+	}
+	if callback.Architecture != "" {
+		fmt.Printf("  Architecture:    %s\n", callback.Architecture)
+	}
+	if callback.Domain != "" {
+		fmt.Printf("  Domain:          %s\n", callback.Domain)
+	}
+	if callback.IntegrityLevel > 0 {
+		fmt.Printf("  Integrity Level: %d\n", callback.IntegrityLevel)
+	}
+	fmt.Printf("\n")
+
+	// Network Context Section
+	fmt.Printf("Network Context\n")
+	if callback.IP != "" {
+		fmt.Printf("  Internal IP:     %s\n", callback.IP)
+	}
+	if callback.ExternalIP != "" {
+		fmt.Printf("  External IP:     %s\n", callback.ExternalIP)
+	}
+	if callback.SleepInfo != "" {
+		fmt.Printf("  Sleep Config:    %s\n", callback.SleepInfo)
+	}
+	if callback.CryptoType != "" {
+		fmt.Printf("  Crypto Type:     %s\n", callback.CryptoType)
+	}
+	fmt.Printf("\n")
+
+	// Payload Information Section
+	fmt.Printf("Payload Information\n")
+	fmt.Printf("  UUID:            %s\n", callback.Payload.UUID)
+	fmt.Printf("  Type:            %s\n", callback.Payload.PayloadType.Name)
+	if callback.Payload.PayloadType.Author != "" {
+		fmt.Printf("  Author:          %s\n", callback.Payload.PayloadType.Author)
+	}
+	if callback.Payload.PayloadType.Note != "" {
+		fmt.Printf("  Type Note:       %s\n", callback.Payload.PayloadType.Note)
+	}
+	if callback.Payload.Description != "" {
+		fmt.Printf("  Description:     %s\n", callback.Payload.Description)
+	}
+	if callback.AgentCallbackID != "" {
+		fmt.Printf("  Agent ID:        %s\n", callback.AgentCallbackID)
+	}
+	// Available Commands Section - only show if --commands flag is used
+	if showCommands {
+		fmt.Printf("\n")
+		fmt.Printf("Available Commands (%d loaded)\n", len(callback.LoadedCommands))
+		if len(callback.LoadedCommands) > 0 {
+			// Sort commands alphabetically
+			commands := make([]mythic.LoadedCommand, len(callback.LoadedCommands))
+			copy(commands, callback.LoadedCommands)
+			sort.Slice(commands, func(i, j int) bool {
+				return commands[i].Command.Cmd < commands[j].Command.Cmd
+			})
+
+			// Find the longest command name for consistent alignment
+			maxCmdLen := 0
+			for _, cmd := range commands {
+				if len(cmd.Command.Cmd) > maxCmdLen {
+					maxCmdLen = len(cmd.Command.Cmd)
+				}
+			}
+
+			// Calculate available width for descriptions
+			termWidth := getTerminalWidth()
+			prefixWidth := 2 + maxCmdLen + 1 // "  longest_command_name "
+			adminFlagMaxWidth := 10 // " [ADMIN]" plus some buffer
+			availableWidth := termWidth - prefixWidth - adminFlagMaxWidth
+
+			// Ensure minimum width for readability
+			if availableWidth < 30 {
+				availableWidth = 30
+			}
+
+			// Create indent string to match the command name alignment
+			indent := "  " + strings.Repeat(" ", maxCmdLen) + " "
+
+			for _, cmd := range commands {
+				adminFlag := ""
+				if cmd.Command.NeedsAdmin {
+					adminFlag = " [ADMIN]"
+				}
+
+				// Wrap the description
+				wrappedDesc := wrapDescription(cmd.Command.Description, availableWidth, indent)
+
+				// Print the command with wrapped description using dynamic width
+				fmt.Printf("  %-*s %s%s\n", maxCmdLen, cmd.Command.Cmd, wrappedDesc, adminFlag)
+			}
+		} else {
+			fmt.Printf("  No commands loaded\n")
+		}
+	}
+
+	// Show extra info if available
+	if callback.ExtraInfo != "" {
+		fmt.Printf("\nExtra Information:\n%s\n", callback.ExtraInfo)
+	}
+
+	return nil
+}
+
+func getStatusString(active bool) string {
+	if active {
+		return "Active"
+	}
+	return "Inactive"
+}
cmd/task.go
@@ -3,11 +3,13 @@ package cmd
 import (
 	"context"
 	"fmt"
+	"mysh/pkg/cache"
 	"mysh/pkg/mythic"
 	"mysh/pkg/mythic/api"
 	"sort"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/spf13/cobra"
 )
@@ -16,14 +18,15 @@ var (
 	taskWaitTime  int
 	taskRawOutput bool
 	taskNoWait    bool
+	taskCopyID    int
 )
 
 var taskCmd = &cobra.Command{
-	Use:     "task <callback_id(s)> <command> [args...]",
+	Use:     "task <callback_id(s)> [command] [args...]",
 	Aliases: []string{"t", "exec"},
 	Short:   "Send a command to one or more agents",
-	Long:    "Send a command to specified agent callback(s) and wait for response. Supports single IDs, comma-separated lists, and ranges (e.g., '1,3-5,10').",
-	Args:    cobra.MinimumNArgs(2),
+	Long:    "Send a command to specified agent callback(s) and wait for response. Supports single IDs, comma-separated lists, and ranges (e.g., '1,3-5,10'). Use --copy-task to copy parameters from an existing task.",
+	Args:    cobra.MinimumNArgs(1),
 	RunE:    runTask,
 }
 
@@ -32,6 +35,7 @@ func init() {
 	taskCmd.Flags().IntVarP(&taskWaitTime, "wait", "w", 30, "Maximum time to wait for response (seconds)")
 	taskCmd.Flags().BoolVar(&taskRawOutput, "raw", false, "Output only raw response bytes")
 	taskCmd.Flags().BoolVar(&taskNoWait, "no-wait", false, "Create task but don't wait for response")
+	taskCmd.Flags().IntVar(&taskCopyID, "copy-task", 0, "Copy command and parameters from existing task ID")
 }
 
 // parseCallbackIDs parses a callback ID string that can contain:
@@ -112,15 +116,68 @@ func runTask(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("invalid callback ID(s): %w", err)
 	}
 
-	command := args[1]
-	params := ""
-	if len(args) > 2 {
-		params = strings.Join(args[2:], " ")
-	}
+	var command, params string
 
 	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
 	ctx := context.Background()
 
+	// Handle task copying
+	if taskCopyID > 0 {
+		// Copy task parameters from existing task
+		copyCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+		defer cancel()
+
+		// Initialize cache for task lookup
+		taskCache, err := cache.New(mythicURL)
+		if err != nil {
+			taskCache = nil
+		}
+
+		// Try to get task from cache first
+		var sourceTask *mythic.Task
+		if taskCache != nil {
+			if cachedTask, found := taskCache.GetCachedTask(taskCopyID, mythicURL); found {
+				sourceTask = cachedTask
+			}
+		}
+
+		// If not in cache, fetch from server
+		if sourceTask == nil {
+			sourceTask, err = client.GetTaskResponse(copyCtx, taskCopyID)
+			if err != nil {
+				return fmt.Errorf("failed to get source task %d: %w", taskCopyID, err)
+			}
+
+			// Cache the result if task is completed and cache is available
+			if taskCache != nil {
+				taskCache.CacheTask(sourceTask, mythicURL)
+			}
+		}
+
+		// Use the source task's command and original parameters
+		command = sourceTask.Command
+		if sourceTask.OriginalParams != "" {
+			params = sourceTask.OriginalParams
+		} else if sourceTask.Params != "" {
+			params = sourceTask.Params
+		}
+
+		fmt.Printf("Copying task %d: %s\n", taskCopyID, command)
+		if params != "" {
+			fmt.Printf("Parameters: %s\n", params)
+		}
+		fmt.Println()
+	} else {
+		// Normal mode: use command line arguments
+		if len(args) < 2 {
+			return fmt.Errorf("command is required when not using --copy-task")
+		}
+		command = args[1]
+		if len(args) > 2 {
+			params = strings.Join(args[2:], " ")
+		}
+	}
+
 	// Track results for multiple callbacks
 	type TaskResult struct {
 		CallbackID int
cmd/task_view.go
@@ -2,11 +2,11 @@ package cmd
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"mysh/pkg/cache"
 	"mysh/pkg/mythic"
 	"mysh/pkg/mythic/api"
-	"os"
 	"strconv"
 	"strings"
 	"time"
@@ -34,6 +34,29 @@ func init() {
 	taskViewCmd.Flags().BoolVarP(&taskViewDetails, "details", "d", false, "Show only task details (no response)")
 }
 
+// formatJSONForDisplay formats JSON with proper indentation for display
+func formatJSONForDisplay(jsonStr, indent string) string {
+	if jsonStr == "" {
+		return ""
+	}
+
+	// Try to parse and format as JSON
+	var parsed interface{}
+	if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
+		// If it's not valid JSON, return as-is with indent
+		return indent + jsonStr
+	}
+
+	// Format with indentation
+	formatted, err := json.MarshalIndent(parsed, indent, "  ")
+	if err != nil {
+		// If formatting fails, return original with indent
+		return indent + jsonStr
+	}
+
+	return string(formatted)
+}
+
 func runTaskView(cmd *cobra.Command, args []string) error {
 	if err := validateConfig(); err != nil {
 		return err
@@ -57,7 +80,7 @@ func runTaskView(cmd *cobra.Command, args []string) error {
 
 	var task *mythic.Task
 
-	// Try to get from cache first for completed tasks
+	// Try to get from cache first if available
 	if taskCache != nil {
 		if cachedTask, found := taskCache.GetCachedTask(taskID, mythicURL); found {
 			task = cachedTask
@@ -71,12 +94,9 @@ func runTaskView(cmd *cobra.Command, args []string) error {
 			return fmt.Errorf("failed to get task %d: %w", taskID, err)
 		}
 
-		// Cache the result if it's completed and cache is available
+		// Cache the result if task is completed and cache is available
 		if taskCache != nil {
-			if err := taskCache.CacheTask(task, mythicURL); err != nil {
-				// Log error but don't fail the command
-				fmt.Fprintf(os.Stderr, "Warning: failed to cache task result: %v\n", err)
-			}
+			taskCache.CacheTask(task, mythicURL)
 		}
 	}
 
@@ -103,37 +123,101 @@ func runTaskView(cmd *cobra.Command, args []string) error {
 	fmt.Println(strings.Repeat("=", 60))
 
 	// Basic task info
-	fmt.Printf("Command:     %s\n", task.Command)
-	fmt.Printf("Parameters:  %s\n", task.Params)
+	if task.Command != "" {
+		fmt.Printf("Command:     %s\n", task.Command)
+	} else {
+		fmt.Printf("Command:     (not available)\n")
+	}
 	fmt.Printf("Status:      %s\n", task.Status)
 	fmt.Printf("Completed:   %t\n", task.Completed)
 
+	// Operator and agent info
+	if task.Operator.Username != "" {
+		fmt.Printf("Operator:    %s\n", task.Operator.Username)
+	}
+	if task.AgentTaskID != "" {
+		fmt.Printf("Agent Task ID: %s\n", task.AgentTaskID)
+	}
+
+	// Parameters section with proper JSON formatting
+	hasParams := task.OriginalParams != "" || task.DisplayParams != "" || task.Params != ""
+	if hasParams {
+		fmt.Printf("\nAgent Parameters:\n")
+
+		// Show original parameters (JSON) if available
+		if task.OriginalParams != "" {
+			fmt.Printf("  Original (JSON):\n    %s\n", task.OriginalParams)
+		}
+
+		// Show display parameters if different from original
+		if task.DisplayParams != "" && task.DisplayParams != task.OriginalParams {
+			fmt.Printf("  Display Format:\n    %s\n", task.DisplayParams)
+		}
+
+		// Show processed parameters if different from both above
+		if task.Params != "" && task.Params != task.DisplayParams && task.Params != task.OriginalParams {
+			fmt.Printf("  Processed:\n    %s\n", task.Params)
+		}
+
+		// If only basic params available, show those
+		if task.OriginalParams == "" && task.DisplayParams == "" && task.Params != "" {
+			fmt.Printf("  Parameters:\n    %s\n", task.Params)
+		}
+
+		// If no parameters at all
+		if !hasParams {
+			fmt.Printf("  (No parameters)\n")
+		}
+	}
+
+	// Task metadata
+	fmt.Printf("\nTask Metadata:\n")
+	if task.ParameterGroupName != "" {
+		fmt.Printf("  Parameter Group:  %s\n", task.ParameterGroupName)
+	}
+	if task.TaskingLocation != "" {
+		fmt.Printf("  Tasking Location: %s\n", task.TaskingLocation)
+	}
+	if task.IsInteractiveTask {
+		fmt.Printf("  Interactive Task: Yes (Type: %d)\n", task.InteractiveTaskType)
+	}
+	if task.ParentTaskID > 0 {
+		fmt.Printf("  Parent Task:     %d\n", task.ParentTaskID)
+	}
+	if task.Token.TokenID > 0 {
+		fmt.Printf("  Token ID:        %d\n", task.Token.TokenID)
+	}
+
 	// Callback information
+	fmt.Printf("\nCallback Information:\n")
 	if callback != nil {
-		fmt.Printf("Callback:    %d (%s@%s)\n", task.CallbackID, callback.User, callback.Host)
-		fmt.Printf("Process:     %s (PID: %d)\n", callback.ProcessName, callback.PID)
+		fmt.Printf("  Callback ID:     %d\n", task.CallbackID)
+		fmt.Printf("  Host/User:       %s@%s\n", callback.User, callback.Host)
+		fmt.Printf("  Process:         %s (PID: %d)\n", callback.ProcessName, callback.PID)
 		if callback.Description != "" {
-			fmt.Printf("Description: %s\n", callback.Description)
+			fmt.Printf("  Description:     %s\n", callback.Description)
 		}
 		agentType := callback.Payload.PayloadType.Name
 		if agentType != "" {
-			fmt.Printf("Agent Type:  %s\n", agentType)
+			fmt.Printf("  Agent Type:      %s\n", agentType)
 		}
 	} else {
-		fmt.Printf("Callback:    %d\n", task.CallbackID)
+		fmt.Printf("  Callback ID:     %d\n", task.CallbackID)
 	}
 
 	// Timing information
+	fmt.Printf("\nTiming:\n")
 	if task.Timestamp != "" {
 		if t, err := time.Parse(time.RFC3339, task.Timestamp); err == nil {
-			fmt.Printf("Created:     %s\n", t.Format("2006-01-02 15:04:05 MST"))
+			fmt.Printf("  Created:         %s\n", t.Format("2006-01-02 15:04:05 MST"))
 		} else {
-			fmt.Printf("Created:     %s\n", task.Timestamp)
+			fmt.Printf("  Created:         %s\n", task.Timestamp)
 		}
 	}
 
-	// Additional technical details
-	fmt.Printf("Internal ID: %d\n", task.ID)
+	// Technical details
+	fmt.Printf("\nTechnical Details:\n")
+	fmt.Printf("  Internal ID:     %d\n", task.ID)
 
 	// If --details flag is set, only show the header
 	if taskViewDetails {
pkg/mythic/client.go
@@ -42,17 +42,96 @@ type Callback struct {
 	} `json:"payload"`
 }
 
+type DetailedCallback struct {
+	// Basic info
+	ID          int    `json:"id"`
+	DisplayID   int    `json:"display_id"`
+	User        string `json:"user"`
+	Host        string `json:"host"`
+	ProcessName string `json:"process_name"`
+	PID         int    `json:"pid"`
+	Description string `json:"description"`
+	LastCheckin string `json:"last_checkin"`
+	Active      bool   `json:"active"`
+
+	// Extended system info
+	Architecture    string `json:"architecture"`
+	OS             string `json:"os"`
+	Domain         string `json:"domain"`
+	IntegrityLevel int    `json:"integrity_level"`
+	IP             string `json:"ip"`
+	ExternalIP     string `json:"external_ip"`
+	SleepInfo      string `json:"sleep_info"`
+	ExtraInfo      string `json:"extra_info"`
+
+	// Payload info
+	RegisteredPayloadID int    `json:"registered_payload_id"`
+	AgentCallbackID     string `json:"agent_callback_id"`
+	CryptoType          string `json:"crypto_type"`
+
+	// Timestamps
+	InitCallback string `json:"init_callback"`
+
+	// Relationships
+	Payload struct {
+		ID          int    `json:"id"`
+		UUID        string `json:"uuid"`
+		Description string `json:"description"`
+		PayloadType struct {
+			Name   string `json:"name"`
+			Author string `json:"author"`
+			Note   string `json:"note"`
+		} `json:"payloadtype"`
+	} `json:"payload"`
+
+	// Operator info
+	Operator struct {
+		Username string `json:"username"`
+	} `json:"operator"`
+
+	// Commands
+	LoadedCommands []LoadedCommand `json:"loadedcommands"`
+}
+
+type LoadedCommand struct {
+	ID        int    `json:"id"`
+	Version   int    `json:"version"`
+	Timestamp string `json:"timestamp"`
+	Command   struct {
+		Cmd         string      `json:"cmd"`
+		Description string      `json:"description"`
+		HelpCmd     string      `json:"help_cmd"`
+		Author      string      `json:"author"`
+		NeedsAdmin  bool        `json:"needs_admin"`
+		Attributes  interface{} `json:"attributes"`
+	} `json:"command"`
+}
+
 type Task struct {
-	ID         int    `json:"id"`
-	DisplayID  int    `json:"display_id"`
-	Command    string `json:"command"`
-	Params     string `json:"params"`
-	Status     string `json:"status"`
-	Response   string `json:"response,omitempty"`
-	CallbackID int    `json:"callback_id"`
-	OperatorID int    `json:"operator_id"`
-	Timestamp  string `json:"timestamp,omitempty"`
-	Completed  bool   `json:"completed"`
+	ID                  int    `json:"id"`
+	DisplayID           int    `json:"display_id"`
+	Command             string `json:"command_name"`
+	Params              string `json:"params"`
+	OriginalParams      string `json:"original_params"`
+	DisplayParams       string `json:"display_params"`
+	Status              string `json:"status"`
+	Response            string `json:"response,omitempty"`
+	CallbackID          int    `json:"callback_id"`
+	OperatorID          int    `json:"operator_id"`
+	Timestamp           string `json:"timestamp,omitempty"`
+	Completed           bool   `json:"completed"`
+	AgentTaskID         string `json:"agent_task_id"`
+	ParameterGroupName  string `json:"parameter_group_name"`
+	TaskingLocation     string `json:"tasking_location"`
+	IsInteractiveTask   bool   `json:"is_interactive_task"`
+	InteractiveTaskType int    `json:"interactive_task_type"`
+	ParentTaskID        int    `json:"parent_task_id"`
+	Operator            struct {
+		Username string `json:"username"`
+	} `json:"operator"`
+	Token struct {
+		TokenID int `json:"token_id"`
+	} `json:"token"`
 }
 
 type File struct {
@@ -219,6 +298,27 @@ func (c *Client) GetActiveCallbacks(ctx context.Context) ([]Callback, error) {
 	return active, nil
 }
 
+func (c *Client) GetDetailedCallback(ctx context.Context, callbackID int) (*DetailedCallback, error) {
+	req := graphql.NewRequest(GetDetailedCallback)
+
+	req.Var("callback_id", callbackID)
+	req.Header.Set("apitoken", c.token)
+
+	var resp struct {
+		Callback *DetailedCallback `json:"callback_by_pk"`
+	}
+
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get detailed callback: %w", err)
+	}
+
+	if resp.Callback == nil {
+		return nil, fmt.Errorf("callback %d not found", callbackID)
+	}
+
+	return resp.Callback, nil
+}
+
 func (c *Client) CreateTask(ctx context.Context, callbackID int, command, params string) (*Task, error) {
 	req := graphql.NewRequest(CreateTask)
 
@@ -263,14 +363,28 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
 
 	var resp struct {
 		Task struct {
-			ID             int    `json:"id"`
-			DisplayID      int    `json:"display_id"`
-			CommandName    string `json:"command_name"`
-			OriginalParams string `json:"original_params"`
-			DisplayParams  string `json:"display_params"`
-			Status         string `json:"status"`
-			Completed      bool   `json:"completed"`
-			Callback       struct {
+			ID                  int    `json:"id"`
+			DisplayID           int    `json:"display_id"`
+			CommandName         string `json:"command_name"`
+			Params              string `json:"params"`
+			OriginalParams      string `json:"original_params"`
+			DisplayParams       string `json:"display_params"`
+			Status              string `json:"status"`
+			Completed           bool   `json:"completed"`
+			Timestamp           string `json:"timestamp"`
+			AgentTaskID         string `json:"agent_task_id"`
+			ParameterGroupName  string `json:"parameter_group_name"`
+			TaskingLocation     string `json:"tasking_location"`
+			IsInteractiveTask   bool   `json:"is_interactive_task"`
+			InteractiveTaskType int    `json:"interactive_task_type"`
+			ParentTaskID        int    `json:"parent_task_id"`
+			Operator            struct {
+				Username string `json:"username"`
+			} `json:"operator"`
+			Token struct {
+				TokenID int `json:"token_id"`
+			} `json:"token"`
+			Callback struct {
 				ID        int `json:"id"`
 				DisplayID int `json:"display_id"`
 			} `json:"callback"`
@@ -281,6 +395,8 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
 		return nil, fmt.Errorf("failed to get task: %w", err)
 	}
 
+
+
 	// Get task responses
 	responseReq := graphql.NewRequest(GetTaskResponses)
 
@@ -312,14 +428,33 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
 	}
 
 	return &Task{
-		ID:         resp.Task.ID,
-		DisplayID:  resp.Task.DisplayID,
-		Command:    resp.Task.CommandName,
-		Params:     resp.Task.DisplayParams,
-		Status:     status,
-		Response:   response,
-		CallbackID: resp.Task.Callback.ID,
-		Completed:  resp.Task.Completed,
+		ID:                  resp.Task.ID,
+		DisplayID:           resp.Task.DisplayID,
+		Command:             resp.Task.CommandName,
+		Params:              resp.Task.Params,
+		OriginalParams:      resp.Task.OriginalParams,
+		DisplayParams:       resp.Task.DisplayParams,
+		Status:              status,
+		Response:            response,
+		CallbackID:          resp.Task.Callback.ID,
+		Completed:           resp.Task.Completed,
+		Timestamp:           resp.Task.Timestamp,
+		AgentTaskID:         resp.Task.AgentTaskID,
+		ParameterGroupName:  resp.Task.ParameterGroupName,
+		TaskingLocation:     resp.Task.TaskingLocation,
+		IsInteractiveTask:   resp.Task.IsInteractiveTask,
+		InteractiveTaskType: resp.Task.InteractiveTaskType,
+		ParentTaskID:        resp.Task.ParentTaskID,
+		Operator: struct {
+			Username string `json:"username"`
+		}{
+			Username: resp.Task.Operator.Username,
+		},
+		Token: struct {
+			TokenID int `json:"token_id"`
+		}{
+			TokenID: resp.Task.Token.TokenID,
+		},
 	}, nil
 }
 
pkg/mythic/queries.go
@@ -363,16 +363,83 @@ query GetCallbacks {
     }
 }`
 
+const GetDetailedCallback = `
+query GetDetailedCallback($callback_id: Int!) {
+    callback_by_pk(id: $callback_id) {
+        id
+        display_id
+        user
+        host
+        process_name
+        pid
+        description
+        last_checkin
+        active
+        architecture
+        os
+        domain
+        integrity_level
+        ip
+        external_ip
+        sleep_info
+        extra_info
+        registered_payload_id
+        agent_callback_id
+        crypto_type
+        init_callback
+        payload {
+            id
+            uuid
+            description
+            payloadtype {
+                name
+                author
+                note
+            }
+        }
+        operator {
+            username
+        }
+        loadedcommands {
+            id
+            version
+            timestamp
+            command {
+                cmd
+                description
+                help_cmd
+                author
+                needs_admin
+                attributes
+            }
+        }
+    }
+}`
+
 const GetTask = `
 query GetTask($id: Int!) {
     task_by_pk(id: $id) {
         id
         display_id
         command_name
+        params
         original_params
         display_params
         status
         completed
+        timestamp
+        agent_task_id
+        parameter_group_name
+        tasking_location
+        is_interactive_task
+        interactive_task_type
+        parent_task_id
+        operator {
+            username
+        }
+        token {
+            token_id
+        }
         callback {
             id
             display_id
TODO.md
@@ -1,25 +1,37 @@
 
-- grep --delete - flag that deletes the tasking from the server that match the grep - require confirmation
-- forge payloads - equivalency with execute_assembly and inline_assembly
+make a cli file browser that tasks apollo ls commands to cd and ls into remote directories
+`get` should download the file
+make it tasks the next ls 1 depth lower than what the user sees to make it seem quick
+search cached tasks with path ls results before retasking (uses the pre-fetch data)
+keep polling the tasks for that cb to make sure results are cached 
+show the current callback number and current working directory in the prompt
+stretch goal: tab complete with the current directories contents 
 
-- MYTHIC_API_INSECURE= boolean --insecure flag - and invert default = false
-- update output table formats to respect terminal width
-- invert cobra-cli nto functions (no global vars)
+- [ ] copy an existing task verbatium into a new task `mysh task [cb-list] --copy-task [task-id]
 
-- ✅ grep --raw - only diplay of all matching items raw output follow grep -R style single-line file dilimeters in the output
-- ✅ use per-server cache dirs
-- ✅ make sure raw output has a newline at the end
-- ✅ make mysh cache default to info output
-- ✅ create a .cache/mysh/<server> to store task results
-- ✅ make callbacks sortable by columns
-- ✅ make tv have a -d --details to only show the header
-- ✅ make tv have more task details in the header
-- ✅ make tv --raw have short -r
-- ✅ update callbacks to show last-checkin as 0s or 0m00s ago
-- ✅ make cb and callback aliases for callbacks
-- ✅ task [callback] arg make it accept lists, comma seperated, or ranges e.g. 1,2,3-5,10
-- ✅ task with --no-wait, doesn't wait for output
-- ✅ task-list without callback lists all tasks and shows callback column
-- ✅ task-list --all (same as --limit 0), and -a
-- ✅ Renamed project from go-mythic to mysh (pronounced mɪʃh)
-- ✅ grep should also look at the command execution strings too, not just output
+- [ ] forge payloads - equivalency with execute_assembly and inline_assembly
+
+- [ ] MYTHIC_API_INSECURE= boolean --insecure flag - and invert default = false
+- [ ] update output table formats to respect terminal width
+- [ ] invert cobra-cli nto functions (no global vars)
+
+- [ ] credentials listing
+
+- [x] task view needs to show Agent Parameters:
+- [x] grep --raw - only diplay of all matching items raw output follow grep -R style single-line file dilimeters in the output
+- [x] use per-server cache dirs
+- [x] make sure raw output has a newline at the end
+- [x] make mysh cache default to info output
+- [x] create a .cache/mysh/<server> to store task results
+- [x] make callbacks sortable by columns
+- [x] make tv have a -d --details to only show the header
+- [x] make tv have more task details in the header
+- [x] make tv --raw have short -r
+- [x] update callbacks to show last-checkin as 0s or 0m00s ago
+- [x] make cb and callback aliases for callbacks
+- [x] task [callback] arg make it accept lists, comma seperated, or ranges e.g. 1,2,3-5,10
+- [x] task with --no-wait, doesn't wait for output
+- [x] task-list without callback lists all tasks and shows callback column
+- [x] task-list --all (same as --limit 0), and -a
+- [x] Renamed project from go-mythic to mysh (pronounced mɪʃh)
+- [x] grep should also look at the command execution strings too, not just output