Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -2799,6 +2801,26 @@ 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<bool>,

/// 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<DisplaySafeUrl>,

/// A list of vulnerability IDs to ignore during auditing.
///
/// Vulnerabilities matching any of the provided IDs (including aliases) will be excluded from
Expand Down Expand Up @@ -2835,11 +2857,23 @@ pub struct MalwareCheckSettings {
pub malware_check_url: Option<DisplaySafeUrl>,
}

impl From<&crate::EnvironmentOptions> for MalwareCheckSettings {
fn from(options: &crate::EnvironmentOptions) -> Self {
impl MalwareCheckSettings {
pub fn resolve(
filesystem: Option<&FilesystemOptions>,
environment: &EnvironmentOptions,
) -> Self {
let audit = filesystem.and_then(|options| options.audit.as_ref());

Self {
enabled: options.malware_check.value == Some(true),
malware_check_url: options.malware_check_url.clone(),
enabled: environment
.malware_check
.value
.or(audit.and_then(|audit| audit.malware_check))
.unwrap_or_default(),
malware_check_url: environment
.malware_check_url
.clone()
.or_else(|| audit.and_then(|audit| audit.malware_check_url.clone())),
}
}
}
Expand Down
16 changes: 7 additions & 9 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 24 additions & 10 deletions crates/uv/tests/sync/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17189,22 +17189,25 @@ 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"
requires-python = ">=3.12"
dependencies = ["iniconfig==2.0.0"]
"#})

[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!({
Expand All @@ -17225,8 +17228,8 @@ async fn sync_malware_detected() {
uv_snapshot!(context.filters(), context
.sync()
.arg("--preview-features").arg("malware-check")
.env(EnvVars::UV_MALWARE_CHECK, "1")
.env(EnvVars::UV_MALWARE_CHECK_URL, server.uri()), @"
.env_remove(EnvVars::UV_MALWARE_CHECK)
.env_remove(EnvVars::UV_MALWARE_CHECK_URL), @"
success: false
exit_code: 2
----- stdout -----
Expand Down Expand Up @@ -17385,6 +17388,10 @@ async fn sync_malware_check_skips_non_mal() {
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig==2.0.0"]

[tool.uv.audit]
malware-check = false
malware-check-url = "https://example.com"
"#})
.unwrap();

Expand Down Expand Up @@ -17432,8 +17439,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");
Expand All @@ -17451,10 +17458,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("[audit]\nmalware-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()
Expand Down
6 changes: 5 additions & 1 deletion docs/concepts/projects/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,8 @@ 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 `audit.malware-check = true` in your uv settings or set
`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.
15 changes: 15 additions & 0 deletions uv.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.