aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-03 22:47:33 +0900
committernsfisis <nsfisis@gmail.com>2026-05-03 22:47:33 +0900
commit2b48ae7bcf857bc35de95968513750c2d6e6de7b (patch)
tree3b76b3e3b673c5f4e8fbd20775e35d062e73b1f7 /crates/mozart-registry
parentcccdce42f6eb5c21179bf7b2fbd482a7d29c3b9d (diff)
downloadphp-mozart-2b48ae7bcf857bc35de95968513750c2d6e6de7b.tar.gz
php-mozart-2b48ae7bcf857bc35de95968513750c2d6e6de7b.tar.zst
php-mozart-2b48ae7bcf857bc35de95968513750c2d6e6de7b.zip
fix(resolver): honor config.audit.block-insecure security-advisory filter
Mozart silently ignored the `security-advisories` block on inline `type: package` repositories and the `config.audit.block-insecure` audit flag, so a `composer update` succeeded with packages a Composer run would have refused to load. Mirror Composer's `SecurityAdvisoryPoolFilter` for the slice that feeds the pool: - Plumb a `security-advisories` field through `RawRepository` and a `block_insecure` flag through `ResolveRequest`, lifted off `composer.json`'s `config.audit.block-insecure`. - Collect every advisory's `affectedVersions` constraint at resolve time. When `block_insecure` is set and an inline package's normalized version satisfies the constraint, drop it from the pool before solving — root requires with no unaffected candidate then fail with the standard "could not be resolved" error.
Diffstat (limited to 'crates/mozart-registry')
-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
5 files changed, 95 insertions, 1 deletions
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;