Skip to content

Commit d029ce4

Browse files
committed
Add tool to report security vulnerability
1 parent 71862a9 commit d029ce4

File tree

5 files changed

+620
-0
lines changed

5 files changed

+620
-0
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,19 @@ The following sets of tools are available:
13221322
- `sort`: Sort field. (string, optional)
13231323
- `state`: Filter by advisory state. (string, optional)
13241324

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

13271340
<details>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
{
2+
"annotations": {
3+
"title": "Report security vulnerability"
4+
},
5+
"description": "Report a security vulnerability to the maintainers of a repository. Creates a private security advisory in 'triage' state.",
6+
"inputSchema": {
7+
"type": "object",
8+
"properties": {
9+
"cvss_vector_string": {
10+
"type": "string",
11+
"description": "The CVSS vector that calculates the severity of the advisory. You must choose between setting this field or severity."
12+
},
13+
"cwe_ids": {
14+
"type": "array",
15+
"items": {
16+
"type": "string"
17+
},
18+
"description": "A list of Common Weakness Enumeration (CWE) IDs (e.g. [\"CWE-79\", \"CWE-89\"])."
19+
},
20+
"description": {
21+
"type": "string",
22+
"description": "A detailed description of what the vulnerability entails."
23+
},
24+
"owner": {
25+
"type": "string",
26+
"description": "The owner of the repository."
27+
},
28+
"repo": {
29+
"type": "string",
30+
"description": "The name of the repository."
31+
},
32+
"severity": {
33+
"type": "string",
34+
"description": "The severity of the advisory. You must choose between setting this field or cvss_vector_string.",
35+
"enum": [
36+
"critical",
37+
"high",
38+
"medium",
39+
"low"
40+
]
41+
},
42+
"start_private_fork": {
43+
"type": "boolean",
44+
"description": "Whether to create a temporary private fork of the repository to collaborate on a fix. Default: false",
45+
"default": false
46+
},
47+
"summary": {
48+
"type": "string",
49+
"description": "A short summary of the security vulnerability."
50+
},
51+
"vulnerabilities": {
52+
"type": "array",
53+
"items": {
54+
"type": "object",
55+
"properties": {
56+
"package": {
57+
"type": "object",
58+
"properties": {
59+
"ecosystem": {
60+
"type": "string",
61+
"description": "The package ecosystem (e.g., npm, pip, maven, rubygems)."
62+
},
63+
"name": {
64+
"type": "string",
65+
"description": "The package name."
66+
}
67+
},
68+
"description": "The package affected by the vulnerability.",
69+
"required": [
70+
"ecosystem",
71+
"name"
72+
]
73+
},
74+
"patched_versions": {
75+
"type": "string",
76+
"description": "The versions that patch the vulnerability (e.g., '1.0.1')."
77+
},
78+
"vulnerable_functions": {
79+
"type": "array",
80+
"items": {
81+
"type": "string"
82+
},
83+
"description": "The names of vulnerable functions in the package."
84+
},
85+
"vulnerable_version_range": {
86+
"type": "string",
87+
"description": "The range of versions that are vulnerable (e.g., '\u003e= 1.0.0, \u003c 1.0.1')."
88+
}
89+
},
90+
"required": [
91+
"package"
92+
]
93+
},
94+
"description": "An array of products affected by the vulnerability."
95+
}
96+
},
97+
"required": [
98+
"owner",
99+
"repo",
100+
"summary",
101+
"description"
102+
]
103+
},
104+
"name": "report_security_vulnerability"
105+
}

