From fdfbc316aa1d2446f6b54668fdbcbc04928d763d Mon Sep 17 00:00:00 2001 From: Rex P Date: Mon, 20 Oct 2025 16:30:11 +1100 Subject: [PATCH 1/2] Scanner MCP --- .github/workflows/checks.yml | 4 +- .../workflows/osv-scanner-unified-action.yml | 4 +- cmd/osv-scanner/main.go | 2 + cmd/osv-scanner/mcp/command.go | 201 ++++++++++++++++++ cmd/osv-scanner/mcp/stats.go | 33 +++ go.mod | 7 +- go.sum | 14 +- internal/output/mcp.go | 55 +++++ internal/output/vertical.go | 13 +- pkg/osvscanner/osvscanner.go | 3 + pkg/osvscanner/scan.go | 10 +- pkg/osvscanner/stats.go | 6 +- 12 files changed, 332 insertions(+), 20 deletions(-) create mode 100644 cmd/osv-scanner/mcp/command.go create mode 100644 cmd/osv-scanner/mcp/stats.go create mode 100644 internal/output/mcp.go 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..6ef3d9aeb37 --- /dev/null +++ b/cmd/osv-scanner/mcp/command.go @@ -0,0 +1,201 @@ +// Package mcp implements the `mcp` command for osv-scanner. +package mcp + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + + "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" +) + +var vulnCacheMap = map[string]*osvschema.Vulnerability{} + +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, + } +} + +type ScanVulnerableDependenciesInput struct { + Paths []string `json:"paths" jsonschema:"A list of absolute or relative path to a file or directory to scan.,required"` + IgnoreGlobPatterns []string `json:"ignore_glob_patterns" jsonschema:"A list of glob patterns to ignore when scanning."` + Recursive bool `json:"recursive" jsonschema:"Scans directory recursively"` +} + +type GetVulnerabilityDetailsInput struct { + VulnID string `json:"vuln_id" jsonschema:"The OSV vulnerability ID to retrieve details for.,required"` +} + +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 this would be a template resource, 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) + + s.AddPrompt(&mcp.Prompt{ + Name: "scan_deps", + Description: "Scans your project dependencies for known vulnerabilities.", + }, handleCodeReview) + + 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) + //nolint:gosec // Having no timeouts is unlikely to cause problems as this is meant to be run locally. + if err := http.ListenAndServe(sseAddr, handler); 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) { + // Security: validate path + // if !isValidPath(path) { + // return mcp.NewToolResultError(fmt.Sprintf("invalid path: %s", path)), nil + //} + + 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[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 +} + +func handleVulnIDRetrieval(ctx context.Context, _ *mcp.CallToolRequest, input *GetVulnerabilityDetailsInput) (*mcp.CallToolResult, *osvschema.Vulnerability, error) { + vuln, found := vulnCacheMap[input.VulnID] + 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[input.VulnID] = vuln + } + + return &mcp.CallToolResult{}, vuln, nil +} + +func handleCodeReview(_ context.Context, _ *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + return &mcp.GetPromptResult{ + Description: "Dependency vulnerability analysis", + Messages: []*mcp.PromptMessage{ + { + Role: "assistant", + Content: &mcp.TextContent{ + Text: ` + +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. +`, + }, + }, + }, + }, nil +} 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 3cde6c7cc3a..d8418766cb9 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/google/osv-scalibr v0.3.6-0.20251017043508-d55b1c134c72 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 @@ -109,12 +110,13 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-yaml v1.18.0 // indirect 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 @@ -173,7 +175,7 @@ require ( github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect - github.com/ulikunitz/xz v0.5.15 // indirect + github.com/ulikunitz/xz v0.5.11 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -181,6 +183,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 e75dcf56052..c0ef153c26b 100644 --- a/go.sum +++ b/go.sum @@ -210,8 +210,8 @@ github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSr github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -250,6 +250,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.6-0.20251017043508-d55b1c134c72 h1:xtvTNa8pnkQGcFXDyqD6cJi41ZO+nyyW3KOdnPv9Dy4= github.com/google/osv-scalibr v0.3.6-0.20251017043508-d55b1c134c72/go.mod h1:U+HMf5wN0mLn5cfAy7mEx1nlM/0cXWXbEh6ndGRF34M= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= @@ -328,6 +330,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= @@ -450,8 +454,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= @@ -469,6 +473,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/mcp.go b/internal/output/mcp.go new file mode 100644 index 00000000000..086854ea71a --- /dev/null +++ b/internal/output/mcp.go @@ -0,0 +1,55 @@ +package output + +import ( + "io" + "strings" + + "github.com/google/osv-scanner/v2/pkg/models" +) + +// PrintMCPReport prints a LLM friendly vulnerability report +func PrintMCPReport(vulnResult *models.VulnerabilityResults, additionalInfo []string, outputWriter io.Writer) error { + stringRes := strings.Builder{} + stringRes.WriteString(` +Output results are grouped into (Ecosystem -> Source file -> Packages -> Vulnerabilities), +with a title for each section, and indentation to indicate that it belongs to the above section. +When resolving these vulnerabilities, avoid manually updating individual packages, and use system tools. +Use https://osv.dev/ as the official record of the vulnerability. +Do not attempt to fix vulnerabilities without fix available. + +Scan Info: + +`) + + for _, s := range additionalInfo { + stringRes.WriteString(s + "\n") + } + + stringRes.WriteString("\n") + + outputResult := BuildResults(vulnResult) + for _, eco := range outputResult.Ecosystems { + amendString(&stringRes, "Ecosystem: "+eco.Name, 1) + for _, sources := range eco.Sources { + amendString(&stringRes, "Source file path: "+sources.Name, 2) + for _, pkg := range sources.Packages { + amendString(&stringRes, "Package Name: "+pkg.Name, 3) + for _, vulns := range pkg.RegularVulns { + amendString(&stringRes, "Vuln ID: "+vulns.ID+" - Minimum Fix Version: "+vulns.FixedVersion, 4) + } + } + } + } + + _, err := outputWriter.Write([]byte(stringRes.String())) + + return err +} + +func amendString(builder *strings.Builder, value string, indent int) { + for range indent { + builder.WriteByte('\t') + } + builder.WriteString(value) + builder.WriteByte('\n') +} diff --git a/internal/output/vertical.go b/internal/output/vertical.go index c989925e0a8..17d2608f7a6 100644 --- a/internal/output/vertical.go +++ b/internal/output/vertical.go @@ -281,14 +281,15 @@ 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 + ")" + builder.WriteString( + fmt.Sprintf("; Severity: '%s'; Minimal Fix Version: '%s'", vulnerability.SeverityScore, vulnerability.FixedVersion)) - return description + return builder.String() } diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go index ab26b0d2b6a..f3ecb26893a 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 } From 66144b4048281c835cd9da67d4f1cc4f213540db Mon Sep 17 00:00:00 2001 From: Rex P Date: Mon, 20 Oct 2025 16:31:35 +1100 Subject: [PATCH 2/2] Bump up dependency versions --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d8418766cb9..90bde888df6 100644 --- a/go.mod +++ b/go.mod @@ -110,7 +110,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect - github.com/go-viper/mapstructure/v2 v2.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -175,7 +175,7 @@ require ( github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect - github.com/ulikunitz/xz v0.5.11 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index c0ef153c26b..739e40fac46 100644 --- a/go.sum +++ b/go.sum @@ -210,8 +210,8 @@ github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSr github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -454,8 +454,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= -github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= -github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=