diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9f7effe..d6f0f4f 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,8 +6,6 @@ name: Docker # documentation. on: - schedule: - - cron: "27 0 * * *" push: branches: ["main"] # Publish semver tags as releases. diff --git a/CLAUDE.md b/CLAUDE.md index 7c3a823..89d0ac5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,6 +60,7 @@ func toolDomainResourceAction(client *v1client.V1ApiClient) mcpserver.ServerTool - Server validates annotations match toolset registration (panics on mismatch) - Add read tools to `ToolsetsReadOnly{Domain}`, write tools to `ToolsetsWrite{Domain}` in respective `tools/*.go` files - Register toolsets in `server.go` +- **REQUIRED:** Update `README.md` Tools section with new tools (include tool name, description, and mode) ### API Paths Paths must NOT start with `/`. The client's `Parse` method handles URL joining: @@ -102,6 +103,7 @@ Used by domains requiring custom headers (e.g., AI Security uses `TMV1-Applicati | Email | `v1client/email.go` | `tools/email.go` | `v3.0/email/` | | Container | `v1client/container.go` | `tools/container.go` | `v3.0/containerSecurity/` | | Endpoint | `v1client/endpoint.go` | `tools/endpoint.go` | `v3.0/endpointSecurity/` | +| Threat Intel | `v1client/threatintel.go` | `tools/threatintel.go` | `v3.0/threatintel/` | **Note:** OAT (Observed Attack Techniques) has its own client file but tools are registered under the Workbench toolset. diff --git a/README.md b/README.md index ffd7ce0..0d04bd8 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,26 @@ Alternatively, copy the following into your `settings.json`. | ---- | ----------- | ---- | | `aisecurity_guardrails_apply` | Evaluates prompts against AI guard policies and returns the recommended action (Allow/Block) with reasons for any policy violations detected | `read` | +### Threat Intelligence + +| Tool | Description | Mode | +| ---- | ----------- | ---- | +| `threatintel_suspicious_objects_list` | Retrieves information about domains, file SHA-1, file SHA-256, IP addresses, email addresses, or URLs in the Suspicious Object List | `read` | +| `threatintel_suspicious_objects_add` | Adds information about domains, file SHA-1, file SHA-256, IP addresses, email addresses, or URLs to the Suspicious Object List | `write` | +| `threatintel_suspicious_objects_delete` | Deletes information about domains, file SHA-1, file SHA-256, IP addresses, email addresses, or URLs from the Suspicious Object List | `write` | +| `threatintel_exceptions_list` | Retrieves information about domains, file SHA-1, file SHA-256, IP addresses, sender addresses, or URLs in the Exception List | `read` | +| `threatintel_exceptions_add` | Adds domains, file SHA-1, file SHA-256, IP addresses, sender addresses, or URLs to the Exception List | `write` | +| `threatintel_exceptions_delete` | Deletes the specified objects from the Exception List | `write` | +| `threatintel_intelligence_reports_list` | Retrieves a list of custom intelligence reports created from imported or retrieved data | `read` | +| `threatintel_intelligence_report_get` | Downloads a custom intelligence report as a STIX Bundle | `read` | +| `threatintel_intelligence_reports_delete` | Deletes the specified custom intelligence reports | `write` | +| `threatintel_sweep_trigger` | Searches your environment for threat indicators specified in a custom intelligence report | `write` | +| `threatintel_tasks_list` | Displays information about threat intelligence tasks and asynchronous jobs | `read` | +| `threatintel_task_results_get` | Retrieves the results of a threat intelligence task | `read` | +| `threatintel_feed_indicators_list` | Retrieves a list of IoCs from Trend Threat Intelligence Feed | `read` | +| `threatintel_feeds_list` | Retrieves a list of intelligence reports from the Trend Threat Intelligence Feed with associated objects and relationships | `read` | +| `threatintel_feed_filter_definition_get` | Retrieves supported filter keys and values for Trend Threat Intelligence Feed queries | `read` | + ## Architecture ![high-level architecture](./doc/images/trend-vision-one-mcp.png) diff --git a/internal/v1client/threatintel.go b/internal/v1client/threatintel.go new file mode 100644 index 0000000..0f2de1f --- /dev/null +++ b/internal/v1client/threatintel.go @@ -0,0 +1,146 @@ +package v1client + +import ( + "fmt" + "net/http" + + "github.com/google/go-querystring/query" +) + +type ThreatIntelQueryParameters struct { + OrderBy string `url:"orderBy,omitempty"` + Top int `url:"top,omitempty"` + SkipToken string `url:"skipToken,omitempty"` + StartDateTime string `url:"startDateTime,omitempty"` + EndDateTime string `url:"endDateTime,omitempty"` + Filter string `url:"filter,omitempty"` +} + +type ThreatIntelFeedParameters struct { + StartDateTime string `url:"startDateTime,omitempty"` + EndDateTime string `url:"endDateTime,omitempty"` + Top int `url:"top,omitempty"` + TopReport int `url:"topReport,omitempty"` + IndicatorObjectFormat string `url:"indicatorObjectFormat,omitempty"` + ResponseObjectFormat string `url:"responseObjectFormat,omitempty"` +} + +type SuspiciousObject struct { + URL string `json:"url,omitempty"` + Domain string `json:"domain,omitempty"` + IP string `json:"ip,omitempty"` + SenderMailAddress string `json:"senderMailAddress,omitempty"` + FileSha1 string `json:"fileSha1,omitempty"` + FileSha256 string `json:"fileSha256,omitempty"` + Description string `json:"description,omitempty"` + ScanAction string `json:"scanAction,omitempty"` + RiskLevel string `json:"riskLevel,omitempty"` + DaysToExpiration int `json:"daysToExpiration,omitempty"` +} + +type SuspiciousObjectException struct { + URL string `json:"url,omitempty"` + Domain string `json:"domain,omitempty"` + IP string `json:"ip,omitempty"` + SenderMailAddress string `json:"senderMailAddress,omitempty"` + FileSha1 string `json:"fileSha1,omitempty"` + FileSha256 string `json:"fileSha256,omitempty"` + Description string `json:"description,omitempty"` +} + +type SuspiciousObjectDelete struct { + URL string `json:"url,omitempty"` + Domain string `json:"domain,omitempty"` + IP string `json:"ip,omitempty"` + SenderMailAddress string `json:"senderMailAddress,omitempty"` + FileSha1 string `json:"fileSha1,omitempty"` + FileSha256 string `json:"fileSha256,omitempty"` +} + +type IntelligenceReportDelete struct { + ID string `json:"id"` +} + +type IntelligenceReportSweep struct { + ID string `json:"id"` + SweepType string `json:"sweepType"` + Description string `json:"description,omitempty"` +} + +func (c *V1ApiClient) ThreatIntelListSuspiciousObjects(filter string, queryParams ThreatIntelQueryParameters) (*http.Response, error) { + return c.searchAndFilter("v3.0/threatintel/suspiciousObjects", filter, queryParams) +} + +func (c *V1ApiClient) ThreatIntelAddSuspiciousObjects(objects []SuspiciousObject) (*http.Response, error) { + return c.genericJSONPost("v3.0/threatintel/suspiciousObjects", objects) +} + +func (c *V1ApiClient) ThreatIntelDeleteSuspiciousObjects(objects []SuspiciousObjectDelete) (*http.Response, error) { + return c.genericJSONPost("v3.0/threatintel/suspiciousObjects/delete", objects) +} + +func (c *V1ApiClient) ThreatIntelListExceptions(filter string, queryParams ThreatIntelQueryParameters) (*http.Response, error) { + return c.searchAndFilter("v3.0/threatintel/suspiciousObjectExceptions", filter, queryParams) +} + +func (c *V1ApiClient) ThreatIntelAddExceptions(objects []SuspiciousObjectException) (*http.Response, error) { + return c.genericJSONPost("v3.0/threatintel/suspiciousObjectExceptions", objects) +} + +func (c *V1ApiClient) ThreatIntelDeleteExceptions(objects []SuspiciousObjectDelete) (*http.Response, error) { + return c.genericJSONPost("v3.0/threatintel/suspiciousObjectExceptions/delete", objects) +} + +func (c *V1ApiClient) ThreatIntelListIntelligenceReports(queryParams ThreatIntelQueryParameters) (*http.Response, error) { + return c.searchAndFilter("v3.0/threatintel/intelligenceReports", "", queryParams) +} + +func (c *V1ApiClient) ThreatIntelGetIntelligenceReport(reportId string) (*http.Response, error) { + return c.genericGet(fmt.Sprintf("v3.0/threatintel/intelligenceReports/%s", reportId)) +} + +func (c *V1ApiClient) ThreatIntelDeleteIntelligenceReports(reportIds []string) (*http.Response, error) { + deleteBody := []IntelligenceReportDelete{} + for _, id := range reportIds { + deleteBody = append(deleteBody, IntelligenceReportDelete{ID: id}) + } + return c.genericJSONPost("v3.0/threatintel/intelligenceReports/delete", deleteBody) +} + +func (c *V1ApiClient) ThreatIntelTriggerSweep(sweeps []IntelligenceReportSweep) (*http.Response, error) { + return c.genericJSONPost("v3.0/threatintel/intelligenceReports/sweep", sweeps) +} + +func (c *V1ApiClient) ThreatIntelListTasks(queryParams ThreatIntelQueryParameters) (*http.Response, error) { + return c.searchAndFilter("v3.0/threatintel/tasks", "", queryParams) +} + +func (c *V1ApiClient) ThreatIntelGetTaskResults(taskId string) (*http.Response, error) { + return c.genericGet(fmt.Sprintf("v3.0/threatintel/tasks/%s", taskId)) +} + +func (c *V1ApiClient) ThreatIntelListFeedIndicators(queryParams ThreatIntelFeedParameters) (*http.Response, error) { + return c.searchAndFilter("v3.0/threatintel/feedIndicators", "", queryParams) +} + +func (c *V1ApiClient) ThreatIntelListFeeds(contextualFilter string, queryParams ThreatIntelFeedParameters) (*http.Response, error) { + p, err := query.Values(queryParams) + if err != nil { + return nil, err + } + r, err := c.newRequest( + http.MethodGet, + "v3.0/threatintel/feeds", + http.NoBody, + withHeader("TMV1-Contextual-Filter", contextualFilter), + withUrlParameters(p), + ) + if err != nil { + return nil, err + } + return c.client.Do(r) +} + +func (c *V1ApiClient) ThreatIntelGetFeedFilterDefinition() (*http.Response, error) { + return c.genericGet("v3.0/threatintel/feeds/filterDefinition") +} diff --git a/internal/v1client/v1client.go b/internal/v1client/v1client.go index 55182cf..ea97dca 100644 --- a/internal/v1client/v1client.go +++ b/internal/v1client/v1client.go @@ -1,6 +1,8 @@ package v1client import ( + "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -181,3 +183,22 @@ func (c *V1ApiClient) genericGet(path string) (*http.Response, error) { } return c.client.Do(r) } + +func (c *V1ApiClient) genericJSONPost(path string, body any, options ...requestOptionFunc) (*http.Response, error) { + b, err := json.Marshal(body) + if err != nil { + return nil, err + } + + opts := append([]requestOptionFunc{withContentTypeJSON()}, options...) + r, err := c.newRequest( + http.MethodPost, + path, + bytes.NewReader(b), + opts..., + ) + if err != nil { + return nil, err + } + return c.client.Do(r) +} diff --git a/internal/v1mcp/server.go b/internal/v1mcp/server.go index 3a53b41..b86c57c 100644 --- a/internal/v1mcp/server.go +++ b/internal/v1mcp/server.go @@ -46,10 +46,12 @@ func NewMcpServer(cfg ServerConfig) (*mcpserver.MCPServer, error) { addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyContainer) addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyEndpoint) addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyAISecurity) + addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyThreatIntel) if !cfg.ReadOnly { addWriteToolset(s, client, tools.ToolsetsWriteCloudPosture) addWriteToolset(s, client, tools.ToolsetsWriteIAM) + addWriteToolset(s, client, tools.ToolsetsWriteThreatIntel) } return s, nil diff --git a/internal/v1mcp/tooldescriptions/descriptions.go b/internal/v1mcp/tooldescriptions/descriptions.go index 60156a9..9189db4 100644 --- a/internal/v1mcp/tooldescriptions/descriptions.go +++ b/internal/v1mcp/tooldescriptions/descriptions.go @@ -1245,3 +1245,125 @@ or Operator 'or' not Operator 'not' () Symbols for grouping operands ` + +var FilterSuspiciousObjects = ` +string <= 4000 characters +Example: type eq 'url' AND riskLevel eq 'high' + +Filter for retrieving a subset of the Suspicious Object List. + +Supported fields: +Field Description Possible values +type The type of a suspicious object url, domain, senderMailAddress, ip, fileSha1, fileSha256 +url Suspicious URL Any value +domain Suspicious domain name Any value +ip Suspicious IP address Any value +senderMailAddress Suspicious email address Any value +fileSha1 SHA1 hash associated to a suspicious file Any value +fileSha256 SHA256 hash associated to a suspicious file Any value +scanAction Action that connected products apply after detecting a suspicious object block, log +riskLevel Risk level of a suspicious object high, medium, low + +Supported operators: +Operator Description +eq Operator 'equal to' +and Operator 'and' +or Operator 'or' +not Operator 'not' +() Symbols for grouping operands with their correct operator +` + +var FilterSuspiciousObjectExceptions = ` +string <= 4000 characters +Example: type eq 'url' AND url eq '*.example.com' + +Filter for retrieving a subset of the Exception List. + +Supported fields: +Field Description Possible values +type The type of a suspicious object url, domain, senderMailAddress, ip, fileSha1, fileSha256 +url Exception URL Any value +domain Exception domain name Any value +ip Exception IP address Any value +senderMailAddress Exception email address Any value +fileSha1 SHA1 hash identifying a file exception Any value +fileSha256 SHA256 hash identifying a file exception Any value + +Supported operators: +Operator Description +eq Operator 'equal to' +and Operator 'and' +or Operator 'or' +not Operator 'not' +() Symbols for grouping operands with their correct operator +` + +var FilterIntelligenceReports = ` +string <= 4000 characters +Example: id eq 'report--2c1091ba-a7d2-46b2-bf97-4137916c30cb' AND name eq 'Report1' + +Filter for retrieving a subset of the custom intelligence reports list. + +Supported fields: +Field Description +id Unique alphanumeric string that identifies a custom intelligence report +name Title of a custom intelligence report (needs to be included with single quotation marks) + +Supported operators: +Operator Description +eq Operator 'equal to' +and Operator 'and' +or Operator 'or' +not Operator 'not' +() Symbols for grouping operands with their correct operator +` + +var FilterThreatIntelTasks = ` +string <= 4000 characters +Example: sweepType eq 'manual' AND isHit eq true + +Filter for retrieving a subset of the sweeping task list. + +Supported fields: +Field Description Possible values +id Unique alphanumeric string that identifies a sweeping task Any value +sweepType Type of sweeping task schedule, manual, stixShifter +isHit States whether indicators were matched during a sweeping task true, false +status Status of a sweeping task notstarted, running, succeeded, failed + +Supported operators: +Operator Description +eq Operator 'equal to' +and Operator 'and' +or Operator 'or' +not Operator 'not' +() Symbols for grouping operands with their correct operator +` + +var FilterThreatIntelFeeds = ` +string <= 4000 characters +Example: (location eq 'Brazil' or location eq 'No specified locations') and (industry in ('Finance', 'Health', 'No specified industries')) + +Defines the criteria for retrieving specific subsets of intelligence objects from the Trend Threat Intelligence Feed by applying contextual relationship-based filtering. + +Supported fields: +Field Description +location Filters intelligence reports based on the associated location (e.g., 'Canada', 'United States of America', 'No specified locations') +industry Filters intelligence reports based on the associated industry sector (e.g., 'Finance', 'Technology', 'Government', 'No specified industries') + +Supported operators: +Operator Description +eq Equals +and Logical AND +or Logical OR +not Logical NOT +in Matches any value in a list +() Grouping expressions for precedence and lists of values + +Example queries: +- Single condition: location eq 'Canada' +- OR condition: location eq 'United States of America' or location eq 'Mexico' +- AND condition: location eq 'France' and industry eq 'Technology' +- NOT condition: not (location eq 'Germany') +- IN operator: industry in ('Financial Services', 'Insurance', 'Healthcare') +` diff --git a/internal/v1mcp/tools/threatintel.go b/internal/v1mcp/tools/threatintel.go new file mode 100644 index 0000000..bf84d79 --- /dev/null +++ b/internal/v1mcp/tools/threatintel.go @@ -0,0 +1,786 @@ +package tools + +import ( + "context" + "net/http" + + "github.com/mark3labs/mcp-go/mcp" + mcpserver "github.com/mark3labs/mcp-go/server" + "github.com/trendmicro/vision-one-mcp-server/internal/v1client" + "github.com/trendmicro/vision-one-mcp-server/internal/v1mcp/tooldescriptions" +) + +var ToolsetsReadOnlyThreatIntel = []func(*v1client.V1ApiClient) mcpserver.ServerTool{ + toolThreatIntelSuspiciousObjectsList, + toolThreatIntelExceptionsList, + toolThreatIntelIntelligenceReportsList, + toolThreatIntelIntelligenceReportGet, + toolThreatIntelTasksList, + toolThreatIntelTaskResultsGet, + toolThreatIntelFeedIndicatorsList, + toolThreatIntelFeedsList, + toolThreatIntelFeedFilterDefinitionGet, +} + +var ToolsetsWriteThreatIntel = []func(*v1client.V1ApiClient) mcpserver.ServerTool{ + toolThreatIntelSuspiciousObjectsAdd, + toolThreatIntelSuspiciousObjectsDelete, + toolThreatIntelExceptionsAdd, + toolThreatIntelExceptionsDelete, + toolThreatIntelIntelligenceReportsDelete, + toolThreatIntelSweepTrigger, +} + +func toolThreatIntelSuspiciousObjectsList(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_suspicious_objects_list", + mcp.WithDescription("Retrieves information about domains, file SHA-1, file SHA-256, IP addresses, email addresses, or URLs in the Suspicious Object List"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("filter", mcp.Description(tooldescriptions.FilterSuspiciousObjects)), + mcp.WithString("orderBy", + mcp.Description("The field by which the results are sorted"), + mcp.Enum(withOrdering(asc_desc, "riskLevel", "lastModifiedDateTime", "expiredDateTime")...), + ), + mcp.WithString("top", + mcp.Description(tooldescriptions.DefaultTop), + mcp.Enum("50", "100", "200"), + ), + mcp.WithString("startDateTime", mcp.Description("The start of the data retrieval range in ISO 8601 format")), + mcp.WithString("endDateTime", mcp.Description("The end of the data retrieval range in ISO 8601 format")), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + filter, err := optionalValue[string]("filter", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + orderBy, err := optionalValue[string]("orderBy", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + top, err := optionalStrInt("top", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + startDateTime, err := optionalValue[string]("startDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + endDateTime, err := optionalValue[string]("endDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + qp := v1client.ThreatIntelQueryParameters{ + OrderBy: orderBy, + Top: top, + StartDateTime: startDateTime, + EndDateTime: endDateTime, + } + + resp, err := client.ThreatIntelListSuspiciousObjects(filter, qp) + return handleStatusResponse(resp, err, http.StatusOK, "failed to list suspicious objects") + }, + } +} + +func toolThreatIntelSuspiciousObjectsAdd(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_suspicious_objects_add", + mcp.WithDescription("Adds information about domains, file SHA-1, file SHA-256, IP addresses, email addresses, or URLs to the Suspicious Object List"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(false), + }), + mcp.WithString("type", + mcp.Required(), + mcp.Description("The type of suspicious object"), + mcp.Enum("url", "domain", "ip", "senderMailAddress", "fileSha1", "fileSha256"), + ), + mcp.WithString("value", + mcp.Required(), + mcp.Description("The value of the suspicious object (URL, domain, IP, email address, or file hash)"), + ), + mcp.WithString("description", + mcp.Description("Brief description of the suspicious object"), + ), + mcp.WithString("scanAction", + mcp.Description("Action that connected products apply after detecting a suspicious object"), + mcp.Enum("block", "log"), + ), + mcp.WithString("riskLevel", + mcp.Description("Risk level of the suspicious object"), + mcp.Enum("high", "medium", "low"), + ), + mcp.WithNumber("daysToExpiration", + mcp.Description("Number of days before the object expires from the list"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + objType, err := requiredValue[string]("type", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + value, err := requiredValue[string]("value", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + description, err := optionalValue[string]("description", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + scanAction, err := optionalValue[string]("scanAction", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + riskLevel, err := optionalValue[string]("riskLevel", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + daysToExpiration, err := optionalIntValue("daysToExpiration", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + obj := v1client.SuspiciousObject{ + Description: description, + ScanAction: scanAction, + RiskLevel: riskLevel, + DaysToExpiration: daysToExpiration, + } + + switch objType { + case "url": + obj.URL = value + case "domain": + obj.Domain = value + case "ip": + obj.IP = value + case "senderMailAddress": + obj.SenderMailAddress = value + case "fileSha1": + obj.FileSha1 = value + case "fileSha256": + obj.FileSha256 = value + } + + resp, err := client.ThreatIntelAddSuspiciousObjects([]v1client.SuspiciousObject{obj}) + return handleStatusResponse(resp, err, http.StatusMultiStatus, "failed to add suspicious object") + }, + } +} + +func toolThreatIntelSuspiciousObjectsDelete(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_suspicious_objects_delete", + mcp.WithDescription("Deletes information about domains, file SHA-1, file SHA-256, IP addresses, email addresses, or URLs from the Suspicious Object List"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(false), + }), + mcp.WithString("type", + mcp.Required(), + mcp.Description("The type of suspicious object"), + mcp.Enum("url", "domain", "ip", "senderMailAddress", "fileSha1", "fileSha256"), + ), + mcp.WithString("value", + mcp.Required(), + mcp.Description("The value of the suspicious object to delete"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + objType, err := requiredValue[string]("type", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + value, err := requiredValue[string]("value", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + obj := v1client.SuspiciousObjectDelete{} + + switch objType { + case "url": + obj.URL = value + case "domain": + obj.Domain = value + case "ip": + obj.IP = value + case "senderMailAddress": + obj.SenderMailAddress = value + case "fileSha1": + obj.FileSha1 = value + case "fileSha256": + obj.FileSha256 = value + } + + resp, err := client.ThreatIntelDeleteSuspiciousObjects([]v1client.SuspiciousObjectDelete{obj}) + return handleStatusResponse(resp, err, http.StatusMultiStatus, "failed to delete suspicious object") + }, + } +} + +func toolThreatIntelExceptionsList(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_exceptions_list", + mcp.WithDescription("Retrieves information about domains, file SHA-1, file SHA-256, IP addresses, sender addresses, or URLs in the Exception List"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("filter", mcp.Description(tooldescriptions.FilterSuspiciousObjectExceptions)), + mcp.WithString("orderBy", + mcp.Description("The field by which the results are sorted"), + mcp.Enum(withOrdering(asc_desc, "lastModifiedDateTime")...), + ), + mcp.WithString("top", + mcp.Description(tooldescriptions.DefaultTop), + mcp.Enum("50", "100", "200"), + ), + mcp.WithString("startDateTime", mcp.Description("The start of the data retrieval range in ISO 8601 format")), + mcp.WithString("endDateTime", mcp.Description("The end of the data retrieval range in ISO 8601 format")), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + filter, err := optionalValue[string]("filter", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + orderBy, err := optionalValue[string]("orderBy", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + top, err := optionalStrInt("top", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + startDateTime, err := optionalValue[string]("startDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + endDateTime, err := optionalValue[string]("endDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + qp := v1client.ThreatIntelQueryParameters{ + OrderBy: orderBy, + Top: top, + StartDateTime: startDateTime, + EndDateTime: endDateTime, + } + + resp, err := client.ThreatIntelListExceptions(filter, qp) + return handleStatusResponse(resp, err, http.StatusOK, "failed to list exception objects") + }, + } +} + +func toolThreatIntelExceptionsAdd(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_exceptions_add", + mcp.WithDescription("Adds domains, file SHA-1, file SHA-256, IP addresses, sender addresses, or URLs to the Exception List"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(false), + }), + mcp.WithString("type", + mcp.Required(), + mcp.Description("The type of exception object"), + mcp.Enum("url", "domain", "ip", "senderMailAddress", "fileSha1", "fileSha256"), + ), + mcp.WithString("value", + mcp.Required(), + mcp.Description("The value of the exception object (URL, domain, IP, email address, or file hash)"), + ), + mcp.WithString("description", + mcp.Description("Brief description of the exception object"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + objType, err := requiredValue[string]("type", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + value, err := requiredValue[string]("value", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + description, err := optionalValue[string]("description", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + obj := v1client.SuspiciousObjectException{ + Description: description, + } + + switch objType { + case "url": + obj.URL = value + case "domain": + obj.Domain = value + case "ip": + obj.IP = value + case "senderMailAddress": + obj.SenderMailAddress = value + case "fileSha1": + obj.FileSha1 = value + case "fileSha256": + obj.FileSha256 = value + } + + resp, err := client.ThreatIntelAddExceptions([]v1client.SuspiciousObjectException{obj}) + return handleStatusResponse(resp, err, http.StatusMultiStatus, "failed to add exception object") + }, + } +} + +func toolThreatIntelExceptionsDelete(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_exceptions_delete", + mcp.WithDescription("Deletes the specified objects from the Exception List"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(false), + }), + mcp.WithString("type", + mcp.Required(), + mcp.Description("The type of exception object"), + mcp.Enum("url", "domain", "ip", "senderMailAddress", "fileSha1", "fileSha256"), + ), + mcp.WithString("value", + mcp.Required(), + mcp.Description("The value of the exception object to delete"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + objType, err := requiredValue[string]("type", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + value, err := requiredValue[string]("value", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + obj := v1client.SuspiciousObjectDelete{} + + switch objType { + case "url": + obj.URL = value + case "domain": + obj.Domain = value + case "ip": + obj.IP = value + case "senderMailAddress": + obj.SenderMailAddress = value + case "fileSha1": + obj.FileSha1 = value + case "fileSha256": + obj.FileSha256 = value + } + + resp, err := client.ThreatIntelDeleteExceptions([]v1client.SuspiciousObjectDelete{obj}) + return handleStatusResponse(resp, err, http.StatusMultiStatus, "failed to delete exception object") + }, + } +} + +func toolThreatIntelIntelligenceReportsList(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_intelligence_reports_list", + mcp.WithDescription("Retrieves a list of custom intelligence reports created from imported or retrieved data"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("filter", mcp.Description(tooldescriptions.FilterIntelligenceReports)), + mcp.WithString("orderBy", + mcp.Description("The field by which the results are sorted"), + mcp.Enum(withOrdering(asc_desc, "updatedDateTime", "createdDateTime")...), + ), + mcp.WithString("top", + mcp.Description(tooldescriptions.DefaultTop), + mcp.Enum("50", "100", "200"), + ), + mcp.WithString("startDateTime", mcp.Description("The start of the data retrieval range in ISO 8601 format")), + mcp.WithString("endDateTime", mcp.Description("The end of the data retrieval range in ISO 8601 format")), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + filter, err := optionalValue[string]("filter", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + orderBy, err := optionalValue[string]("orderBy", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + top, err := optionalStrInt("top", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + startDateTime, err := optionalValue[string]("startDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + endDateTime, err := optionalValue[string]("endDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + qp := v1client.ThreatIntelQueryParameters{ + Filter: filter, + OrderBy: orderBy, + Top: top, + StartDateTime: startDateTime, + EndDateTime: endDateTime, + } + + resp, err := client.ThreatIntelListIntelligenceReports(qp) + return handleStatusResponse(resp, err, http.StatusOK, "failed to list intelligence reports") + }, + } +} + +func toolThreatIntelIntelligenceReportGet(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_intelligence_report_get", + mcp.WithDescription("Downloads a custom intelligence report as a STIX Bundle"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("reportId", + mcp.Required(), + mcp.Description("The unique identifier of the intelligence report"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + reportId, err := requiredValue[string]("reportId", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + resp, err := client.ThreatIntelGetIntelligenceReport(reportId) + return handleStatusResponse(resp, err, http.StatusOK, "failed to get intelligence report") + }, + } +} + +func toolThreatIntelIntelligenceReportsDelete(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_intelligence_reports_delete", + mcp.WithDescription("Deletes the specified custom intelligence reports"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(false), + }), + mcp.WithArray("reportIds", + mcp.Required(), + mcp.Description("Array of intelligence report IDs to delete"), + mcp.Items(map[string]any{"type": "string"}), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + reportIds := []string{} + if ids, ok := request.GetArguments()["reportIds"].([]any); ok && len(ids) > 0 { + for _, id := range ids { + reportId, ok := id.(string) + if !ok { + return mcp.NewToolResultError("each report ID must be a string"), nil + } + reportIds = append(reportIds, reportId) + } + } + + resp, err := client.ThreatIntelDeleteIntelligenceReports(reportIds) + return handleStatusResponse(resp, err, http.StatusMultiStatus, "failed to delete intelligence reports") + }, + } +} + +func toolThreatIntelSweepTrigger(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_sweep_trigger", + mcp.WithDescription("Searches your environment for threat indicators specified in a custom intelligence report"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(false), + }), + mcp.WithString("reportId", + mcp.Required(), + mcp.Description("The unique identifier of the intelligence report to sweep"), + ), + mcp.WithString("sweepType", + mcp.Required(), + mcp.Description("The type of sweeping task"), + mcp.Enum("manual", "stixShifter"), + ), + mcp.WithString("description", + mcp.Description("Brief description of the sweep task"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + reportId, err := requiredValue[string]("reportId", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + sweepType, err := requiredValue[string]("sweepType", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + description, err := optionalValue[string]("description", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + sweep := v1client.IntelligenceReportSweep{ + ID: reportId, + SweepType: sweepType, + Description: description, + } + + resp, err := client.ThreatIntelTriggerSweep([]v1client.IntelligenceReportSweep{sweep}) + return handleStatusResponse(resp, err, http.StatusMultiStatus, "failed to trigger sweep") + }, + } +} + +func toolThreatIntelTasksList(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_tasks_list", + mcp.WithDescription("Displays information about threat intelligence tasks and asynchronous jobs"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("filter", mcp.Description(tooldescriptions.FilterThreatIntelTasks)), + mcp.WithString("orderBy", + mcp.Description("The field by which the results are sorted"), + mcp.Enum(withOrdering(asc_desc, "lastActionDateTime")...), + ), + mcp.WithString("top", + mcp.Description(tooldescriptions.DefaultTop), + mcp.Enum("50", "100", "200"), + ), + mcp.WithString("startDateTime", mcp.Description("The start of the data retrieval range in ISO 8601 format")), + mcp.WithString("endDateTime", mcp.Description("The end of the data retrieval range in ISO 8601 format")), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + filter, err := optionalValue[string]("filter", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + orderBy, err := optionalValue[string]("orderBy", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + top, err := optionalStrInt("top", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + startDateTime, err := optionalValue[string]("startDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + endDateTime, err := optionalValue[string]("endDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + qp := v1client.ThreatIntelQueryParameters{ + Filter: filter, + OrderBy: orderBy, + Top: top, + StartDateTime: startDateTime, + EndDateTime: endDateTime, + } + + resp, err := client.ThreatIntelListTasks(qp) + return handleStatusResponse(resp, err, http.StatusOK, "failed to list tasks") + }, + } +} + +func toolThreatIntelTaskResultsGet(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_task_results_get", + mcp.WithDescription("Retrieves the results of a threat intelligence task"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("taskId", + mcp.Required(), + mcp.Description("The unique identifier of the task"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + taskId, err := requiredValue[string]("taskId", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + resp, err := client.ThreatIntelGetTaskResults(taskId) + return handleStatusResponse(resp, err, http.StatusOK, "failed to get task results") + }, + } +} + +func toolThreatIntelFeedIndicatorsList(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_feed_indicators_list", + mcp.WithDescription("Retrieves a list of IoCs from Trend Threat Intelligence Feed"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("startDateTime", mcp.Description("The start of the data retrieval range in ISO 8601 format")), + mcp.WithString("endDateTime", mcp.Description("The end of the data retrieval range in ISO 8601 format")), + mcp.WithString("top", + mcp.Description("The number of IoCs returned by a query (maximum 10,000)"), + mcp.Enum("1000", "5000", "10000"), + ), + mcp.WithString("indicatorObjectFormat", + mcp.Description("The desired format for the query response"), + mcp.Enum("stixBundle", "taxiiEnvelope"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + startDateTime, err := optionalValue[string]("startDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + endDateTime, err := optionalValue[string]("endDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + top, err := optionalStrInt("top", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + indicatorObjectFormat, err := optionalValue[string]("indicatorObjectFormat", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + qp := v1client.ThreatIntelFeedParameters{ + StartDateTime: startDateTime, + EndDateTime: endDateTime, + Top: top, + IndicatorObjectFormat: indicatorObjectFormat, + } + + resp, err := client.ThreatIntelListFeedIndicators(qp) + return handleStatusResponse(resp, err, http.StatusOK, "failed to list feed indicators") + }, + } +} + +func toolThreatIntelFeedsList(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_feeds_list", + mcp.WithDescription("Retrieves a list of intelligence reports from the Trend Threat Intelligence Feed with associated objects and relationships"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + mcp.WithString("contextualFilter", mcp.Description(tooldescriptions.FilterThreatIntelFeeds)), + mcp.WithString("startDateTime", mcp.Description("The start of the data retrieval range in ISO 8601 format")), + mcp.WithString("endDateTime", mcp.Description("The end of the data retrieval range in ISO 8601 format")), + mcp.WithString("topReport", + mcp.Description("The number of reports returned by a query (maximum 20)"), + mcp.Enum("5", "10", "20"), + ), + mcp.WithString("responseObjectFormat", + mcp.Description("The preferred format for the query response"), + mcp.Enum("stixBundle", "taxiiEnvelope"), + ), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + contextualFilter, err := optionalValue[string]("contextualFilter", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + startDateTime, err := optionalValue[string]("startDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + endDateTime, err := optionalValue[string]("endDateTime", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + topReport, err := optionalStrInt("topReport", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + responseObjectFormat, err := optionalValue[string]("responseObjectFormat", request.GetArguments()) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + qp := v1client.ThreatIntelFeedParameters{ + StartDateTime: startDateTime, + EndDateTime: endDateTime, + TopReport: topReport, + ResponseObjectFormat: responseObjectFormat, + } + + resp, err := client.ThreatIntelListFeeds(contextualFilter, qp) + return handleStatusResponse(resp, err, http.StatusOK, "failed to list feeds") + }, + } +} + +func toolThreatIntelFeedFilterDefinitionGet(client *v1client.V1ApiClient) mcpserver.ServerTool { + return mcpserver.ServerTool{ + Tool: mcp.NewTool( + "threatintel_feed_filter_definition_get", + mcp.WithDescription("Retrieves supported filter keys and values for Trend Threat Intelligence Feed queries"), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + ReadOnlyHint: toPtr(true), + }), + ), + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resp, err := client.ThreatIntelGetFeedFilterDefinition() + return handleStatusResponse(resp, err, http.StatusOK, "failed to get feed filter definition") + }, + } +}