Commit 750a447

bryfry <bryon@fryer.io>
2025-09-17 09:56:04
rename to mysh
1 parent d77e9da
cmd/callbacks.go
@@ -3,7 +3,7 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"os"
 	"text/tabwriter"
 	"time"
cmd/files.go
@@ -3,8 +3,8 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
-	"go-mythic/pkg/mythic/api"
+	"mysh/pkg/mythic"
+	"mysh/pkg/mythic/api"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -162,8 +162,8 @@ func runFilesList(cmd *cobra.Command, args []string) error {
 
 	w.Flush()
 
-	fmt.Println("\nTo download a file: go-mythic files download <uuid>")
-	fmt.Println("To view full UUIDs: go-mythic files | grep -v UUID")
+	fmt.Println("\nTo download a file: mysh files download <uuid>")
+	fmt.Println("To view full UUIDs: mysh files | grep -v UUID")
 
 	return nil
 }
cmd/grep.go
@@ -3,11 +3,10 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"os"
 	"regexp"
 	"strings"
-	"sync"
 	"text/tabwriter"
 	"time"
 
@@ -20,15 +19,18 @@ var (
 	grepLimit           int
 	grepRegex           bool
 	grepInvert          bool
-	grepWorkers         int
+	grepSearchCommands  bool
+	grepSearchOutput    bool
 )
 
 var grepCmd = &cobra.Command{
 	Use:   "grep <pattern>",
-	Short: "Search through task outputs",
-	Long: `Search through task response outputs for a specific pattern.
+	Short: "Search through task outputs and commands",
+	Long: `Search through task response outputs and/or command execution strings for a specific pattern.
 Shows a table of matching tasks with their commands instead of full output.
-This is like grep for Mythic task results - find specific strings, commands, or data across all your task history.`,
+This is like grep for Mythic task results - find specific strings, commands, or data across all your task history.
+
+By default, searches both command strings and output. Use --commands-only or --output-only to restrict search scope.`,
 	Args: cobra.ExactArgs(1),
 	RunE: runGrep,
 }
@@ -40,20 +42,30 @@ func init() {
 	grepCmd.Flags().IntVarP(&grepLimit, "limit", "l", 0, "Maximum number of tasks to search (0 = no limit)")
 	grepCmd.Flags().BoolVarP(&grepRegex, "regexp", "E", false, "Treat pattern as regular expression")
 	grepCmd.Flags().BoolVarP(&grepInvert, "invert-match", "v", false, "Show tasks that don't match")
-	grepCmd.Flags().IntVarP(&grepWorkers, "workers", "w", 10, "Number of parallel workers for searching")
+	grepCmd.Flags().BoolVar(&grepSearchCommands, "commands-only", false, "Search only in command execution strings (command + params)")
+	grepCmd.Flags().BoolVar(&grepSearchOutput, "output-only", false, "Search only in task response outputs")
 }
 
