chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
ghErrors "github.com/github/github-mcp-server/pkg/errors"
|
||||
"github.com/github/github-mcp-server/pkg/ifc"
|
||||
"github.com/github/github-mcp-server/pkg/inventory"
|
||||
"github.com/github/github-mcp-server/pkg/scopes"
|
||||
"github.com/github/github-mcp-server/pkg/translations"
|
||||
"github.com/github/github-mcp-server/pkg/utils"
|
||||
"github.com/google/go-github/v89/github"
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
func ListGlobalSecurityAdvisories(t translations.TranslationHelperFunc) inventory.ServerTool {
|
||||
return NewTool(
|
||||
ToolsetMetadataSecurityAdvisories,
|
||||
mcp.Tool{
|
||||
Name: "list_global_security_advisories",
|
||||
Description: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_DESCRIPTION", "List global security advisories from GitHub."),
|
||||
Annotations: &mcp.ToolAnnotations{
|
||||
Title: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_USER_TITLE", "List global security advisories"),
|
||||
ReadOnlyHint: true,
|
||||
},
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"ghsaId": {
|
||||
Type: "string",
|
||||
Description: "Filter by GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx).",
|
||||
},
|
||||
"type": {
|
||||
Type: "string",
|
||||
Description: "Advisory type.",
|
||||
Enum: []any{"reviewed", "malware", "unreviewed"},
|
||||
Default: json.RawMessage(`"reviewed"`),
|
||||
},
|
||||
"cveId": {
|
||||
Type: "string",
|
||||
Description: "Filter by CVE ID.",
|
||||
},
|
||||
"ecosystem": {
|
||||
Type: "string",
|
||||
Description: "Filter by package ecosystem.",
|
||||
Enum: []any{"actions", "composer", "erlang", "go", "maven", "npm", "nuget", "other", "pip", "pub", "rubygems", "rust"},
|
||||
},
|
||||
"severity": {
|
||||
Type: "string",
|
||||
Description: "Filter by severity.",
|
||||
Enum: []any{"unknown", "low", "medium", "high", "critical"},
|
||||
},
|
||||
"cwes": {
|
||||
Type: "array",
|
||||
Description: "Filter by Common Weakness Enumeration IDs (e.g. [\"79\", \"284\", \"22\"]).",
|
||||
Items: &jsonschema.Schema{
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
"isWithdrawn": {
|
||||
Type: "boolean",
|
||||
Description: "Whether to only return withdrawn advisories.",
|
||||
},
|
||||
"affects": {
|
||||
Type: "string",
|
||||
Description: "Filter advisories by affected package or version (e.g. \"package1,package2@1.0.0\").",
|
||||
},
|
||||
"published": {
|
||||
Type: "string",
|
||||
Description: "Filter by publish date or date range (ISO 8601 date or range).",
|
||||
},
|
||||
"updated": {
|
||||
Type: "string",
|
||||
Description: "Filter by update date or date range (ISO 8601 date or range).",
|
||||
},
|
||||
"modified": {
|
||||
Type: "string",
|
||||
Description: "Filter by publish or update date or date range (ISO 8601 date or range).",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]scopes.Scope{scopes.SecurityEvents},
|
||||
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
||||
client, err := deps.GetClient(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
||||
}
|
||||
|
||||
ghsaID, err := OptionalParam[string](args, "ghsaId")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
typ, err := OptionalParam[string](args, "type")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid type: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
cveID, err := OptionalParam[string](args, "cveId")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid cveId: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
eco, err := OptionalParam[string](args, "ecosystem")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid ecosystem: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
sev, err := OptionalParam[string](args, "severity")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid severity: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
cwes, err := OptionalStringArrayParam(args, "cwes")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid cwes: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
isWithdrawn, err := OptionalParam[bool](args, "isWithdrawn")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid isWithdrawn: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
affects, err := OptionalParam[string](args, "affects")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid affects: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
published, err := OptionalParam[string](args, "published")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid published: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
updated, err := OptionalParam[string](args, "updated")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid updated: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
modified, err := OptionalParam[string](args, "modified")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid modified: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
opts := &github.ListGlobalSecurityAdvisoriesOptions{}
|
||||
|
||||
if ghsaID != "" {
|
||||
opts.GHSAID = &ghsaID
|
||||
}
|
||||
if typ != "" {
|
||||
opts.Type = &typ
|
||||
}
|
||||
if cveID != "" {
|
||||
opts.CVEID = &cveID
|
||||
}
|
||||
if eco != "" {
|
||||
opts.Ecosystem = &eco
|
||||
}
|
||||
if sev != "" {
|
||||
opts.Severity = &sev
|
||||
}
|
||||
if len(cwes) > 0 {
|
||||
opts.CWEs = cwes
|
||||
}
|
||||
|
||||
if isWithdrawn {
|
||||
opts.IsWithdrawn = &isWithdrawn
|
||||
}
|
||||
|
||||
if affects != "" {
|
||||
opts.Affects = &affects
|
||||
}
|
||||
if published != "" {
|
||||
opts.Published = &published
|
||||
}
|
||||
if updated != "" {
|
||||
opts.Updated = &updated
|
||||
}
|
||||
if modified != "" {
|
||||
opts.Modified = &modified
|
||||
}
|
||||
|
||||
advisories, resp, err := client.SecurityAdvisories.ListGlobalSecurityAdvisories(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to list global security advisories: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list advisories", resp, body), nil, nil
|
||||
}
|
||||
|
||||
r, err := json.Marshal(advisories)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal advisories: %w", err)
|
||||
}
|
||||
|
||||
result := utils.NewToolResultText(string(r))
|
||||
// Global advisories come from the world-readable GitHub Advisory
|
||||
// Database (public) but contain externally authored prose
|
||||
// (untrusted).
|
||||
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelGlobalSecurityAdvisory())
|
||||
return result, nil, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func ListRepositorySecurityAdvisories(t translations.TranslationHelperFunc) inventory.ServerTool {
|
||||
return NewTool(
|
||||
ToolsetMetadataSecurityAdvisories,
|
||||
mcp.Tool{
|
||||
Name: "list_repository_security_advisories",
|
||||
Description: t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub repository."),
|
||||
Annotations: &mcp.ToolAnnotations{
|
||||
Title: t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List repository security advisories"),
|
||||
ReadOnlyHint: true,
|
||||
},
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"owner": {
|
||||
Type: "string",
|
||||
Description: "The owner of the repository.",
|
||||
},
|
||||
"repo": {
|
||||
Type: "string",
|
||||
Description: "The name of the repository.",
|
||||
},
|
||||
"direction": {
|
||||
Type: "string",
|
||||
Description: "Sort direction.",
|
||||
Enum: []any{"asc", "desc"},
|
||||
},
|
||||
"sort": {
|
||||
Type: "string",
|
||||
Description: "Sort field.",
|
||||
Enum: []any{"created", "updated", "published"},
|
||||
},
|
||||
"state": {
|
||||
Type: "string",
|
||||
Description: "Filter by advisory state.",
|
||||
Enum: []any{"triage", "draft", "published", "closed"},
|
||||
},
|
||||
},
|
||||
Required: []string{"owner", "repo"},
|
||||
},
|
||||
},
|
||||
[]scopes.Scope{scopes.SecurityEvents},
|
||||
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
||||
owner, err := RequiredParam[string](args, "owner")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
repo, err := RequiredParam[string](args, "repo")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
|
||||
direction, err := OptionalParam[string](args, "direction")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
sortField, err := OptionalParam[string](args, "sort")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
state, err := OptionalParam[string](args, "state")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
|
||||
client, err := deps.GetClient(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
||||
}
|
||||
|
||||
opts := &github.ListRepositorySecurityAdvisoriesOptions{}
|
||||
if direction != "" {
|
||||
opts.Direction = direction
|
||||
}
|
||||
if sortField != "" {
|
||||
opts.Sort = sortField
|
||||
}
|
||||
if state != "" {
|
||||
opts.State = state
|
||||
}
|
||||
|
||||
advisories, resp, err := client.SecurityAdvisories.ListRepositorySecurityAdvisories(ctx, owner, repo, opts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to list repository security advisories: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list repository advisories", resp, body), nil, nil
|
||||
}
|
||||
|
||||
r, err := json.Marshal(advisories)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal advisories: %w", err)
|
||||
}
|
||||
|
||||
result := utils.NewToolResultText(string(r))
|
||||
// Repository advisories carry externally authored prose (untrusted).
|
||||
// Confidentiality follows repo visibility, but draft/triage/closed
|
||||
// advisories are not world-readable even on a public repo, so the
|
||||
// result is only public when every returned advisory is published.
|
||||
allPublished := allAdvisoriesPublished(advisories)
|
||||
result = attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, result,
|
||||
func(isPrivate bool) ifc.SecurityLabel {
|
||||
return ifc.LabelRepositorySecurityAdvisory(isPrivate, allPublished)
|
||||
})
|
||||
return result, nil, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func GetGlobalSecurityAdvisory(t translations.TranslationHelperFunc) inventory.ServerTool {
|
||||
return NewTool(
|
||||
ToolsetMetadataSecurityAdvisories,
|
||||
mcp.Tool{
|
||||
Name: "get_global_security_advisory",
|
||||
Description: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_DESCRIPTION", "Get a global security advisory"),
|
||||
Annotations: &mcp.ToolAnnotations{
|
||||
Title: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_USER_TITLE", "Get a global security advisory"),
|
||||
ReadOnlyHint: true,
|
||||
},
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"ghsaId": {
|
||||
Type: "string",
|
||||
Description: "GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx).",
|
||||
},
|
||||
},
|
||||
Required: []string{"ghsaId"},
|
||||
},
|
||||
},
|
||||
[]scopes.Scope{scopes.SecurityEvents},
|
||||
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
||||
client, err := deps.GetClient(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
||||
}
|
||||
|
||||
ghsaID, err := RequiredParam[string](args, "ghsaId")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(fmt.Sprintf("invalid ghsaId: %v", err)), nil, nil
|
||||
}
|
||||
|
||||
advisory, resp, err := client.SecurityAdvisories.GetGlobalSecurityAdvisories(ctx, ghsaID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get advisory: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get advisory", resp, body), nil, nil
|
||||
}
|
||||
|
||||
r, err := json.Marshal(advisory)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal advisory: %w", err)
|
||||
}
|
||||
|
||||
result := utils.NewToolResultText(string(r))
|
||||
// A global advisory is world-readable (public) but externally
|
||||
// authored (untrusted).
|
||||
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelGlobalSecurityAdvisory())
|
||||
return result, nil, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func ListOrgRepositorySecurityAdvisories(t translations.TranslationHelperFunc) inventory.ServerTool {
|
||||
return NewTool(
|
||||
ToolsetMetadataSecurityAdvisories,
|
||||
mcp.Tool{
|
||||
Name: "list_org_repository_security_advisories",
|
||||
Description: t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub organization."),
|
||||
Annotations: &mcp.ToolAnnotations{
|
||||
Title: t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List org repository security advisories"),
|
||||
ReadOnlyHint: true,
|
||||
},
|
||||
InputSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"org": {
|
||||
Type: "string",
|
||||
Description: "The organization login.",
|
||||
},
|
||||
"direction": {
|
||||
Type: "string",
|
||||
Description: "Sort direction.",
|
||||
Enum: []any{"asc", "desc"},
|
||||
},
|
||||
"sort": {
|
||||
Type: "string",
|
||||
Description: "Sort field.",
|
||||
Enum: []any{"created", "updated", "published"},
|
||||
},
|
||||
"state": {
|
||||
Type: "string",
|
||||
Description: "Filter by advisory state.",
|
||||
Enum: []any{"triage", "draft", "published", "closed"},
|
||||
},
|
||||
},
|
||||
Required: []string{"org"},
|
||||
},
|
||||
},
|
||||
[]scopes.Scope{scopes.SecurityEvents},
|
||||
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
||||
org, err := RequiredParam[string](args, "org")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
direction, err := OptionalParam[string](args, "direction")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
sortField, err := OptionalParam[string](args, "sort")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
state, err := OptionalParam[string](args, "state")
|
||||
if err != nil {
|
||||
return utils.NewToolResultError(err.Error()), nil, nil
|
||||
}
|
||||
|
||||
client, err := deps.GetClient(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
||||
}
|
||||
|
||||
opts := &github.ListRepositorySecurityAdvisoriesOptions{}
|
||||
if direction != "" {
|
||||
opts.Direction = direction
|
||||
}
|
||||
if sortField != "" {
|
||||
opts.Sort = sortField
|
||||
}
|
||||
if state != "" {
|
||||
opts.State = state
|
||||
}
|
||||
|
||||
advisories, resp, err := client.SecurityAdvisories.ListRepositorySecurityAdvisoriesForOrg(ctx, org, opts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to list organization repository security advisories: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list organization repository advisories", resp, body), nil, nil
|
||||
}
|
||||
|
||||
r, err := json.Marshal(advisories)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal advisories: %w", err)
|
||||
}
|
||||
|
||||
result := utils.NewToolResultText(string(r))
|
||||
// Org-wide advisory listings span the organization's repositories
|
||||
// (including private ones) and are restricted to org members, so
|
||||
// they are conservatively labeled private-untrusted (isPrivate=true,
|
||||
// which forces private regardless of publication state).
|
||||
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelRepositorySecurityAdvisory(true, false))
|
||||
return result, nil, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// allAdvisoriesPublished reports whether every advisory in the slice is in the
|
||||
// "published" state. Repository security advisories can also be in draft,
|
||||
// triage, or closed states, none of which are world-readable even on a public
|
||||
// repository. An empty slice is treated as published (true) since there is no
|
||||
// non-public content to protect. Used to decide whether a repository advisory
|
||||
// listing may carry a public confidentiality label.
|
||||
func allAdvisoriesPublished(advisories []*github.SecurityAdvisory) bool {
|
||||
for _, advisory := range advisories {
|
||||
if advisory.GetState() != "published" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user