From cacb24e628dd75052b6b037cd3f4c787fff5d2c9 Mon Sep 17 00:00:00 2001 From: Xiaoyun Ding Date: Thu, 17 Jul 2025 23:50:23 +0800 Subject: [PATCH] Add ListGlobalSecurityAdvisories and GetGlobalSecurityAdvisory --- .../get_global_security_advisory.snap | 20 ++ .../list_global_security_advisories.snap | 126 ++++++++ pkg/github/global_security_advisories.go | 277 ++++++++++++++++++ pkg/github/global_security_advisories_test.go | 248 ++++++++++++++++ pkg/github/tools.go | 6 + 5 files changed, 677 insertions(+) create mode 100644 pkg/github/__toolsnaps__/get_global_security_advisory.snap create mode 100644 pkg/github/__toolsnaps__/list_global_security_advisories.snap create mode 100644 pkg/github/global_security_advisories.go create mode 100644 pkg/github/global_security_advisories_test.go diff --git a/pkg/github/__toolsnaps__/get_global_security_advisory.snap b/pkg/github/__toolsnaps__/get_global_security_advisory.snap new file mode 100644 index 000000000..6e43063be --- /dev/null +++ b/pkg/github/__toolsnaps__/get_global_security_advisory.snap @@ -0,0 +1,20 @@ +{ + "annotations": { + "title": "Get global security advisory", + "readOnlyHint": true + }, + "description": "Get a global security advisory using its GitHub Security Advisory (GHSA) identifier.", + "inputSchema": { + "properties": { + "ghsa_id": { + "description": "The GHSA (GitHub Security Advisory) identifier of the advisory.", + "type": "string" + } + }, + "required": [ + "ghsa_id" + ], + "type": "object" + }, + "name": "get_global_security_advisory" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_global_security_advisories.snap b/pkg/github/__toolsnaps__/list_global_security_advisories.snap new file mode 100644 index 000000000..bf1701c4c --- /dev/null +++ b/pkg/github/__toolsnaps__/list_global_security_advisories.snap @@ -0,0 +1,126 @@ +{ + "annotations": { + "title": "List global security advisories", + "readOnlyHint": true + }, + "description": "List global security advisories from the GitHub Advisory Database.", + "inputSchema": { + "properties": { + "affects": { + "description": "If specified, only return advisories that affect any of package or package@version. A maximum of 1000 packages can be specified. Example: affects=package1,package2@1.0.0,package3@^2.0.0", + "type": "string" + }, + "after": { + "description": "A cursor, as given in the Link header. If specified, the query only searches for results after this cursor.", + "type": "string" + }, + "before": { + "description": "A cursor, as given in the Link header. If specified, the query only searches for results before this cursor.", + "type": "string" + }, + "cve_id": { + "description": "If specified, only advisories with this CVE (Common Vulnerabilities and Exposures) identifier will be returned.", + "type": "string" + }, + "cwes": { + "description": "If specified, only advisories with these CWEs will be returned. Multiple CWEs can be separated by commas. Example: cwes=79,284,22", + "type": "string" + }, + "direction": { + "description": "The direction to sort the results by.", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "ecosystem": { + "description": "If specified, only advisories for this ecosystem will be returned.", + "enum": [ + "rubygems", + "npm", + "pip", + "maven", + "nuget", + "composer", + "go", + "rust", + "erlang", + "actions", + "pub", + "other", + "swift" + ], + "type": "string" + }, + "epss_percentage": { + "description": "If specified, only return advisories that have an EPSS percentage score that matches the provided value. The EPSS percentage represents the likelihood of a CVE being exploited.", + "type": "string" + }, + "epss_percentile": { + "description": "If specified, only return advisories that have an EPSS percentile score that matches the provided value. The EPSS percentile represents the relative rank of the CVE's likelihood of being exploited compared to other CVEs.", + "type": "string" + }, + "ghsa_id": { + "description": "If specified, only advisories with this GHSA (GitHub Security Advisory) identifier will be returned.", + "type": "string" + }, + "is_withdrawn": { + "description": "Whether to only return advisories that have been withdrawn.", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "modified": { + "description": "If specified, only show advisories that were updated or published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range.", + "type": "string" + }, + "per_page": { + "description": "The number of results per page (max 100). Default: 30", + "type": "number" + }, + "published": { + "description": "If specified, only return advisories that were published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range.", + "type": "string" + }, + "severity": { + "description": "If specified, only advisories with this severity will be returned.", + "enum": [ + "unknown", + "low", + "medium", + "high", + "critical" + ], + "type": "string" + }, + "sort": { + "description": "The property to sort the results by.", + "enum": [ + "updated", + "published", + "epss_percentage", + "epss_percentile" + ], + "type": "string" + }, + "type": { + "description": "If specified, only advisories of this type will be returned. By default, a request with no other parameters defined will only return reviewed advisories that are not malware.", + "enum": [ + "reviewed", + "malware", + "unreviewed" + ], + "type": "string" + }, + "updated": { + "description": "If specified, only return advisories that were updated on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range.", + "type": "string" + } + }, + "type": "object" + }, + "name": "list_global_security_advisories" +} \ No newline at end of file diff --git a/pkg/github/global_security_advisories.go b/pkg/github/global_security_advisories.go new file mode 100644 index 000000000..8c9b35517 --- /dev/null +++ b/pkg/github/global_security_advisories.go @@ -0,0 +1,277 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v73/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func ListGlobalSecurityAdvisories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_global_security_advisories", + mcp.WithDescription(t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_DESCRIPTION", "List global security advisories from the GitHub Advisory Database.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_USER_TITLE", "List global security advisories"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("ghsa_id", + mcp.Description("If specified, only advisories with this GHSA (GitHub Security Advisory) identifier will be returned."), + ), + mcp.WithString("type", + mcp.Description("If specified, only advisories of this type will be returned. By default, a request with no other parameters defined will only return reviewed advisories that are not malware."), + mcp.Enum("reviewed", "malware", "unreviewed"), + ), + mcp.WithString("cve_id", + mcp.Description("If specified, only advisories with this CVE (Common Vulnerabilities and Exposures) identifier will be returned."), + ), + mcp.WithString("ecosystem", + mcp.Description("If specified, only advisories for this ecosystem will be returned."), + mcp.Enum("rubygems", "npm", "pip", "maven", "nuget", "composer", "go", "rust", "erlang", "actions", "pub", "other", "swift"), + ), + mcp.WithString("severity", + mcp.Description("If specified, only advisories with this severity will be returned."), + mcp.Enum("unknown", "low", "medium", "high", "critical"), + ), + mcp.WithString("cwes", + mcp.Description("If specified, only advisories with these CWEs will be returned. Multiple CWEs can be separated by commas. Example: cwes=79,284,22"), + ), + mcp.WithString("is_withdrawn", + mcp.Description("Whether to only return advisories that have been withdrawn."), + mcp.Enum("true", "false"), + ), + mcp.WithString("affects", + mcp.Description("If specified, only return advisories that affect any of package or package@version. A maximum of 1000 packages can be specified. Example: affects=package1,package2@1.0.0,package3@^2.0.0"), + ), + mcp.WithString("published", + mcp.Description("If specified, only return advisories that were published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range."), + ), + mcp.WithString("updated", + mcp.Description("If specified, only return advisories that were updated on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range."), + ), + mcp.WithString("modified", + mcp.Description("If specified, only show advisories that were updated or published on a date or date range. Format: YYYY-MM-DD or YYYY-MM-DD..YYYY-MM-DD for range."), + ), + mcp.WithString("epss_percentage", + mcp.Description("If specified, only return advisories that have an EPSS percentage score that matches the provided value. The EPSS percentage represents the likelihood of a CVE being exploited."), + ), + mcp.WithString("epss_percentile", + mcp.Description("If specified, only return advisories that have an EPSS percentile score that matches the provided value. The EPSS percentile represents the relative rank of the CVE's likelihood of being exploited compared to other CVEs."), + ), + mcp.WithString("before", + mcp.Description("A cursor, as given in the Link header. If specified, the query only searches for results before this cursor."), + ), + mcp.WithString("after", + mcp.Description("A cursor, as given in the Link header. If specified, the query only searches for results after this cursor."), + ), + mcp.WithString("direction", + mcp.Description("The direction to sort the results by."), + mcp.Enum("asc", "desc"), + ), + mcp.WithNumber("per_page", + mcp.Description("The number of results per page (max 100). Default: 30"), + ), + mcp.WithString("sort", + mcp.Description("The property to sort the results by."), + mcp.Enum("updated", "published", "epss_percentage", "epss_percentile"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Parse optional parameters + opts := &github.ListGlobalSecurityAdvisoriesOptions{} + + if ghsaID, err := OptionalParam[string](request, "ghsa_id"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if ghsaID != "" { + opts.GHSAID = &ghsaID + } + + if advisoryType, err := OptionalParam[string](request, "type"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if advisoryType != "" { + opts.Type = &advisoryType + } + + if cveID, err := OptionalParam[string](request, "cve_id"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if cveID != "" { + opts.CVEID = &cveID + } + + if ecosystem, err := OptionalParam[string](request, "ecosystem"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if ecosystem != "" { + opts.Ecosystem = &ecosystem + } + + if severity, err := OptionalParam[string](request, "severity"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if severity != "" { + opts.Severity = &severity + } + + if cwes, err := OptionalParam[string](request, "cwes"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if cwes != "" { + // Split comma-separated CWEs + opts.CWEs = strings.Split(cwes, ",") + } + + if isWithdrawn, err := OptionalParam[string](request, "is_withdrawn"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if isWithdrawn != "" { + withdrawn := isWithdrawn == "true" + opts.IsWithdrawn = &withdrawn + } + + if affects, err := OptionalParam[string](request, "affects"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if affects != "" { + opts.Affects = &affects + } + + if published, err := OptionalParam[string](request, "published"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if published != "" { + opts.Published = &published + } + + if updated, err := OptionalParam[string](request, "updated"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if updated != "" { + opts.Updated = &updated + } + + if modified, err := OptionalParam[string](request, "modified"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if modified != "" { + opts.Modified = &modified + } + + // Note: EPSS parameters may not be supported in current Go SDK version + // Check if these fields exist before using them + // For now, we accept the parameters but don't use them in the API call + if _, err := OptionalParam[string](request, "epss_percentage"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + if _, err := OptionalParam[string](request, "epss_percentile"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + if before, err := OptionalParam[string](request, "before"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if before != "" { + opts.Before = before + } + + if after, err := OptionalParam[string](request, "after"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if after != "" { + opts.After = after + } + + // Note: Direction and Sort parameters may not be supported in current Go SDK version + // For now, we accept the parameters but don't use them in the API call + if _, err := OptionalParam[string](request, "direction"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + if _, err := OptionalParam[string](request, "sort"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + if perPage, err := OptionalIntParam(request, "per_page"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if perPage != 0 { + opts.PerPage = perPage + } + + advisories, resp, err := client.SecurityAdvisories.ListGlobalSecurityAdvisories(ctx, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to list global security advisories", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to list global security advisories: %s", string(body))), nil + } + + r, err := json.Marshal(advisories) + if err != nil { + return nil, fmt.Errorf("failed to marshal advisories: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +func GetGlobalSecurityAdvisory(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_global_security_advisory", + mcp.WithDescription(t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_DESCRIPTION", "Get a global security advisory using its GitHub Security Advisory (GHSA) identifier.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_USER_TITLE", "Get global security advisory"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("ghsa_id", + mcp.Required(), + mcp.Description("The GHSA (GitHub Security Advisory) identifier of the advisory."), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ghsaID, err := RequiredParam[string](request, "ghsa_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + advisory, resp, err := client.SecurityAdvisories.GetGlobalSecurityAdvisories(ctx, ghsaID) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to get global security advisory '%s'", ghsaID), + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to get global security advisory: %s", string(body))), nil + } + + r, err := json.Marshal(advisory) + if err != nil { + return nil, fmt.Errorf("failed to marshal advisory: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/global_security_advisories_test.go b/pkg/github/global_security_advisories_test.go new file mode 100644 index 000000000..c4db0903d --- /dev/null +++ b/pkg/github/global_security_advisories_test.go @@ -0,0 +1,248 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/github/github-mcp-server/internal/toolsnaps" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v73/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListGlobalSecurityAdvisories(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := ListGlobalSecurityAdvisories(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "list_global_security_advisories", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "ghsa_id") + assert.Contains(t, tool.InputSchema.Properties, "severity") + assert.Contains(t, tool.InputSchema.Properties, "ecosystem") + + // Mock advisory data + mockAdvisories := []*github.GlobalSecurityAdvisory{ + { + ID: github.Ptr(int64(123)), + }, + { + ID: github.Ptr(int64(456)), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedCount int + expectedErrMsg string + }{ + { + name: "successful listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/advisories", + Method: "GET", + }, + mockResponse(t, http.StatusOK, mockAdvisories), + ), + ), + requestArgs: map[string]interface{}{}, + expectError: false, + expectedCount: 2, + }, + { + name: "with severity filter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/advisories", + Method: "GET", + }, + expectQueryParams(t, map[string]string{"severity": "high"}).andThen( + mockResponse(t, http.StatusOK, []*github.GlobalSecurityAdvisory{mockAdvisories[0]}), + ), + ), + ), + requestArgs: map[string]interface{}{ + "severity": "high", + }, + expectError: false, + expectedCount: 1, + }, + { + name: "with ecosystem filter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/advisories", + Method: "GET", + }, + expectQueryParams(t, map[string]string{"ecosystem": "go"}).andThen( + mockResponse(t, http.StatusOK, mockAdvisories), + ), + ), + ), + requestArgs: map[string]interface{}{ + "ecosystem": "go", + }, + expectError: false, + expectedCount: 2, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := ListGlobalSecurityAdvisories(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + if tc.expectedErrMsg != "" { + assert.Contains(t, err.Error(), tc.expectedErrMsg) + } + return + } + + require.NoError(t, err) + require.NotNil(t, result) + require.False(t, result.IsError) + + // Parse and verify the response + textContent := getTextResult(t, result) + var advisories []*github.GlobalSecurityAdvisory + err = json.Unmarshal([]byte(textContent.Text), &advisories) + require.NoError(t, err) + assert.Len(t, advisories, tc.expectedCount) + }) + } +} + +func TestGetGlobalSecurityAdvisory(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := GetGlobalSecurityAdvisory(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "get_global_security_advisory", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "ghsa_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"ghsa_id"}) + + // Mock advisory data + mockAdvisory := &github.GlobalSecurityAdvisory{ + ID: github.Ptr(int64(123)), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedID int64 + expectedErrMsg string + }{ + { + name: "successful get", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/advisories/GHSA-xxxx-xxxx-xxxx", + Method: "GET", + }, + mockResponse(t, http.StatusOK, mockAdvisory), + ), + ), + requestArgs: map[string]interface{}{ + "ghsa_id": "GHSA-xxxx-xxxx-xxxx", + }, + expectError: false, + expectedID: 123, + }, + { + name: "missing ghsa_id parameter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/advisories/GHSA-xxxx-xxxx-xxxx", + Method: "GET", + }, + mockResponse(t, http.StatusOK, mockAdvisory), + ), + ), + requestArgs: map[string]interface{}{}, + expectError: true, + }, + { + name: "advisory not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/advisories/GHSA-nonexistent", + Method: "GET", + }, + mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`), + ), + ), + requestArgs: map[string]interface{}{ + "ghsa_id": "GHSA-nonexistent", + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetGlobalSecurityAdvisory(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + if tc.expectedErrMsg != "" { + if err != nil { + assert.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + assert.True(t, result.IsError) + textContent := getErrorResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedErrMsg) + } + } + return + } + + require.NoError(t, err) + require.NotNil(t, result) + require.False(t, result.IsError) + + // Parse and verify the response + textContent := getTextResult(t, result) + var advisory *github.GlobalSecurityAdvisory + err = json.Unmarshal([]byte(textContent.Text), &advisory) + require.NoError(t, err) + assert.Equal(t, tc.expectedID, *advisory.ID) + }) + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 77a1ccd3b..d05aeb48a 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -103,6 +103,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), ) + globalSecurityAdvisories := toolsets.NewToolset("global_security_advisories", "Global security advisories from the GitHub Advisory Database"). + AddReadTools( + toolsets.NewServerTool(ListGlobalSecurityAdvisories(getClient, t)), + toolsets.NewServerTool(GetGlobalSecurityAdvisory(getClient, t)), + ) dependabot := toolsets.NewToolset("dependabot", "Dependabot tools"). AddReadTools( toolsets.NewServerTool(GetDependabotAlert(getClient, t)), @@ -167,6 +172,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(actions) tsg.AddToolset(codeSecurity) tsg.AddToolset(secretProtection) + tsg.AddToolset(globalSecurityAdvisories) tsg.AddToolset(dependabot) tsg.AddToolset(notifications) tsg.AddToolset(experiments) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy