main
Raw Download raw file
   1package mythic
   2
   3import (
   4	"context"
   5	"crypto/tls"
   6	"encoding/base64"
   7	"fmt"
   8	"io"
   9	"net"
  10	"net/http"
  11	"net/url"
  12	"regexp"
  13	"slices"
  14	"strings"
  15	"unicode/utf8"
  16
  17	"github.com/machinebox/graphql"
  18	"golang.org/x/net/proxy"
  19)
  20
  21type Client struct {
  22	client     *graphql.Client
  23	token      string
  24	httpClient *http.Client
  25	baseURL    string
  26}
  27
  28type Callback struct {
  29	ID          int    `json:"id"`
  30	DisplayID   int    `json:"display_id"`
  31	User        string `json:"user"`
  32	Host        string `json:"host"`
  33	ProcessName string `json:"process_name"`
  34	PID         int    `json:"pid"`
  35	Description string `json:"description"`
  36	LastCheckin string `json:"last_checkin"`
  37	Active      bool   `json:"active"`
  38	Payload     struct {
  39		PayloadType struct {
  40			Name string `json:"name"`
  41		} `json:"payloadtype"`
  42	} `json:"payload"`
  43}
  44
  45type DetailedCallback struct {
  46	// Basic info
  47	ID          int    `json:"id"`
  48	DisplayID   int    `json:"display_id"`
  49	User        string `json:"user"`
  50	Host        string `json:"host"`
  51	ProcessName string `json:"process_name"`
  52	PID         int    `json:"pid"`
  53	Description string `json:"description"`
  54	LastCheckin string `json:"last_checkin"`
  55	Active      bool   `json:"active"`
  56
  57	// Extended system info
  58	Architecture    string `json:"architecture"`
  59	OS             string `json:"os"`
  60	Domain         string `json:"domain"`
  61	IntegrityLevel int    `json:"integrity_level"`
  62	IP             string `json:"ip"`
  63	ExternalIP     string `json:"external_ip"`
  64	SleepInfo      string `json:"sleep_info"`
  65	ExtraInfo      string `json:"extra_info"`
  66
  67	// Payload info
  68	RegisteredPayloadID int    `json:"registered_payload_id"`
  69	AgentCallbackID     string `json:"agent_callback_id"`
  70	CryptoType          string `json:"crypto_type"`
  71
  72	// Timestamps
  73	InitCallback string `json:"init_callback"`
  74
  75	// Relationships
  76	Payload struct {
  77		ID          int    `json:"id"`
  78		UUID        string `json:"uuid"`
  79		Description string `json:"description"`
  80		PayloadType struct {
  81			Name   string `json:"name"`
  82			Author string `json:"author"`
  83			Note   string `json:"note"`
  84		} `json:"payloadtype"`
  85	} `json:"payload"`
  86
  87	// Operator info
  88	Operator struct {
  89		Username string `json:"username"`
  90	} `json:"operator"`
  91
  92	// Commands
  93	LoadedCommands []LoadedCommand `json:"loadedcommands"`
  94}
  95
  96type LoadedCommand struct {
  97	ID        int    `json:"id"`
  98	Version   int    `json:"version"`
  99	Timestamp string `json:"timestamp"`
 100	Command   struct {
 101		Cmd         string      `json:"cmd"`
 102		Description string      `json:"description"`
 103		HelpCmd     string      `json:"help_cmd"`
 104		Author      string      `json:"author"`
 105		NeedsAdmin  bool        `json:"needs_admin"`
 106		Attributes  interface{} `json:"attributes"`
 107	} `json:"command"`
 108}
 109
 110type Task struct {
 111	ID                  int    `json:"id"`
 112	DisplayID           int    `json:"display_id"`
 113	Command             string `json:"command_name"`
 114	Params              string `json:"params"`
 115	OriginalParams      string `json:"original_params"`
 116	DisplayParams       string `json:"display_params"`
 117	Status              string `json:"status"`
 118	Response            string `json:"response,omitempty"`
 119	CallbackID          int    `json:"callback_id"`
 120	OperatorID          int    `json:"operator_id"`
 121	Timestamp           string `json:"timestamp,omitempty"`
 122	Completed           bool   `json:"completed"`
 123	AgentTaskID         string `json:"agent_task_id"`
 124	ParameterGroupName  string `json:"parameter_group_name"`
 125	TaskingLocation     string `json:"tasking_location"`
 126	IsInteractiveTask   bool   `json:"is_interactive_task"`
 127	InteractiveTaskType int    `json:"interactive_task_type"`
 128	ParentTaskID        int    `json:"parent_task_id"`
 129	Operator            struct {
 130		Username string `json:"username"`
 131	} `json:"operator"`
 132	Token struct {
 133		TokenID int `json:"token_id"`
 134	} `json:"token"`
 135}
 136
 137type File struct {
 138	ID                  int    `json:"id"`
 139	AgentFileID         string `json:"agent_file_id"`
 140	Filename            string `json:"filename_utf8"`
 141	FullRemotePath      string `json:"full_remote_path_utf8"`
 142	Host                string `json:"host"`
 143	Complete            bool   `json:"complete"`
 144	ChunksReceived      int    `json:"chunks_received"`
 145	TotalChunks         int    `json:"total_chunks"`
 146	Timestamp           string `json:"timestamp"`
 147	MD5                 string `json:"md5"`
 148	SHA1                string `json:"sha1"`
 149	IsDownloadFromAgent bool   `json:"is_download_from_agent"`
 150	IsPayload           bool   `json:"is_payload"`
 151	IsScreenshot        bool   `json:"is_screenshot"`
 152	OperatorUsername    string `json:"operator_username"`
 153	TaskID              int    `json:"task_id"`
 154	TaskDisplayID       int    `json:"task_display_id"`
 155	CallbackID          int    `json:"callback_id"`
 156	CallbackHost        string `json:"callback_host"`
 157	CallbackUser        string `json:"callback_user"`
 158}
 159
 160type Payload struct {
 161	ID            int    `json:"id"`
 162	UUID          string `json:"uuid"`
 163	Description   string `json:"description"`
 164	BuildPhase    string `json:"build_phase"`
 165	BuildMessage  string `json:"build_message"`
 166	BuildStderr   string `json:"build_stderr"`
 167	CallbackAlert bool   `json:"callback_alert"`
 168	CreationTime  string `json:"creation_time"`
 169	AutoGenerated bool   `json:"auto_generated"`
 170	Deleted       bool   `json:"deleted"`
 171
 172	// Nested structs
 173	OperatorID       int    `json:"operator_id"`
 174	OperatorUsername string `json:"operator_username"`
 175	PayloadTypeID    int    `json:"payloadtype_id"`
 176	PayloadTypeName  string `json:"payloadtype_name"`
 177
 178	// File info
 179	FileID      int    `json:"file_id"`
 180	Filename    string `json:"filename"`
 181	AgentFileID string `json:"agent_file_id"`
 182
 183	// C2 Profiles
 184	C2Profiles []string `json:"c2_profiles"`
 185}
 186
 187func NewClient(apiURL, token string, insecure bool, socksProxy string) *Client {
 188	client := graphql.NewClient(apiURL)
 189
 190	// Create transport with TLS config
 191	tr := &http.Transport{
 192		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
 193	}
 194
 195	// Configure SOCKS5 proxy if provided
 196	if socksProxy != "" {
 197		dialer, err := proxy.SOCKS5("tcp", socksProxy, nil, proxy.Direct)
 198		if err == nil {
 199			tr.Dial = func(network, addr string) (net.Conn, error) {
 200				return dialer.Dial(network, addr)
 201			}
 202		}
 203	}
 204
 205	httpClient := &http.Client{Transport: tr}
 206	client = graphql.NewClient(apiURL, graphql.WithHTTPClient(httpClient))
 207
 208	// Extract base URL for file downloads (GraphQL URL -> base server URL)
 209	parsedURL, _ := url.Parse(apiURL)
 210	baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
 211
 212	return &Client{
 213		client:     client,
 214		token:      token,
 215		httpClient: httpClient,
 216		baseURL:    baseURL,
 217	}
 218}
 219
 220// isBase64 checks if a string is valid base64 and likely contains binary data
 221func isBase64(s string) bool {
 222	// Must be at least 4 characters for valid base64
 223	if len(s) < 4 {
 224		return false
 225	}
 226
 227	// Check if it matches base64 pattern
 228	base64Regex := regexp.MustCompile(`^[A-Za-z0-9+/]*={0,2}$`)
 229	if !base64Regex.MatchString(s) {
 230		return false
 231	}
 232
 233	// Try to decode
 234	decoded, err := base64.StdEncoding.DecodeString(s)
 235	if err != nil {
 236		return false
 237	}
 238
 239	// If decoded content is not valid UTF-8, it's likely binary data
 240	if !utf8.Valid(decoded) {
 241		return true
 242	}
 243
 244	// If it's valid UTF-8 but the original string looks like base64
 245	// and is significantly longer than the decoded version, it's likely base64
 246	if float64(len(s)) > float64(len(decoded))*1.2 {
 247		return true
 248	}
 249
 250	return false
 251}
 252
 253// decodeResponseText tries to base64 decode response_text, as Mythic typically encodes all responses
 254func decodeResponseText(responseText string) string {
 255	// First, try to base64 decode - Mythic typically base64 encodes all response_text
 256	if decoded, err := base64.StdEncoding.DecodeString(responseText); err == nil {
 257		// If decoded is valid UTF-8 text, return it
 258		if utf8.Valid(decoded) {
 259			return string(decoded)
 260		}
 261		// If it's binary data, return a placeholder message
 262		return fmt.Sprintf("[Binary data: %d bytes]", len(decoded))
 263	}
 264
 265	// If base64 decode fails, return original text
 266	return responseText
 267}
 268
 269func (c *Client) GetCallbacks(ctx context.Context) ([]Callback, error) {
 270	req := graphql.NewRequest(GetCallbacks)
 271
 272	req.Header.Set("apitoken", c.token)
 273
 274	var resp struct {
 275		Callback []Callback `json:"callback"`
 276	}
 277
 278	if err := c.client.Run(ctx, req, &resp); err != nil {
 279		return nil, fmt.Errorf("failed to get callbacks: %w", err)
 280	}
 281
 282	return resp.Callback, nil
 283}
 284
 285func (c *Client) GetActiveCallbacks(ctx context.Context) ([]Callback, error) {
 286	callbacks, err := c.GetCallbacks(ctx)
 287	if err != nil {
 288		return nil, err
 289	}
 290
 291	var active []Callback
 292	for _, callback := range callbacks {
 293		if callback.Active {
 294			active = append(active, callback)
 295		}
 296	}
 297
 298	return active, nil
 299}
 300
 301func (c *Client) GetDetailedCallback(ctx context.Context, callbackID int) (*DetailedCallback, error) {
 302	req := graphql.NewRequest(GetDetailedCallback)
 303
 304	req.Var("callback_id", callbackID)
 305	req.Header.Set("apitoken", c.token)
 306
 307	var resp struct {
 308		Callback *DetailedCallback `json:"callback_by_pk"`
 309	}
 310
 311	if err := c.client.Run(ctx, req, &resp); err != nil {
 312		return nil, fmt.Errorf("failed to get detailed callback: %w", err)
 313	}
 314
 315	if resp.Callback == nil {
 316		return nil, fmt.Errorf("callback %d not found", callbackID)
 317	}
 318
 319	return resp.Callback, nil
 320}
 321
 322func (c *Client) CreateTask(ctx context.Context, callbackID int, command, params string) (*Task, error) {
 323	req := graphql.NewRequest(CreateTask)
 324
 325	req.Var("callback_id", callbackID)
 326	req.Var("command", command)
 327	req.Var("params", params)
 328	req.Var("tasking_location", "command_line")
 329	req.Header.Set("apitoken", c.token)
 330
 331	var resp struct {
 332		CreateTask struct {
 333			Status    string `json:"status"`
 334			ID        int    `json:"id"`
 335			DisplayID int    `json:"display_id"`
 336			Error     string `json:"error"`
 337		} `json:"createTask"`
 338	}
 339
 340	if err := c.client.Run(ctx, req, &resp); err != nil {
 341		return nil, fmt.Errorf("failed to create task: %w", err)
 342	}
 343
 344	if resp.CreateTask.Status != "success" {
 345		return nil, fmt.Errorf("task creation failed: %s", resp.CreateTask.Error)
 346	}
 347
 348	return &Task{
 349		ID:         resp.CreateTask.ID,
 350		DisplayID:  resp.CreateTask.DisplayID,
 351		Command:    command,
 352		Params:     params,
 353		Status:     "submitted",
 354		CallbackID: callbackID,
 355	}, nil
 356}
 357
 358func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error) {
 359	req := graphql.NewRequest(GetTask)
 360
 361	req.Var("id", taskID)
 362	req.Header.Set("apitoken", c.token)
 363
 364	var resp struct {
 365		Task struct {
 366			ID                  int    `json:"id"`
 367			DisplayID           int    `json:"display_id"`
 368			CommandName         string `json:"command_name"`
 369			Params              string `json:"params"`
 370			OriginalParams      string `json:"original_params"`
 371			DisplayParams       string `json:"display_params"`
 372			Status              string `json:"status"`
 373			Completed           bool   `json:"completed"`
 374			Timestamp           string `json:"timestamp"`
 375			AgentTaskID         string `json:"agent_task_id"`
 376			ParameterGroupName  string `json:"parameter_group_name"`
 377			TaskingLocation     string `json:"tasking_location"`
 378			IsInteractiveTask   bool   `json:"is_interactive_task"`
 379			InteractiveTaskType int    `json:"interactive_task_type"`
 380			ParentTaskID        int    `json:"parent_task_id"`
 381			Operator            struct {
 382				Username string `json:"username"`
 383			} `json:"operator"`
 384			Token struct {
 385				TokenID int `json:"token_id"`
 386			} `json:"token"`
 387			Callback struct {
 388				ID        int `json:"id"`
 389				DisplayID int `json:"display_id"`
 390			} `json:"callback"`
 391		} `json:"task_by_pk"`
 392	}
 393
 394	if err := c.client.Run(ctx, req, &resp); err != nil {
 395		return nil, fmt.Errorf("failed to get task: %w", err)
 396	}
 397
 398
 399
 400	// Get task responses
 401	responseReq := graphql.NewRequest(GetTaskResponses)
 402
 403	responseReq.Var("task_display_id", resp.Task.DisplayID)
 404	responseReq.Header.Set("apitoken", c.token)
 405
 406	var responseResp struct {
 407		Response []struct {
 408			ResponseText string `json:"response_text"`
 409			Timestamp    string `json:"timestamp"`
 410		} `json:"response"`
 411	}
 412
 413	if err := c.client.Run(ctx, responseReq, &responseResp); err != nil {
 414		return nil, fmt.Errorf("failed to get task response: %w", err)
 415	}
 416
 417	// Concatenate all response chunks in chronological order
 418	var responseBuilder strings.Builder
 419	for _, resp := range responseResp.Response {
 420		decodedText := decodeResponseText(resp.ResponseText)
 421		responseBuilder.WriteString(decodedText)
 422	}
 423	response := responseBuilder.String()
 424
 425	status := resp.Task.Status
 426	if resp.Task.Completed {
 427		status = "completed"
 428	}
 429
 430	return &Task{
 431		ID:                  resp.Task.ID,
 432		DisplayID:           resp.Task.DisplayID,
 433		Command:             resp.Task.CommandName,
 434		Params:              resp.Task.Params,
 435		OriginalParams:      resp.Task.OriginalParams,
 436		DisplayParams:       resp.Task.DisplayParams,
 437		Status:              status,
 438		Response:            response,
 439		CallbackID:          resp.Task.Callback.ID,
 440		Completed:           resp.Task.Completed,
 441		Timestamp:           resp.Task.Timestamp,
 442		AgentTaskID:         resp.Task.AgentTaskID,
 443		ParameterGroupName:  resp.Task.ParameterGroupName,
 444		TaskingLocation:     resp.Task.TaskingLocation,
 445		IsInteractiveTask:   resp.Task.IsInteractiveTask,
 446		InteractiveTaskType: resp.Task.InteractiveTaskType,
 447		ParentTaskID:        resp.Task.ParentTaskID,
 448		Operator: struct {
 449			Username string `json:"username"`
 450		}{
 451			Username: resp.Task.Operator.Username,
 452		},
 453		Token: struct {
 454			TokenID int `json:"token_id"`
 455		}{
 456			TokenID: resp.Task.Token.TokenID,
 457		},
 458	}, nil
 459}
 460
 461// GetTasksWithResponsesByIDs efficiently fetches multiple tasks with their responses in a single query
 462func (c *Client) GetTasksWithResponsesByIDs(ctx context.Context, taskIDs []int) ([]Task, error) {
 463	if len(taskIDs) == 0 {
 464		return []Task{}, nil
 465	}
 466
 467	req := graphql.NewRequest(GetTasksWithResponsesByIDs)
 468	req.Var("task_ids", taskIDs)
 469	req.Header.Set("apitoken", c.token)
 470
 471	var resp struct {
 472		Task []struct {
 473			ID             int    `json:"id"`
 474			DisplayID      int    `json:"display_id"`
 475			CommandName    string `json:"command_name"`
 476			OriginalParams string `json:"original_params"`
 477			DisplayParams  string `json:"display_params"`
 478			Status         string `json:"status"`
 479			Completed      bool   `json:"completed"`
 480			Timestamp      string `json:"timestamp"`
 481			Callback       struct {
 482				ID        int    `json:"id"`
 483				DisplayID int    `json:"display_id"`
 484				Host      string `json:"host"`
 485				User      string `json:"user"`
 486			} `json:"callback"`
 487			Responses []struct {
 488				ResponseText string `json:"response_text"`
 489				Timestamp    string `json:"timestamp"`
 490			} `json:"responses"`
 491		} `json:"task"`
 492	}
 493
 494	if err := c.client.Run(ctx, req, &resp); err != nil {
 495		return nil, fmt.Errorf("failed to get tasks with responses: %w", err)
 496	}
 497
 498	tasks := make([]Task, 0, len(resp.Task))
 499	for _, task := range resp.Task {
 500		// Concatenate all response chunks in chronological order
 501		var responseBuilder strings.Builder
 502		for _, respChunk := range task.Responses {
 503			decodedText := decodeResponseText(respChunk.ResponseText)
 504			responseBuilder.WriteString(decodedText)
 505		}
 506		response := responseBuilder.String()
 507
 508		status := task.Status
 509		if task.Completed {
 510			status = "completed"
 511		}
 512
 513		tasks = append(tasks, Task{
 514			ID:         task.ID,
 515			DisplayID:  task.DisplayID,
 516			Command:    task.CommandName,
 517			Params:     task.DisplayParams,
 518			Status:     status,
 519			Response:   response,
 520			CallbackID: task.Callback.ID,
 521			Completed:  task.Completed,
 522			Timestamp:  task.Timestamp,
 523		})
 524	}
 525
 526	return tasks, nil
 527}
 528
 529func (c *Client) GetCallbackTasks(ctx context.Context, callbackID int) ([]Task, error) {
 530	req := graphql.NewRequest(GetCallbackTasks)
 531
 532	req.Var("callback_id", callbackID)
 533	req.Header.Set("apitoken", c.token)
 534
 535	var resp struct {
 536		Task []struct {
 537			ID             int    `json:"id"`
 538			DisplayID      int    `json:"display_id"`
 539			CommandName    string `json:"command_name"`
 540			OriginalParams string `json:"original_params"`
 541			DisplayParams  string `json:"display_params"`
 542			Status         string `json:"status"`
 543			Completed      bool   `json:"completed"`
 544			Timestamp      string `json:"timestamp"`
 545			Callback       struct {
 546				ID        int `json:"id"`
 547				DisplayID int `json:"display_id"`
 548			} `json:"callback"`
 549		} `json:"task"`
 550	}
 551
 552	if err := c.client.Run(ctx, req, &resp); err != nil {
 553		return nil, fmt.Errorf("failed to get callback tasks: %w", err)
 554	}
 555
 556	var tasks []Task
 557	for _, t := range resp.Task {
 558		status := t.Status
 559		if t.Completed {
 560			status = "completed"
 561		}
 562
 563		tasks = append(tasks, Task{
 564			ID:         t.ID,
 565			DisplayID:  t.DisplayID,
 566			Command:    t.CommandName,
 567			Params:     t.DisplayParams,
 568			Status:     status,
 569			CallbackID: t.Callback.ID,
 570			Timestamp:  t.Timestamp,
 571			Completed:  t.Completed,
 572		})
 573	}
 574
 575	return tasks, nil
 576}
 577
 578func (c *Client) GetDownloadedFiles(ctx context.Context) ([]File, error) {
 579	req := graphql.NewRequest(GetDownloadedFiles)
 580
 581	req.Header.Set("apitoken", c.token)
 582
 583	var resp struct {
 584		Filemeta []struct {
 585			ID             int    `json:"id"`
 586			AgentFileID    string `json:"agent_file_id"`
 587			Filename       string `json:"filename_utf8"`
 588			FullRemotePath string `json:"full_remote_path_utf8"`
 589			Host           string `json:"host"`
 590			Complete       bool   `json:"complete"`
 591			ChunksReceived int    `json:"chunks_received"`
 592			TotalChunks    int    `json:"total_chunks"`
 593			Timestamp      string `json:"timestamp"`
 594			MD5            string `json:"md5"`
 595			SHA1           string `json:"sha1"`
 596			Operator       struct {
 597				Username string `json:"username"`
 598			} `json:"operator"`
 599			Task struct {
 600				ID        int `json:"id"`
 601				DisplayID int `json:"display_id"`
 602				Callback  struct {
 603					ID        int    `json:"id"`
 604					DisplayID int    `json:"display_id"`
 605					Host      string `json:"host"`
 606					User      string `json:"user"`
 607				} `json:"callback"`
 608			} `json:"task"`
 609		} `json:"filemeta"`
 610	}
 611
 612	if err := c.client.Run(ctx, req, &resp); err != nil {
 613		return nil, fmt.Errorf("failed to get downloaded files: %w", err)
 614	}
 615
 616	var files []File
 617	for _, f := range resp.Filemeta {
 618		files = append(files, File{
 619			ID:               f.ID,
 620			AgentFileID:      f.AgentFileID,
 621			Filename:         f.Filename,
 622			FullRemotePath:   f.FullRemotePath,
 623			Host:             f.Host,
 624			Complete:         f.Complete,
 625			ChunksReceived:   f.ChunksReceived,
 626			TotalChunks:      f.TotalChunks,
 627			Timestamp:        f.Timestamp,
 628			MD5:              f.MD5,
 629			SHA1:             f.SHA1,
 630			OperatorUsername: f.Operator.Username,
 631			TaskID:           f.Task.ID,
 632			TaskDisplayID:    f.Task.DisplayID,
 633			CallbackID:       f.Task.Callback.ID,
 634			CallbackHost:     f.Task.Callback.Host,
 635			CallbackUser:     f.Task.Callback.User,
 636		})
 637	}
 638
 639	return files, nil
 640}
 641
 642func (c *Client) GetAllFiles(ctx context.Context) ([]File, error) {
 643	req := graphql.NewRequest(GetAllFiles)
 644
 645	req.Header.Set("apitoken", c.token)
 646
 647	var resp struct {
 648		Filemeta []struct {
 649			ID                  int    `json:"id"`
 650			AgentFileID         string `json:"agent_file_id"`
 651			Filename            string `json:"filename_utf8"`
 652			FullRemotePath      string `json:"full_remote_path_utf8"`
 653			Host                string `json:"host"`
 654			Complete            bool   `json:"complete"`
 655			ChunksReceived      int    `json:"chunks_received"`
 656			TotalChunks         int    `json:"total_chunks"`
 657			Timestamp           string `json:"timestamp"`
 658			MD5                 string `json:"md5"`
 659			SHA1                string `json:"sha1"`
 660			IsDownloadFromAgent bool   `json:"is_download_from_agent"`
 661			IsPayload           bool   `json:"is_payload"`
 662			IsScreenshot        bool   `json:"is_screenshot"`
 663			Operator            struct {
 664				Username string `json:"username"`
 665			} `json:"operator"`
 666			Task struct {
 667				ID        int `json:"id"`
 668				DisplayID int `json:"display_id"`
 669				Callback  struct {
 670					ID        int    `json:"id"`
 671					DisplayID int    `json:"display_id"`
 672					Host      string `json:"host"`
 673					User      string `json:"user"`
 674				} `json:"callback"`
 675			} `json:"task"`
 676		} `json:"filemeta"`
 677	}
 678
 679	if err := c.client.Run(ctx, req, &resp); err != nil {
 680		return nil, fmt.Errorf("failed to get all files: %w", err)
 681	}
 682
 683	var files []File
 684	for _, f := range resp.Filemeta {
 685		files = append(files, File{
 686			ID:                  f.ID,
 687			AgentFileID:         f.AgentFileID,
 688			Filename:            f.Filename,
 689			FullRemotePath:      f.FullRemotePath,
 690			Host:                f.Host,
 691			Complete:            f.Complete,
 692			ChunksReceived:      f.ChunksReceived,
 693			TotalChunks:         f.TotalChunks,
 694			Timestamp:           f.Timestamp,
 695			MD5:                 f.MD5,
 696			SHA1:                f.SHA1,
 697			IsDownloadFromAgent: f.IsDownloadFromAgent,
 698			IsPayload:           f.IsPayload,
 699			IsScreenshot:        f.IsScreenshot,
 700			OperatorUsername:    f.Operator.Username,
 701			TaskID:              f.Task.ID,
 702			TaskDisplayID:       f.Task.DisplayID,
 703			CallbackID:          f.Task.Callback.ID,
 704			CallbackHost:        f.Task.Callback.Host,
 705			CallbackUser:        f.Task.Callback.User,
 706		})
 707	}
 708
 709	return files, nil
 710}
 711
 712func (c *Client) DownloadFile(ctx context.Context, fileUUID string) ([]byte, error) {
 713	downloadURL := fmt.Sprintf("%s/direct/download/%s", c.baseURL, fileUUID)
 714
 715	req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
 716	if err != nil {
 717		return nil, fmt.Errorf("failed to create download request: %w", err)
 718	}
 719
 720	req.Header.Set("apitoken", c.token)
 721
 722	resp, err := c.httpClient.Do(req)
 723	if err != nil {
 724		return nil, fmt.Errorf("failed to download file: %w", err)
 725	}
 726	defer resp.Body.Close()
 727
 728	if resp.StatusCode != http.StatusOK {
 729		return nil, fmt.Errorf("download failed with status %d: %s", resp.StatusCode, resp.Status)
 730	}
 731
 732	data, err := io.ReadAll(resp.Body)
 733	if err != nil {
 734		return nil, fmt.Errorf("failed to read file data: %w", err)
 735	}
 736
 737	return data, nil
 738}
 739
 740func (c *Client) GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]Task, error) {
 741	req := graphql.NewRequest(GetTasksWithResponses)
 742
 743	if callbackID > 0 {
 744		req.Var("callback_id", callbackID)
 745	}
 746	if limit > 0 {
 747		req.Var("limit", limit)
 748	}
 749	req.Header.Set("apitoken", c.token)
 750
 751	var resp struct {
 752		Task []struct {
 753			ID             int    `json:"id"`
 754			DisplayID      int    `json:"display_id"`
 755			CommandName    string `json:"command_name"`
 756			OriginalParams string `json:"original_params"`
 757			DisplayParams  string `json:"display_params"`
 758			Status         string `json:"status"`
 759			Completed      bool   `json:"completed"`
 760			Timestamp      string `json:"timestamp"`
 761			Callback       struct {
 762				ID        int    `json:"id"`
 763				DisplayID int    `json:"display_id"`
 764				Host      string `json:"host"`
 765				User      string `json:"user"`
 766			} `json:"callback"`
 767			Responses []struct {
 768				ResponseText string `json:"response_text"`
 769				Timestamp    string `json:"timestamp"`
 770			} `json:"responses"`
 771		} `json:"task"`
 772	}
 773
 774	if err := c.client.Run(ctx, req, &resp); err != nil {
 775		return nil, fmt.Errorf("failed to get tasks with responses: %w", err)
 776	}
 777
 778	var tasks []Task
 779	for _, t := range resp.Task {
 780		// Concatenate all response chunks in chronological order
 781		var responseBuilder strings.Builder
 782		for _, resp := range t.Responses {
 783			decodedText := decodeResponseText(resp.ResponseText)
 784			responseBuilder.WriteString(decodedText)
 785		}
 786		response := responseBuilder.String()
 787
 788		status := t.Status
 789		if t.Completed {
 790			status = "completed"
 791		}
 792
 793		tasks = append(tasks, Task{
 794			ID:         t.ID,
 795			DisplayID:  t.DisplayID,
 796			Command:    t.CommandName,
 797			Params:     t.DisplayParams,
 798			Status:     status,
 799			Response:   response,
 800			CallbackID: t.Callback.ID,
 801			Timestamp:  t.Timestamp,
 802			Completed:  t.Completed,
 803		})
 804	}
 805
 806	return tasks, nil
 807}
 808
 809func (c *Client) GetAllTasks(ctx context.Context, limit int) ([]Task, error) {
 810	req := graphql.NewRequest(GetAllTasks)
 811
 812	if limit > 0 {
 813		req.Var("limit", limit)
 814	}
 815	req.Header.Set("apitoken", c.token)
 816
 817	var resp struct {
 818		Task []struct {
 819			ID             int    `json:"id"`
 820			DisplayID      int    `json:"display_id"`
 821			CommandName    string `json:"command_name"`
 822			OriginalParams string `json:"original_params"`
 823			DisplayParams  string `json:"display_params"`
 824			Status         string `json:"status"`
 825			Completed      bool   `json:"completed"`
 826			Timestamp      string `json:"timestamp"`
 827			Callback       struct {
 828				ID        int    `json:"id"`
 829				DisplayID int    `json:"display_id"`
 830				Host      string `json:"host"`
 831				User      string `json:"user"`
 832			} `json:"callback"`
 833		} `json:"task"`
 834	}
 835
 836	if err := c.client.Run(ctx, req, &resp); err != nil {
 837		return nil, fmt.Errorf("failed to get all tasks: %w", err)
 838	}
 839
 840	var tasks []Task
 841	for _, t := range resp.Task {
 842		status := t.Status
 843		if t.Completed {
 844			status = "completed"
 845		}
 846
 847		tasks = append(tasks, Task{
 848			ID:         t.ID,
 849			DisplayID:  t.DisplayID,
 850			Command:    t.CommandName,
 851			Params:     t.DisplayParams,
 852			Status:     status,
 853			CallbackID: t.Callback.ID,
 854			Timestamp:  t.Timestamp,
 855			Completed:  t.Completed,
 856		})
 857	}
 858
 859	// Reverse to show newest (highest ID) last
 860	slices.Reverse(tasks)
 861
 862	return tasks, nil
 863}
 864
 865func (c *Client) GetAllTasksWithResponses(ctx context.Context, limit int) ([]Task, error) {
 866	req := graphql.NewRequest(GetAllTasksWithResponsesFromAllCallbacks)
 867
 868	if limit > 0 {
 869		req.Var("limit", limit)
 870	}
 871	req.Header.Set("apitoken", c.token)
 872
 873	var resp struct {
 874		Task []struct {
 875			ID             int    `json:"id"`
 876			DisplayID      int    `json:"display_id"`
 877			CommandName    string `json:"command_name"`
 878			OriginalParams string `json:"original_params"`
 879			DisplayParams  string `json:"display_params"`
 880			Status         string `json:"status"`
 881			Completed      bool   `json:"completed"`
 882			Timestamp      string `json:"timestamp"`
 883			Callback       struct {
 884				ID        int    `json:"id"`
 885				DisplayID int    `json:"display_id"`
 886				Host      string `json:"host"`
 887				User      string `json:"user"`
 888			} `json:"callback"`
 889			Responses []struct {
 890				ResponseText string `json:"response_text"`
 891				Timestamp    string `json:"timestamp"`
 892			} `json:"responses"`
 893		} `json:"task"`
 894	}
 895
 896	if err := c.client.Run(ctx, req, &resp); err != nil {
 897		return nil, fmt.Errorf("failed to get all tasks with responses: %w", err)
 898	}
 899
 900	var tasks []Task
 901	for _, t := range resp.Task {
 902		// Concatenate all response chunks in chronological order
 903		var responseBuilder strings.Builder
 904		for _, resp := range t.Responses {
 905			decodedText := decodeResponseText(resp.ResponseText)
 906			responseBuilder.WriteString(decodedText)
 907		}
 908		response := responseBuilder.String()
 909
 910		status := t.Status
 911		if t.Completed {
 912			status = "completed"
 913		}
 914
 915		tasks = append(tasks, Task{
 916			ID:         t.ID,
 917			DisplayID:  t.DisplayID,
 918			Command:    t.CommandName,
 919			Params:     t.DisplayParams,
 920			Status:     status,
 921			Response:   response,
 922			CallbackID: t.Callback.ID,
 923			Timestamp:  t.Timestamp,
 924			Completed:  t.Completed,
 925		})
 926	}
 927
 928	return tasks, nil
 929}
 930
 931func (c *Client) GetPayloads(ctx context.Context) ([]Payload, error) {
 932	req := graphql.NewRequest(GetPayloads)
 933
 934	req.Header.Set("apitoken", c.token)
 935
 936	var resp struct {
 937		Payload []struct {
 938			ID            int    `json:"id"`
 939			UUID          string `json:"uuid"`
 940			Description   string `json:"description"`
 941			BuildPhase    string `json:"build_phase"`
 942			BuildMessage  string `json:"build_message"`
 943			BuildStderr   string `json:"build_stderr"`
 944			CallbackAlert bool   `json:"callback_alert"`
 945			CreationTime  string `json:"creation_time"`
 946			AutoGenerated bool   `json:"auto_generated"`
 947			Deleted       bool   `json:"deleted"`
 948			Operator      struct {
 949				ID       int    `json:"id"`
 950				Username string `json:"username"`
 951			} `json:"operator"`
 952			PayloadType struct {
 953				ID   int    `json:"id"`
 954				Name string `json:"name"`
 955			} `json:"payloadtype"`
 956			Filemetum struct {
 957				ID          int    `json:"id"`
 958				AgentFileID string `json:"agent_file_id"`
 959				Filename    string `json:"filename_utf8"`
 960			} `json:"filemetum"`
 961			PayloadC2Profiles []struct {
 962				C2Profile struct {
 963					Name             string `json:"name"`
 964					Running          bool   `json:"running"`
 965					IsP2P            bool   `json:"is_p2p"`
 966					ContainerRunning bool   `json:"container_running"`
 967				} `json:"c2profile"`
 968			} `json:"payloadc2profiles"`
 969		} `json:"payload"`
 970	}
 971
 972	if err := c.client.Run(ctx, req, &resp); err != nil {
 973		return nil, fmt.Errorf("failed to get payloads: %w", err)
 974	}
 975
 976	var payloads []Payload
 977	for _, p := range resp.Payload {
 978		// Collect C2 profile names
 979		var c2Profiles []string
 980		for _, profile := range p.PayloadC2Profiles {
 981			c2Profiles = append(c2Profiles, profile.C2Profile.Name)
 982		}
 983
 984		payloads = append(payloads, Payload{
 985			ID:               p.ID,
 986			UUID:             p.UUID,
 987			Description:      p.Description,
 988			BuildPhase:       p.BuildPhase,
 989			BuildMessage:     p.BuildMessage,
 990			BuildStderr:      p.BuildStderr,
 991			CallbackAlert:    p.CallbackAlert,
 992			CreationTime:     p.CreationTime,
 993			AutoGenerated:    p.AutoGenerated,
 994			Deleted:          p.Deleted,
 995			OperatorID:       p.Operator.ID,
 996			OperatorUsername: p.Operator.Username,
 997			PayloadTypeID:    p.PayloadType.ID,
 998			PayloadTypeName:  p.PayloadType.Name,
 999			FileID:           p.Filemetum.ID,
1000			Filename:         p.Filemetum.Filename,
1001			AgentFileID:      p.Filemetum.AgentFileID,
1002			C2Profiles:       c2Profiles,
1003		})
1004	}
1005
1006	return payloads, nil
1007}