diff --git a/README.md b/README.md index c5742968644..f52476c5174 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ This command will recursively scan the specified directory for any supported pac OSV-Scanner has the option of using call analysis to determine if a vulnerable function is actually being used in the project, resulting in fewer false positives, and actionable alerts. +> [!WARNING] +> Rust call analysis can execute build scripts (`build.rs`) during compilation and may run arbitrary code from the scanned project. +> Use it only for trusted projects and pass `--allow-risky-rust-call-analysis` when enabling `--call-analysis=rust`. + OSV-Scanner can also detect vendored C/C++ code for vulnerability scanning. See [here](https://google.github.io/osv-scanner/usage/#cc-scanning) for details. #### Supported Lockfiles diff --git a/cmd/osv-scanner/internal/helper/flags.go b/cmd/osv-scanner/internal/helper/flags.go index c7601c2906f..fc68e55bca3 100644 --- a/cmd/osv-scanner/internal/helper/flags.go +++ b/cmd/osv-scanner/internal/helper/flags.go @@ -165,6 +165,10 @@ func BuildCommonScanFlags(defaultExtractors []string) []cli.Flag { Name: "call-analysis", Usage: "Enable call analysis for specific languages (e.g. --call-analysis=go). Supported: go, rust (*). (*) Will run build scripts.", }, + &cli.BoolFlag{ + Name: "allow-risky-rust-call-analysis", + Usage: "acknowledge that rust call analysis may execute untrusted build scripts (build.rs)", + }, &cli.StringSliceFlag{ Name: "no-call-analysis", Usage: "disables call graph analysis", diff --git a/cmd/osv-scanner/scan/source/command.go b/cmd/osv-scanner/scan/source/command.go index 06301571ae3..6d42da8ccd9 100644 --- a/cmd/osv-scanner/scan/source/command.go +++ b/cmd/osv-scanner/scan/source/command.go @@ -126,6 +126,10 @@ func action(_ context.Context, cmd *cli.Command, stdout, stderr io.Writer, clien } scannerAction := helper.GetCommonScannerActions(cmd, scanLicensesAllowlist) + // Rust call analysis builds the scanned project, so require explicit trust acknowledgement. + if scannerAction.CallAnalysisStates["rust"] && !cmd.Bool("allow-risky-rust-call-analysis") { + return errors.New("refusing to run rust call analysis without explicit acknowledgement: pass --allow-risky-rust-call-analysis if you trust this project") + } scannerAction.LockfilePaths = cmd.StringSlice("lockfile") //nolint:staticcheck // ignore our own deprecated field diff --git a/cmd/osv-scanner/scan/source/command_test.go b/cmd/osv-scanner/scan/source/command_test.go index f5fae2b96cc..63d82b06a0b 100644 --- a/cmd/osv-scanner/scan/source/command_test.go +++ b/cmd/osv-scanner/scan/source/command_test.go @@ -5,8 +5,10 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" + "github.com/google/osv-scanner/v2/cmd/osv-scanner/internal/helper" "github.com/google/osv-scanner/v2/cmd/osv-scanner/internal/testcmd" "github.com/google/osv-scanner/v2/internal/testutility" ) @@ -819,6 +821,50 @@ func TestCommand_CallAnalysis(t *testing.T) { } } +func TestCommand_CallAnalysisRustWithoutUnsafeFlagFails(t *testing.T) { + t.Parallel() + + testDir := testutility.CreateTestDir(t) + + _, stderr := testcmd.RunAndNormalize(t, testcmd.Case{ + Name: "rust_call_analysis_without_acknowledgement", + Args: []string{ + "", + "source", + "--offline", + "--call-analysis=rust", + "--allow-no-lockfiles", + testDir, + }, + Exit: 127, + }) + + requiredFlag := "--" + helper.AllowUnsafeRustCallAnalysisFlag + if !strings.Contains(stderr, requiredFlag) { + t.Fatalf("expected stderr to contain %q, got:\n%s", requiredFlag, stderr) + } +} + +func TestCommand_CallAnalysisRustWithUnsafeFlagSucceeds(t *testing.T) { + t.Parallel() + + testDir := testutility.CreateTestDir(t) + + testcmd.RunAndNormalize(t, testcmd.Case{ + Name: "rust_call_analysis_with_acknowledgement", + Args: []string{ + "", + "source", + "--offline", + "--call-analysis=rust", + "--" + helper.AllowUnsafeRustCallAnalysisFlag, + "--allow-no-lockfiles", + testDir, + }, + Exit: 0, + }) +} + func TestCommand_LockfileWithExplicitParseAs(t *testing.T) { t.Parallel()