Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,19 @@ The following sets of tools are available:
- `sort`: Sort field. (string, optional)
- `state`: Filter by advisory state. (string, optional)

- **report_security_vulnerability** - Report security vulnerability
- **Required OAuth Scopes**: `security_events`
- **Accepted OAuth Scopes**: `repo`, `security_events`
- `cvss_vector_string`: The CVSS vector that calculates the severity of the advisory. You must choose between setting this field or severity. (string, optional)
- `cwe_ids`: A list of Common Weakness Enumeration (CWE) IDs (e.g. ["CWE-79", "CWE-89"]). (string[], optional)
- `description`: A detailed description of what the vulnerability entails. (string, required)
- `owner`: The owner of the repository. (string, required)
- `repo`: The name of the repository. (string, required)
- `severity`: The severity of the advisory. You must choose between setting this field or cvss_vector_string. (string, optional)
- `start_private_fork`: Whether to create a temporary private fork of the repository to collaborate on a fix. Default: false (boolean, optional)
- `summary`: A short summary of the security vulnerability. (string, required)
- `vulnerabilities`: An array of products affected by the vulnerability. (object[], optional)

</details>

<details>
Expand Down
105 changes: 105 additions & 0 deletions pkg/github/__toolsnaps__/report_security_vulnerability.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"annotations": {
"title": "Report security vulnerability"
},
"description": "Report a security vulnerability to the maintainers of a repository. Creates a private security advisory in 'triage' state.",
"inputSchema": {
"type": "object",
"properties": {
"cvss_vector_string": {
"type": "string",
"description": "The CVSS vector that calculates the severity of the advisory. You must choose between setting this field or severity."
},
"cwe_ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of Common Weakness Enumeration (CWE) IDs (e.g. [\"CWE-79\", \"CWE-89\"])."
},
"description": {
"type": "string",
"description": "A detailed description of what the vulnerability entails."
},
"owner": {
"type": "string",
"description": "The owner of the repository."
},
"repo": {
"type": "string",
"description": "The name of the repository."
},
"severity": {
"type": "string",
"description": "The severity of the advisory. You must choose between setting this field or cvss_vector_string.",
"enum": [
"critical",
"high",
"medium",
"low"
]
},
"start_private_fork": {
"type": "boolean",
"description": "Whether to create a temporary private fork of the repository to collaborate on a fix. Default: false",
"default": false
},
"summary": {
"type": "string",
"description": "A short summary of the security vulnerability."
},
"vulnerabilities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"package": {
"type": "object",
"properties": {
"ecosystem": {
"type": "string",
"description": "The package ecosystem (e.g., npm, pip, maven, rubygems)."
},
"name": {
"type": "string",
"description": "The package name."
}
},
"description": "The package affected by the vulnerability.",
"required": [
"ecosystem",
"name"
]
},
"patched_versions": {
"type": "string",
"description": "The versions that patch the vulnerability (e.g., '1.0.1')."
},
"vulnerable_functions": {
"type": "array",
"items": {
"type": "string"
},
"description": "The names of vulnerable functions in the package."
},
"vulnerable_version_range": {
"type": "string",
"description": "The range of versions that are vulnerable (e.g., '\u003e= 1.0.0, \u003c 1.0.1')."
}
},
"required": [
"package"
]
},
"description": "An array of products affected by the vulnerability."
}
},
"required": [
"owner",
"repo",
"summary",
"description"
]
},
"name": "report_security_vulnerability"
}
252 changes: 252 additions & 0 deletions pkg/github/security_advisories.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,255 @@ func ListOrgRepositorySecurityAdvisories(t translations.TranslationHelperFunc) i
},
)
}

