Repository: google/osv-scanner

Issue:
Allow configuration of the library's logging
The OpenSSF Scorecard project uses osv-scanner as a library, specifically `osvscanner.DoScan`. I experimented briefly with the migration to v2 recently and started getting a flood of logs to our stderr which are originating from osv-scanner.

```
2025/07/16 11:03:49 INFO Scanning dir <...>
2025/07/16 11:04:15 ERROR Error during extraction: (extracting as java/pomxmlnet) failed resolving
2025/07/16 11:04:39 INFO Scanned <...>/package-lock.json file and found 1568 packages
```

Is there anyway to introduce a control to disable `cmdlogger`'s output? It seems most of the codebase uses `cmdlogger.Info` (and friends), which write to the default (global) slog handler.
https://github.com/google/osv-scanner/blob/6515dc09a74355444b56c7dbf85b025bf9161b94/internal/cmdlogger/fmt.go#L8-L22

I can set the slog default to something which would ignore it, like [slog.DiscardHandler](https://pkg.go.dev/log/slog#DiscardHandler), but this would break many of our own logging.

Discussion:
@spencerschrock: Maybe instead of using the global default and setting it with `slog.SetDefault` in your various binaries, you can have a package level variable such as `cmdlogger.Default`, and then the various helpers can call `cmdlogger.Default.Info` etc instead of `slog.Info`?

Since this is an internal package, we would still have problems setting/clearing `cmdlogger.Default`@another-rex: A perhaps hacky fix: 
```go
origHandler :=  slog.GetDefault() // Or something similar
slog.SetDefault(slog.DiscardHandler)
osvscanner.Run(...)
slog.SetDefault(origHandler)
```

This would only reliably work if scorecard is running osv-scanner non concurrently. @another-rex: > Maybe instead of using the global default and setting it with `slog.SetDefault` in your various binaries, you can have a package level variable such as `cmdlogger.Default`, and then the various helpers can call `cmdlogger.Default.Info` etc instead of `slog.Info`?
> 
> Since this is an internal package, we would still have problems setting/clearing `cmdlogger.Default`

We can definitely look into that, I think we can just make a public function in osvscanner that sets the default used in cmdlogger to fix that issue. @spencerschrock: > A perhaps hacky fix:
> 
> origHandler :=  slog.GetDefault() // Or something similar
> slog.SetDefault(slog.DiscardHandler)
> osvscanner.Run(...)
> slog.SetDefault(origHandler)
> This would only reliably work if scorecard is running osv-scanner non concurrently.

All of the checks run in a separate goroutine, so there is concurrency to worry about.@spencerschrock: This is still an issue on v2.3.3, I'll send a patch

Solution patch:
diff --git a/cmd/osv-scanner/fix/__snapshots__/command_test.snap b/cmd/osv-scanner/fix/__snapshots__/command_test.snap
index 73da44d7..35833640 100755
--- a/cmd/osv-scanner/fix/__snapshots__/command_test.snap
+++ b/cmd/osv-scanner/fix/__snapshots__/command_test.snap
@@ -10,14 +10,14 @@ unsupported data-source "github" - must be one of: deps.dev, native
 
 [TestCommand/fix_non-interactive_in-place_package-lock.json - 1]
 Scanning <tempdir>/package-lock.json...
-Found 8 vulnerabilities matching the filter
-Can fix 3/8 matching vulnerabilities by changing 3 dependencies
+Found 9 vulnerabilities matching the filter
+Can fix 3/9 matching vulnerabilities by changing 3 dependencies
 UPGRADED-PACKAGE: brace-expansion,1.1.11,1.1.12
 UPGRADED-PACKAGE: concat-stream,1.5.0,1.6.1
 UPGRADED-PACKAGE: hosted-git-info,2.1.4,2.8.9
 FIXED-VULN-IDS: GHSA-43f8-2h32-f4cj,GHSA-g74r-ffvr-5q9f,GHSA-v6h2-p8h4-qcjw
-REMAINING-VULNS: 5
-UNFIXABLE-VULNS: 5
+REMAINING-VULNS: 6
+UNFIXABLE-VULNS: 6
 Rewriting <tempdir>/package-lock.json...
 
 ---
@@ -1807,6 +1807,16 @@ Rewriting <tempdir>/package-lock.json...
       ],
       "unactionable": true
     },
