Commit 5197804

bryfry <bryon@fryer.io>
2025-09-15 11:42:50
init
cmd/callbacks.go
@@ -0,0 +1,69 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"os"
+	"text/tabwriter"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var callbacksCmd = &cobra.Command{
+	Use:   "callbacks",
+	Short: "List active callbacks",
+	Long:  "Display all active callbacks from the Mythic server, equivalent to the Active Callbacks view in the UI.",
+	RunE:  runCallbacks,
+}
+
+func init() {
+	rootCmd.AddCommand(callbacksCmd)
+}
+
+func runCallbacks(cmd *cobra.Command, args []string) error {
+	if err := validateAuth(); err != nil {
+		return err
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	callbacks, err := client.GetActiveCallbacks(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get active callbacks: %w", err)
+	}
+
+	if len(callbacks) == 0 {
+		fmt.Println("No active callbacks found")
+		return nil
+	}
+
+	// Create a tab writer for formatted output
+	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+	fmt.Fprintln(w, "ID\tTYPE\tHOST\tUSER\tPROCESS\tPID\tLAST CHECKIN\tDESCRIPTION")
+	fmt.Fprintln(w, "--\t----\t----\t----\t-------\t---\t------------\t-----------")
+
+	for _, callback := range callbacks {
+		agentType := callback.Payload.PayloadType.Name
+		if agentType == "" {
+			agentType = "unknown"
+		}
+
+		fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%d\t%s\t%s\n",
+			callback.DisplayID,
+			agentType,
+			callback.Host,
+			callback.User,
+			callback.ProcessName,
+			callback.PID,
+			callback.LastCheckin,
+			callback.Description,
+		)
+	}
+
+	w.Flush()
+	return nil
+}
\ No newline at end of file
cmd/exec.go
@@ -0,0 +1,103 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var execCmd = &cobra.Command{
+	Use:   "exec <callback_id> <command> [args...]",
+	Short: "Execute a command on a specific callback",
+	Long:  "Execute a command on the specified callback and wait for the response. Supports commands like help, ps, sleep, etc.",
+	Args:  cobra.MinimumNArgs(2),
+	RunE:  runExec,
+}
+
+var (
+	waitTime int
+)
+
+func init() {
+	rootCmd.AddCommand(execCmd)
+	execCmd.Flags().IntVarP(&waitTime, "wait", "w", 30, "Maximum time to wait for response (seconds)")
+}
+
+func runExec(cmd *cobra.Command, args []string) error {
+	if err := validateAuth(); err != nil {
+		return err
+	}
+
+	callbackID, err := strconv.Atoi(args[0])
+	if err != nil {
+		return fmt.Errorf("invalid callback ID: %s", args[0])
+	}
+
+	command := args[1]
+	params := ""
+	if len(args) > 2 {
+		params = strings.Join(args[2:], " ")
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx := context.Background()
+
+	// Verify callback exists and is active
+	callbacks, err := client.GetActiveCallbacks(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get callbacks: %w", err)
+	}
+
+	var targetCallback *mythic.Callback
+	for _, callback := range callbacks {
+		if callback.DisplayID == callbackID {
+			targetCallback = &callback
+			break
+		}
+	}
+
+	if targetCallback == nil {
+		return fmt.Errorf("callback %d not found or not active", callbackID)
+	}
+
+	fmt.Printf("Executing '%s %s' on callback %d (%s@%s)\n", 
+		command, params, callbackID, targetCallback.User, targetCallback.Host)
+
+	// Create the task
+	task, err := client.CreateTask(ctx, targetCallback.ID, command, params)
+	if err != nil {
+		return fmt.Errorf("failed to create task: %w", err)
+	}
+
+	fmt.Printf("Task %d created, waiting for response...\n", task.DisplayID)
+
+	// Poll for response
+	for i := 0; i < waitTime; i++ {
+		time.Sleep(1 * time.Second)
+		
+		updatedTask, err := client.GetTaskResponse(ctx, task.ID)
+		if err != nil {
+			return fmt.Errorf("failed to get task response: %w", err)
+		}
+
+		if updatedTask.Status == "completed" || updatedTask.Status == "error" {
+			fmt.Printf("\nTask Status: %s\n", updatedTask.Status)
+			if updatedTask.Response != "" {
+				fmt.Printf("Response:\n%s\n", updatedTask.Response)
+			}
+			return nil
+		}
+
+		if i > 0 && i%5 == 0 {
+			fmt.Printf(".")
+		}
+	}
+
+	fmt.Printf("\nTimeout waiting for response after %d seconds\n", waitTime)
+	return nil
+}
\ No newline at end of file
cmd/interact.go
@@ -0,0 +1,145 @@
+package cmd
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var interactCmd = &cobra.Command{
+	Use:   "interact <callback_id>",
+	Short: "Interact with a specific callback",
+	Long:  "Start an interactive session with a callback, equivalent to right-clicking and selecting 'Interact' in the UI.",
+	Args:  cobra.ExactArgs(1),
+	RunE:  runInteract,
+}
+
+func init() {
+	rootCmd.AddCommand(interactCmd)
+}
+
+func runInteract(cmd *cobra.Command, args []string) error {
+	if err := validateAuth(); err != nil {
+		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 := context.Background()
+
+	// Verify callback exists and is active
+	callbacks, err := client.GetActiveCallbacks(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get callbacks: %w", err)
+	}
+
+	var targetCallback *mythic.Callback
+	for _, callback := range callbacks {
+		if callback.DisplayID == callbackID {
+			targetCallback = &callback
+			break
+		}
+	}
+
+	if targetCallback == nil {
+		return fmt.Errorf("callback %d not found or not active", callbackID)
+	}
+
+	agentType := targetCallback.Payload.PayloadType.Name
+	if agentType == "" {
+		agentType = "agent" // fallback if payload type is not available
+	}
+
+	fmt.Printf("Interacting with callback %d (%s@%s - %s [%s])\n", 
+		targetCallback.DisplayID, 
+		targetCallback.User, 
+		targetCallback.Host, 
+		targetCallback.ProcessName,
+		agentType)
+	fmt.Println("Type 'exit' to quit the interactive session")
+	fmt.Println("---")
+
+	scanner := bufio.NewScanner(os.Stdin)
+	for {
+		fmt.Printf("%s[%d]> ", agentType, callbackID)
+		if !scanner.Scan() {
+			break
+		}
+
+		input := strings.TrimSpace(scanner.Text())
+		if input == "" {
+			continue
+		}
+
+		if input == "exit" {
+			fmt.Println("Exiting interactive session")
+			break
+		}
+
+		if err := executeCommand(ctx, client, targetCallback.ID, input); err != nil {
+			fmt.Printf("Error: %v\n", err)
+		}
+	}
+
+	return scanner.Err()
+}
+
+func executeCommand(ctx context.Context, client *mythic.Client, callbackID int, input string) error {
+	parts := strings.Fields(input)
+	if len(parts) == 0 {
+		return nil
+	}
+
+	command := parts[0]
+	params := ""
+	if len(parts) > 1 {
+		params = strings.Join(parts[1:], " ")
+	}
+
+	// Create the task
+	task, err := client.CreateTask(ctx, callbackID, command, params)
+	if err != nil {
+		return fmt.Errorf("failed to create task: %w", err)
+	}
+
+	fmt.Printf("Task %d created\n", task.DisplayID)
+
+	// Poll for response
+	for i := 0; i < 30; i++ { // Poll for up to 30 seconds
+		time.Sleep(1 * time.Second)
+		
+		updatedTask, err := client.GetTaskResponse(ctx, task.ID)
+		if err != nil {
+			return fmt.Errorf("failed to get task response: %w", err)
+		}
+
+		if updatedTask.Status == "completed" || updatedTask.Status == "error" {
+			if updatedTask.Response != "" {
+				fmt.Printf("Response:\n%s\n", updatedTask.Response)
+			} else {
+				fmt.Printf("Task completed with status: %s\n", updatedTask.Status)
+			}
+			return nil
+		}
+
+		if i == 0 {
+			fmt.Printf("Waiting for response...")
+		} else if i%5 == 0 {
+			fmt.Printf(".")
+		}
+	}
+
+	fmt.Println("\nTimeout waiting for response")
+	return nil
+}
\ No newline at end of file
cmd/root.go
@@ -0,0 +1,53 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	mythicURL   string
+	token       string
+	insecure    bool
+	socksProxy  string
+)
+
+var rootCmd = &cobra.Command{
+	Use:   "go-mythic",
+	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.
+It provides programmatic access to Mythic operations, replacing the need for UI interactions.`,
+}
+
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	rootCmd.PersistentFlags().StringVar(&mythicURL, "url", "https://mythic.adversarytactics.local:7443/graphql/", "Mythic GraphQL API URL")
+	rootCmd.PersistentFlags().StringVar(&token, "token", "", "JWT authentication token")
+	rootCmd.PersistentFlags().BoolVar(&insecure, "insecure", true, "Skip TLS certificate verification")
+	rootCmd.PersistentFlags().StringVar(&socksProxy, "socks", "", "SOCKS5 proxy address (e.g., 127.0.0.1:9050)")
+	
+	// Set default URL from environment if available
+	if urlEnv := os.Getenv("MYTHIC_API_URL"); urlEnv != "" {
+		mythicURL = urlEnv
+	}
+	
+	// Set default token from environment if available
+	if tokenEnv := os.Getenv("MYTHIC_API_TOKEN"); tokenEnv != "" {
+		token = tokenEnv
+	}
+}
+
+func validateAuth() error {
+	if token == "" {
+		return fmt.Errorf("authentication token is required. Use --token flag or set MYTHIC_API_TOKEN environment variable")
+	}
+	return nil
+}
\ No newline at end of file
labs/01.md
@@ -0,0 +1,23 @@
+## Explore the User Interface
+
+Next, briefly familiarize yourself with the layout of the Mythic UI. If you’re new to Mythic, refer to the Mythic Documentation.
+
+Try tasking your active Apollo agent with a few commands:
+
+    - In the left-side menu, select Active Callbacks (telephone with arrow icon).
+    - Right-click on the agent with the most recent checkin time (< 60 seconds) and select “Interact” or Double-click to open the console.
+    - Type help and press enter to obtain a list of agent commands and their description.
+    - Type help ps and press enter to see detailed description of a command.
+    - Type ps to list the running processes on the system.
+
+Next, task your agent to increase its checkin frequency so you can act more interactively with the remote system:
+
+    - Type sleep 5 to set your Beacon’s checkin frequency to 5 seconds
+
+Try out any additional commands you found interesting from the help menu.
+
+Further information on each command can be found in the documentations provided with every agent:
+
+    - In the left-side menu, select Payloads/C2 Services (headphones icon)
+    - One the right side under Actions, select Documentation for the agent or service you want more information on (book icon)
+
pkg/mythic/client.go
@@ -0,0 +1,307 @@
+package mythic
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/base64"
+	"fmt"
+	"net"
+	"net/http"
+	"regexp"
+	"unicode/utf8"
+
+	"github.com/machinebox/graphql"
+	"golang.org/x/net/proxy"
+)
+
+type Client struct {
+	client *graphql.Client
+	token  string
+}
+
+type Callback struct {
+	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"`
+	Payload     struct {
+		PayloadType struct {
+			Name string `json:"name"`
+		} `json:"payloadtype"`
+	} `json:"payload"`
+}
+
+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"`
+}
+
+func NewClient(url, token string, insecure bool, socksProxy string) *Client {
+	client := graphql.NewClient(url)
+	
+	// Create transport with TLS config
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
+	}
+	
+	// Configure SOCKS5 proxy if provided
+	if socksProxy != "" {
+		dialer, err := proxy.SOCKS5("tcp", socksProxy, nil, proxy.Direct)
+		if err == nil {
+			tr.Dial = func(network, addr string) (net.Conn, error) {
+				return dialer.Dial(network, addr)
+			}
+		}
+	}
+	
+	httpClient := &http.Client{Transport: tr}
+	client = graphql.NewClient(url, graphql.WithHTTPClient(httpClient))
+	
+	return &Client{
+		client: client,
+		token:  token,
+	}
+}
+
+// isBase64 checks if a string is valid base64 and likely contains binary data
+func isBase64(s string) bool {
+	// Must be at least 4 characters for valid base64
+	if len(s) < 4 {
+		return false
+	}
+	
+	// Check if it matches base64 pattern
+	base64Regex := regexp.MustCompile(`^[A-Za-z0-9+/]*={0,2}$`)
+	if !base64Regex.MatchString(s) {
+		return false
+	}
+	
+	// Try to decode
+	decoded, err := base64.StdEncoding.DecodeString(s)
+	if err != nil {
+		return false
+	}
+	
+	// If decoded content is not valid UTF-8, it's likely binary data
+	if !utf8.Valid(decoded) {
+		return true
+	}
+	
+	// If it's valid UTF-8 but the original string looks like base64 
+	// and is significantly longer than the decoded version, it's likely base64
+	if float64(len(s)) > float64(len(decoded))*1.2 {
+		return true
+	}
+	
+	return false
+}
+
+// decodeResponseText tries to base64 decode response_text, as Mythic typically encodes all responses
+func decodeResponseText(responseText string) string {
+	// First, try to base64 decode - Mythic typically base64 encodes all response_text
+	if decoded, err := base64.StdEncoding.DecodeString(responseText); err == nil {
+		// If decoded is valid UTF-8 text, return it
+		if utf8.Valid(decoded) {
+			return string(decoded)
+		}
+		// If it's binary data, return a placeholder message
+		return fmt.Sprintf("[Binary data: %d bytes]", len(decoded))
+	}
+	
+	// If base64 decode fails, return original text
+	return responseText
+}
+
+func (c *Client) GetCallbacks(ctx context.Context) ([]Callback, error) {
+	req := graphql.NewRequest(`
+		query GetCallbacks {
+			callback {
+				id
+				display_id
+				user
+				host
+				process_name
+				pid
+				description
+				last_checkin
+				active
+				payload {
+					payloadtype {
+						name
+					}
+				}
+			}
+		}
+	`)
+	
+	req.Header.Set("Authorization", "Bearer "+c.token)
+	
+	var resp struct {
+		Callback []Callback `json:"callback"`
+	}
+	
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get callbacks: %w", err)
+	}
+	
+	return resp.Callback, nil
+}
+
+func (c *Client) GetActiveCallbacks(ctx context.Context) ([]Callback, error) {
+	callbacks, err := c.GetCallbacks(ctx)
+	if err != nil {
+		return nil, err
+	}
+	
+	var active []Callback
+	for _, callback := range callbacks {
+		if callback.Active {
+			active = append(active, callback)
+		}
+	}
+	
+	return active, nil
+}
+
+func (c *Client) CreateTask(ctx context.Context, callbackID int, command, params string) (*Task, error) {
+	req := graphql.NewRequest(`
+		mutation createTasking($callback_id: Int!, $command: String!, $params: String!, $tasking_location: String) {
+			createTask(callback_id: $callback_id, command: $command, params: $params, tasking_location: $tasking_location) {
+				status
+				id
+				display_id
+				error
+			}
+		}
+	`)
+	
+	req.Var("callback_id", callbackID)
+	req.Var("command", command)
+	req.Var("params", params)
+	req.Var("tasking_location", "command_line")
+	req.Header.Set("Authorization", "Bearer "+c.token)
+	
+	var resp struct {
+		CreateTask struct {
+			Status    string `json:"status"`
+			ID        int    `json:"id"`
+			DisplayID int    `json:"display_id"`
+			Error     string `json:"error"`
+		} `json:"createTask"`
+	}
+	
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to create task: %w", err)
+	}
+	
+	if resp.CreateTask.Status != "success" {
+		return nil, fmt.Errorf("task creation failed: %s", resp.CreateTask.Error)
+	}
+	
+	return &Task{
+		ID:        resp.CreateTask.ID,
+		DisplayID: resp.CreateTask.DisplayID,
+		Command:   command,
+		Params:    params,
+		Status:    "submitted",
+		CallbackID: callbackID,
+	}, nil
+}
+
+func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error) {
+	req := graphql.NewRequest(`
+		query GetTask($id: Int!) {
+			task_by_pk(id: $id) {
+				id
+				display_id
+				command_name
+				original_params
+				display_params
+				status
+				completed
+				callback {
+					id
+					display_id
+				}
+			}
+		}
+	`)
+	
+	req.Var("id", taskID)
+	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"`
+			Callback      struct {
+				ID        int `json:"id"`
+				DisplayID int `json:"display_id"`
+			} `json:"callback"`
+		} `json:"task_by_pk"`
+	}
+	
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get task: %w", err)
+	}
+	
+	// Get task responses
+	responseReq := graphql.NewRequest(`
+		query GetTaskResponses($task_display_id: Int!) {
+			response(where: {task: {display_id: {_eq: $task_display_id}}}, order_by: {id: desc}, limit: 1) {
+				response_text
+				timestamp
+			}
+		}
+	`)
+	
+	responseReq.Var("task_display_id", resp.Task.DisplayID)
+	responseReq.Header.Set("Authorization", "Bearer "+c.token)
+	
+	var responseResp struct {
+		Response []struct {
+			ResponseText string `json:"response_text"`
+			Timestamp    string `json:"timestamp"`
+		} `json:"response"`
+	}
+	
+	if err := c.client.Run(ctx, responseReq, &responseResp); err != nil {
+		return nil, fmt.Errorf("failed to get task response: %w", err)
+	}
+	
+	response := ""
+	if len(responseResp.Response) > 0 {
+		response = decodeResponseText(responseResp.Response[0].ResponseText)
+	}
+	
+	status := resp.Task.Status
+	if resp.Task.Completed {
+		status = "completed"
+	}
+	
+	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,
+	}, nil
+}
\ No newline at end of file
.gitignore
@@ -0,0 +1,3 @@
+bin/*
+mythic.sh
+contrib/Mythic_Scripting
go.mod
@@ -0,0 +1,16 @@
+module go-mythic
+
+go 1.24.2
+
+require (
+	github.com/machinebox/graphql v0.2.2
+	github.com/spf13/cobra v1.10.1
+)
+
+require (
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/matryer/is v1.4.1 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/spf13/pflag v1.0.9 // indirect
+	golang.org/x/net v0.44.0 // indirect
+)
go.sum
@@ -0,0 +1,18 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
+github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
+golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
main.go
@@ -0,0 +1,7 @@
+package main
+
+import "go-mythic/cmd"
+
+func main() {
+	cmd.Execute()
+}
\ No newline at end of file
Makefile
@@ -0,0 +1,163 @@
+# Project variables
+BINARY_NAME=go-mythic
+PACKAGE_PATH=.
+BIN_DIR=./bin
+VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
+BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
+COMMIT_HASH=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+
+# Go variables
+GOCMD=go
+GOBUILD=$(GOCMD) build
+GOCLEAN=$(GOCMD) clean
+GOTEST=$(GOCMD) test
+GOGET=$(GOCMD) get
+GOMOD=$(GOCMD) mod
+GOFMT=$(GOCMD) fmt
+
+# Build flags
+LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.CommitHash=$(COMMIT_HASH)"
+
+# Default target
+.PHONY: all
+all: clean build
+
+# Create bin directory
+$(BIN_DIR):
+	@mkdir -p $(BIN_DIR)
+
+# Build for current platform
+.PHONY: build
+build: $(BIN_DIR)
+	@echo "Building $(BINARY_NAME) for current platform..."
+	$(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME) $(PACKAGE_PATH)
+	@echo "Binary built: $(BIN_DIR)/$(BINARY_NAME)"
+
+# Build for Linux AMD64
+.PHONY: build-linux
+build-linux: $(BIN_DIR)
+	@echo "Building $(BINARY_NAME) for Linux AMD64..."
+	GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-linux-amd64 $(PACKAGE_PATH)
+
+# Build for Linux ARM64
+.PHONY: build-linux-arm64
+build-linux-arm64: $(BIN_DIR)
+	@echo "Building $(BINARY_NAME) for Linux ARM64..."
+	GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-linux-arm64 $(PACKAGE_PATH)
+
+# Build for macOS AMD64
+.PHONY: build-darwin
+build-darwin: $(BIN_DIR)
+	@echo "Building $(BINARY_NAME) for macOS AMD64..."
+	GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-darwin-amd64 $(PACKAGE_PATH)
+
+# Build for macOS ARM64 (Apple Silicon)
+.PHONY: build-darwin-arm64
+build-darwin-arm64: $(BIN_DIR)
+	@echo "Building $(BINARY_NAME) for macOS ARM64..."
+	GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-darwin-arm64 $(PACKAGE_PATH)
+
+# Build for Windows AMD64
+.PHONY: build-windows
+build-windows: $(BIN_DIR)
+	@echo "Building $(BINARY_NAME) for Windows AMD64..."
+	GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-windows-amd64.exe $(PACKAGE_PATH)
+
+# Build for all platforms
+.PHONY: build-all
+build-all: build-linux build-linux-arm64 build-darwin build-darwin-arm64 build-windows
+	@echo "All platform builds completed"
+
+# Run tests
+.PHONY: test
+test:
+	@echo "Running tests..."
+	$(GOTEST) -v ./...
+
+# Run tests with coverage
+.PHONY: test-coverage
+test-coverage:
+	@echo "Running tests with coverage..."
+	$(GOTEST) -v -coverprofile=coverage.out ./...
+	$(GOCMD) tool cover -html=coverage.out -o coverage.html
+	@echo "Coverage report generated: coverage.html"
+
+# Clean build artifacts
+.PHONY: clean
+clean:
+	@echo "Cleaning..."
+	$(GOCLEAN)
+	@rm -rf $(BIN_DIR)
+	@rm -f coverage.out coverage.html
+	@echo "Clean completed"
+
+# Install dependencies
+.PHONY: deps
+deps:
+	@echo "Installing dependencies..."
+	$(GOMOD) download
+	$(GOMOD) tidy
+
+# Format code
+.PHONY: fmt
+fmt:
+	@echo "Formatting code..."
+	$(GOFMT) ./...
+
+# Run linter (if golangci-lint is installed)
+.PHONY: lint
+lint:
+	@if command -v golangci-lint >/dev/null 2>&1; then \
+		echo "Running linter..."; \
+		golangci-lint run; \
+	else \
+		echo "golangci-lint not installed, skipping..."; \
+	fi
+
+# Install binary to system
+.PHONY: install
+install: build
+	@echo "Installing $(BINARY_NAME) to /usr/local/bin..."
+	@sudo cp $(BIN_DIR)/$(BINARY_NAME) /usr/local/bin/
+	@echo "Installation completed"
+
+# Uninstall binary from system
+.PHONY: uninstall
+uninstall:
+	@echo "Removing $(BINARY_NAME) from /usr/local/bin..."
+	@sudo rm -f /usr/local/bin/$(BINARY_NAME)
+	@echo "Uninstall completed"
+
+# Development build (fast, no optimizations)
+.PHONY: dev
+dev: $(BIN_DIR)
+	@echo "Building development version..."
+	$(GOBUILD) -o $(BIN_DIR)/$(BINARY_NAME) $(PACKAGE_PATH)
+
+# Release build (optimized)
+.PHONY: release
+release: clean test build-all
+	@echo "Release build completed"
+
+# Show help
+.PHONY: help
+help:
+	@echo "Available targets:"
+	@echo "  build         - Build for current platform"
+	@echo "  build-linux   - Build for Linux AMD64"
+	@echo "  build-linux-arm64 - Build for Linux ARM64"
+	@echo "  build-darwin  - Build for macOS AMD64"
+	@echo "  build-darwin-arm64 - Build for macOS ARM64"
+	@echo "  build-windows - Build for Windows AMD64"
+	@echo "  build-all     - Build for all platforms"
+	@echo "  test          - Run tests"
+	@echo "  test-coverage - Run tests with coverage report"
+	@echo "  clean         - Clean build artifacts"
+	@echo "  deps          - Install dependencies"
+	@echo "  fmt           - Format code"
+	@echo "  lint          - Run linter"
+	@echo "  install       - Install binary to system"
+	@echo "  uninstall     - Remove binary from system"
+	@echo "  dev           - Fast development build"
+	@echo "  release       - Full release build (all platforms)"
+	@echo "  help          - Show this help"
\ No newline at end of file
README.md
@@ -0,0 +1,28 @@
+# `go-mythic`
+
+## 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.
+
+## resources
+
+### the mythic server and `mythic.sh`
+
+This mythic system is setup in a test range with full athorized red team testing (it's a training course).
+We have a user-level access to this server.
+
+`mythic.sh` provides an example curl interface to a test mythic api.
+The `TOKEN` provided in this file should be used for testing all developed functions.
+
+### `Mythic_Scripting`
+
+The `contrib/Mythic_Scripting` directory contains a python implementation of accessing and interacting with the mythic api.
+It is a clone of the `https://github.com/MythicMeta/Mythic_Scripting.git`.
+
+## labs
+
+The `labs` directory will contain markdown files that will contain the steps that are desired to be accomplished/
+They will always be instructing the user to click and use the UI. 
+We will be translating these instructions into functions and cli subcommands.
+
USAGE.md
@@ -0,0 +1,137 @@
+# go-mythic Usage Examples
+
+## Building
+
+### Using Makefile (Recommended)
+```bash
+# Build for current platform
+make build
+
+# Build for all platforms
+make build-all
+
+# Development build (faster)
+make dev
+
+# Clean build artifacts
+make clean
+
+# Show all available targets
+make help
+```
+
+### Manual Build
+```bash
+go build -o bin/go-mythic .
+```
+
+## Basic Commands
+
+### List Active Callbacks
+```bash
+./go-mythic callbacks
+```
+
+### Execute Commands
+```bash
+# Get help for all commands
+./go-mythic exec 1 help
+
+# Get detailed help for a specific command
+./go-mythic exec 1 help ps
+
+# List processes
+./go-mythic exec 1 ps
+
+# Set checkin frequency to 5 seconds
+./go-mythic exec 1 sleep 5
+```
+
+### Interactive Session
+```bash
+./go-mythic interact 1
+```
+
+## Using SOCKS5 Proxy
+
+The CLI supports SOCKS5 proxy routing for all network traffic, replacing the need for proxychains:
+
+```bash
+# Use SOCKS5 proxy (equivalent to proxychains)
+./go-mythic --socks 127.0.0.1:9050 callbacks
+
+# Execute command through proxy
+./go-mythic --socks 127.0.0.1:9050 exec 1 ps
+
+# Interactive session through proxy
+./go-mythic --socks 127.0.0.1:9050 interact 1
+```
+
+## Configuration
+
+### Authentication Token
+Authentication is required via JWT token. Set it using one of these methods:
+
+```bash
+# Set via environment variable (recommended)
+export MYTHIC_API_TOKEN="your-jwt-token-here"
+./go-mythic callbacks
+
+# Set via command line flag
+./go-mythic --token "your-jwt-token-here" callbacks
+
+# Get token from mythic.sh for testing
+export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
+```
+
+### Custom Mythic Server
+```bash
+# Set via environment variable
+export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
+./go-mythic callbacks
+
+# Set via command line flag
+./go-mythic --url "https://your-mythic-server:7443/graphql/" callbacks
+```
+
+### Complete Environment Setup
+```bash
+# Set both URL and token via environment variables
+export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
+export MYTHIC_API_TOKEN="your-jwt-token-here"
+./go-mythic callbacks
+```
+
+## Lab 01 Equivalents
+
+These commands accomplish the goals from labs/01.md:
+
+1. **View Active Callbacks** (equivalent to clicking Active Callbacks in UI):
+   ```bash
+   ./go-mythic callbacks
+   ```
+
+2. **Interact with Agent** (equivalent to right-click → Interact):
+   ```bash
+   ./go-mythic interact <callback_id>
+   ```
+
+3. **Get Help** (equivalent to typing "help" in UI console):
+   ```bash
+   ./go-mythic exec <callback_id> help
+   ```
+
+4. **Get Command Details** (equivalent to typing "help ps"):
+   ```bash
+   ./go-mythic exec <callback_id> help ps
+   ```
+
+5. **List Processes** (equivalent to typing "ps"):
+   ```bash
+   ./go-mythic exec <callback_id> ps
+   ```
+
+6. **Set Checkin Frequency** (equivalent to typing "sleep 5"):
+   ```bash
+   ./go-mythic exec <callback_id> sleep 5
+   ```