From 6580dc4cb04c0ebd99db729eed1f4bd6680d3800 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 21 Jul 2026 14:36:41 -0400 Subject: [PATCH 1/4] Add malware-check configuration setting --- crates/uv-settings/src/lib.rs | 5 +++++ crates/uv-settings/src/settings.rs | 30 +++++++++++++++++++++++---- crates/uv-static/src/env_vars.rs | 3 ++- crates/uv/src/settings.rs | 16 +++++++------- crates/uv/tests/sync/show_settings.rs | 2 +- crates/uv/tests/sync/sync.rs | 23 +++++++++++++++----- docs/concepts/projects/sync.md | 3 ++- uv.schema.json | 4 ++++ 8 files changed, 65 insertions(+), 21 deletions(-) diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index dbb5dce096f1f..9e3c0eebcc8c3 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -294,6 +294,7 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { add: _, audit: _, pip: _, + malware_check: _, cache_keys: _, override_dependencies: _, exclude_dependencies: _, @@ -443,6 +444,7 @@ fn warn_uv_toml_masked_fields(options: &Options) { add: AddOptions { add_bounds }, audit: _, pip, + malware_check, cache_keys, override_dependencies, exclude_dependencies, @@ -630,6 +632,9 @@ fn warn_uv_toml_masked_fields(options: &Options) { if pip.is_some() { masked_fields.push("pip"); } + if malware_check.is_some() { + masked_fields.push("malware-check"); + } if cache_keys.is_some() { masked_fields.push("cache_keys"); } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 2fc929f4f6f15..c4af33b6fc564 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -30,6 +30,8 @@ use uv_torch::TorchMode; use uv_workspace::pyproject::{ExtraBuildDependencies, OverrideDependency}; use uv_workspace::pyproject_mut::AddBoundsKind; +use crate::{EnvironmentOptions, FilesystemOptions}; + /// A `pyproject.toml` with an (optional) `[tool.uv]` section. #[allow(dead_code)] #[derive(Debug, Clone, Default, Deserialize)] @@ -98,6 +100,16 @@ pub struct Options { #[option_group] pub pip: Option, + /// Whether to run the automatic malware check during sync operations. + #[option( + default = "false", + value_type = "bool", + example = r#" + malware-check = true + "# + )] + pub malware_check: Option, + /// The keys to consider when caching builds for the project. /// /// Cache keys enable you to specify the files or directories that should trigger a rebuild when @@ -2519,6 +2531,7 @@ struct OptionsWire { audit: Option, pip: Option, + malware_check: Option, cache_keys: Option>, // NOTE(charlie): These fields are shared with `ToolUv` in @@ -2605,6 +2618,7 @@ impl TryFrom for Options { torch_backend, audit, pip, + malware_check, cache_keys, override_dependencies, exclude_dependencies, @@ -2685,6 +2699,7 @@ impl TryFrom for Options { torch_backend, }, pip, + malware_check, cache_keys, build_backend, override_dependencies, @@ -2835,11 +2850,18 @@ pub struct MalwareCheckSettings { pub malware_check_url: Option, } -impl From<&crate::EnvironmentOptions> for MalwareCheckSettings { - fn from(options: &crate::EnvironmentOptions) -> Self { +impl MalwareCheckSettings { + pub fn resolve( + filesystem: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, + ) -> Self { Self { - enabled: options.malware_check.value == Some(true), - malware_check_url: options.malware_check_url.clone(), + enabled: environment + .malware_check + .value + .or(filesystem.and_then(|options| options.malware_check)) + .unwrap_or_default(), + malware_check_url: environment.malware_check_url.clone(), } } } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 89b0c77af6e4d..28699e9a4fb56 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -603,7 +603,8 @@ impl EnvVars { /// Set to `1` to enable the automatic malware check that runs after `uv sync`. /// /// When enabled, uv performs a lightweight check against the OSV database for known - /// malware advisories after every lockfile sync. Set this variable to `0` to opt out. + /// malware advisories after every lockfile sync. This variable overrides the `malware-check` + /// setting. Set this variable to `0` to opt out. #[attr_added_in("0.11.16")] pub const UV_MALWARE_CHECK: &'static str = "UV_MALWARE_CHECK"; diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 7870400c71475..85beb635dadce 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -794,7 +794,7 @@ impl RunSettings { let show_resolution = show_resolution || environment.show_resolution.value == Some(true); let no_env_file = no_env_file || environment.no_env_file.value == Some(true); - let malware_settings = MalwareCheckSettings::from(&environment); + let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); Self { lock_check: resolve_lock_check(locked), @@ -1849,6 +1849,7 @@ impl SyncSettings { .map(|fs| fs.install_mirrors.clone()) .unwrap_or_default(); + let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); let settings = ResolverInstallerSettings::combine( resolver_installer_options(installer, build), filesystem, @@ -1926,8 +1927,6 @@ impl SyncSettings { let no_install_local = no_install_local.is_enabled(); let only_install_local = only_install_local.is_enabled(); - let malware_settings = MalwareCheckSettings::from(&environment); - Self { output_format, lock_check: resolve_lock_check(locked), @@ -2141,7 +2140,7 @@ impl MetadataSettings { // Check for conflicts between locked and frozen. check_conflicts(locked, frozen); - let malware_settings = MalwareCheckSettings::from(&environment); + let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); Self { script, @@ -2401,7 +2400,7 @@ impl AddSettings { let no_install_local = no_install_local.is_enabled(); let only_install_local = only_install_local.is_enabled(); - let malware_settings = MalwareCheckSettings::from(&environment); + let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); Self { lock_check: resolve_lock_check(locked), @@ -2532,7 +2531,7 @@ impl RemoveSettings { // Check for conflicts between no_sync and frozen. check_conflicts(no_sync, frozen); - let malware_settings = MalwareCheckSettings::from(&environment); + let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); Self { lock_check: resolve_lock_check(locked), @@ -2619,7 +2618,7 @@ impl VersionSettings { // Check for conflicts between no_sync and frozen. check_conflicts(no_sync, frozen); - let malware_settings = MalwareCheckSettings::from(&environment); + let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); Self { value, @@ -3056,13 +3055,12 @@ impl CheckSettings { Some(environment.dev), Some(environment.no_dev), ); + let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); let settings = ResolverInstallerSettings::combine( resolver_installer_options(installer, build), filesystem, &environment, ); - let malware_settings = MalwareCheckSettings::from(&environment); - Self { ty_path: environment.ty_path, script, diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index bfe5b5bceee63..30890f6cc542d 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -2680,7 +2680,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `malware-check`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` " ); diff --git a/crates/uv/tests/sync/sync.rs b/crates/uv/tests/sync/sync.rs index 568441c9ab1ab..10e3c1bed1559 100644 --- a/crates/uv/tests/sync/sync.rs +++ b/crates/uv/tests/sync/sync.rs @@ -17198,6 +17198,9 @@ async fn sync_malware_detected() { version = "0.1.0" requires-python = ">=3.12" dependencies = ["iniconfig==2.0.0"] + + [tool.uv] + malware-check = true "#}) .unwrap(); @@ -17225,7 +17228,7 @@ async fn sync_malware_detected() { uv_snapshot!(context.filters(), context .sync() .arg("--preview-features").arg("malware-check") - .env(EnvVars::UV_MALWARE_CHECK, "1") + .env_remove(EnvVars::UV_MALWARE_CHECK) .env(EnvVars::UV_MALWARE_CHECK_URL, server.uri()), @" success: false exit_code: 2 @@ -17385,6 +17388,9 @@ async fn sync_malware_check_skips_non_mal() { version = "0.1.0" requires-python = ">=3.12" dependencies = ["iniconfig==2.0.0"] + + [tool.uv] + malware-check = false "#}) .unwrap(); @@ -17432,8 +17438,8 @@ async fn sync_malware_check_skips_non_mal() { "); } -/// Ensure that `UV_MALWARE_CHECK=0` keeps the malware check disabled even when the preview -/// feature is enabled. +/// Ensure that `UV_MALWARE_CHECK=0` keeps the malware check disabled even when enabled in user +/// configuration. #[tokio::test] async fn sync_malware_check_disabled() { let context = uv_test::test_context!("3.12"); @@ -17451,10 +17457,17 @@ async fn sync_malware_check_disabled() { context.lock().assert().success(); + let user_config_dir = context.user_config_dir.child("uv"); + user_config_dir.create_dir_all().unwrap(); + user_config_dir + .child("uv.toml") + .write_str("malware-check = true") + .unwrap(); + let server = MockServer::start().await; - // Even though the preview feature is enabled, the check is explicitly disabled via env - // var so no request should be made. (No mocks are mounted, so any request would fail.) + // The check is explicitly disabled via env var, so no request should be made. (No mocks are + // mounted, so any request would fail.) uv_snapshot!(context.filters(), context .sync() diff --git a/docs/concepts/projects/sync.md b/docs/concepts/projects/sync.md index 4045c4e1d97e9..295a1106223f6 100644 --- a/docs/concepts/projects/sync.md +++ b/docs/concepts/projects/sync.md @@ -233,4 +233,5 @@ against [OSV](https://osv.dev). OSV references MAL advisories from the OpenSSF's If a locked dependency matches a malware advisory, the sync will be terminated. -To enable malware checks, set `UV_MALWARE_CHECK=1` in your environment. +To enable malware checks, set `malware-check = true` in your uv settings or set `UV_MALWARE_CHECK=1` +in your environment. When set, `UV_MALWARE_CHECK` overrides the `malware-check` setting. diff --git a/uv.schema.json b/uv.schema.json index 52671378cd6c8..5b1d99d255bc7 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -327,6 +327,10 @@ } ] }, + "malware-check": { + "description": "Whether to run the automatic malware check during sync operations.", + "type": ["boolean", "null"] + }, "managed": { "description": "Whether the project is managed by uv. If `false`, uv will ignore the project when\n`uv run` is invoked.", "type": ["boolean", "null"] From c2db2d19441af6044fc8b8f9e3a11d7cd1fd525b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 21 Jul 2026 14:48:30 -0400 Subject: [PATCH 2/4] Move malware-check into audit configuration --- crates/uv-settings/src/lib.rs | 5 ----- crates/uv-settings/src/settings.rs | 27 +++++++++++++-------------- crates/uv-static/src/env_vars.rs | 4 ++-- crates/uv/tests/sync/show_settings.rs | 2 +- crates/uv/tests/sync/sync.rs | 6 +++--- docs/concepts/projects/sync.md | 5 +++-- uv.schema.json | 8 ++++---- 7 files changed, 26 insertions(+), 31 deletions(-) diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 9e3c0eebcc8c3..dbb5dce096f1f 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -294,7 +294,6 @@ fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> { add: _, audit: _, pip: _, - malware_check: _, cache_keys: _, override_dependencies: _, exclude_dependencies: _, @@ -444,7 +443,6 @@ fn warn_uv_toml_masked_fields(options: &Options) { add: AddOptions { add_bounds }, audit: _, pip, - malware_check, cache_keys, override_dependencies, exclude_dependencies, @@ -632,9 +630,6 @@ fn warn_uv_toml_masked_fields(options: &Options) { if pip.is_some() { masked_fields.push("pip"); } - if malware_check.is_some() { - masked_fields.push("malware-check"); - } if cache_keys.is_some() { masked_fields.push("cache_keys"); } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index c4af33b6fc564..a8d55423fe2d1 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -100,16 +100,6 @@ pub struct Options { #[option_group] pub pip: Option, - /// Whether to run the automatic malware check during sync operations. - #[option( - default = "false", - value_type = "bool", - example = r#" - malware-check = true - "# - )] - pub malware_check: Option, - /// The keys to consider when caching builds for the project. /// /// Cache keys enable you to specify the files or directories that should trigger a rebuild when @@ -2531,7 +2521,6 @@ struct OptionsWire { audit: Option, pip: Option, - malware_check: Option, cache_keys: Option>, // NOTE(charlie): These fields are shared with `ToolUv` in @@ -2618,7 +2607,6 @@ impl TryFrom for Options { torch_backend, audit, pip, - malware_check, cache_keys, override_dependencies, exclude_dependencies, @@ -2699,7 +2687,6 @@ impl TryFrom for Options { torch_backend, }, pip, - malware_check, cache_keys, build_backend, override_dependencies, @@ -2814,6 +2801,16 @@ pub struct AddOptions { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct AuditOptions { + /// Whether to run the automatic malware check during sync operations. + #[option( + default = "false", + value_type = "bool", + example = r#" + malware-check = true + "# + )] + pub malware_check: Option, + /// A list of vulnerability IDs to ignore during auditing. /// /// Vulnerabilities matching any of the provided IDs (including aliases) will be excluded from @@ -2859,7 +2856,9 @@ impl MalwareCheckSettings { enabled: environment .malware_check .value - .or(filesystem.and_then(|options| options.malware_check)) + .or(filesystem + .and_then(|options| options.audit.as_ref()) + .and_then(|audit| audit.malware_check)) .unwrap_or_default(), malware_check_url: environment.malware_check_url.clone(), } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 28699e9a4fb56..2f7d30f371bcf 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -603,8 +603,8 @@ impl EnvVars { /// Set to `1` to enable the automatic malware check that runs after `uv sync`. /// /// When enabled, uv performs a lightweight check against the OSV database for known - /// malware advisories after every lockfile sync. This variable overrides the `malware-check` - /// setting. Set this variable to `0` to opt out. + /// malware advisories after every lockfile sync. This variable overrides the + /// `audit.malware-check` setting. Set this variable to `0` to opt out. #[attr_added_in("0.11.16")] pub const UV_MALWARE_CHECK: &'static str = "UV_MALWARE_CHECK"; diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index 30890f6cc542d..bfe5b5bceee63 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -2680,7 +2680,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `malware-check`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` " ); diff --git a/crates/uv/tests/sync/sync.rs b/crates/uv/tests/sync/sync.rs index 10e3c1bed1559..018e0a04deac7 100644 --- a/crates/uv/tests/sync/sync.rs +++ b/crates/uv/tests/sync/sync.rs @@ -17199,7 +17199,7 @@ async fn sync_malware_detected() { requires-python = ">=3.12" dependencies = ["iniconfig==2.0.0"] - [tool.uv] + [tool.uv.audit] malware-check = true "#}) .unwrap(); @@ -17389,7 +17389,7 @@ async fn sync_malware_check_skips_non_mal() { requires-python = ">=3.12" dependencies = ["iniconfig==2.0.0"] - [tool.uv] + [tool.uv.audit] malware-check = false "#}) .unwrap(); @@ -17461,7 +17461,7 @@ async fn sync_malware_check_disabled() { user_config_dir.create_dir_all().unwrap(); user_config_dir .child("uv.toml") - .write_str("malware-check = true") + .write_str("[audit]\nmalware-check = true") .unwrap(); let server = MockServer::start().await; diff --git a/docs/concepts/projects/sync.md b/docs/concepts/projects/sync.md index 295a1106223f6..9bee596e65309 100644 --- a/docs/concepts/projects/sync.md +++ b/docs/concepts/projects/sync.md @@ -233,5 +233,6 @@ against [OSV](https://osv.dev). OSV references MAL advisories from the OpenSSF's If a locked dependency matches a malware advisory, the sync will be terminated. -To enable malware checks, set `malware-check = true` in your uv settings or set `UV_MALWARE_CHECK=1` -in your environment. When set, `UV_MALWARE_CHECK` overrides the `malware-check` setting. +To enable malware checks, set `audit.malware-check = true` in your uv settings or set +`UV_MALWARE_CHECK=1` in your environment. When set, `UV_MALWARE_CHECK` overrides the +`audit.malware-check` setting. diff --git a/uv.schema.json b/uv.schema.json index 5b1d99d255bc7..acf4d65bab111 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -327,10 +327,6 @@ } ] }, - "malware-check": { - "description": "Whether to run the automatic malware check during sync operations.", - "type": ["boolean", "null"] - }, "managed": { "description": "Whether the project is managed by uv. If `false`, uv will ignore the project when\n`uv run` is invoked.", "type": ["boolean", "null"] @@ -657,6 +653,10 @@ "items": { "type": "string" } + }, + "malware-check": { + "description": "Whether to run the automatic malware check during sync operations.", + "type": ["boolean", "null"] } } }, From 27763f90eb2913af8cb00e4ea84d52b6ed9a71fd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 21 Jul 2026 14:53:59 -0400 Subject: [PATCH 3/4] Add malware-check URL configuration setting --- crates/uv-settings/src/settings.rs | 21 +++++++++++++++++---- crates/uv-static/src/env_vars.rs | 3 ++- crates/uv/tests/sync/sync.rs | 11 ++++++----- docs/concepts/projects/sync.md | 4 ++++ uv.schema.json | 11 +++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index a8d55423fe2d1..e8b4e8baee431 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -2811,6 +2811,16 @@ pub struct AuditOptions { )] pub malware_check: Option, + /// The vulnerability service URL to use for automatic malware checks. + #[option( + default = "\"https://api.osv.dev/\"", + value_type = "str", + example = r#" + malware-check-url = "https://example.com" + "# + )] + pub malware_check_url: Option, + /// A list of vulnerability IDs to ignore during auditing. /// /// Vulnerabilities matching any of the provided IDs (including aliases) will be excluded from @@ -2852,15 +2862,18 @@ impl MalwareCheckSettings { filesystem: Option<&FilesystemOptions>, environment: &EnvironmentOptions, ) -> Self { + let audit = filesystem.and_then(|options| options.audit.as_ref()); + Self { enabled: environment .malware_check .value - .or(filesystem - .and_then(|options| options.audit.as_ref()) - .and_then(|audit| audit.malware_check)) + .or(audit.and_then(|audit| audit.malware_check)) .unwrap_or_default(), - malware_check_url: environment.malware_check_url.clone(), + malware_check_url: environment + .malware_check_url + .clone() + .or_else(|| audit.and_then(|audit| audit.malware_check_url.clone())), } } } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 2f7d30f371bcf..f2823de566904 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -610,7 +610,8 @@ impl EnvVars { /// Override the vulnerability service URL for the automatic malware check. /// - /// Defaults to the OSV API endpoint (`https://api.osv.dev/`). + /// This variable overrides the `audit.malware-check-url` setting. Defaults to the OSV API + /// endpoint (`https://api.osv.dev/`). #[attr_added_in("0.11.16")] pub const UV_MALWARE_CHECK_URL: &'static str = "UV_MALWARE_CHECK_URL"; diff --git a/crates/uv/tests/sync/sync.rs b/crates/uv/tests/sync/sync.rs index 018e0a04deac7..5df826a598877 100644 --- a/crates/uv/tests/sync/sync.rs +++ b/crates/uv/tests/sync/sync.rs @@ -17189,10 +17189,11 @@ fn sync_reinstalls_on_version_change() -> Result<()> { #[tokio::test] async fn sync_malware_detected() { let context = uv_test::test_context!("3.12"); + let server = MockServer::start().await; let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml - .write_str(indoc! {r#" + .write_str(&formatdoc! {r#" [project] name = "project" version = "0.1.0" @@ -17201,13 +17202,12 @@ async fn sync_malware_detected() { [tool.uv.audit] malware-check = true - "#}) + malware-check-url = "{}" + "#, server.uri()}) .unwrap(); context.lock().assert().success(); - let server = MockServer::start().await; - Mock::given(method("POST")) .and(path("/v1/querybatch")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ @@ -17229,7 +17229,7 @@ async fn sync_malware_detected() { .sync() .arg("--preview-features").arg("malware-check") .env_remove(EnvVars::UV_MALWARE_CHECK) - .env(EnvVars::UV_MALWARE_CHECK_URL, server.uri()), @" + .env_remove(EnvVars::UV_MALWARE_CHECK_URL), @" success: false exit_code: 2 ----- stdout ----- @@ -17391,6 +17391,7 @@ async fn sync_malware_check_skips_non_mal() { [tool.uv.audit] malware-check = false + malware-check-url = "https://example.com" "#}) .unwrap(); diff --git a/docs/concepts/projects/sync.md b/docs/concepts/projects/sync.md index 9bee596e65309..f0d05a2ac084f 100644 --- a/docs/concepts/projects/sync.md +++ b/docs/concepts/projects/sync.md @@ -236,3 +236,7 @@ If a locked dependency matches a malware advisory, the sync will be terminated. To enable malware checks, set `audit.malware-check = true` in your uv settings or set `UV_MALWARE_CHECK=1` in your environment. When set, `UV_MALWARE_CHECK` overrides the `audit.malware-check` setting. + +To use an alternative vulnerability service, set `audit.malware-check-url` in your uv settings or +set `UV_MALWARE_CHECK_URL` in your environment. When set, `UV_MALWARE_CHECK_URL` overrides the +`audit.malware-check-url` setting. diff --git a/uv.schema.json b/uv.schema.json index acf4d65bab111..e7dac716dc8d3 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -657,6 +657,17 @@ "malware-check": { "description": "Whether to run the automatic malware check during sync operations.", "type": ["boolean", "null"] + }, + "malware-check-url": { + "description": "The vulnerability service URL to use for automatic malware checks.", + "anyOf": [ + { + "$ref": "#/definitions/DisplaySafeUrl" + }, + { + "type": "null" + } + ] } } }, From 605e9a52d81ca2b09d5da534e972caeda5df518b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 21 Jul 2026 14:56:05 -0400 Subject: [PATCH 4/4] Simplify malware-check configuration documentation --- crates/uv-static/src/env_vars.rs | 6 ++---- docs/concepts/projects/sync.md | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index f2823de566904..89b0c77af6e4d 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -603,15 +603,13 @@ impl EnvVars { /// Set to `1` to enable the automatic malware check that runs after `uv sync`. /// /// When enabled, uv performs a lightweight check against the OSV database for known - /// malware advisories after every lockfile sync. This variable overrides the - /// `audit.malware-check` setting. Set this variable to `0` to opt out. + /// malware advisories after every lockfile sync. Set this variable to `0` to opt out. #[attr_added_in("0.11.16")] pub const UV_MALWARE_CHECK: &'static str = "UV_MALWARE_CHECK"; /// Override the vulnerability service URL for the automatic malware check. /// - /// This variable overrides the `audit.malware-check-url` setting. Defaults to the OSV API - /// endpoint (`https://api.osv.dev/`). + /// Defaults to the OSV API endpoint (`https://api.osv.dev/`). #[attr_added_in("0.11.16")] pub const UV_MALWARE_CHECK_URL: &'static str = "UV_MALWARE_CHECK_URL"; diff --git a/docs/concepts/projects/sync.md b/docs/concepts/projects/sync.md index f0d05a2ac084f..d48b3df008b03 100644 --- a/docs/concepts/projects/sync.md +++ b/docs/concepts/projects/sync.md @@ -234,9 +234,7 @@ against [OSV](https://osv.dev). OSV references MAL advisories from the OpenSSF's If a locked dependency matches a malware advisory, the sync will be terminated. To enable malware checks, set `audit.malware-check = true` in your uv settings or set -`UV_MALWARE_CHECK=1` in your environment. When set, `UV_MALWARE_CHECK` overrides the -`audit.malware-check` setting. +`UV_MALWARE_CHECK=1` in your environment. To use an alternative vulnerability service, set `audit.malware-check-url` in your uv settings or -set `UV_MALWARE_CHECK_URL` in your environment. When set, `UV_MALWARE_CHECK_URL` overrides the -`audit.malware-check-url` setting. +set `UV_MALWARE_CHECK_URL` in your environment.