+    {
+      "id": "GHSA-fjxv-7rqg-78g4",
+      "packages": [
+        {
+          "name": "form-data",
+          "version": "2.3.3"
+        }
+      ],
+      "unactionable": true
+    },
     {
       "id": "GHSA-g74r-ffvr-5q9f",
       "packages": [
@@ -4011,6 +4021,16 @@ Rewriting <tempdir>/pom.xml...
         }
       ]
     },
+    {
+      "id": "GHSA-fjxv-7rqg-78g4",
+      "packages": [
+        {
+          "name": "form-data",
+          "version": "2.3.3"
+        }
+      ],
+      "unactionable": true
+    },
     {
       "id": "GHSA-p8p7-x288-28g6",
       "packages": [
@@ -4160,12 +4180,12 @@ Rewriting <tempdir>/pom.xml...
 
 [TestCommand/fix_non-interactive_relax_package.json - 1]
 Resolving <tempdir>/package.json...
-Found 5 vulnerabilities matching the filter
-Can fix 3/5 matching vulnerabilities by changing 1 dependencies
+Found 6 vulnerabilities matching the filter
+Can fix 3/6 matching vulnerabilities by changing 1 dependencies
 UPGRADED-PACKAGE: npm-registry-client,6.2.0,^7.5.0
 FIXED-VULN-IDS: GHSA-43f8-2h32-f4cj,GHSA-c2qf-rxjj-qqgw,GHSA-c6rq-rjc2-86v2
-REMAINING-VULNS: 2
-UNFIXABLE-VULNS: 2
+REMAINING-VULNS: 3
+UNFIXABLE-VULNS: 3
 Rewriting <tempdir>/package.json...
 
 ---
@@ -4194,14 +4214,14 @@ Rewriting <tempdir>/package.json...
 
 [TestCommand/fix_non_interactive_in_place_package_lock_json_with_native_data_source - 1]
 Scanning <tempdir>/package-lock.json...
-Found 8 vulnerabilities matching the filter
-Can fix 3/8 matching vulnerabilities by changing 3 dependencies
+Found 9 vulnerabilities matching the filter
+Can fix 3/9 matching vulnerabilities by changing 3 dependencies
 UPGRADED-PACKAGE: brace-expansion,1.1.11,1.1.12
 UPGRADED-PACKAGE: concat-stream,1.5.0,1.6.1
 UPGRADED-PACKAGE: hosted-git-info,2.1.4,2.8.9
 FIXED-VULN-IDS: GHSA-43f8-2h32-f4cj,GHSA-g74r-ffvr-5q9f,GHSA-v6h2-p8h4-qcjw
-REMAINING-VULNS: 5
-UNFIXABLE-VULNS: 5
+REMAINING-VULNS: 6
+UNFIXABLE-VULNS: 6
 Rewriting <tempdir>/package-lock.json...
 
 ---
diff --git a/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap b/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap
index a6593759..4d295dc2 100755
--- a/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap
+++ b/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap
@@ -3445,14 +3445,15 @@ Total 1 package affected by 1 known vulnerability (0 Critical, 0 High, 0 Medium,
 
 [TestCommand_MoreLockfiles/gems.locked - 1]
 Scanned <rootdir>/fixtures/locks-scalibr/gems.locked file and found 25 packages
-Total 2 packages affected by 4 known vulnerabilities (0 Critical, 2 High, 0 Medium, 0 Low, 2 Unknown) from 1 ecosystem.
-4 vulnerabilities can be fixed.
+Total 2 packages affected by 5 known vulnerabilities (0 Critical, 2 High, 0 Medium, 0 Low, 3 Unknown) from 1 ecosystem.
+5 vulnerabilities can be fixed.
 
 
 +-------------------------------------+------+-----------+----------+---------+---------------+------------------------------------+
 | OSV URL                             | CVSS | ECOSYSTEM | PACKAGE  | VERSION | FIXED VERSION | SOURCE                             |
 +-------------------------------------+------+-----------+----------+---------+---------------+------------------------------------+
 | https://osv.dev/GHSA-9m3q-rhmv-5q44 | 7.5  | RubyGems  | json     | 2.10.1  | 2.10.2        | fixtures/locks-scalibr/gems.locked |
+| https://osv.dev/GHSA-353f-x4gh-cqq8 |      | RubyGems  | nokogiri | 1.18.2  | 1.18.9        | fixtures/locks-scalibr/gems.locked |
 | https://osv.dev/GHSA-5w6v-399v-w3cc |      | RubyGems  | nokogiri | 1.18.2  | 1.18.8        | fixtures/locks-scalibr/gems.locked |
 | https://osv.dev/GHSA-mrxw-mxhj-p664 | 7.8  | RubyGems  | nokogiri | 1.18.2  | 1.18.4        | fixtures/locks-scalibr/gems.locked |
 | https://osv.dev/GHSA-vvfq-8hwr-qm4m |      | RubyGems  | nokogiri | 1.18.2  | 1.18.3        | fixtures/locks-scalibr/gems.locked |
diff --git a/go.mod b/go.mod
index 0e4ee260..71cbd345 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/go-git/go-billy/v5 v5.6.2
 	github.com/go-git/go-git/v5 v5.16.2
 	github.com/google/go-cmp v0.7.0
-	github.com/google/osv-scalibr v0.3.1
+	github.com/google/osv-scalibr v0.3.2-0.20250710172718-d8537726da6b
 	github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5
 	github.com/jedib0t/go-pretty/v6 v6.6.7
 	github.com/muesli/reflow v0.3.0
diff --git a/go.sum b/go.sum
index 223317c5..e0f5256d 100644
--- a/go.sum
+++ b/go.sum
@@ -213,8 +213,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/osv-scalibr v0.3.1 h1:bXSaKfhlh7Q4ysLLjfFRJ1HONFsLyYvPOXD9v94yi0M=
-github.com/google/osv-scalibr v0.3.1/go.mod h1:dUF/iQx3OboIV/z1N1S2Bji1esSvAj/sWlLzQ7BXhmo=
+github.com/google/osv-scalibr v0.3.2-0.20250710172718-d8537726da6b h1:ZdtTQPKfG5s40L1kgsG7EardLYKkc7BeqRRn9AVmKwc=
+github.com/google/osv-scalibr v0.3.2-0.20250710172718-d8537726da6b/go.mod h1:dUF/iQx3OboIV/z1N1S2Bji1esSvAj/sWlLzQ7BXhmo=
 github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
 github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
diff --git a/internal/cmdlogger/handler.go b/internal/cmdlogger/handler.go
index cedab555..71da7938 100644
--- a/internal/cmdlogger/handler.go
+++ b/internal/cmdlogger/handler.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 )
 
+var GlobalHandler slog.Handler
+
 type Handler struct {
 	stdout             io.Writer
 	stderr             io.Writer
@@ -39,15 +41,19 @@ func (c *Handler) writer(level slog.Level) io.Writer {
 	return c.stdout
 }
 
-func (c *Handler) Enabled(_ context.Context, level slog.Level) bool {
+func (c *Handler) Enabled(ctx context.Context, level slog.Level) bool {
 	if level == slog.LevelError {
 		c.hasErrored = true
 	}
 
+	if GlobalHandler != nil {
+		return GlobalHandler.Enabled(ctx, level)
+	}
+
 	return level >= c.Level.Level()
 }
 
-func (c *Handler) Handle(_ context.Context, record slog.Record) error {
+func (c *Handler) Handle(ctx context.Context, record slog.Record) error {
 	if record.Level == slog.LevelError {
 		c.hasErrored = true
 
@@ -56,6 +62,10 @@ func (c *Handler) Handle(_ context.Context, record slog.Record) error {
 		}
 	}
 
+	if GlobalHandler != nil {
+		return GlobalHandler.Handle(ctx, record)
+	}
+
 	_, err := fmt.Fprint(c.writer(record.Level), record.Message+"\n")
 
 	return err
@@ -73,11 +83,17 @@ func (c *Handler) HasErroredBecauseInvalidConfig() bool {
 	return c.hasErroredBecauseInvalidConfig
 }
 
-func (c *Handler) WithAttrs(_ []slog.Attr) slog.Handler {
+func (c *Handler) WithAttrs(a []slog.Attr) slog.Handler {
+	if GlobalHandler != nil {
+		return GlobalHandler.WithAttrs(a)
+	}
 	panic("not supported")
 }
 
-func (c *Handler) WithGroup(_ string) slog.Handler {
+func (c *Handler) WithGroup(g string) slog.Handler {
+	if GlobalHandler != nil {
+		return GlobalHandler.WithGroup(g)
+	}
 	panic("not supported")
 }
 
diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go
index 3eee5d2c..f438d153 100644
--- a/pkg/osvscanner/osvscanner.go
+++ b/pkg/osvscanner/osvscanner.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 	"maps"
 	"net/http"
 	"os"
@@ -548,3 +549,8 @@ func overrideGoVersion(scanResults *results.ScanResults) {
 		}
 	}
 }
+
+// SetLogger sets the global slog handler for the cmdlogger.
+func SetLogger(handler slog.Handler) {
+	cmdlogger.GlobalHandler = handler
+}
diff --git a/pkg/osvscanner/scan.go b/pkg/osvscanner/scan.go
index b21fc3c6..6df4ec79 100644
--- a/pkg/osvscanner/scan.go
+++ b/pkg/osvscanner/scan.go
@@ -75,7 +75,7 @@ func getExtractors(defaultExtractorNames []string, accessors ExternalAccessors,
 	return extractors
 }
 
-// scan essentially converts ScannerActions into PackageScanResult by performing the extractions
+// scan essentially converts ScannerActions into imodels.ScanResult by performing the extractions
 func scan(accessors ExternalAccessors, actions ScannerActions) (*imodels.ScanResult, error) {
 	//nolint:prealloc // We don't know how many inventories we will retrieve
 	var scannedInventories []*extractor.Package
diff --git a/renovate.json b/renovate.json
index 7fe3e7fa..76a6945c 100644
--- a/renovate.json
+++ b/renovate.json
@@ -26,6 +26,10 @@
     {
       "matchDepTypes": ["golang"],
       "enabled": false
+    },
+    {
+      "matchPackageNames": ["osv-scalibr"],
+      "enabled": true
     }
   ],
   "ignorePaths": ["**/fixtures/**", "**/fixtures-go/**"],



Test patch:
diff --git a/cmd/osv-scanner/scan/image/command_test.go b/cmd/osv-scanner/scan/image/command_test.go
index 9b546e1b..2db3973c 100644
--- a/cmd/osv-scanner/scan/image/command_test.go
+++ b/cmd/osv-scanner/scan/image/command_test.go
@@ -370,6 +370,7 @@ func TestCommand_OCIImage_JSONFormat(t *testing.T) {
 
 func TestCommand_HtmlFile(t *testing.T) {
 	t.Parallel()
+	testutility.SkipIfNotAcceptanceTesting(t, "Needs container image")
 
 	testDir := testutility.CreateTestDir(t)