aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/mozart-core/src/package.rs14
-rw-r--r--crates/mozart-registry/src/composer_repo.rs2
-rw-r--r--crates/mozart-registry/src/inline_package.rs60
-rw-r--r--crates/mozart-registry/src/lockfile.rs1
-rw-r--r--crates/mozart-registry/src/repository_filter.rs1
-rw-r--r--crates/mozart-registry/src/resolver.rs32
-rw-r--r--crates/mozart/src/commands/create_project.rs1
-rw-r--r--crates/mozart/src/commands/init.rs2
-rw-r--r--crates/mozart/src/commands/remove.rs4
-rw-r--r--crates/mozart/src/commands/require.rs3
-rw-r--r--crates/mozart/src/commands/update.rs20
-rw-r--r--crates/mozart/tests/installer.rs12
12 files changed, 138 insertions, 14 deletions
diff --git a/crates/mozart-core/src/package.rs b/crates/mozart-core/src/package.rs
index 0a5c0fb..18714ec 100644
--- a/crates/mozart-core/src/package.rs
+++ b/crates/mozart-core/src/package.rs
@@ -566,6 +566,19 @@ pub struct RawRepository {
/// `FilterRepository::loadPackages`'s `namesFound = []` reset.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical: Option<bool>,
+
+ /// Inline `security-advisories` block on a repository entry. Maps package
+ /// name → list of advisory objects whose `affectedVersions` constraint
+ /// (and `advisoryId`) is read by the resolver when
+ /// `config.audit.block-insecure` is set: matching versions are filtered
+ /// out of the pool before solving, mirroring Composer's
+ /// `SecurityAdvisoryPoolFilter`.
+ #[serde(
+ rename = "security-advisories",
+ default,
+ skip_serializing_if = "Option::is_none"
+ )]
+ pub security_advisories: Option<serde_json::Value>,
}
/// Default root-package name when `composer.json` omits the `name` field.
@@ -677,6 +690,7 @@ mod tests {
only: None,
exclude: None,
canonical: None,
+ security_advisories: None,
}];
let mut psr4 = BTreeMap::new();
diff --git a/crates/mozart-registry/src/composer_repo.rs b/crates/mozart-registry/src/composer_repo.rs
index 6594668..ef091ef 100644
--- a/crates/mozart-registry/src/composer_repo.rs
+++ b/crates/mozart-registry/src/composer_repo.rs
@@ -120,6 +120,7 @@ mod tests {
only: None,
exclude: None,
canonical: None,
+ security_advisories: None,
}
}
@@ -157,6 +158,7 @@ mod tests {
only: None,
exclude: None,
canonical: None,
+ security_advisories: None,
}];
assert!(collect_composer_packages(&repos).is_empty());
}
diff --git a/crates/mozart-registry/src/inline_package.rs b/crates/mozart-registry/src/inline_package.rs
index 509193b..a7df987 100644
--- a/crates/mozart-registry/src/inline_package.rs
+++ b/crates/mozart-registry/src/inline_package.rs
@@ -77,6 +77,64 @@ pub fn collect_inline_packages(repositories: &[RawRepository]) -> Vec<InlinePack
packages
}
+/// One advisory extracted from a repository's `security-advisories` block.
+/// Carries enough to filter affected versions out of the pool when
+/// `config.audit.block-insecure` is set, matching the slice of Composer's
+/// `SecurityAdvisoryPoolFilter` Mozart needs for resolution-time blocking.
+#[derive(Debug, Clone)]
+pub struct SecurityAdvisory {
+ pub advisory_id: String,
+ pub affected_versions: String,
+}
+
+/// Collect every `security-advisories` entry across all repositories.
+/// Returned map is keyed by lowercase package name so the resolver can
+/// look up affected versions in lockstep with the rest of its
+/// case-insensitive name handling. Repository order is preserved within
+/// each list.
+pub fn collect_security_advisories(
+ repositories: &[RawRepository],
+) -> indexmap::IndexMap<String, Vec<SecurityAdvisory>> {
+ let mut out: indexmap::IndexMap<String, Vec<SecurityAdvisory>> = indexmap::IndexMap::new();
+ for repo in repositories {
+ let Some(advisories) = &repo.security_advisories else {
+ continue;
+ };
+ let Some(map) = advisories.as_object() else {
+ continue;
+ };
+ for (pkg_name, list) in map {
+ let Some(arr) = list.as_array() else {
+ continue;
+ };
+ for entry in arr {
+ let Some(obj) = entry.as_object() else {
+ continue;
+ };
+ let Some(affected) = obj
+ .get("affectedVersions")
+ .and_then(|v| v.as_str())
+ .map(String::from)
+ else {
+ continue;
+ };
+ let advisory_id = obj
+ .get("advisoryId")
+ .and_then(|v| v.as_str())
+ .map(String::from)
+ .unwrap_or_default();
+ out.entry(pkg_name.to_lowercase())
+ .or_default()
+ .push(SecurityAdvisory {
+ advisory_id,
+ affected_versions: affected,
+ });
+ }
+ }
+ }
+ out
+}
+
fn parse_inline_package(value: &serde_json::Value) -> Option<InlinePackage> {
let obj = value.as_object()?;
let name = obj.get("name")?.as_str()?.to_string();
@@ -114,6 +172,7 @@ mod tests {
only: None,
exclude: None,
canonical: None,
+ security_advisories: None,
}
}
@@ -151,6 +210,7 @@ mod tests {
only: None,
exclude: None,
canonical: None,
+ security_advisories: None,
}];
assert!(collect_inline_packages(&repos).is_empty());
}
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs
index 197335f..98c0fdc 100644
--- a/crates/mozart-registry/src/lockfile.rs
+++ b/crates/mozart-registry/src/lockfile.rs
@@ -1681,6 +1681,7 @@ mod tests {
block_abandoned: false,
root_branch_alias: None,
preferred_versions: IndexMap::new(),
+ block_insecure: false,
};
let resolved = resolve(&resolve_request)
diff --git a/crates/mozart-registry/src/repository_filter.rs b/crates/mozart-registry/src/repository_filter.rs
index fcdcfeb..facbb36 100644
--- a/crates/mozart-registry/src/repository_filter.rs
+++ b/crates/mozart-registry/src/repository_filter.rs
@@ -81,6 +81,7 @@ mod tests {
only,
exclude,
canonical,
+ security_advisories: None,
}
}
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index 9fb5cec..b323764 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -16,7 +16,7 @@ use mozart_sat_resolver::{
DefaultPolicy, PoolBuilder, PoolLink, PoolPackageInput, RuleSetGenerator, Solver,
make_pool_links,
};
-use mozart_semver::Version;
+use mozart_semver::{Version, VersionConstraint};
// ─────────────────────────────────────────────────────────────────────────────
// Version helpers
@@ -757,6 +757,10 @@ pub struct ResolveRequest {
/// a package when a constraint actually forces a different version.
/// Empty for a normal full update.
pub preferred_versions: IndexMap<String, String>,
+ /// When true, drop versions the repositories advertise as covered by an
+ /// active security advisory before solving. Mirrors Composer's
+ /// `SecurityAdvisoryPoolFilter` under `config.audit.block-insecure: true`.
+ pub block_insecure: bool,
}
/// Full data for a lock-pinned package, used in partial updates. Carried on
@@ -1150,6 +1154,28 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
.or_default()
.push(ipkg);
}
+ // Build the security-advisory filter once. Mirrors Composer's
+ // `SecurityAdvisoryPoolFilter`: when `block-insecure` is on, every
+ // version listed by a repository's `security-advisories` is removed
+ // from the pool before solving.
+ let security_advisories =
+ crate::inline_package::collect_security_advisories(&request.raw_repositories);
+ let security_blocks_version = |name: &str, version_normalized: &str| -> bool {
+ if !request.block_insecure {
+ return false;
+ }
+ let Some(advisories) = security_advisories.get(&name.to_lowercase()) else {
+ return false;
+ };
+ let Ok(parsed) = Version::parse(version_normalized) else {
+ return false;
+ };
+ advisories.iter().any(|adv| {
+ VersionConstraint::parse(&adv.affected_versions)
+ .map(|c| c.matches(&parsed))
+ .unwrap_or(false)
+ })
+ };
let add_inline_for = |name: &str, builder: &mut PoolBuilder| -> bool {
let Some(packages) = inline_packages_by_name.get(name) else {
return false;
@@ -1158,6 +1184,9 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
if request.block_abandoned && is_abandoned(&ipkg.version) {
continue;
}
+ if security_blocks_version(&ipkg.name, &ipkg.version.version_normalized) {
+ continue;
+ }
let inputs = packagist_to_pool_inputs(
&ipkg.name,
&ipkg.version,
@@ -1861,6 +1890,7 @@ mod tests {
block_abandoned: false,
root_branch_alias: None,
preferred_versions: IndexMap::new(),
+ block_insecure: false,
};
let result = resolve(&request).await;
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs
index a7964ae..61cb886 100644
--- a/crates/mozart/src/commands/create_project.rs
+++ b/crates/mozart/src/commands/create_project.rs
@@ -445,6 +445,7 @@ pub async fn execute(
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
console.info("Resolving dependencies...");
diff --git a/crates/mozart/src/commands/init.rs b/crates/mozart/src/commands/init.rs
index 1a6df37..209be4b 100644
--- a/crates/mozart/src/commands/init.rs
+++ b/crates/mozart/src/commands/init.rs
@@ -707,6 +707,7 @@ fn parse_repositories(repos: &[String]) -> anyhow::Result<Vec<RawRepository>> {
only: None,
exclude: None,
canonical: None,
+ security_advisories: None,
});
} else {
// Plain URL
@@ -717,6 +718,7 @@ fn parse_repositories(repos: &[String]) -> anyhow::Result<Vec<RawRepository>> {
only: None,
exclude: None,
canonical: None,
+ security_advisories: None,
});
}
}
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs
index c52d410..d4b3aef 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -279,6 +279,7 @@ pub async fn execute(
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
// Print header messages
@@ -567,6 +568,7 @@ async fn remove_unused(
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
console.info("Resolving dependencies to detect unused packages...");
@@ -925,6 +927,7 @@ mod tests {
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
let resolved = resolve(&request)
.await
@@ -986,6 +989,7 @@ mod tests {
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
let resolved2 = resolve(&request2)
.await
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index 3ff5ced..b302ed9 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -667,6 +667,7 @@ pub async fn execute(
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
// Print header messages
@@ -1079,6 +1080,7 @@ mod tests {
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
let resolved = resolver::resolve(&request)
@@ -1158,6 +1160,7 @@ mod tests {
block_abandoned: false,
root_branch_alias: None,
preferred_versions: indexmap::IndexMap::new(),
+ block_insecure: false,
};
let resolved = resolver::resolve(&request)
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index 3736266..0439cfa 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -1249,6 +1249,17 @@ pub async fn run(
.and_then(|a| a.get("block-abandoned"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
+ // Mirrors `Composer\Advisory\AuditConfig::fromConfig`: `block-insecure`
+ // turns the security-advisory data into a hard filter — affected
+ // versions are dropped from the pool, so a root require with no
+ // unaffected candidates fails resolution before any side effects.
+ let block_insecure = composer_json
+ .extra_fields
+ .get("config")
+ .and_then(|c| c.get("audit"))
+ .and_then(|a| a.get("block-insecure"))
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
// For `--minimal-changes`, feed the lock's pinned versions into the
// resolver as preferred-version overrides. The packages the user
@@ -1327,6 +1338,7 @@ pub async fn run(
block_abandoned,
root_branch_alias: extract_root_branch_alias(&composer_json),
preferred_versions,
+ block_insecure,
};
// Step 6: Print header and run resolver
@@ -1496,10 +1508,9 @@ pub async fn run(
// doesn't masquerade as a content update. When the source or dist type
// changed (`hg` → `git`, etc.), the new entry is left as-is so the
// change still emits the install-step Update operation.
- if update_mirrors
- && let Some(old) = &old_lock {
- apply_mirror_ref_overrides(&mut new_lock, old);
- }
+ if update_mirrors && let Some(old) = &old_lock {
+ apply_mirror_ref_overrides(&mut new_lock, old);
+ }
// Step 10: Compute and print change report
let changes = compute_update_changes(old_lock.as_ref(), &new_lock, dev_mode);
@@ -2476,6 +2487,7 @@ mod tests {
block_abandoned: false,
root_branch_alias: None,
preferred_versions: IndexMap::new(),
+ block_insecure: false,
};
let resolved = resolve(&request).await.expect("Resolution should succeed");
diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs
index f304b72..198dd9f 100644
--- a/crates/mozart/tests/installer.rs
+++ b/crates/mozart/tests/installer.rs
@@ -294,10 +294,7 @@ installer_fixture!(partial_update_keeps_older_dep_if_still_required);
installer_fixture!(partial_update_keeps_older_dep_if_still_required_with_provide);
installer_fixture!(partial_update_loads_root_aliases_for_path_repos, ignore);
installer_fixture!(partial_update_security_advisory_matching_locked_dep);
-installer_fixture!(
- partial_update_security_advisory_matching_locked_dep_with_dependencies,
- ignore
-);
+installer_fixture!(partial_update_security_advisory_matching_locked_dep_with_dependencies);
installer_fixture!(partial_update_with_dependencies_provide);
installer_fixture!(partial_update_with_dependencies_replace);
installer_fixture!(partial_update_with_deps_warns_root);
@@ -402,11 +399,8 @@ installer_fixture!(update_reference);
installer_fixture!(update_reference_picks_latest);
installer_fixture!(update_removes_unused_locked_dep);
installer_fixture!(update_requiring_decision_reverts_and_learning_positive_literals);
-installer_fixture!(update_security_advisory_matching_direct_dependency, ignore);
-installer_fixture!(
- update_security_advisory_matching_indirect_dependency,
- ignore
-);
+installer_fixture!(update_security_advisory_matching_direct_dependency);
+installer_fixture!(update_security_advisory_matching_indirect_dependency);
installer_fixture!(update_syncs_outdated);
installer_fixture!(update_to_empty_from_blank);
installer_fixture!(update_to_empty_from_locked);