pkg/github/security_advisories.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,255 @@ func ListOrgRepositorySecurityAdvisories(t translations.TranslationHelperFunc) i
463463
},
464464
)
465465
}
466+
467+
func ReportSecurityVulnerability(t translations.TranslationHelperFunc) inventory.ServerTool {
468+
return NewTool(
469+
ToolsetMetadataSecurityAdvisories,
470+
mcp.Tool{
471+
Name: "report_security_vulnerability",
472+
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."),
473+
Annotations: &mcp.ToolAnnotations{
474+
Title: t("TOOL_REPORT_SECURITY_VULNERABILITY_USER_TITLE", "Report security vulnerability"),
475+
ReadOnlyHint: false,
476+
},
477+
InputSchema: &jsonschema.Schema{
478+
Type: "object",
479+
Properties: map[string]*jsonschema.Schema{
480+
"owner": {
481+
Type: "string",
482+
Description: "The owner of the repository.",
483+
},
484+
"repo": {
485+
Type: "string",
486+
Description: "The name of the repository.",
487+
},
488+
"summary": {
489+
Type: "string",
490+
Description: "A short summary of the security vulnerability.",
491+
},
492+
"description": {
493+
Type: "string",
494+
Description: "A detailed description of what the vulnerability entails.",
495+
},
496+
"severity": {
497+
Type: "string",
498+
Description: "The severity of the advisory. You must choose between setting this field or cvss_vector_string.",
499+
Enum: []any{"critical", "high", "medium", "low"},
500+
},
501+
"cvss_vector_string": {
502+
Type: "string",
503+
Description: "The CVSS vector that calculates the severity of the advisory. You must choose between setting this field or severity.",
504+
},
505+
"cwe_ids": {
506+
Type: "array",
507+
Description: "A list of Common Weakness Enumeration (CWE) IDs (e.g. [\"CWE-79\", \"CWE-89\"]).",
508+
Items: &jsonschema.Schema{
509+
Type: "string",
510+
},
511+
},
512+
"vulnerabilities": {
513+
Type: "array",
514+
Description: "An array of products affected by the vulnerability.",
515+
Items: &jsonschema.Schema{
516+
Type: "object",
517+
Properties: map[string]*jsonschema.Schema{
518+
"package": {
519+
Type: "object",
520+
Description: "The package affected by the vulnerability.",
521+
Properties: map[string]*jsonschema.Schema{
522+
"ecosystem": {
523+
Type: "string",
524+
Description: "The package ecosystem (e.g., npm, pip, maven, rubygems).",
525+
},
526+
"name": {
527+
Type: "string",
528+
Description: "The package name.",
529+
},
530+
},
531+
Required: []string{"ecosystem", "name"},
532+
},
533+
"vulnerable_version_range": {
534+
Type: "string",
535+
Description: "The range of versions that are vulnerable (e.g., '>= 1.0.0, < 1.0.1').",
536+
},
537+
"patched_versions": {
538+
Type: "string",
539+
Description: "The versions that patch the vulnerability (e.g., '1.0.1').",
540+
},
541+
"vulnerable_functions": {
542+
Type: "array",
543+
Description: "The names of vulnerable functions in the package.",
544+
Items: &jsonschema.Schema{
545+
Type: "string",
546+
},
547+
},
548+
},
549+
Required: []string{"package"},
550+
},
551+
},
552+
"start_private_fork": {
553+
Type: "boolean",
554+
Description: "Whether to create a temporary private fork of the repository to collaborate on a fix. Default: false",
555+
Default: json.RawMessage(`false`),
556+
},
557+
},
558+
Required: []string{"owner", "repo", "summary", "description"},
559+
},
560+
},
561+
[]scopes.Scope{scopes.SecurityEvents},
562+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
563+
owner, err := RequiredParam[string](args, "owner")
564+
if err != nil {
565+
return utils.NewToolResultError(err.Error()), nil, nil
566+
}
567+
repo, err := RequiredParam[string](args, "repo")
568+
if err != nil {
569+
return utils.NewToolResultError(err.Error()), nil, nil
570+
}
571+
summary, err := RequiredParam[string](args, "summary")
572+
if err != nil {
573+
return utils.NewToolResultError(err.Error()), nil, nil
574+
}
575+
description, err := RequiredParam[string](args, "description")
576+
if err != nil {
577+
return utils.NewToolResultError(err.Error()), nil, nil
578+
}
579+
580+
severity, err := OptionalParam[string](args, "severity")
581+
if err != nil {
582+
return utils.NewToolResultError(err.Error()), nil, nil
583+
}
584+
cvssVectorString, err := OptionalParam[string](args, "cvss_vector_string")
585+
if err != nil {
586+
return utils.NewToolResultError(err.Error()), nil, nil
587+
}
588+
589+
// Validate that only one of severity or cvss_vector_string is set
590+
if severity != "" && cvssVectorString != "" {
591+
return utils.NewToolResultError("cannot specify both severity and cvss_vector_string"), nil, nil
592+
}
593+
594+
cweIDs, err := OptionalStringArrayParam(args, "cwe_ids")
595+
if err != nil {
596+
return utils.NewToolResultError(err.Error()), nil, nil
597+
}
598+
599+
startPrivateFork, err := OptionalParam[bool](args, "start_private_fork")
600+
if err != nil {
601+
return utils.NewToolResultError(err.Error()), nil, nil
602+
}
603+
604+
client, err := deps.GetClient(ctx)
605+
if err != nil {
606+
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
607+
}
608+
609+
// Build the request body
610+
type vulnerabilityReport struct {
611+
Summary string `json:"summary"`
612+
Description string `json:"description"`
613+
Severity *string `json:"severity,omitempty"`
614+
CVSSVectorString *string `json:"cvss_vector_string,omitempty"`
615+
CWEIDs *[]string `json:"cwe_ids,omitempty"`
616+
Vulnerabilities *[]*github.AdvisoryVulnerability `json:"vulnerabilities,omitempty"`
617+
StartPrivateFork *bool `json:"start_private_fork,omitempty"`
618+
}
619+
620+
report := &vulnerabilityReport{
621+
Summary: summary,
622+
Description: description,
623+
}
624+
625+
if severity != "" {
626+
report.Severity = &severity
627+
}
628+
if cvssVectorString != "" {
629+
report.CVSSVectorString = &cvssVectorString
630+
}
631+
632+
if len(cweIDs) > 0 {
633+
report.CWEIDs = &cweIDs
634+
}
635+
636+
// Handle vulnerabilities array
637+
if vulnsData, ok := args["vulnerabilities"]; ok {
638+
if vulnsArray, ok := vulnsData.([]any); ok {
639+
var vulnerabilities []*github.AdvisoryVulnerability
640+
for _, v := range vulnsArray {
641+
if vulnMap, ok := v.(map[string]any); ok {
642+
vuln := &github.AdvisoryVulnerability{}
643+
644+
// Parse package
645+
if pkgData, ok := vulnMap["package"].(map[string]any); ok {
646+
pkg := &github.VulnerabilityPackage{}
647+
if ecosystem, ok := pkgData["ecosystem"].(string); ok {
648+
pkg.Ecosystem = &ecosystem
649+
}
650+
if name, ok := pkgData["name"].(string); ok {
651+
pkg.Name = &name
652+
}
653+
vuln.Package = pkg
654+
}
655+
656+
// Parse other fields
657+
if versionRange, ok := vulnMap["vulnerable_version_range"].(string); ok {
658+
vuln.VulnerableVersionRange = &versionRange
659+
}
660+
if patchedVersions, ok := vulnMap["patched_versions"].(string); ok {
661+
vuln.PatchedVersions = &patchedVersions
662+
}
663+
if vulnFuncs, ok := vulnMap["vulnerable_functions"].([]any); ok {
664+
var functions []string
665+
for _, f := range vulnFuncs {
666+
if funcStr, ok := f.(string); ok {
667+
functions = append(functions, funcStr)
668+
}
669+
}
670+
if len(functions) > 0 {
671+
vuln.VulnerableFunctions = functions
672+
}
673+
}
674+
675+
vulnerabilities = append(vulnerabilities, vuln)
676+
}
677+
}
678+
report.Vulnerabilities = &vulnerabilities
679+
}
680+
}
681+
682+
if startPrivateFork {
683+
report.StartPrivateFork = &startPrivateFork
684+
}
685+
686+
// Make HTTP POST request to the security-advisories/reports endpoint
687+
// The go-github library doesn't have this method yet, so we use NewRequest directly
688+
url := fmt.Sprintf("repos/%s/%s/security-advisories/reports", owner, repo)
689+
req, err := client.NewRequest("POST", url, report)
690+
if err != nil {
691+
return nil, nil, fmt.Errorf("failed to create request: %w", err)
692+
}
693+
694+
var advisory github.SecurityAdvisory
695+
resp, err := client.Do(ctx, req, &advisory)
696+
if err != nil {
697+
return nil, nil, fmt.Errorf("failed to report security vulnerability: %w", err)
698+
}
699+
defer func() { _ = resp.Body.Close() }()
700+
701+
if resp.StatusCode != http.StatusCreated {
702+
body, err := io.ReadAll(resp.Body)
703+
if err != nil {
704+
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
705+
}
706+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to report security vulnerability", resp, body), nil, nil
707+
}
708+
709+
r, err := json.Marshal(advisory)
710+
if err != nil {
711+
return nil, nil, fmt.Errorf("failed to marshal advisory response: %w", err)
712+
}
713+
714+
return utils.NewToolResultText(string(r)), nil, nil
715+
},
716+
)
717+
}

0 commit comments

Comments
 (0)