From 1e5af4932b872ccc38b2090b1e75274ba16f03c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Fri, 22 May 2026 15:31:09 +0200 Subject: [PATCH 1/4] refactor!: remove Sysdig Secure tools, redirect to @sysdig/secure-mcp-server BREAKING CHANGE: list_runtime_events, get_event_info and get_event_process_tree have been removed from this server. Use the dedicated @sysdig/secure-mcp-server npm package for Sysdig Secure workflows. SysQL tools (generate_sysql, run_sysql) remain because they target both Monitor and Secure datasets. --- AGENTS.md | 20 +- README.md | 41 ++- cmd/server/main.go | 3 - internal/infra/mcp/tools/README.md | 12 +- .../infra/mcp/tools/tool_get_event_info.go | 54 ---- .../mcp/tools/tool_get_event_info_test.go | 134 --------- .../mcp/tools/tool_get_event_process_tree.go | 72 ----- .../tools/tool_get_event_process_tree_test.go | 183 ------------ .../mcp/tools/tool_list_runtime_events.go | 122 -------- .../tools/tool_list_runtime_events_test.go | 157 ---------- internal/infra/sysdig/client_extension.go | 2 - internal/infra/sysdig/client_permissions.go | 95 ++++++ internal/infra/sysdig/client_process_tree.go | 270 ------------------ .../client_process_tree_integration_test.go | 48 ---- .../infra/sysdig/mocks/client_extension.go | 40 --- 15 files changed, 119 insertions(+), 1134 deletions(-) delete mode 100644 internal/infra/mcp/tools/tool_get_event_info.go delete mode 100644 internal/infra/mcp/tools/tool_get_event_info_test.go delete mode 100644 internal/infra/mcp/tools/tool_get_event_process_tree.go delete mode 100644 internal/infra/mcp/tools/tool_get_event_process_tree_test.go delete mode 100644 internal/infra/mcp/tools/tool_list_runtime_events.go delete mode 100644 internal/infra/mcp/tools/tool_list_runtime_events_test.go create mode 100644 internal/infra/sysdig/client_permissions.go delete mode 100644 internal/infra/sysdig/client_process_tree.go delete mode 100644 internal/infra/sysdig/client_process_tree_integration_test.go diff --git a/AGENTS.md b/AGENTS.md index 71d9c3d..badf5d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,13 +4,13 @@ This document is a comprehensive guide for an AI agent tasked with developing an ## 1. Project Overview -**Sysdig MCP Server** is a Go-based Model Context Protocol (MCP) server that exposes Sysdig Secure platform capabilities to LLMs. It provides tools for querying runtime security events, Kubernetes metrics, and executing SysQL queries through multiple transport protocols (stdio, streamable-http, SSE). +**Sysdig MCP Server** is a Go-based Model Context Protocol (MCP) server that exposes Sysdig Monitor platform capabilities to LLMs. It provides tools for querying Kubernetes metrics and executing SysQL queries through multiple transport protocols (stdio, streamable-http, SSE). Sysdig Secure-specific tools live in the separate [@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server) package. ### 1.1. Quick Facts | Topic | Details | | --- | --- | -| **Purpose** | Expose vetted Sysdig Secure workflows to LLMs through MCP tools. | +| **Purpose** | Expose vetted Sysdig Monitor workflows (plus shared SysQL tooling) to LLMs through MCP tools. | | **Tech Stack** | Go 1.25+, `mcp-go`, Cobra CLI, Ginkgo/Gomega, `golangci-lint`, Nix. | | **Entry Point** | `cmd/server/main.go` (Cobra CLI that wires config, Sysdig client, etc.). | | **Dev Shell** | `nix develop` provides a consistent development environment. | @@ -32,10 +32,10 @@ direnv allow ### 2.2. Required Environment Variables -The server requires API credentials to connect to Sysdig Secure. +The server requires API credentials to connect to the Sysdig platform. -- `SYSDIG_MCP_API_HOST`: Sysdig Secure instance URL (e.g., `https://us2.app.sysdig.com`). -- `SYSDIG_MCP_API_TOKEN`: Sysdig Secure API token. +- `SYSDIG_MCP_API_HOST`: Sysdig instance URL (e.g., `https://us2.app.sysdig.com`). +- `SYSDIG_MCP_API_TOKEN`: Sysdig API token. For a full list of optional variables (e.g., for transport configuration), see the project's `README.md`. @@ -133,16 +133,6 @@ fix: correct API endpoint URL chore: update dependencies ``` -### 4.5. Known Flaky Integration Tests - -The process tree integration tests in `internal/infra/sysdig/client_process_tree_integration_test.go` use a **hardcoded event ID** that points to a real Sysdig event. Since Sysdig events have a retention period, this event will eventually be deleted and the tests will fail with a `not found` error. - -**How to fix it:** - -1. Use the `list_runtime_events` MCP tool (or the Sysdig API) to find a recent runtime event that originates from a **syscall/workload source** (not cloud/cloudtrail), as only these have process trees. Filter for `category = "runtime"` and `source = "syscall"`. -2. Verify the event has a process tree by calling `get_event_process_tree` with the event ID. -3. Update the `eventID` variable in the test's `BeforeEach` block with the new event ID. - ## 5. Guides & Reference * **Tools & New Tool Creation:** See `internal/infra/mcp/tools/README.md` diff --git a/README.md b/README.md index 79a5697..55141f8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ [![App Test](https://github.com/sysdiglabs/sysdig-mcp-server/actions/workflows/publish.yaml/badge.svg?branch=main)](https://github.com/sysdiglabs/sysdig-mcp-server/actions/workflows/publish.yaml) +> [!IMPORTANT] +> **Breaking change — this MCP server now focuses on Sysdig Monitor.** +> +> Starting with the next major release, the dedicated Sysdig Secure tools (`list_runtime_events`, `get_event_info`, `get_event_process_tree`) have been removed from this server. For Sysdig Secure use cases, install the new **[@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server)** package, which provides comprehensive coverage of Sysdig Secure capabilities. +> +> The SysQL tools (`generate_sysql`, `run_sysql`) remain available here because they can be used against both Monitor and Secure datasets. + --- ## Table of contents @@ -28,14 +35,16 @@ ## Description -This is an implementation of an [MCP (Model Context Protocol) Server](https://modelcontextprotocol.io/quickstart/server) to allow different LLMs to query information from the Sysdig platform (Monitor and Secure). New tools and functionalities will be added over time following semantic versioning. The goal is to provide a simple and easy-to-use interface for querying information from the Sysdig platform using LLMs. +This is an implementation of an [MCP (Model Context Protocol) Server](https://modelcontextprotocol.io/quickstart/server) that exposes Sysdig Monitor capabilities to LLMs, plus the cross-cutting SysQL tooling. New tools and functionalities will be added over time following semantic versioning. The goal is to provide a simple and easy-to-use interface for querying information from the Sysdig platform using LLMs. + +For Sysdig Secure-specific workflows, use the dedicated [@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server). ## Quickstart Guide Get up and running with the Sysdig MCP Server quickly using our pre-built Docker image. 1. **Get your API Token**: - Go to your Sysdig instance and navigate to **Settings > Sysdig Secure API** (or **Sysdig Monitor API**). Either a Sysdig Secure or Sysdig Monitor API token works. This token is required to authenticate requests to the Sysdig Platform (See the [Configuration](#configuration) section for more details). + Go to your Sysdig instance and navigate to **Settings > Sysdig Monitor API**. This token is required to authenticate requests to the Sysdig Platform (See the [Configuration](#configuration) section for more details). 2. **Configure your MCP client**: @@ -133,28 +142,6 @@ The server dynamically filters the available tools based on the permissions asso > **Note:** When a time window is provided, the underlying PromQL is wrapped in the aggregation appropriate for each tool (`avg_over_time`, `max_over_time`, `min_over_time`, `increase`, etc.) and evaluated at `end`. See [`internal/infra/mcp/tools/README.md`](./internal/infra/mcp/tools/README.md) for the per-tool aggregation table. -### Sysdig Secure - -- **`list_runtime_events`** - - **Description**: List runtime security events from the last given hours, optionally filtered by severity level. - - **Required Permission**: `policy-events.read` - - **Sample Prompt**: "Show me high severity events from the last 2 hours in cluster1" - -- **`get_event_info`** - - **Description**: Retrieve detailed information for a specific security event by its ID. - - **Required Permission**: `policy-events.read` - - **Sample Prompt**: "Get full details for event ID 123abc" - -- **`get_event_process_tree`** - - **Description**: Retrieve the process tree for a specific event (if available). - - **Required Permission**: `policy-events.read` - - **Sample Prompt**: "Get the process tree for event ID abc123" - -- **`run_sysql`** - - **Description**: Execute a pre-written SysQL query directly (use only when user provides explicit query). - - **Required Permission**: `sage.exec`, `risks.read` - - **Sample Prompt**: "Run this query: MATCH CloudResource WHERE type = 'aws_s3_bucket' LIMIT 10" - ### Sysdig Monitor & Sysdig Secure - **`generate_sysql`** @@ -163,6 +150,11 @@ The server dynamically filters the available tools based on the permissions asso - **Sample Prompt**: "List top 10 pods by memory usage in the last hour" - **Note**: The `generate_sysql` tool currently does not work with Service Account tokens and will return a 500 error. For this tool, use an API token assigned to a regular user account. +- **`run_sysql`** + - **Description**: Execute a pre-written SysQL query directly (use only when user provides explicit query). + - **Required Permission**: `sage.exec`, `risks.read` + - **Sample Prompt**: "Run this query: MATCH CloudResource WHERE type = 'aws_s3_bucket' LIMIT 10" + ## Requirements - [Go](https://go.dev/doc/install) 1.26 or higher (if running without Docker). @@ -224,7 +216,6 @@ To use the MCP server tools, your API token needs specific permissions in Sysdig | Permission | Sysdig UI Permission Name | |----------------------|---------------------------------------------| | `metrics-data.read` | Data Access Settings: "Metrics Data" (Read) | -| `policy-events.read` | Threats: "Policy Events" (Read) | | `risks.read` | Risks: "Access to risk feature" (Read) | | `sage.exec` | SysQL: "AI Query Generation" (Exec) | diff --git a/cmd/server/main.go b/cmd/server/main.go index ff7a4ba..84b82a6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -113,9 +113,6 @@ func setupHandler(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *mcp systemClock := clock.NewSystemClock() handler := mcp.NewHandler(Version, sysdigClient) handler.RegisterTools( - tools.NewToolListRuntimeEvents(sysdigClient, systemClock), - tools.NewToolGetEventInfo(sysdigClient), - tools.NewToolGetEventProcessTree(sysdigClient), tools.NewToolRunSysql(sysdigClient), tools.NewToolGenerateSysql(sysdigClient), diff --git a/internal/infra/mcp/tools/README.md b/internal/infra/mcp/tools/README.md index 4f3e312..aafea75 100644 --- a/internal/infra/mcp/tools/README.md +++ b/internal/infra/mcp/tools/README.md @@ -23,20 +23,14 @@ The handler filters tools dynamically based on the Sysdig user's permissions. Ea | `k8s_list_underutilized_pods_cpu_quota` | `tool_k8s_list_underutilized_pods_cpu_quota.go` | List Kubernetes pods with CPU usage below 25% of the quota limit. | `metrics-data.read` | "Show the top 10 underutilized pods by CPU quota in cluster 'production'" | | `k8s_list_underutilized_pods_memory_quota` | `tool_k8s_list_underutilized_pods_memory_quota.go` | List Kubernetes pods with memory usage below 25% of the limit. | `metrics-data.read` | "Show the top 10 underutilized pods by memory quota in cluster 'production'" | -### Sysdig Secure - -| Tool | File | Capability | Required Permissions | Useful Prompts | -|---|---|---|---|---| -| `list_runtime_events` | `tool_list_runtime_events.go` | Query runtime events with filters, cursor, scope. | `policy-events.read` | "Show high severity runtime events from last 2h." | -| `get_event_info` | `tool_get_event_info.go` | Pull full payload for a single policy event. | `policy-events.read` | "Fetch event `abc123` details." | -| `get_event_process_tree` | `tool_get_event_process_tree.go` | Retrieve the process tree for an event when available. | `policy-events.read` | "Show the process tree behind event `abc123`." | -| `run_sysql` | `tool_run_sysql.go` | Execute caller-supplied Sysdig SysQL queries safely. | `sage.exec`, `risks.read` | "Run the following SysQL…". | - ### Sysdig Monitor & Sysdig Secure | Tool | File | Capability | Required Permissions | Useful Prompts | |---|---|---|---|---| | `generate_sysql` | `tool_generate_sysql.go` | Convert natural language to SysQL via Sysdig Sage. | `sage.exec` (does not work with Service Accounts) | "Create a SysQL to list S3 buckets." | +| `run_sysql` | `tool_run_sysql.go` | Execute caller-supplied Sysdig SysQL queries safely. | `sage.exec`, `risks.read` | "Run the following SysQL…". | + +> Dedicated Sysdig Secure tools (runtime events, event details, process trees) live in the separate [@sysdig/secure-mcp-server](https://www.npmjs.com/package/@sysdig/secure-mcp-server) package. ## Historical range (start / end) diff --git a/internal/infra/mcp/tools/tool_get_event_info.go b/internal/infra/mcp/tools/tool_get_event_info.go deleted file mode 100644 index 7b328b0..0000000 --- a/internal/infra/mcp/tools/tool_get_event_info.go +++ /dev/null @@ -1,54 +0,0 @@ -package tools - -import ( - "context" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" -) - -type ToolGetEventInfo struct { - sysdigClient sysdig.ExtendedClientWithResponsesInterface -} - -func NewToolGetEventInfo(client sysdig.ExtendedClientWithResponsesInterface) *ToolGetEventInfo { - return &ToolGetEventInfo{ - sysdigClient: client, - } -} - -func (h *ToolGetEventInfo) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - eventID := request.GetString("event_id", "") - if eventID == "" { - return mcp.NewToolResultErrorf("event_id is required"), nil - } - - response, err := h.sysdigClient.GetEventV1WithResponse(ctx, eventID) - if err != nil { - return mcp.NewToolResultErrorFromErr("error triggering request", err), nil - } - if response.StatusCode() >= 400 { - return mcp.NewToolResultErrorf("error retrieving event, status code: %d, response: %s", response.StatusCode(), response.Body), nil - } - - return mcp.NewToolResultJSON(response.JSON200) -} - -func (h *ToolGetEventInfo) RegisterInServer(s *server.MCPServer) { - tool := mcp.NewTool( - "get_event_info", - mcp.WithDescription("Retrieve detailed information for a specific security event by its ID"), - mcp.WithString( - "event_id", - mcp.Description("The unique identifier of the security event."), - mcp.Required(), - ), - mcp.WithOutputSchema[map[string]any](), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - WithRequiredPermissions("policy-events.read"), - ) - - s.AddTool(tool, h.handle) -} diff --git a/internal/infra/mcp/tools/tool_get_event_info_test.go b/internal/infra/mcp/tools/tool_get_event_info_test.go deleted file mode 100644 index c61b95b..0000000 --- a/internal/infra/mcp/tools/tool_get_event_info_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package tools_test - -import ( - "context" - "fmt" - "net/http" - - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/mcp" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "go.uber.org/mock/gomock" - - inframcp "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp/tools" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig/mocks" -) - -var _ = Describe("ToolGetEventInfo", func() { - var ( - mockClient *mocks.MockExtendedClientWithResponsesInterface - tool *tools.ToolGetEventInfo - ctrl *gomock.Controller - handler *inframcp.Handler - mcpClient *client.Client - ) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - mockClient = mocks.NewMockExtendedClientWithResponsesInterface(ctrl) - mockClient.EXPECT().GetMyPermissionsWithResponse(gomock.Any(), gomock.Any()).Return(&sysdig.GetMyPermissionsResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.UserPermissions{ - Permissions: []string{"policy-events.read"}, - }, - }, nil).AnyTimes() - tool = tools.NewToolGetEventInfo(mockClient) - handler = inframcp.NewHandler("dev", mockClient) - handler.RegisterTools(tool) - - var err error - mcpClient, err = handler.ServeInProcessClient() - Expect(err).NotTo(HaveOccurred()) - - _, err = mcpClient.Initialize(context.Background(), mcp.InitializeRequest{}) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - ctrl.Finish() - }) - - It("should handle a successful request", func(ctx SpecContext) { - eventID := "12345" - mockClient.EXPECT().GetEventV1WithResponse(gomock.Any(), eventID).Return(&sysdig.GetEventV1Response{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.Event{ - Id: eventID, - }, - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_info", - Arguments: map[string]any{ - "event_id": eventID, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - }) - - It("should return an error if event_id is missing", func(ctx SpecContext) { - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_info", - Arguments: map[string]any{}, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should handle a client error", func(ctx SpecContext) { - eventID := "12345" - mockClient.EXPECT().GetEventV1WithResponse(gomock.Any(), eventID).Return(nil, fmt.Errorf("client error")) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_info", - Arguments: map[string]any{ - "event_id": eventID, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should handle a non-200 status code", func(ctx SpecContext) { - eventID := "12345" - mockClient.EXPECT().GetEventV1WithResponse(gomock.Any(), eventID).Return(&sysdig.GetEventV1Response{ - HTTPResponse: &http.Response{ - StatusCode: 404, - }, - Body: []byte("Not Found"), - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_info", - Arguments: map[string]any{ - "event_id": eventID, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) -}) diff --git a/internal/infra/mcp/tools/tool_get_event_process_tree.go b/internal/infra/mcp/tools/tool_get_event_process_tree.go deleted file mode 100644 index d031b79..0000000 --- a/internal/infra/mcp/tools/tool_get_event_process_tree.go +++ /dev/null @@ -1,72 +0,0 @@ -package tools - -import ( - "context" - "errors" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" -) - -type ToolGetEventProcessTree struct { - sysdigClient sysdig.ExtendedClientWithResponsesInterface -} - -func NewToolGetEventProcessTree(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *ToolGetEventProcessTree { - return &ToolGetEventProcessTree{ - sysdigClient: sysdigClient, - } -} - -func (h *ToolGetEventProcessTree) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - eventID := request.GetString("event_id", "") - if eventID == "" { - return mcp.NewToolResultErrorf("event_id is required"), nil - } - - branchesResp, branchesErr := h.sysdigClient.GetProcessTreeBranchesWithResponse(ctx, eventID) - if branchesErr != nil && !errors.Is(branchesErr, sysdig.ErrNotFound) { - return mcp.NewToolResultErrorFromErr("error requesting process tree branches", branchesErr), nil - } - - treesResp, treesErr := h.sysdigClient.GetProcessTreeTreesWithResponse(ctx, eventID) - if treesErr != nil && !errors.Is(treesErr, sysdig.ErrNotFound) { - return mcp.NewToolResultErrorFromErr("error requesting process tree trees", treesErr), nil - } - - if errors.Is(branchesErr, sysdig.ErrNotFound) || errors.Is(treesErr, sysdig.ErrNotFound) { - return mcp.NewToolResultJSON(map[string]any{ - "branches": map[string]any{}, - "tree": map[string]any{}, - "metadata": map[string]any{ - "note": "Process tree not available for this event", - }, - }) - } - - result := map[string]any{ - "branches": *branchesResp.JSON200, - "tree": *treesResp.JSON200, - } - - return mcp.NewToolResultJSON(result) -} - -func (h *ToolGetEventProcessTree) RegisterInServer(s *server.MCPServer) { - tool := mcp.NewTool( - "get_event_process_tree", - mcp.WithDescription("Retrieves the process tree for a specific security event.\nNot every event has a process tree, so this may return an empty tree."), - mcp.WithString( - "event_id", - mcp.Description("The unique identifier of the security event."), - mcp.Required(), - ), - mcp.WithOutputSchema[map[string]any](), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - WithRequiredPermissions("policy-events.read"), - ) - - s.AddTool(tool, h.handle) -} diff --git a/internal/infra/mcp/tools/tool_get_event_process_tree_test.go b/internal/infra/mcp/tools/tool_get_event_process_tree_test.go deleted file mode 100644 index 9f487a4..0000000 --- a/internal/infra/mcp/tools/tool_get_event_process_tree_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package tools_test - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/mcp" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "go.uber.org/mock/gomock" - - inframcp "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp/tools" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig/mocks" -) - -var _ = Describe("ToolGetEventProcessTree", func() { - var ( - mockClient *mocks.MockExtendedClientWithResponsesInterface - tool *tools.ToolGetEventProcessTree - ctrl *gomock.Controller - handler *inframcp.Handler - mcpClient *client.Client - ) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - mockClient = mocks.NewMockExtendedClientWithResponsesInterface(ctrl) - mockClient.EXPECT().GetMyPermissionsWithResponse(gomock.Any(), gomock.Any()).Return(&sysdig.GetMyPermissionsResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.UserPermissions{ - Permissions: []string{"policy-events.read"}, - }, - }, nil).AnyTimes() - tool = tools.NewToolGetEventProcessTree(mockClient) - handler = inframcp.NewHandler("dev", mockClient) - handler.RegisterTools(tool) - - var err error - mcpClient, err = handler.ServeInProcessClient() - Expect(err).NotTo(HaveOccurred()) - - _, err = mcpClient.Initialize(context.Background(), mcp.InitializeRequest{}) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - ctrl.Finish() - }) - - It("should handle a successful request", func(ctx SpecContext) { - eventID := "12345" - branchesData := map[string]any{"branches": "data"} - treesData := map[string]any{"tree": "data"} - - branchesResponse := &sysdig.GetProcessTreeBranchesResponse{ - JSON200: &branchesData, - } - treesResponse := &sysdig.GetProcessTreeTreesResponse{ - JSON200: &treesData, - } - - mockClient.EXPECT().GetProcessTreeBranchesWithResponse(gomock.Any(), eventID).Return(branchesResponse, nil) - mockClient.EXPECT().GetProcessTreeTreesWithResponse(gomock.Any(), eventID).Return(treesResponse, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_process_tree", - Arguments: map[string]any{ - "event_id": eventID, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - - Expect(result.Content).To(HaveLen(1)) - contentItem := result.Content[0] - - textContent, ok := mcp.AsTextContent(contentItem) - Expect(ok).To(BeTrue()) - - var resultMap map[string]any - err = json.Unmarshal([]byte(textContent.Text), &resultMap) - Expect(err).NotTo(HaveOccurred()) - - Expect(resultMap).To(HaveKeyWithValue("branches", branchesData)) - Expect(resultMap).To(HaveKeyWithValue("tree", treesData)) - }) - - It("should return an error if event_id is missing", func(ctx SpecContext) { - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_process_tree", - Arguments: map[string]any{}, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should handle a not found error", func(ctx SpecContext) { - eventID := "12345" - mockClient.EXPECT().GetProcessTreeBranchesWithResponse(gomock.Any(), eventID).Return(nil, sysdig.ErrNotFound) - mockClient.EXPECT().GetProcessTreeTreesWithResponse(gomock.Any(), eventID).Return(nil, sysdig.ErrNotFound) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_process_tree", - Arguments: map[string]any{ - "event_id": eventID, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - - Expect(result.Content).To(HaveLen(1)) - contentItem := result.Content[0] - - textContent, ok := mcp.AsTextContent(contentItem) - Expect(ok).To(BeTrue()) - - var resultMap map[string]any - err = json.Unmarshal([]byte(textContent.Text), &resultMap) - Expect(err).NotTo(HaveOccurred()) - - Expect(resultMap["metadata"]).To(HaveKeyWithValue("note", "Process tree not available for this event")) - }) - - It("should handle a client error on branches request", func(ctx SpecContext) { - eventID := "12345" - mockClient.EXPECT().GetProcessTreeBranchesWithResponse(gomock.Any(), eventID).Return(nil, fmt.Errorf("client error")) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_process_tree", - Arguments: map[string]any{ - "event_id": eventID, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should handle a client error on trees request", func(ctx SpecContext) { - eventID := "12345" - branchesData := map[string]any{"branches": "data"} - branchesResponse := &sysdig.GetProcessTreeBranchesResponse{ - JSON200: &branchesData, - } - mockClient.EXPECT().GetProcessTreeBranchesWithResponse(gomock.Any(), eventID).Return(branchesResponse, nil) - mockClient.EXPECT().GetProcessTreeTreesWithResponse(gomock.Any(), eventID).Return(nil, fmt.Errorf("client error")) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "get_event_process_tree", - Arguments: map[string]any{ - "event_id": eventID, - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) -}) diff --git a/internal/infra/mcp/tools/tool_list_runtime_events.go b/internal/infra/mcp/tools/tool_list_runtime_events.go deleted file mode 100644 index d3782da..0000000 --- a/internal/infra/mcp/tools/tool_list_runtime_events.go +++ /dev/null @@ -1,122 +0,0 @@ -package tools - -import ( - "context" - "time" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/clock" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" -) - -const baseFilter = `not originator in ("benchmarks","compliance","cloudsec","scanning","hostscanning")` - -type ToolListRuntimeEvents struct { - sysdigClient sysdig.ExtendedClientWithResponsesInterface - clock clock.Clock -} - -func NewToolListRuntimeEvents(client sysdig.ExtendedClientWithResponsesInterface, clock clock.Clock) *ToolListRuntimeEvents { - return &ToolListRuntimeEvents{ - sysdigClient: client, - clock: clock, - } -} - -func (h *ToolListRuntimeEvents) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - params := toolRequestToEventsV1Params(request, h.clock) - - response, err := h.sysdigClient.GetEventsV1WithResponse(ctx, params) - if err != nil { - return mcp.NewToolResultErrorFromErr("error triggering request", err), nil - } - if response.StatusCode() >= 400 { - return mcp.NewToolResultErrorf("error retrieving events, status code: %d, response: %s", response.StatusCode(), response.Body), nil - } - - return mcp.NewToolResultJSON(response.JSON200) -} - -func toolRequestToEventsV1Params(request mcp.CallToolRequest, clock clock.Clock) *sysdig.GetEventsV1Params { - params := &sysdig.GetEventsV1Params{ - Limit: new(int32(request.GetInt("limit", 50))), - } - - if cursor := request.GetString("cursor", ""); cursor != "" { - params.Cursor = &cursor - } else { - scopeHours := request.GetInt("scope_hours", 1) - to := clock.Now() - from := to.Add(-time.Duration(scopeHours) * time.Hour) - params.To = new(to.UnixNano()) - params.From = new(from.UnixNano()) - } - - params.Filter = new(baseFilter) - if filterExpr := request.GetString("filter_expr", ""); filterExpr != "" { - params.Filter = new(baseFilter + " and " + filterExpr) - } - - return params -} - -func (h *ToolListRuntimeEvents) RegisterInServer(s *server.MCPServer) { - tool := mcp.NewTool( - "list_runtime_events", - mcp.WithDescription("List runtime security events from the last given hours, optionally filtered by severity level. Includes both Falco-based and machine learning (ML) detections such as crypto mining, anomalous logins, and other ML-detected threats."), - mcp.WithString( - "cursor", - mcp.Description("Cursor for pagination."), - ), - mcp.WithNumber( - "scope_hours", - mcp.Description("Number of hours back from now to include events."), - mcp.DefaultNumber(1), - ), - mcp.WithNumber( - "limit", - mcp.Description("Maximum number of events to return. Maximum allowed value is 200."), - mcp.DefaultNumber(50), - ), - mcp.WithString( - "filter_expr", - mcp.Description(`Logical filter expression to select runtime security events. -Supports operators: =, !=, in, contains, startsWith, exists. -Combine with and/or/not. -Key attributes include: severity (codes "0"-"7"), originator, sourceType, ruleName, rawEventCategory, engine, source, category, kubernetes.cluster.name, host.hostName, container.imageName, aws.accountId, azure.subscriptionId, gcp.projectId, policyId, trigger. - -To find machine learning (ML) detections (e.g. crypto mining, anomalous logins), use engine or source filters: -- All ML events: 'engine = "machineLearning"' -- AWS ML detections: 'source = "agentless-aws-ml"' -- Okta ML detections: 'source = "agentless-okta-ml"' -- By category: 'category = "machine-learning"' - -You can specify the severity of the events based on the following cases: -- high-severity: 'severity in ("0","1","2","3")' -- medium: 'severity in ("4","5")' -- low: 'severity in ("6")' -- info: 'severity in ("7")' -`), - Examples( - `originator in ("awsCloudConnector","gcp") and not sourceType = "auditTrail"`, - `ruleName contains "Login"`, - `severity in ("0","1","2","3")`, - `kubernetes.cluster.name = "cluster1"`, - `host.hostName startsWith "web-"`, - `container.imageName = "nginx:latest" and originator = "hostscanning"`, - `aws.accountId = "123456789012"`, - `policyId = "CIS_Docker_Benchmark"`, - `engine = "machineLearning"`, - `source = "agentless-aws-ml"`, - `engine = "machineLearning" and aws.accountId = "123456789012"`, - ), - ), - mcp.WithOutputSchema[map[string]any](), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - WithRequiredPermissions("policy-events.read"), - ) - - s.AddTool(tool, h.handle) -} diff --git a/internal/infra/mcp/tools/tool_list_runtime_events_test.go b/internal/infra/mcp/tools/tool_list_runtime_events_test.go deleted file mode 100644 index bbc5eb2..0000000 --- a/internal/infra/mcp/tools/tool_list_runtime_events_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package tools_test - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/mcp" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "go.uber.org/mock/gomock" - - mocks_clock "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/clock/mocks" - inframcp "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp/tools" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig/mocks" -) - -var _ = Describe("ToolListRuntimeEvents", func() { - var ( - mockClient *mocks.MockExtendedClientWithResponsesInterface - mockClock *mocks_clock.MockClock - tool *tools.ToolListRuntimeEvents - ctrl *gomock.Controller - handler *inframcp.Handler - mcpClient *client.Client - ) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - mockClient = mocks.NewMockExtendedClientWithResponsesInterface(ctrl) - mockClient.EXPECT().GetMyPermissionsWithResponse(gomock.Any(), gomock.Any()).Return(&sysdig.GetMyPermissionsResponse{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.UserPermissions{ - Permissions: []string{"policy-events.read"}, - }, - }, nil).AnyTimes() - mockClock = mocks_clock.NewMockClock(ctrl) - mockClock.EXPECT().Now().AnyTimes().Return(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)) - tool = tools.NewToolListRuntimeEvents(mockClient, mockClock) - handler = inframcp.NewHandler("dev", mockClient) - handler.RegisterTools(tool) - - var err error - mcpClient, err = handler.ServeInProcessClient() - Expect(err).NotTo(HaveOccurred()) - - _, err = mcpClient.Initialize(context.Background(), mcp.InitializeRequest{}) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - ctrl.Finish() - }) - - It("should handle a successful request and convert params correctly", func(ctx SpecContext) { - mockClient.EXPECT().GetEventsV1WithResponse(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, params *sysdig.GetEventsV1Params, _ ...sysdig.RequestEditorFn) (*sysdig.GetEventsV1Response, error) { - Expect(*params.Limit).To(Equal(int32(10))) - Expect(*params.Filter).To(ContainSubstring("severity = 4")) - Expect(*params.To).To(Equal(int64(946684800000000000))) - Expect(*params.From).To(Equal(int64(946677600000000000))) - - return &sysdig.GetEventsV1Response{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.ListEventsResponse{ - Data: []sysdig.Event{}, - }, - }, nil - }) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "list_runtime_events", - Arguments: map[string]any{ - "limit": 10, - "scope_hours": 2, - "filter_expr": "severity = 4", - }, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - }) - - It("should use default values when no params are provided", func(ctx SpecContext) { - mockClient.EXPECT().GetEventsV1WithResponse(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, params *sysdig.GetEventsV1Params, _ ...sysdig.RequestEditorFn) (*sysdig.GetEventsV1Response, error) { - Expect(*params.Limit).To(Equal(int32(50))) - Expect(*params.Filter).To(Equal(`not originator in ("benchmarks","compliance","cloudsec","scanning","hostscanning")`)) - Expect(*params.To).To(Equal(int64(946684800000000000))) - Expect(*params.From).To(Equal(int64(946681200000000000))) - - return &sysdig.GetEventsV1Response{ - HTTPResponse: &http.Response{ - StatusCode: 200, - }, - JSON200: &sysdig.ListEventsResponse{ - Data: []sysdig.Event{}, - }, - }, nil - }) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "list_runtime_events", - Arguments: map[string]any{}, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeFalse()) - }) - - It("should handle a client error", func(ctx SpecContext) { - mockClient.EXPECT().GetEventsV1WithResponse(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("client error")) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "list_runtime_events", - Arguments: map[string]any{}, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) - - It("should handle a non-200 status code", func(ctx SpecContext) { - mockClient.EXPECT().GetEventsV1WithResponse(gomock.Any(), gomock.Any()).Return(&sysdig.GetEventsV1Response{ - HTTPResponse: &http.Response{ - StatusCode: 401, - }, - Body: []byte("Unauthorized"), - }, nil) - - result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: "list_runtime_events", - Arguments: map[string]any{}, - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(result.Result).NotTo(BeNil()) - Expect(result.IsError).To(BeTrue()) - }) -}) diff --git a/internal/infra/sysdig/client_extension.go b/internal/infra/sysdig/client_extension.go index 7d89e34..2f8f8ad 100644 --- a/internal/infra/sysdig/client_extension.go +++ b/internal/infra/sysdig/client_extension.go @@ -7,8 +7,6 @@ import "context" type ExtendedClientWithResponsesInterface interface { ClientInterface ClientWithResponsesInterface - GetProcessTreeBranchesWithResponse(ctx context.Context, eventID string, reqEditors ...RequestEditorFn) (*GetProcessTreeBranchesResponse, error) - GetProcessTreeTreesWithResponse(ctx context.Context, eventID string, reqEditors ...RequestEditorFn) (*GetProcessTreeTreesResponse, error) GetMyPermissionsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetMyPermissionsResponse, error) GenerateSysqlWithResponse(ctx context.Context, question string, reqEditors ...RequestEditorFn) (*GenerateSysqlResponse, error) } diff --git a/internal/infra/sysdig/client_permissions.go b/internal/infra/sysdig/client_permissions.go new file mode 100644 index 0000000..304e061 --- /dev/null +++ b/internal/infra/sysdig/client_permissions.go @@ -0,0 +1,95 @@ +package sysdig + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" +) + +// UserPermissions defines model for user permissions. +type UserPermissions struct { + CustomerID int `json:"customerId"` + UserID int `json:"userId"` + TeamID int `json:"teamId"` + Permissions []string `json:"permissions"` +} + +// GetMyPermissionsResponse defines model for GetMyPermissions response. +type GetMyPermissionsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *UserPermissions +} + +// ParseGetMyPermissionsResponse parses an HTTP response from a GetMyPermissions call +func ParseGetMyPermissionsResponse(rsp *http.Response) (*GetMyPermissionsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetMyPermissionsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch rsp.StatusCode { + case http.StatusOK: + var dest UserPermissions + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + } + return response, nil +} + +// NewGetMyPermissionsRequest generates requests for GetMyPermissions +func NewGetMyPermissionsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := "/api/users/me/permissions" + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) GetMyPermissions(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetMyPermissionsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *ClientWithResponses) GetMyPermissionsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetMyPermissionsResponse, error) { + rsp, err := c.ClientInterface.(*Client).GetMyPermissions(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetMyPermissionsResponse(rsp) +} diff --git a/internal/infra/sysdig/client_process_tree.go b/internal/infra/sysdig/client_process_tree.go deleted file mode 100644 index 951992d..0000000 --- a/internal/infra/sysdig/client_process_tree.go +++ /dev/null @@ -1,270 +0,0 @@ -package sysdig - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - - "github.com/oapi-codegen/runtime" -) - -// GetProcessTreeBranchesResponse defines model for GetProcessTreeBranches response. -type GetProcessTreeBranchesResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *map[string]any -} - -// ParseGetProcessTreeBranchesResponse parses an HTTP response from a GetProcessTreeBranches call -func ParseGetProcessTreeBranchesResponse(rsp *http.Response) (*GetProcessTreeBranchesResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetProcessTreeBranchesResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch rsp.StatusCode { - case http.StatusOK: - var dest map[string]any - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - case http.StatusNotFound: - return nil, ErrNotFound - } - return response, nil -} - -// GetProcessTreeTreesResponse defines model for GetProcessTreeTrees response. -type GetProcessTreeTreesResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *map[string]any -} - -// ParseGetProcessTreeTreesResponse parses an HTTP response from a GetProcessTreeTrees call -func ParseGetProcessTreeTreesResponse(rsp *http.Response) (*GetProcessTreeTreesResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetProcessTreeTreesResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch rsp.StatusCode { - case http.StatusOK: - var dest map[string]any - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - case http.StatusNotFound: - return nil, ErrNotFound - } - return response, nil -} - -// NewGetProcessTreeBranchesRequest generates requests for GetProcessTreeBranches -func NewGetProcessTreeBranchesRequest(server string, eventID string) (*http.Request, error) { - var err error - - var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "eventId", runtime.ParamLocationPath, eventID) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/api/process-tree/v1/process-branches/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewGetProcessTreeTreesRequest generates requests for GetProcessTreeTrees -func NewGetProcessTreeTreesRequest(server string, eventID string) (*http.Request, error) { - var err error - - var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "eventId", runtime.ParamLocationPath, eventID) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/api/process-tree/v1/process-trees/%s", pathParam0) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -func (c *Client) GetProcessTreeBranches(ctx context.Context, eventID string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetProcessTreeBranchesRequest(c.Server, eventID) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) GetProcessTreeTrees(ctx context.Context, eventID string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetProcessTreeTreesRequest(c.Server, eventID) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *ClientWithResponses) GetProcessTreeBranchesWithResponse(ctx context.Context, eventID string, reqEditors ...RequestEditorFn) (*GetProcessTreeBranchesResponse, error) { - rsp, err := c.ClientInterface.(*Client).GetProcessTreeBranches(ctx, eventID, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetProcessTreeBranchesResponse(rsp) -} - -func (c *ClientWithResponses) GetProcessTreeTreesWithResponse(ctx context.Context, eventID string, reqEditors ...RequestEditorFn) (*GetProcessTreeTreesResponse, error) { - rsp, err := c.ClientInterface.(*Client).GetProcessTreeTrees(ctx, eventID, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetProcessTreeTreesResponse(rsp) -} - -// UserPermissions defines model for user permissions. -type UserPermissions struct { - CustomerID int `json:"customerId"` - UserID int `json:"userId"` - TeamID int `json:"teamId"` - Permissions []string `json:"permissions"` -} - -// GetMyPermissionsResponse defines model for GetMyPermissions response. -type GetMyPermissionsResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *UserPermissions -} - -// ParseGetMyPermissionsResponse parses an HTTP response from a GetMyPermissions call -func ParseGetMyPermissionsResponse(rsp *http.Response) (*GetMyPermissionsResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetMyPermissionsResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch rsp.StatusCode { - case http.StatusOK: - var dest UserPermissions - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - } - return response, nil -} - -// NewGetMyPermissionsRequest generates requests for GetMyPermissions -func NewGetMyPermissionsRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := "/api/users/me/permissions" - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -func (c *Client) GetMyPermissions(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetMyPermissionsRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *ClientWithResponses) GetMyPermissionsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetMyPermissionsResponse, error) { - rsp, err := c.ClientInterface.(*Client).GetMyPermissions(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetMyPermissionsResponse(rsp) -} diff --git a/internal/infra/sysdig/client_process_tree_integration_test.go b/internal/infra/sysdig/client_process_tree_integration_test.go deleted file mode 100644 index 25a6404..0000000 --- a/internal/infra/sysdig/client_process_tree_integration_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package sysdig_test - -import ( - "context" - "net/http" - "os" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" -) - -var _ = Describe("Sysdig Process Tree Client", func() { - var ( - client sysdig.ExtendedClientWithResponsesInterface - eventID string - ) - - BeforeEach(func() { - sysdigURL := os.Getenv("SYSDIG_MCP_API_HOST") - sysdigToken := os.Getenv("SYSDIG_MCP_API_TOKEN") - - var err error - client, err = sysdig.NewSysdigClient(sysdig.WithFixedHostAndToken(sysdigURL, sysdigToken)) - Expect(err).ToNot(HaveOccurred()) - - // This event ID points to a real syscall event with a process tree. It will expire - // due to Sysdig's retention policy. When it does, replace it with a recent syscall event - // that has a process tree (see AGENTS.md §4.5 for instructions). - eventID = "18ad8dc65d2d18e391f742f8e55e7778" - }) - - Context("when fetching the process tree for an event", func() { - It("should return the process tree branches successfully", func() { - resp, err := client.GetProcessTreeBranchesWithResponse(context.Background(), eventID) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.HTTPResponse.StatusCode).To(Equal(http.StatusOK)) - Expect(resp.JSON200).ToNot(BeNil()) - }) - - It("should return the process tree successfully", func() { - resp, err := client.GetProcessTreeTreesWithResponse(context.Background(), eventID) - Expect(err).ToNot(HaveOccurred()) - Expect(resp.HTTPResponse.StatusCode).To(Equal(http.StatusOK)) - Expect(resp.JSON200).ToNot(BeNil()) - }) - }) -}) diff --git a/internal/infra/sysdig/mocks/client_extension.go b/internal/infra/sysdig/mocks/client_extension.go index 08e83ca..0dc8961 100644 --- a/internal/infra/sysdig/mocks/client_extension.go +++ b/internal/infra/sysdig/mocks/client_extension.go @@ -5243,46 +5243,6 @@ func (mr *MockExtendedClientWithResponsesInterfaceMockRecorder) GetPricingV1With return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPricingV1WithResponse", reflect.TypeOf((*MockExtendedClientWithResponsesInterface)(nil).GetPricingV1WithResponse), varargs...) } -// GetProcessTreeBranchesWithResponse mocks base method. -func (m *MockExtendedClientWithResponsesInterface) GetProcessTreeBranchesWithResponse(ctx context.Context, eventID string, reqEditors ...sysdig.RequestEditorFn) (*sysdig.GetProcessTreeBranchesResponse, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, eventID} - for _, a := range reqEditors { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetProcessTreeBranchesWithResponse", varargs...) - ret0, _ := ret[0].(*sysdig.GetProcessTreeBranchesResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetProcessTreeBranchesWithResponse indicates an expected call of GetProcessTreeBranchesWithResponse. -func (mr *MockExtendedClientWithResponsesInterfaceMockRecorder) GetProcessTreeBranchesWithResponse(ctx, eventID any, reqEditors ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, eventID}, reqEditors...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProcessTreeBranchesWithResponse", reflect.TypeOf((*MockExtendedClientWithResponsesInterface)(nil).GetProcessTreeBranchesWithResponse), varargs...) -} - -// GetProcessTreeTreesWithResponse mocks base method. -func (m *MockExtendedClientWithResponsesInterface) GetProcessTreeTreesWithResponse(ctx context.Context, eventID string, reqEditors ...sysdig.RequestEditorFn) (*sysdig.GetProcessTreeTreesResponse, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, eventID} - for _, a := range reqEditors { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetProcessTreeTreesWithResponse", varargs...) - ret0, _ := ret[0].(*sysdig.GetProcessTreeTreesResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetProcessTreeTreesWithResponse indicates an expected call of GetProcessTreeTreesWithResponse. -func (mr *MockExtendedClientWithResponsesInterfaceMockRecorder) GetProcessTreeTreesWithResponse(ctx, eventID any, reqEditors ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, eventID}, reqEditors...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProcessTreeTreesWithResponse", reflect.TypeOf((*MockExtendedClientWithResponsesInterface)(nil).GetProcessTreeTreesWithResponse), varargs...) -} - // GetQueryRangeV1 mocks base method. func (m *MockExtendedClientWithResponsesInterface) GetQueryRangeV1(ctx context.Context, params *sysdig.GetQueryRangeV1Params, reqEditors ...sysdig.RequestEditorFn) (*http.Response, error) { m.ctrl.T.Helper() From b1723dfcbf602fefb24a981fd8a3f38e4e7bab27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Fri, 22 May 2026 15:35:24 +0200 Subject: [PATCH 2/4] build: release v2.0.0 --- package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nix b/package.nix index ce5b687..1a2f3b9 100644 --- a/package.nix +++ b/package.nix @@ -1,7 +1,7 @@ { buildGo126Module, versionCheckHook }: buildGo126Module (finalAttrs: { pname = "sysdig-mcp-server"; - version = "1.0.9"; + version = "2.0.0"; src = ./.; # This hash is automatically re-calculated with `just rehash-package-nix`. This is automatically called as well by `just update`. vendorHash = "sha256-/+0Vi3jhlx7wsQWct4kgfDho2BJRqSqXz8LnzvY7BcI="; From 7eb441b1768e4db1cc887cfd647c9f582962a300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Fri, 22 May 2026 15:44:34 +0200 Subject: [PATCH 3/4] docs: address Copilot review feedback --- AGENTS.md | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index badf5d5..a213bd0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ This document is a comprehensive guide for an AI agent tasked with developing an | Topic | Details | | --- | --- | | **Purpose** | Expose vetted Sysdig Monitor workflows (plus shared SysQL tooling) to LLMs through MCP tools. | -| **Tech Stack** | Go 1.25+, `mcp-go`, Cobra CLI, Ginkgo/Gomega, `golangci-lint`, Nix. | +| **Tech Stack** | Go 1.26+, `mcp-go`, Cobra CLI, Ginkgo/Gomega, `golangci-lint`, Nix. | | **Entry Point** | `cmd/server/main.go` (Cobra CLI that wires config, Sysdig client, etc.). | | **Dev Shell** | `nix develop` provides a consistent development environment. | | **Key Commands** | `just fmt`, `just lint`, `just test`, `just check`, `just update`. | diff --git a/README.md b/README.md index 55141f8..218d246 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ For Sysdig Secure-specific workflows, use the dedicated [@sysdig/secure-mcp-serv Get up and running with the Sysdig MCP Server quickly using our pre-built Docker image. 1. **Get your API Token**: - Go to your Sysdig instance and navigate to **Settings > Sysdig Monitor API**. This token is required to authenticate requests to the Sysdig Platform (See the [Configuration](#configuration) section for more details). + Go to your Sysdig instance and navigate to **Settings > Sysdig Monitor API** (or **Sysdig Secure API** — either works, since SysQL tools accept both). This token is required to authenticate requests to the Sysdig Platform (See the [Configuration](#configuration) section for more details). 2. **Configure your MCP client**: @@ -209,7 +209,7 @@ SYSDIG_MCP_MOUNT_PATH=/sysdig-mcp-server ### API Permissions -To use the MCP server tools, your API token needs specific permissions in Sysdig Secure. We recommend creating a dedicated Service Account (SA) with a custom role containing only the required permissions. +To use the MCP server tools, your API token needs specific permissions on the Sysdig platform. We recommend creating a dedicated Service Account (SA) with a custom role containing only the required permissions. **Permissions Mapping:** @@ -225,7 +225,7 @@ To use the MCP server tools, your API token needs specific permissions in Sysdig **Setting up Permissions:** -1. Go to **Settings > Users & Teams > Roles** in your Sysdig Secure instance +1. Go to **Settings > Users & Teams > Roles** in your Sysdig instance 2. Create a new role with the permissions listed above 3. Assign this role to a Service Account or user 4. Use the API token from that account with the MCP server From 516b01bfbc57b542907a3e9ee0bd8e1cabb1c09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Fri, 22 May 2026 16:03:56 +0200 Subject: [PATCH 4/4] build: refresh base images to fix OS vulns --- docker-base-aarch64.nix | 4 ++-- docker-base-amd64.nix | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-base-aarch64.nix b/docker-base-aarch64.nix index afbf1a6..db25c4d 100644 --- a/docker-base-aarch64.nix +++ b/docker-base-aarch64.nix @@ -1,7 +1,7 @@ { imageName = "quay.io/sysdig/sysdig-mini-ubi9"; - imageDigest = "sha256:e41fa798f88f07e065720f62186b65552ac70d1f33ba36e45c063980950b7bef"; - hash = "sha256-tbngNKTGLym3rENyVKmfYl0PUGAZERyoS78xb3W7AlM="; + imageDigest = "sha256:e88452e808cc23ea2d8659d5800248a6dfa5e143b2d795ea22e59a3b80e8cbfa"; + hash = "sha256-PqrZPRQozf+tcbxNkoHc+887scC+L3jn9O4Mi/y5/rk="; finalImageName = "quay.io/sysdig/sysdig-mini-ubi9"; finalImageTag = "1"; } diff --git a/docker-base-amd64.nix b/docker-base-amd64.nix index b2a2d62..d4a3d7d 100644 --- a/docker-base-amd64.nix +++ b/docker-base-amd64.nix @@ -1,7 +1,7 @@ { imageName = "quay.io/sysdig/sysdig-mini-ubi9"; - imageDigest = "sha256:e41fa798f88f07e065720f62186b65552ac70d1f33ba36e45c063980950b7bef"; - hash = "sha256-T/imK3ZAQXorg1KtCJ2RYseRkI3TmLzQ4U6832ThZCM="; + imageDigest = "sha256:e88452e808cc23ea2d8659d5800248a6dfa5e143b2d795ea22e59a3b80e8cbfa"; + hash = "sha256-VC7ChUceYH2CN3gSlpol3fRAij2QvS71llXN1kwnLSk="; finalImageName = "quay.io/sysdig/sysdig-mini-ubi9"; finalImageTag = "1"; }