-type taskResult struct {
-	task  mythic.Task
-	match bool
-	err   error
-}
 
 func runGrep(cmd *cobra.Command, args []string) error {
 	if err := validateConfig(); err != nil {
 		return err
 	}
 
+	// Validate search flags
+	if grepSearchCommands && grepSearchOutput {
+		return fmt.Errorf("cannot use both --commands-only and --output-only flags together")
+	}
+
+	// Set defaults: if neither flag is specified, search both
+	searchCommands := true
+	searchOutput := true
+	if grepSearchCommands {
+		searchOutput = false
+	} else if grepSearchOutput {
+		searchCommands = false
+	}
+
 	pattern := args[0]
 	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
 
@@ -64,30 +76,30 @@ func runGrep(cmd *cobra.Command, args []string) error {
 	var tasks []mythic.Task
 	var err error
 
+	// Determine search scope description
+	var searchScope string
+	if searchCommands && searchOutput {
+		searchScope = "commands and outputs"
+	} else if searchCommands {
+		searchScope = "command strings"
+	} else {
+		searchScope = "outputs"
+	}
+
 	if grepCallbackID > 0 {
-		// Search tasks for specific callback
-		tasks, err = client.GetCallbackTasks(listCtx, grepCallbackID)
+		// Search tasks for specific callback - use efficient query that includes responses
+		tasks, err = client.GetTasksWithResponses(listCtx, grepCallbackID, grepLimit)
 		if err != nil {
 			return fmt.Errorf("failed to get tasks for callback %d: %w", grepCallbackID, err)
 		}
-		fmt.Printf("Searching tasks from callback %d for pattern: %s\n", grepCallbackID, pattern)
+		fmt.Printf("Searching %s from callback %d for pattern: %s\n", searchScope, grepCallbackID, pattern)
 	} else {
-		// Get all tasks from all callbacks
-		callbacks, err := client.GetActiveCallbacks(listCtx)
+		// Get all tasks from all callbacks with responses in a single query
+		tasks, err = client.GetAllTasksWithResponses(listCtx, grepLimit)
 		if err != nil {
-			return fmt.Errorf("failed to get callbacks: %w", err)
-		}
-
-		fmt.Printf("Searching all tasks for pattern: %s\n", pattern)
-
-		for _, callback := range callbacks {
-			callbackTasks, err := client.GetCallbackTasks(listCtx, callback.ID)
-			if err != nil {
-				fmt.Printf("Warning: Failed to get tasks for callback %d: %v\n", callback.DisplayID, err)
-				continue
-			}
-			tasks = append(tasks, callbackTasks...)
+			return fmt.Errorf("failed to get all tasks with responses: %w", err)
 		}
+		fmt.Printf("Searching %s in all tasks for pattern: %s\n", searchScope, pattern)
 	}
 
 	if len(tasks) == 0 {
@@ -95,13 +107,7 @@ func runGrep(cmd *cobra.Command, args []string) error {
 		return nil
 	}
 
-	// Apply limit if specified
-	if grepLimit > 0 && len(tasks) > grepLimit {
-		// Take the most recent tasks
-		tasks = tasks[len(tasks)-grepLimit:]
-	}
-
-	fmt.Printf("Searching through %d tasks with %d workers...\n\n", len(tasks), grepWorkers)
+	fmt.Printf("Searching through %d tasks...\n\n", len(tasks))
 
 	// Prepare pattern for matching
 	var matcher func(string) bool
@@ -129,38 +135,38 @@ func runGrep(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	// Use worker pool to process tasks in parallel
-	results := processTasksWithWorkerPool(client, tasks, matcher, grepWorkers)
-
+	// Search through tasks in memory (much faster since we have all data)
 	var matchingTasks []mythic.Task
 	processedTasks := 0
-	errorCount := 0
 
-	for result := range results {
+	for _, task := range tasks {
 		processedTasks++
-		if result.err != nil {
-			errorCount++
-			if errorCount <= 5 { // Only show first 5 errors
-				fmt.Printf("Warning: Failed to get response for task %d: %v\n", result.task.DisplayID, result.err)
+		var matches bool
+
+		// Check command string if enabled
+		if searchCommands {
+			commandString := task.Command
+			if task.Params != "" {
+				commandString += " " + task.Params
 			}
-			continue
+			matches = matcher(commandString)
+		}
+
+		// Check response output if enabled and not already matched
+		if !matches && searchOutput && task.Response != "" {
+			matches = matcher(task.Response)
 		}
 
 		// Apply invert logic
-		matches := result.match
 		if grepInvert {
 			matches = !matches
 		}
 
 		if matches {
-			matchingTasks = append(matchingTasks, result.task)
+			matchingTasks = append(matchingTasks, task)
 		}
 	}
 
-	if errorCount > 5 {
-		fmt.Printf("Warning: Suppressed %d additional errors\n", errorCount-5)
-	}
-
 	if len(matchingTasks) == 0 {
 		fmt.Printf("No matches found in %d tasks searched\n", processedTasks)
 		return nil
@@ -169,67 +175,10 @@ func runGrep(cmd *cobra.Command, args []string) error {
 	// Show results in table format
 	showMatchingTasksTable(matchingTasks)
 
-	fmt.Printf("\nFound %d matches in %d tasks searched", len(matchingTasks), processedTasks)
-	if errorCount > 0 {
-		fmt.Printf(" (%d errors)", errorCount)
-	}
-	fmt.Println()
+	fmt.Printf("\nFound %d matches in %d tasks searched\n", len(matchingTasks), processedTasks)
 	return nil
 }
 
-func processTasksWithWorkerPool(client *mythic.Client, tasks []mythic.Task, matcher func(string) bool, numWorkers int) <-chan taskResult {
-	taskChan := make(chan mythic.Task, len(tasks))
-	resultChan := make(chan taskResult, len(tasks))
-
-	// Start workers
-	var wg sync.WaitGroup
-	for i := 0; i < numWorkers; i++ {
-		wg.Add(1)
-		go worker(&wg, client, taskChan, resultChan, matcher)
-	}
-
-	// Send tasks to workers
-	go func() {
-		for _, task := range tasks {
-			taskChan <- task
-		}
-		close(taskChan)
-	}()
-
-	// Close results channel when all workers are done
-	go func() {
-		wg.Wait()
-		close(resultChan)
-	}()
-
-	return resultChan
-}
-
-func worker(wg *sync.WaitGroup, client *mythic.Client, taskChan <-chan mythic.Task, resultChan chan<- taskResult, matcher func(string) bool) {
-	defer wg.Done()
-
-	for task := range taskChan {
-		// Use per-request timeout to avoid global timeouts
-		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
-		fullTask, err := client.GetTaskResponse(ctx, task.ID)
-		cancel()
-
-		if err != nil {
-			resultChan <- taskResult{task: task, match: false, err: err}
-			continue
-		}
-
-		if fullTask.Response == "" {
-			resultChan <- taskResult{task: task, match: false, err: nil}
-			continue
-		}
-
-		// Check if response matches pattern
-		matches := matcher(fullTask.Response)
-		resultChan <- taskResult{task: *fullTask, match: matches, err: nil}
-	}
-}
 
 func showMatchingTasksTable(tasks []mythic.Task) {
 	// Create a tab writer for formatted output
cmd/grep_test.go
@@ -0,0 +1,144 @@
+package cmd
+
+import (
+	"regexp"
+	"strings"
+	"testing"
+)
+
+func TestGrepMatcher(t *testing.T) {
+	tests := []struct {
+		name            string
+		pattern         string
+		text            string
+		caseInsensitive bool
+		regex           bool
+		expected        bool
+	}{
+		{
+			name:     "simple string match",
+			pattern:  "hello",
+			text:     "hello world",
+			expected: true,
+		},
+		{
+			name:     "simple string no match",
+			pattern:  "hello",
+			text:     "goodbye world",
+			expected: false,
+		},
+		{
+			name:            "case insensitive match",
+			pattern:         "HELLO",
+			text:            "hello world",
+			caseInsensitive: true,
+			expected:        true,
+		},
+		{
+			name:            "case sensitive no match",
+			pattern:         "HELLO",
+			text:            "hello world",
+			caseInsensitive: false,
+			expected:        false,
+		},
+		{
+			name:     "regex match",
+			pattern:  "hel+o",
+			text:     "hello world",
+			regex:    true,
+			expected: true,
+		},
+		{
+			name:     "regex no match",
+			pattern:  "hel{2,}o",
+			text:     "helo world",
+			regex:    true,
+			expected: false,
+		},
+		{
+			name:            "regex case insensitive",
+			pattern:         "HEL+O",
+			text:            "hello world",
+			regex:           true,
+			caseInsensitive: true,
+			expected:        true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Simulate the matcher creation logic from runGrep
+			var matcher func(string) bool
+			if tt.regex {
+				regexPattern := tt.pattern
+				if tt.caseInsensitive {
+					regexPattern = "(?i)" + tt.pattern
+				}
+				re, err := regexp.Compile(regexPattern)
+				if err != nil {
+					t.Fatalf("Failed to compile regex: %v", err)
+				}
+				matcher = re.MatchString
+			} else {
+				searchPattern := tt.pattern
+				if tt.caseInsensitive {
+					searchPattern = strings.ToLower(tt.pattern)
+				}
+				matcher = func(text string) bool {
+					searchText := text
+					if tt.caseInsensitive {
+						searchText = strings.ToLower(text)
+					}
+					return strings.Contains(searchText, searchPattern)
+				}
+			}
+
+			result := matcher(tt.text)
+			if result != tt.expected {
+				t.Errorf("Expected %v, got %v for pattern %q against text %q", tt.expected, result, tt.pattern, tt.text)
+			}
+		})
+	}
+}
+
+func TestGrepCommandStringConstruction(t *testing.T) {
+	tests := []struct {
+		name     string
+		command  string
+		params   string
+		expected string
+	}{
+		{
+			name:     "command with params",
+			command:  "ls",
+			params:   "-la /tmp",
+			expected: "ls -la /tmp",
+		},
+		{
+			name:     "command without params",
+			command:  "whoami",
+			params:   "",
+			expected: "whoami",
+		},
+		{
+			name:     "command with complex params",
+			command:  "powershell",
+			params:   "-ExecutionPolicy Bypass -Command \"Get-Process\"",
+			expected: "powershell -ExecutionPolicy Bypass -Command \"Get-Process\"",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Simulate the command string construction from worker function
+			commandString := tt.command
+			if tt.params != "" {
+				commandString += " " + tt.params
+			}
+
+			if commandString != tt.expected {
+				t.Errorf("Expected %q, got %q", tt.expected, commandString)
+			}
+		})
+	}
+}
\ No newline at end of file
cmd/interact.go
@@ -4,8 +4,8 @@ import (
 	"bufio"
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
-	"go-mythic/pkg/mythic/api"
+	"mysh/pkg/mythic"
+	"mysh/pkg/mythic/api"
 	"os"
 	"strconv"
 	"strings"
cmd/payload.go
@@ -3,7 +3,7 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"os"
 	"strconv"
 	"strings"
@@ -129,8 +129,8 @@ func showPayloadList(ctx context.Context, client *mythic.Client) error {
 	w.Flush()
 
 	fmt.Println("\n* = Auto-generated payload")
-	fmt.Println("\nTo view payload details: go-mythic payload <id>")
-	fmt.Println("To download a payload file: go-mythic files download <agent_file_id>")
+	fmt.Println("\nTo view payload details: mysh payload <id>")
+	fmt.Println("To download a payload file: mysh files download <agent_file_id>")
 
 	return nil
 }
@@ -212,7 +212,7 @@ func showPayloadDetails(ctx context.Context, client *mythic.Client, payloadID in
 	// Download instructions
 	if targetPayload.AgentFileID != "" {
 		fmt.Printf("\nTo download this payload:\n")
-		fmt.Printf("  go-mythic files download %s\n", targetPayload.AgentFileID)
+		fmt.Printf("  mysh files download %s\n", targetPayload.AgentFileID)
 	}
 
 	return nil
cmd/root.go
@@ -15,9 +15,9 @@ var (
 )
 
 var rootCmd = &cobra.Command{
-	Use:   "go-mythic",
+	Use:   "mysh",
 	Short: "A CLI tool for interacting with Mythic C2 framework",
-	Long: `go-mythic is a command-line interface for interacting with the Mythic C2 framework.
+	Long: `mysh (pronounced mɪʃ) is a command-line interface for interacting with the Mythic C2 framework.
 It provides programmatic access to Mythic operations, replacing the need for UI interactions.`,
 }
 
cmd/task.go
@@ -3,8 +3,8 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
-	"go-mythic/pkg/mythic/api"
+	"mysh/pkg/mythic"
+	"mysh/pkg/mythic/api"
 	"strconv"
 	"strings"
 
cmd/task_list.go
@@ -3,7 +3,7 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"os"
 	"strconv"
 	"strings"
cmd/task_view.go
@@ -3,7 +3,7 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"strconv"
 	"strings"
 	"time"
cmd/upload.go
@@ -5,7 +5,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"os"
 	"path/filepath"
 	"strings"
pkg/mythic/api/callbacks.go
@@ -3,7 +3,7 @@ package api
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 )
 
 // FindActiveCallback looks up an active callback by its display ID
pkg/mythic/api/callbacks_test.go
@@ -2,7 +2,7 @@ package api
 
 import (
 	"context"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"testing"
 )
 
@@ -27,6 +27,14 @@ func (m *MockClient) GetTaskResponse(ctx context.Context, taskID int) (*mythic.T
 	return nil, nil // Not implemented for this test
 }
 
+func (m *MockClient) GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]mythic.Task, error) {
+	return nil, nil // Not implemented for this test
+}
+
+func (m *MockClient) GetAllTasksWithResponses(ctx context.Context, limit int) ([]mythic.Task, error) {
+	return nil, nil // Not implemented for this test
+}
+
 func TestFindActiveCallback(t *testing.T) {
 	tests := []struct {
 		name        string
pkg/mythic/api/interfaces.go
@@ -2,7 +2,7 @@ package api
 
 import (
 	"context"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 )
 
 // MythicClient defines the interface for Mythic operations needed by the api package
@@ -10,6 +10,8 @@ type MythicClient interface {
 	GetActiveCallbacks(ctx context.Context) ([]mythic.Callback, error)
 	CreateTask(ctx context.Context, callbackID int, command, params string) (*mythic.Task, error)
 	GetTaskResponse(ctx context.Context, taskID int) (*mythic.Task, error)
+	GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]mythic.Task, error)
+	GetAllTasksWithResponses(ctx context.Context, limit int) ([]mythic.Task, error)
 }
 
 // Ensure that the real Client implements the interface
pkg/mythic/api/tasks.go
@@ -3,7 +3,7 @@ package api
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"time"
 )
 
pkg/mythic/api/tasks_test.go
@@ -3,7 +3,7 @@ package api
 import (
 	"context"
 	"fmt"
-	"go-mythic/pkg/mythic"
+	"mysh/pkg/mythic"
 	"testing"
 	"time"
 )
@@ -72,6 +72,14 @@ func (m *MockTaskClient) SetTaskCompleted(taskID int, response string) {
 	}
 }
 
+func (m *MockTaskClient) GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]mythic.Task, error) {
+	return nil, nil // Not implemented for this test
+}
+
+func (m *MockTaskClient) GetAllTasksWithResponses(ctx context.Context, limit int) ([]mythic.Task, error) {
+	return nil, nil // Not implemented for this test
+}
+
 func TestDefaultTaskPollConfig(t *testing.T) {
 	config := DefaultTaskPollConfig()
 
pkg/mythic/client.go
@@ -533,6 +533,141 @@ func (c *Client) DownloadFile(ctx context.Context, fileUUID string) ([]byte, err
 	return data, nil
 }
 
+func (c *Client) GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]Task, error) {
+	req := graphql.NewRequest(GetTasksWithResponses)
+
+	if callbackID > 0 {
+		req.Var("callback_id", callbackID)
+	}
+	if limit > 0 {
+		req.Var("limit", limit)
+	}
+	req.Header.Set("Authorization", "Bearer "+c.token)
+
+	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"`
+			Timestamp     string `json:"timestamp"`
+			Callback      struct {
+				ID        int    `json:"id"`
+				DisplayID int    `json:"display_id"`
+				Host      string `json:"host"`
+				User      string `json:"user"`
+			} `json:"callback"`
+			Responses []struct {
+				ResponseText string `json:"response_text"`
+				Timestamp    string `json:"timestamp"`
+			} `json:"responses"`
+		} `json:"task"`
+	}
+
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get tasks with responses: %w", err)
+	}
+
+	var tasks []Task
+	for _, t := range resp.Task {
+		// Concatenate all response chunks in chronological order
+		var responseBuilder strings.Builder
+		for _, resp := range t.Responses {
+			decodedText := decodeResponseText(resp.ResponseText)
+			responseBuilder.WriteString(decodedText)
+		}
+		response := responseBuilder.String()
+
+		status := t.Status
+		if t.Completed {
+			status = "completed"
+		}
+
+		tasks = append(tasks, Task{
+			ID:         t.ID,
+			DisplayID:  t.DisplayID,
+			Command:    t.CommandName,
+			Params:     t.DisplayParams,
+			Status:     status,
+			Response:   response,
+			CallbackID: t.Callback.ID,
+			Timestamp:  t.Timestamp,
+			Completed:  t.Completed,
+		})
+	}
+
+	return tasks, nil
+}
+
+func (c *Client) GetAllTasksWithResponses(ctx context.Context, limit int) ([]Task, error) {
+	req := graphql.NewRequest(GetAllTasksWithResponsesFromAllCallbacks)
+
+	if limit > 0 {
+		req.Var("limit", limit)
+	}
+	req.Header.Set("Authorization", "Bearer "+c.token)
+
+	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"`
+			Timestamp     string `json:"timestamp"`
+			Callback      struct {
+				ID        int    `json:"id"`
+				DisplayID int    `json:"display_id"`
+				Host      string `json:"host"`
+				User      string `json:"user"`
+			} `json:"callback"`
+			Responses []struct {
+				ResponseText string `json:"response_text"`
+				Timestamp    string `json:"timestamp"`
+			} `json:"responses"`
+		} `json:"task"`
+	}
+
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get all tasks with responses: %w", err)
+	}
+
+	var tasks []Task
+	for _, t := range resp.Task {
+		// Concatenate all response chunks in chronological order
+		var responseBuilder strings.Builder
+		for _, resp := range t.Responses {
+			decodedText := decodeResponseText(resp.ResponseText)
+			responseBuilder.WriteString(decodedText)
+		}
+		response := responseBuilder.String()
+
+		status := t.Status
+		if t.Completed {
+			status = "completed"
+		}
+
+		tasks = append(tasks, Task{
+			ID:         t.ID,
+			DisplayID:  t.DisplayID,
+			Command:    t.CommandName,
+			Params:     t.DisplayParams,
+			Status:     status,
+			Response:   response,
+			CallbackID: t.Callback.ID,
+			Timestamp:  t.Timestamp,
+			Completed:  t.Completed,
+		})
+	}
+
+	return tasks, nil
+}
+
 func (c *Client) GetPayloads(ctx context.Context) ([]Payload, error) {
 	req := graphql.NewRequest(GetPayloads)
 
pkg/mythic/client_test.go
@@ -1,6 +1,7 @@
 package mythic
 
 import (
+	"context"
 	"testing"
 )
 
@@ -128,4 +129,31 @@ func TestNewClientWithSocksProxy(t *testing.T) {
 	if client.token != token {
 		t.Errorf("Expected token %q, got %q", token, client.token)
 	}
+}
+
+func TestClientMethodsExist(t *testing.T) {
+	apiURL := "https://example.com/graphql"
+	token := "test-token"
+	client := NewClient(apiURL, token, true, "")
+
+	// Test that the new methods exist and can be called (they'll fail due to no server, but should not panic)
+	// This ensures the GraphQL queries compile and the methods are properly defined
+	if client == nil {
+		t.Fatal("NewClient returned nil")
+	}
+
+	// We can't make actual calls without a server, but we can verify the methods exist
+	// by checking they don't panic when called with a canceled context
+	ctx, cancel := context.WithCancel(context.Background())
+	cancel() // Cancel immediately to avoid network calls
+
+	_, err := client.GetTasksWithResponses(ctx, 1, 10)
+	if err == nil {
+		t.Error("Expected error due to canceled context")
+	}
+
+	_, err = client.GetAllTasksWithResponses(ctx, 10)
+	if err == nil {
+		t.Error("Expected error due to canceled context")
+	}
 }
\ No newline at end of file
pkg/mythic/queries.go
@@ -524,4 +524,52 @@ query GetAllFiles {
             }
         }
     }
+}`
+
+const GetTasksWithResponses = `
+query GetTasksWithResponses($callback_id: Int, $limit: Int) {
+    task(where: {callback_id: {_eq: $callback_id}}, order_by: {id: desc}, limit: $limit) {
+        id
+        display_id
+        command_name
+        original_params
+        display_params
+        status
+        completed
+        timestamp
+        callback {
+            id
+            display_id
+            host
+            user
+        }
+        responses(order_by: {id: asc}) {
+            response_text
+            timestamp
+        }
+    }
+}`
+
+const GetAllTasksWithResponsesFromAllCallbacks = `
+query GetAllTasksWithResponsesFromAllCallbacks($limit: Int) {
+    task(order_by: {id: desc}, limit: $limit) {
+        id
+        display_id
+        command_name
+        original_params
+        display_params
+        status
+        completed
+        timestamp
+        callback {
+            id
+            display_id
+            host
+            user
+        }
+        responses(order_by: {id: asc}) {
+            response_text
+            timestamp
+        }
+    }
 }`
\ No newline at end of file
go.mod
@@ -1,4 +1,4 @@
-module go-mythic
+module mysh
 
 go 1.24.2
 
investigate_forge.py
@@ -1,68 +0,0 @@
-#!/usr/bin/env python3
-
-import asyncio
-import sys
-import os
-
-# Add the contrib directory to the path
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'contrib', 'Mythic_Scripting'))
-
-from mythic import mythic
-
-async def investigate_forge_tasks():
-    try:
-        # Connect to Mythic using environment variables
-        server_ip = "mythic.adversarytactics.local"
-        apitoken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTgwNTc3ODUsImlhdCI6MTc1ODA0MzM4NSwidXNlcl9pZCI6NCwiYXV0aCI6ImFwaSIsImV2ZW50c3RlcGluc3RhbmNlX2lkIjowLCJhcGl0b2tlbnNfaWQiOjI2LCJvcGVyYXRpb25faWQiOjB9.dNx93CuQLMMhvbJEgVs1Rg_IQYDfhtTRjEiupXlEpYc"
-
-        mythic_conn = await mythic.login(
-            server_ip=server_ip,
-            apitoken=apitoken,
-            server_port=7443,
-            ssl=True
-        )
-
-        print("Connected to Mythic successfully")
-
-        # Get all tasks
-        tasks = await mythic_conn.get_all_tasks()
-
-        # Filter for forge-related tasks
-        forge_tasks = []
-        for task in tasks:
-            if task.command and 'forge' in task.command.lower():
-                forge_tasks.append(task)
-
-        if not forge_tasks:
-            print("No forge-related tasks found")
-            return
-
-        print(f"Found {len(forge_tasks)} forge-related tasks")
-        print("=" * 60)
-
-        for task in forge_tasks:
-            print(f"Task ID: {task.display_id}")
-            print(f"Command: {task.command}")
-            print(f"Status: {task.status}")
-            print(f"Completed: {task.completed}")
-            print(f"Callback: {task.callback.display_id if task.callback else 'Unknown'}")
-            print(f"Parameters: {task.original_params if task.original_params else 'None'}")
-
-            # Get task responses
-            responses = await mythic_conn.get_all_responses_for_task(task.id)
-            if responses:
-                print("Responses:")
-                for resp in responses:
-                    print(f"  - {resp.response[:200]}{'...' if len(resp.response) > 200 else ''}")
-            else:
-                print("No responses found")
-
-            print("-" * 40)
-
-    except Exception as e:
-        print(f"Error: {e}")
-        import traceback
-        traceback.print_exc()
-
-if __name__ == "__main__":
-    asyncio.run(investigate_forge_tasks())
\ No newline at end of file
main.go
@@ -1,6 +1,6 @@
 package main
 
-import "go-mythic/cmd"
+import "mysh/cmd"
 
 func main() {
 	cmd.Execute()
Makefile
@@ -1,5 +1,5 @@
 # Project variables
-BINARY_NAME=go-mythic
+BINARY_NAME=mysh
 PACKAGE_PATH=.
 BIN_DIR=./bin
 VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
README.md
@@ -1,9 +1,9 @@
-# `go-mythic`
+# `mysh` (pronounced mɪʃ)
 
 ## project goals
 
 For each lab we're going to be instructed to click on the mythic user interface - yuck.
-This project is focused on creating a golang cli application to accomplish the same tasks.
+This project is focused on creating a golang CLI application called `mysh` (pronounced mɪʃ) to accomplish the same tasks.
 
 ## resources
 
TODO.md
@@ -1,4 +1,5 @@
-- grep should also look at the command execution strings too, not just output
+- ✅ grep should also look at the command execution strings too, not just output
+- ✅ Renamed project from go-mythic to mysh (pronounced mɪʃ)
 - forge equivalency with execute_assembly and inline_assembly
 - task-list --all (same as --limit 0)
 
USAGE.md
@@ -1,4 +1,4 @@
-# go-mythic Usage Examples
+# mysh (pronounced mɪʃ) Usage Examples
 
 ## Building
 
@@ -22,7 +22,7 @@ make help
 
 ### Manual Build
 ```bash
-go build -o bin/go-mythic .
+go build -o bin/mysh .
 ```
 
 ## Project Structure
@@ -42,27 +42,27 @@ To sync with upstream updates:
 
 ### List Active Callbacks
 ```bash
-./go-mythic callbacks
+./mysh callbacks
 ```
 
 ### Execute Commands
 ```bash
 # Get help for all commands
-./go-mythic exec 1 help
+./mysh exec 1 help
 
 # Get detailed help for a specific command
-./go-mythic exec 1 help ps
+./mysh exec 1 help ps
 
 # List processes
-./go-mythic exec 1 ps
+./mysh exec 1 ps
 
 # Set checkin frequency to 5 seconds
-./go-mythic exec 1 sleep 5
+./mysh exec 1 sleep 5
 ```
 
 ### Interactive Session
 ```bash
-./go-mythic interact 1
+./mysh interact 1
 ```
 
 ## Using SOCKS5 Proxy
@@ -71,13 +71,13 @@ The CLI supports SOCKS5 proxy routing for all network traffic, replacing the nee
 
 ```bash
 # Use SOCKS5 proxy (equivalent to proxychains)
-./go-mythic --socks 127.0.0.1:9050 callbacks
+./mysh --socks 127.0.0.1:9050 callbacks
 
 # Execute command through proxy
-./go-mythic --socks 127.0.0.1:9050 exec 1 ps
+./mysh --socks 127.0.0.1:9050 exec 1 ps
 
 # Interactive session through proxy
-./go-mythic --socks 127.0.0.1:9050 interact 1
+./mysh --socks 127.0.0.1:9050 interact 1
 ```
 
 ## Configuration
@@ -89,12 +89,12 @@ The CLI supports SOCKS5 proxy routing for all network traffic, replacing the nee
 # Set via environment variables (recommended)
 export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
 export MYTHIC_API_TOKEN="your-jwt-token-here"
-./go-mythic callbacks
+./mysh callbacks
 
 # For testing with mythic.sh
 export MYTHIC_API_URL="https://mythic.adversarytactics.local:7443/graphql/"
 export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
-./go-mythic callbacks
+./mysh callbacks
 ```
 
 ### Individual Configuration Options
@@ -105,7 +105,7 @@ export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
 export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
 
 # Set via command line flag
-./go-mythic --url "https://your-mythic-server:7443/graphql/" callbacks
+./mysh --url "https://your-mythic-server:7443/graphql/" callbacks
 ```
 
 #### Authentication Token
@@ -114,7 +114,7 @@ export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
 export MYTHIC_API_TOKEN="your-jwt-token-here"
 
 # Set via command line flag
-./go-mythic --token "your-jwt-token-here" callbacks
+./mysh --token "your-jwt-token-here" callbacks
 
 # Get token from mythic.sh for testing
 export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
@@ -126,30 +126,30 @@ These commands accomplish the goals from labs/01.md:
 
 1. **View Active Callbacks** (equivalent to clicking Active Callbacks in UI):
    ```bash
-   ./go-mythic callbacks
+   ./mysh callbacks
    ```
 
 2. **Interact with Agent** (equivalent to right-click → Interact):
    ```bash
-   ./go-mythic interact <callback_id>
+   ./mysh interact <callback_id>
    ```
 
 3. **Get Help** (equivalent to typing "help" in UI console):
    ```bash
-   ./go-mythic exec <callback_id> help
+   ./mysh exec <callback_id> help
    ```
 
 4. **Get Command Details** (equivalent to typing "help ps"):
    ```bash
-   ./go-mythic exec <callback_id> help ps
+   ./mysh exec <callback_id> help ps
    ```
 
 5. **List Processes** (equivalent to typing "ps"):
    ```bash
-   ./go-mythic exec <callback_id> ps
+   ./mysh exec <callback_id> ps
    ```
 
 6. **Set Checkin Frequency** (equivalent to typing "sleep 5"):
    ```bash
-   ./go-mythic exec <callback_id> sleep 5
+   ./mysh exec <callback_id> sleep 5
    ```