-
Notifications
You must be signed in to change notification settings - Fork 736
feat: OSV-Scanner MCP server #2256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
af10173
Scanner MCP
another-rex a54f15a
Bump up dependency versions
another-rex ff89e6a
Fix severity
another-rex d08835c
Update snapshots
another-rex afb3cca
Add ignoring config capabilities
another-rex 89ddaf3
Merge remote-tracking branch 'upstream/main' into scanner-mcp
another-rex b16835b
Fix PR comments and add additional docs
another-rex 9579208
Update internal/output/mcp.go
another-rex 8e6750a
Go mod tidy
another-rex 896393c
Remove unnecessary mcp report option
another-rex 4bf82e7
Use a syncmap to store vulns
another-rex 76f7548
Address PR comments
another-rex ac4141c
Merge branch 'main' into scanner-mcp
another-rex File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| // Package mcp implements the `mcp` command for osv-scanner. | ||
| package mcp | ||
|
|
||
| import ( | ||
| "context" | ||
| _ "embed" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "net/http" | ||
|
|
||
| "github.com/google/osv-scanner/v2/internal/cmdlogger" | ||
| "github.com/google/osv-scanner/v2/internal/output" | ||
| "github.com/google/osv-scanner/v2/internal/version" | ||
| "github.com/google/osv-scanner/v2/pkg/osvscanner" | ||
| "github.com/jedib0t/go-pretty/v6/text" | ||
| "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| "github.com/ossf/osv-schema/bindings/go/osvschema" | ||
| "github.com/urfave/cli/v3" | ||
| "osv.dev/bindings/go/osvdev" | ||
| ) | ||
|
|
||
| // vulnCacheMap is a cache of vulnerability details that have been retrieved from the OSV API during normal scanning. | ||
| // This avoids unnecessary double queries to the osv.dev API. | ||
| // vulnCacheMap: map[string]*osvschema.Vulnerability | ||
| var vulnCacheMap = sync.Map{} | ||
|
another-rex marked this conversation as resolved.
|
||
|
|
||
| // Command is the entry point for the `mcp` subcommand. | ||
| func Command(_, _ io.Writer) *cli.Command { | ||
| return &cli.Command{ | ||
| Name: "experimental-mcp", | ||
| Usage: "Run osv-scanner as an MCP service (experimental)", | ||
| Description: "Run osv-scanner as an MCP service, speaking the MCP protocol over stdin/stdout.", | ||
| Flags: []cli.Flag{ | ||
| &cli.StringFlag{ | ||
| Name: "sse", | ||
| DefaultText: "localhost:8080", | ||
| Value: "localhost:8080", | ||
| Usage: "The listening address for the SSE server, e.g. localhost:8080", | ||
| }, | ||
| }, | ||
| Action: action, | ||
| } | ||
| } | ||
|
|
||
| // scanVulnerableDependenciesInput is the input for the scan_vulnerable_dependencies tool. | ||
| type scanVulnerableDependenciesInput struct { | ||
| Paths []string `json:"paths" jsonschema:"A list of absolute or relative path to a file or directory to scan."` | ||
| IgnoreGlobPatterns []string `json:"ignore_glob_patterns" jsonschema:"A list of glob patterns to ignore when scanning."` | ||
| Recursive bool `json:"recursive" jsonschema:"Scans directory recursively"` | ||
| } | ||
|
|
||
| func action(ctx context.Context, cmd *cli.Command) error { | ||
| s := mcp.NewServer(&mcp.Implementation{ | ||
| Name: "OSV-Scanner", Version: version.OSVVersion, | ||
| }, nil) | ||
|
|
||
| mcp.AddTool(s, &mcp.Tool{ | ||
| Name: "scan_vulnerable_dependencies", | ||
| Description: "Scans a source directory for vulnerable dependencies." + | ||
| " Walks the given directory and uses osv.dev to query for vulnerabilities matching the found dependencies." + | ||
| " Use this tool to check that the user's project is not depending on known vulnerable code.", | ||
| }, handleScan) | ||
|
|
||
| // TODO(another-rex): Ideally both of the following tools would be resources, but gemini-cli does not support those yet. | ||
| mcp.AddTool(s, &mcp.Tool{ | ||
| Name: "get_vulnerability_details", | ||
| Description: "Retrieves the full JSON details for a given vulnerability ID.", | ||
| }, handleVulnIDRetrieval) | ||
|
|
||
| mcp.AddTool(s, &mcp.Tool{ | ||
| Name: "ignore_vulnerability", | ||
| Description: "Provides instructions for writing a config file to exclude vulnerabilities from the scan report.", | ||
| }, handleIgnoreVulnerability) | ||
|
|
||
| s.AddPrompt(&mcp.Prompt{ | ||
| Name: "scan_deps", | ||
| Description: "Scans your project dependencies for known vulnerabilities.", | ||
| }, handleScanDepsPrompt) | ||
|
|
||
| // Provide two options, sse on a network port, or stdio. | ||
| if cmd.IsSet("sse") { | ||
| sseAddr := cmd.String("sse") | ||
| cmdlogger.Infof("Starting SSE server on %s", sseAddr) | ||
| handler := mcp.NewSSEHandler(func(_ *http.Request) *mcp.Server { | ||
| return s | ||
| }, nil) | ||
| srv := &http.Server{ | ||
| Addr: sseAddr, | ||
| Handler: handler, | ||
| ReadTimeout: 30 * time.Second, | ||
| WriteTimeout: 30 * time.Second, | ||
| IdleTimeout: 120 * time.Second, | ||
| } | ||
| if err := srv.ListenAndServe(); err != nil { | ||
| cmdlogger.Errorf("mcp error: %s", err) | ||
| return err | ||
| } | ||
| } else { | ||
| cmdlogger.SendEverythingToStderr() | ||
| cmdlogger.Infof("Starting MCP server on stdio") | ||
| if err := s.Run(ctx, &mcp.StdioTransport{}); err != nil { | ||
| cmdlogger.Errorf("mcp error: %s", err) | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func handleScan(_ context.Context, _ *mcp.CallToolRequest, input *scanVulnerableDependenciesInput) (*mcp.CallToolResult, any, error) { | ||
| statsCollector := fileOpenedLogger{} | ||
|
|
||
| action := osvscanner.ScannerActions{ | ||
| DirectoryPaths: input.Paths, | ||
| ScanLicensesSummary: false, | ||
| ExperimentalScannerActions: osvscanner.ExperimentalScannerActions{ | ||
| StatsCollector: &statsCollector, | ||
| }, | ||
| CallAnalysisStates: map[string]bool{ | ||
| "go": true, | ||
| }, | ||
| Recursive: input.Recursive, | ||
| } | ||
|
|
||
| //nolint:contextcheck // passing the context in would be a breaking change | ||
| scanResults, err := osvscanner.DoScan(action) | ||
|
another-rex marked this conversation as resolved.
|
||
| if err != nil && !errors.Is(err, osvscanner.ErrVulnerabilitiesFound) { | ||
| return nil, nil, fmt.Errorf("failed to run scanner: %w", err) | ||
| } | ||
|
|
||
| for _, vuln := range scanResults.Flatten() { | ||
| vulnCacheMap.Store(vuln.Vulnerability.ID, &vuln.Vulnerability) | ||
| } | ||
|
|
||
| if err == nil { | ||
| return &mcp.CallToolResult{ | ||
| Content: []mcp.Content{ | ||
| &mcp.TextContent{Text: "No issues found"}, | ||
| }, | ||
| }, nil, nil | ||
| } | ||
|
|
||
| buf := strings.Builder{} | ||
|
|
||
| for _, s := range statsCollector.collectedLines { | ||
| buf.WriteString(s + "\n") | ||
| } | ||
|
|
||
| text.DisableColors() | ||
| output.PrintVerticalResults(&scanResults, &buf, false) | ||
|
|
||
| return &mcp.CallToolResult{ | ||
| Content: []mcp.Content{ | ||
| &mcp.TextContent{Text: buf.String()}, | ||
| }, | ||
| }, nil, nil | ||
| } | ||
|
|
||
| // getVulnerabilityDetailsInput is the input for the get_vulnerability_details tool. | ||
| type getVulnerabilityDetailsInput struct { | ||
| VulnID string `json:"vuln_id" jsonschema:"The OSV vulnerability ID to retrieve details for."` | ||
| } | ||
|
|
||
| func handleVulnIDRetrieval(ctx context.Context, _ *mcp.CallToolRequest, input *getVulnerabilityDetailsInput) (*mcp.CallToolResult, *osvschema.Vulnerability, error) { | ||
| vulnAny, found := vulnCacheMap.Load(input.VulnID) | ||
| vuln := vulnAny.(*osvschema.Vulnerability) | ||
| if !found { | ||
| var err error | ||
| vuln, err = osvdev.DefaultClient().GetVulnByID(ctx, input.VulnID) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("vulnerability with ID %s not found: %w", input.VulnID, err) | ||
| } | ||
|
|
||
| vulnCacheMap.Store(input.VulnID, vuln) | ||
| } | ||
|
|
||
| return &mcp.CallToolResult{}, vuln, nil | ||
| } | ||
|
|
||
| // ignoreVulnerabilityInput is a placeholder to enable the tool call, | ||
| // as it seems like go-sdk mcp does not support a tool call with no arguments. | ||
| type ignoreVulnerabilityInput struct { | ||
| // Extra field is needed as a placeholder to prevent the llm from erroring when calling the tool | ||
| Verbose bool `json:"verbose" jsonschema:"ignore this parameter"` | ||
|
another-rex marked this conversation as resolved.
|
||
| } | ||
|
|
||
| //go:embed configuration-instructions.md | ||
| var configInstructions string | ||
|
|
||
| // handleIgnoreVulnerability does not perform any actual actions, but instead provides the instructions of how | ||
| // to write an ignore file to the LLM using this tool, so that it can correctly write the ignore file. | ||
| func handleIgnoreVulnerability(_ context.Context, _ *mcp.CallToolRequest, _ *ignoreVulnerabilityInput) (*mcp.CallToolResult, any, error) { | ||
|
another-rex marked this conversation as resolved.
|
||
| return &mcp.CallToolResult{ | ||
| Content: []mcp.Content{ | ||
| &mcp.TextContent{Text: configInstructions}, | ||
| }, | ||
| }, nil, nil | ||
| } | ||
|
|
||
| // scanDepsPrompt is the prompt that is sent to the AI model when the scan_deps prompt is requested. | ||
| // | ||
| //go:embed scan-deps-prompt.md | ||
| var scanDepsPrompt string | ||
|
|
||
| func handleScanDepsPrompt(_ context.Context, _ *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { | ||
| return &mcp.GetPromptResult{ | ||
| Description: "Dependency vulnerability analysis", | ||
| Messages: []*mcp.PromptMessage{ | ||
| { | ||
| Role: "assistant", | ||
| Content: &mcp.TextContent{ | ||
| Text: scanDepsPrompt, | ||
| }, | ||
| }, | ||
| }, | ||
| }, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| --- | ||
| layout: page | ||
| permalink: /configuration/ | ||
| nav_order: 5 | ||
| --- | ||
|
|
||
| # Configuration | ||
|
|
||
| To configure scanning, place an osv-scanner.toml file in the scanned file's directory. This does not propagate to child directories. | ||
|
|
||
| **Example:** | ||
|
|
||
| ``` | ||
| /Cargo.lock | ||
| /osv-scanner.toml (1) | ||
| /child-dir/go.mod | ||
| /child-dir/osv-scanner.toml (2) | ||
| /child-dir/nested-dir/package-lock.json | ||
| ``` | ||
|
|
||
| `osv-scanner.toml (1)` will only apply to `Cargo.lock`, `osv-scanner.toml (2)` will only apply to `go.mod`, and no config will apply to `package-lock.json`. | ||
|
|
||
| To override `osv-scanner.toml` files, pass the `--config=/path/to/config.toml` flag with the path to the configuration you want to apply instead, this will apply `config.toml` to all files parsed, and ignore `osv-scanner.toml` in all directories. | ||
|
|
||
| ## Ignore vulnerabilities by ID | ||
|
|
||
| To ignore a vulnerability, enter the ID under the `IgnoreVulns` key. Optionally, add an expiry date or reason. | ||
|
|
||
| ### Example | ||
|
|
||
| ```toml | ||
| [[IgnoredVulns]] | ||
| id = "GO-2022-0968" | ||
| # ignoreUntil = 2022-11-09 # Optional exception expiry date | ||
| reason = "No ssh servers are connected to or hosted in Go lang" | ||
|
|
||
| [[IgnoredVulns]] | ||
| id = "GO-2022-1059" | ||
| # ignoreUntil = 2022-11-09 # Optional exception expiry date | ||
| reason = "No external http servers are written in Go lang." | ||
| ``` | ||
|
|
||
| Ignoring a vulnerability will also ignore vulnerabilities that are considered aliases of that vulnerability. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| You are a highly skilled senior security analyst. | ||
| Your primary task is to conduct a security audit of the vulnerabilities in the dependencies of this project. | ||
| Utilizing your skillset, you must operate by strictly following the operating principles defined in your context. | ||
|
|
||
| **Step 1: Perform initial scan** | ||
|
|
||
| Use the scan_vulnerable_dependencies with recursive on the project, always use the absolute path. | ||
| This will return a report of all the relevant lockfiles and all vulnerable dependencies in those files. | ||
|
|
||
| **Step 2: Analyse the report** | ||
|
|
||
| Go through the report and determine the relevant project lockfiles (ignoring lockfiles in test directories), | ||
| and prioritise which vulnerability to fix based on the description and severity. | ||
| If more information is needed about a vulnerability, use get_vulnerability_details. | ||
|
|
||
| **Step 3: Prioritisation** | ||
|
|
||
| Give advice on which vulnerabilities to prioritise fixing, and general advice on how to go about fixing | ||
| them by updating. Don't try to automatically update for the user without input. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package mcp | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "path/filepath" | ||
|
|
||
| "github.com/google/osv-scalibr/stats" | ||
| "github.com/google/osv-scanner/v2/internal/output" | ||
| ) | ||
|
|
||
| type fileOpenedLogger struct { | ||
| stats.NoopCollector | ||
|
|
||
| collectedLines []string | ||
| } | ||
|
|
||
| var _ stats.Collector = &fileOpenedLogger{} | ||
|
|
||
| func (c *fileOpenedLogger) AfterExtractorRun(_ string, extractorstats *stats.AfterExtractorStats) { | ||
| if extractorstats.Error != nil { // Don't log scanned if error occurred | ||
| return | ||
| } | ||
|
|
||
| pkgsFound := len(extractorstats.Inventory.Packages) | ||
|
|
||
| c.collectedLines = append(c.collectedLines, | ||
| fmt.Sprintf( | ||
| "Scanned %s file and found %d %s", | ||
| filepath.Join(extractorstats.Root, extractorstats.Path), | ||
| pkgsFound, | ||
| output.Form(pkgsFound, "package", "packages"), | ||
| )) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.