diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9b45ad3a7a2..2bfa6031608 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -16,10 +16,10 @@ name: Checks on: push: - branches: [main, v1] + branches: ["main", "v1", "mcp"] pull_request: # The branches below must be a subset of the branches above - branches: [main, v1] + branches: ["main", "v1", "mcp"] workflow_dispatch: concurrency: diff --git a/.github/workflows/osv-scanner-unified-action.yml b/.github/workflows/osv-scanner-unified-action.yml index c209e0ff73e..7634f4194f8 100644 --- a/.github/workflows/osv-scanner-unified-action.yml +++ b/.github/workflows/osv-scanner-unified-action.yml @@ -16,11 +16,11 @@ name: OSV-Scanner Scheduled Scan on: pull_request: - branches: ["main", "v1"] + branches: ["main", "v1", "mcp"] schedule: - cron: "12 12 * * 1" push: - branches: ["main", "v1"] + branches: ["main", "v1", "mcp"] # Restrict jobs in this workflow to have no permissions by default; permissions # should be granted per job as needed using a dedicated `permissions` block diff --git a/cmd/osv-scanner/main.go b/cmd/osv-scanner/main.go index f3cf03dd3b0..b65d85e1769 100644 --- a/cmd/osv-scanner/main.go +++ b/cmd/osv-scanner/main.go @@ -5,6 +5,7 @@ import ( "github.com/google/osv-scanner/v2/cmd/osv-scanner/fix" "github.com/google/osv-scanner/v2/cmd/osv-scanner/internal/cmd" + "github.com/google/osv-scanner/v2/cmd/osv-scanner/mcp" "github.com/google/osv-scanner/v2/cmd/osv-scanner/scan" "github.com/google/osv-scanner/v2/cmd/osv-scanner/update" ) @@ -15,6 +16,7 @@ func main() { scan.Command, fix.Command, update.Command, + mcp.Command, }), ) } diff --git a/cmd/osv-scanner/mcp/command.go b/cmd/osv-scanner/mcp/command.go new file mode 100644 index 00000000000..e1a15562439 --- /dev/null +++ b/cmd/osv-scanner/mcp/command.go @@ -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{} + +// 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) + 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"` +} + +//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) { + 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 +} diff --git a/cmd/osv-scanner/mcp/configuration-instructions.md b/cmd/osv-scanner/mcp/configuration-instructions.md new file mode 100644 index 00000000000..2ed7ca9e4fc --- /dev/null +++ b/cmd/osv-scanner/mcp/configuration-instructions.md @@ -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. diff --git a/cmd/osv-scanner/mcp/scan-deps-prompt.md b/cmd/osv-scanner/mcp/scan-deps-prompt.md new file mode 100644 index 00000000000..1a39b924f49 --- /dev/null +++ b/cmd/osv-scanner/mcp/scan-deps-prompt.md @@ -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. diff --git a/cmd/osv-scanner/mcp/stats.go b/cmd/osv-scanner/mcp/stats.go new file mode 100644 index 00000000000..e8e579f25b2 --- /dev/null +++ b/cmd/osv-scanner/mcp/stats.go @@ -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"), + )) +} diff --git a/go.mod b/go.mod index 9b93720cdb6..570a2425243 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/google/osv-scalibr v0.3.7-0.20251023161426-90e9ac9cc1b3 github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 github.com/jedib0t/go-pretty/v6 v6.6.8 + github.com/modelcontextprotocol/go-sdk v1.0.0 github.com/muesli/reflow v0.3.0 github.com/opencontainers/go-digest v1.0.0 github.com/ossf/osv-schema/bindings/go v0.0.0-20251012234424-434020c6442f @@ -116,6 +117,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-containerregistry v0.20.6 // indirect + github.com/google/jsonschema-go v0.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -183,6 +185,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yuin/goldmark v1.7.12 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/go.sum b/go.sum index 057651b0d13..1be1adba6df 100644 --- a/go.sum +++ b/go.sum @@ -252,6 +252,8 @@ github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/go-cpy v0.0.0-20211218193943-a9c933c06932 h1:5/4TSDzpDnHQ8rKEEQBjRlYx77mHOvXu08oGchxej7o= github.com/google/go-cpy v0.0.0-20211218193943-a9c933c06932/go.mod h1:cC6EdPbj/17GFCPDK39NRarlMI+kt+O60S12cNB5J9Y= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/osv-scalibr v0.3.7-0.20251023161426-90e9ac9cc1b3 h1:JNLsaIi+lHdzsKoxcDmMBZCbE/qKi/mt9N3eQRxrThc= github.com/google/osv-scalibr v0.3.7-0.20251023161426-90e9ac9cc1b3/go.mod h1:XN3PWSKiShu3MXb+9nGtwLGHWXarZ/0lfbovOhPKTRc= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= @@ -330,6 +332,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= +github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -473,6 +477,8 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/internal/output/__snapshots__/vertical_test.snap b/internal/output/__snapshots__/vertical_test.snap index c518f7a9d70..05d354dbe52 100755 --- a/internal/output/__snapshots__/vertical_test.snap +++ b/internal/output/__snapshots__/vertical_test.snap @@ -367,7 +367,8 @@ Hiding 1 number of vulnerabilities deemed unimportant, use --all-vulns to show t sbom:/path/to/my/second/lockfile: found 1 package with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile no license violations found @@ -394,7 +395,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -406,7 +408,8 @@ lockfile:/path/to/my/first/lockfile: found 1 package with issues sbom:/path/to/my/second/lockfile: found 1 package with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile no license violations found @@ -414,7 +417,8 @@ sbom:/path/to/my/second/lockfile: found 1 package with issues unknown:/path/to/my/third/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in unknown:/path/to/my/third/lockfile @@ -437,7 +441,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -449,7 +454,8 @@ lockfile:/path/to/my/first/lockfile: found 1 package with issues sbom:/path/to/my/second/lockfile: found 1 package with issues mine2@ has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile no license violations found @@ -457,7 +463,8 @@ sbom:/path/to/my/second/lockfile: found 1 package with issues unknown:/path/to/my/third/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in unknown:/path/to/my/third/lockfile @@ -480,7 +487,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -502,7 +510,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -542,7 +551,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -564,7 +574,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile no license violations found @@ -590,20 +601,26 @@ npm lockfile:/path/to/my/first/lockfile: found 2 packages with issues mine1@1.2.2 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 3 known vulnerabilities found in lockfile:/path/to/my/first/lockfile sbom:/path/to/my/second/lockfile: found 2 packages with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; mine3@0.4.1 has the following known vulnerabilities: - OSV-3: Something mildly scary! (https://osv.dev/OSV-3) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-3: Something mildly scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 3 known vulnerabilities found in sbom:/path/to/my/second/lockfile @@ -620,20 +637,26 @@ npm lockfile:/path/to/my/first/lockfile: found 2 packages with issues mine1@1.2.2 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 3 known vulnerabilities found in lockfile:/path/to/my/first/lockfile sbom:/path/to/my/second/lockfile: found 2 packages with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; mine3@0.4.1 has the following known vulnerabilities: - OSV-3: Something mildly scary! (https://osv.dev/OSV-3) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-3: Something mildly scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 3 known vulnerabilities found in sbom:/path/to/my/second/lockfile @@ -669,21 +692,24 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile sbom:/path/to/my/second/lockfile: found 1 package with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile unknown:/path/to/my/third/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in unknown:/path/to/my/third/lockfile @@ -700,7 +726,8 @@ NuGet sbom:/path/to/my/second/lockfile: found 1 package with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile @@ -709,16 +736,20 @@ Packagist lockfile:/path/to/my/first/lockfile: found 1 package with issues author1/mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 2 known vulnerabilities found in lockfile:/path/to/my/first/lockfile sbom:/path/to/my/second/lockfile: found 1 package with issues author3/mine3@0.4.1 has the following known vulnerabilities: - OSV-3: Something mildly scary! (https://osv.dev/OSV-3) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-3: Something mildly scary! + Severity: '4.3'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 2 known vulnerabilities found in sbom:/path/to/my/second/lockfile @@ -727,7 +758,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.2 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -744,7 +776,8 @@ NuGet sbom:/path/to/my/second/lockfile: found 1 package with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile @@ -753,20 +786,24 @@ Packagist lockfile:/path/to/my/first/lockfile: found 1 package with issues author1/mine1@1.2.3 has the following known vulnerabilities: - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile author1/mine1@1.2.3 has the following uncalled vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 uncalled/unimportant vulnerability found in lockfile:/path/to/my/first/lockfile (filtered out) sbom:/path/to/my/second/lockfile: found 1 package with issues author3/mine3@0.4.1 has the following known vulnerabilities: - OSV-3: Something mildly scary! (https://osv.dev/OSV-3) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-3: Something mildly scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 2 known vulnerabilities found in sbom:/path/to/my/second/lockfile @@ -775,7 +812,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.2 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -792,7 +830,8 @@ NuGet sbom:/path/to/my/second/lockfile: found 1 package with issues mine2@3.2.5 has the following known vulnerabilities: - OSV-2: Something less scary! (https://osv.dev/OSV-2) + OSV-2: Something less scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile @@ -801,16 +840,20 @@ Packagist lockfile:/path/to/my/first/lockfile: found 1 package with issues author1/mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 2 known vulnerabilities found in lockfile:/path/to/my/first/lockfile sbom:/path/to/my/second/lockfile: found 1 package with issues author3/mine3@0.4.1 has the following known vulnerabilities: - OSV-3: Something mildly scary! (https://osv.dev/OSV-3) - OSV-5: Something scarier! (https://osv.dev/OSV-5) + OSV-3: Something mildly scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; + OSV-5: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 2 known vulnerabilities found in sbom:/path/to/my/second/lockfile @@ -819,7 +862,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@ has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -890,12 +934,14 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile mine1@1.2.3 has the following uncalled vulnerabilities: - GHSA-123: Something scarier! (https://osv.dev/GHSA-123) + GHSA-123: Something scarier! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 uncalled/unimportant vulnerability found in lockfile:/path/to/my/first/lockfile (filtered out) @@ -912,7 +958,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: '9'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -929,7 +976,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -946,7 +994,8 @@ npm lockfile:/path/to/my/first/lockfile: found 0 packages with issues mine1@1.2.3 has the following uncalled vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 uncalled/unimportant vulnerability found in lockfile:/path/to/my/first/lockfile (filtered out) @@ -963,7 +1012,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -980,7 +1030,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -997,7 +1048,8 @@ npm lockfile:/path/to/my/first/lockfile: found 0 packages with issues mine1@1.2.3 has the following uncalled vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 uncalled/unimportant vulnerability found in lockfile:/path/to/my/first/lockfile (filtered out) @@ -1014,7 +1066,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: '8.3'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -1031,7 +1084,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -1048,7 +1102,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -1065,7 +1120,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@ has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -1082,9 +1138,11 @@ npm lockfile:/path/to/my/first/lockfile: found 2 packages with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: This vulnerability allows for some very scary stuff to happen - seriously,... (https://osv.dev/OSV-1) + OSV-1: This vulnerability allows for some very scary stuff to happen - seriously,... + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; mine3@0.10.2-rc has the following known vulnerabilities: - OSV-2: (no details available) (https://osv.dev/OSV-2) + OSV-2: (no details available) + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 2 known vulnerabilities found in lockfile:/path/to/my/first/lockfile @@ -1101,7 +1159,8 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile @@ -1121,14 +1180,16 @@ npm lockfile:/path/to/my/first/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in lockfile:/path/to/my/first/lockfile sbom:/path/to/my/second/lockfile: found 1 package with issues mine1@1.2.3 has the following known vulnerabilities: - OSV-1: Something scary! (https://osv.dev/OSV-1) + OSV-1: Something scary! + Severity: 'N/A'; Minimal Fix Version: 'No fix available'; 1 known vulnerability found in sbom:/path/to/my/second/lockfile diff --git a/internal/output/vertical.go b/internal/output/vertical.go index c989925e0a8..43a7ff0de83 100644 --- a/internal/output/vertical.go +++ b/internal/output/vertical.go @@ -213,6 +213,12 @@ func printVerticalVulnerabilitiesForPackages(packages []PackageResult, out io.Wr text.FgCyan.Sprintf("%s:", vulnerability.ID), describe(vulnerability), ) + + fmt.Fprintf(out, + " Severity: '%s'; Minimal Fix Version: '%s';\n", + vulnerability.SeverityScore, + vulnerability.FixedVersion, + ) } } } @@ -281,14 +287,12 @@ func truncate(str string, limit int) string { } func describe(vulnerability VulnResult) string { - description := vulnerability.Description - if description == "" { - description += "(no details available)" + builder := strings.Builder{} + if vulnerability.Description == "" { + builder.WriteString("(no details available)") } else { - description = truncate(vulnerability.Description, 80) + builder.WriteString(truncate(vulnerability.Description, 80)) } - description += " (" + OSVBaseVulnerabilityURL + vulnerability.ID + ")" - - return description + return builder.String() } diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go index 6f6f7595062..793d1f0efd4 100644 --- a/pkg/osvscanner/osvscanner.go +++ b/pkg/osvscanner/osvscanner.go @@ -22,6 +22,7 @@ import ( "github.com/google/osv-scalibr/extractor" "github.com/google/osv-scalibr/inventory" "github.com/google/osv-scalibr/plugin" + "github.com/google/osv-scalibr/stats" "github.com/google/osv-scanner/v2/internal/clients/clientimpl/licensematcher" "github.com/google/osv-scanner/v2/internal/clients/clientimpl/localmatcher" "github.com/google/osv-scanner/v2/internal/clients/clientimpl/osvmatcher" @@ -74,6 +75,8 @@ type ExperimentalScannerActions struct { PluginsEnabled []string PluginsDisabled []string PluginsNoDefaults bool + + StatsCollector stats.Collector } type TransitiveScanningActions struct { diff --git a/pkg/osvscanner/scan.go b/pkg/osvscanner/scan.go index d83b2b94eb8..7751b07b261 100644 --- a/pkg/osvscanner/scan.go +++ b/pkg/osvscanner/scan.go @@ -18,6 +18,7 @@ import ( "github.com/google/osv-scalibr/fs" "github.com/google/osv-scalibr/inventory" "github.com/google/osv-scalibr/plugin" + "github.com/google/osv-scalibr/stats" "github.com/google/osv-scanner/v2/internal/cmdlogger" "github.com/google/osv-scanner/v2/internal/imodels" "github.com/google/osv-scanner/v2/internal/scalibrextract" @@ -171,6 +172,13 @@ func scan(accessors ExternalAccessors, actions ScannerActions) (*imodels.ScanRes testlogger.BeginDirScanMarker() osCapability := determineOS() + var statsCollector stats.Collector + if actions.StatsCollector != nil { + statsCollector = actions.StatsCollector + } else { + statsCollector = fileOpenedPrinter{} + } + // For each root, run scalibr's scan() once. for root, paths := range rootMap { capabilities := plugin.Capabilities{ @@ -194,7 +202,7 @@ func scan(accessors ExternalAccessors, actions ScannerActions) (*imodels.ScanRes SkipDirRegex: nil, SkipDirGlob: nil, UseGitignore: !actions.NoIgnore, - Stats: FileOpenedPrinter{}, + Stats: statsCollector, ReadSymlinks: false, MaxInodes: 0, StoreAbsolutePath: true, diff --git a/pkg/osvscanner/stats.go b/pkg/osvscanner/stats.go index 5f02020d3ab..0ec3a7755d5 100644 --- a/pkg/osvscanner/stats.go +++ b/pkg/osvscanner/stats.go @@ -8,13 +8,13 @@ import ( "github.com/google/osv-scanner/v2/internal/output" ) -type FileOpenedPrinter struct { +type fileOpenedPrinter struct { stats.NoopCollector } -var _ stats.Collector = &FileOpenedPrinter{} +var _ stats.Collector = &fileOpenedPrinter{} -func (c FileOpenedPrinter) AfterExtractorRun(_ string, extractorstats *stats.AfterExtractorStats) { +func (c fileOpenedPrinter) AfterExtractorRun(_ string, extractorstats *stats.AfterExtractorStats) { if extractorstats.Error != nil { // Don't log scanned if error occurred return }