diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-03 22:47:33 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-03 22:47:33 +0900 |
| commit | 2b48ae7bcf857bc35de95968513750c2d6e6de7b (patch) | |
| tree | 3b76b3e3b673c5f4e8fbd20775e35d062e73b1f7 /crates/mozart-registry | |
| parent | cccdce42f6eb5c21179bf7b2fbd482a7d29c3b9d (diff) | |
| download | php-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.rs | 2 | ||||
| -rw-r--r-- | crates/mozart-registry/src/inline_package.rs | 60 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/repository_filter.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 32 |
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; |