func ReportSecurityVulnerability(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataSecurityAdvisories,
mcp.Tool{
Name: "report_security_vulnerability",
Description: t("TOOL_REPORT_SECURITY_VULNERABILITY_DESCRIPTION", "Report a security vulnerability to the maintainers of a repository. Creates a private security advisory in 'triage' state."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_REPORT_SECURITY_VULNERABILITY_USER_TITLE", "Report security vulnerability"),
ReadOnlyHint: false,
},
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.",
},
"summary": {
Type: "string",
Description: "A short summary of the security vulnerability.",
},
"description": {
Type: "string",
Description: "A detailed description of what the vulnerability entails.",
},
"severity": {
Type: "string",
Description: "The severity of the advisory. You must choose between setting this field or cvss_vector_string.",
Enum: []any{"critical", "high", "medium", "low"},
},
"cvss_vector_string": {
Type: "string",
Description: "The CVSS vector that calculates the severity of the advisory. You must choose between setting this field or severity.",
},
"cwe_ids": {
Type: "array",
Description: "A list of Common Weakness Enumeration (CWE) IDs (e.g. [\"CWE-79\", \"CWE-89\"]).",
Items: &jsonschema.Schema{
Type: "string",
},
},
"vulnerabilities": {
Type: "array",
Description: "An array of products affected by the vulnerability.",
Items: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"package": {
Type: "object",
Description: "The package affected by the vulnerability.",
Properties: map[string]*jsonschema.Schema{
"ecosystem": {
Type: "string",
Description: "The package ecosystem (e.g., npm, pip, maven, rubygems).",
},
"name": {
Type: "string",
Description: "The package name.",
},
},
Required: []string{"ecosystem", "name"},
},
"vulnerable_version_range": {
Type: "string",
Description: "The range of versions that are vulnerable (e.g., '>= 1.0.0, < 1.0.1').",
},
"patched_versions": {
Type: "string",
Description: "The versions that patch the vulnerability (e.g., '1.0.1').",
},
"vulnerable_functions": {
Type: "array",
Description: "The names of vulnerable functions in the package.",
Items: &jsonschema.Schema{
Type: "string",
},
},
},
Required: []string{"package"},
},
},
"start_private_fork": {
Type: "boolean",
Description: "Whether to create a temporary private fork of the repository to collaborate on a fix. Default: false",
Default: json.RawMessage(`false`),
},
},
Required: []string{"owner", "repo", "summary", "description"},
},
},
[]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
}
summary, err := RequiredParam[string](args, "summary")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
description, err := RequiredParam[string](args, "description")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

severity, err := OptionalParam[string](args, "severity")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
cvssVectorString, err := OptionalParam[string](args, "cvss_vector_string")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

// Validate that only one of severity or cvss_vector_string is set
if severity != "" && cvssVectorString != "" {
return utils.NewToolResultError("cannot specify both severity and cvss_vector_string"), nil, nil
}

cweIDs, err := OptionalStringArrayParam(args, "cwe_ids")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

startPrivateFork, err := OptionalParam[bool](args, "start_private_fork")
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)
}

// Build the request body
type vulnerabilityReport struct {
Summary string `json:"summary"`
Description string `json:"description"`
Severity *string `json:"severity,omitempty"`
CVSSVectorString *string `json:"cvss_vector_string,omitempty"`
CWEIDs *[]string `json:"cwe_ids,omitempty"`
Vulnerabilities *[]*github.AdvisoryVulnerability `json:"vulnerabilities,omitempty"`
StartPrivateFork *bool `json:"start_private_fork,omitempty"`
}

report := &vulnerabilityReport{
Summary: summary,
Description: description,
}

if severity != "" {
report.Severity = &severity
}
if cvssVectorString != "" {
report.CVSSVectorString = &cvssVectorString
}

if len(cweIDs) > 0 {
report.CWEIDs = &cweIDs
}

// Handle vulnerabilities array
if vulnsData, ok := args["vulnerabilities"]; ok {
if vulnsArray, ok := vulnsData.([]any); ok {
var vulnerabilities []*github.AdvisoryVulnerability
for _, v := range vulnsArray {
if vulnMap, ok := v.(map[string]any); ok {
vuln := &github.AdvisoryVulnerability{}

// Parse package
if pkgData, ok := vulnMap["package"].(map[string]any); ok {
pkg := &github.VulnerabilityPackage{}
if ecosystem, ok := pkgData["ecosystem"].(string); ok {
pkg.Ecosystem = &ecosystem
}
if name, ok := pkgData["name"].(string); ok {
pkg.Name = &name
}
vuln.Package = pkg
}

// Parse other fields
if versionRange, ok := vulnMap["vulnerable_version_range"].(string); ok {
vuln.VulnerableVersionRange = &versionRange
}
if patchedVersions, ok := vulnMap["patched_versions"].(string); ok {
vuln.PatchedVersions = &patchedVersions
}
if vulnFuncs, ok := vulnMap["vulnerable_functions"].([]any); ok {
var functions []string
for _, f := range vulnFuncs {
if funcStr, ok := f.(string); ok {
functions = append(functions, funcStr)
}
}
if len(functions) > 0 {
vuln.VulnerableFunctions = functions
}
}

vulnerabilities = append(vulnerabilities, vuln)
}
}
report.Vulnerabilities = &vulnerabilities
}
}
Comment on lines +636 to +680

if startPrivateFork {
report.StartPrivateFork = &startPrivateFork
}

// Make HTTP POST request to the security-advisories/reports endpoint
// The go-github library doesn't have this method yet, so we use NewRequest directly
url := fmt.Sprintf("repos/%s/%s/security-advisories/reports", owner, repo)
req, err := client.NewRequest("POST", url, report)
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}

var advisory github.SecurityAdvisory
resp, err := client.Do(ctx, req, &advisory)
if err != nil {
return nil, nil, fmt.Errorf("failed to report security vulnerability: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusCreated {
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 report security vulnerability", resp, body), nil, nil
}

r, err := json.Marshal(advisory)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal advisory response: %w", err)
}

return utils.NewToolResultText(string(r)), nil, nil
},
)
}
Loading
Loading