aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry/src/repository/mod.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-08 23:22:34 +0900
committernsfisis <nsfisis@gmail.com>2026-05-08 23:22:34 +0900
commitd770693bac655da4a21144b4cae7592536fecb8b (patch)
tree5d29005db018416c03a14c9d367f412b8148650c /crates/mozart-registry/src/repository/mod.rs
parenteeb845f2f8629e3ccfb8ee1a1ec0602c0f186427 (diff)
downloadphp-mozart-d770693bac655da4a21144b4cae7592536fecb8b.tar.gz
php-mozart-d770693bac655da4a21144b4cae7592536fecb8b.tar.zst
php-mozart-d770693bac655da4a21144b4cae7592536fecb8b.zip
fix(audit): align with Composer's AuditCommand pipeline
- Add mozart-core::advisory::{AuditFormat, AbandonedHandling, AuditConfig} mirroring Composer\Advisory\AuditConfig; reads audit.ignore, audit.ignore-severity, audit.ignore-abandoned, audit.abandoned, audit.block-insecure, audit.block-abandoned, audit.ignore-unreachable from composer.json config with full apply-scope support - Add mozart-registry::advisory::Auditor mirroring Composer\Advisory\Auditor; process_advisories() filters by package name, advisory ID, CVE, source remote ID, and severity; filter_abandoned_packages() respects ignore-abandoned - Add RepositorySet::get_matching_security_advisories() wrapping fetch_security_advisories with version-matching and unreachable-repo tracking - JSON output now includes ignored-advisories and unreachable-repositories keys - --abandoned falls back to audit.abandoned config (was hardcoded to "fail") - --ignore-severity merges with audit.ignore-severity config - --ignore-unreachable ORs with audit.ignore-unreachable config - Move normalize_or_separator into repository/mod.rs alongside version matching Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src/repository/mod.rs')
-rw-r--r--crates/mozart-registry/src/repository/mod.rs125
1 files changed, 125 insertions, 0 deletions
diff --git a/crates/mozart-registry/src/repository/mod.rs b/crates/mozart-registry/src/repository/mod.rs
index 6642638..46f62f0 100644
--- a/crates/mozart-registry/src/repository/mod.rs
+++ b/crates/mozart-registry/src/repository/mod.rs
@@ -10,6 +10,9 @@
//! the live Packagist HTTP repo, [`inline_package_repo`] for `type: package`
//! entries embedded in `composer.json`, and [`vcs_repo`] for VCS repositories.
+use std::collections::BTreeMap;
+
+use crate::advisory::{MatchedAdvisory, PackageInfo};
use crate::packagist::{PackagistVersion, SearchResult};
pub mod inline_package_repo;
@@ -191,4 +194,126 @@ impl RepositorySet {
}
Ok(all)
}
+
+ /// Fetch security advisories matching the installed packages, with version filtering.
+ ///
+ /// Mirrors `Composer\Repository\RepositorySet::getMatchingSecurityAdvisories()`.
+ /// Returns the matched advisories (already filtered by installed version) and a list
+ /// of unreachable repository URLs. When `ignore_unreachable` is false and a repository
+ /// is unreachable, the error is propagated instead.
+ pub async fn get_matching_security_advisories(
+ &self,
+ packages: &[PackageInfo],
+ _allow_partial: bool,
+ ignore_unreachable: bool,
+ ) -> anyhow::Result<(BTreeMap<String, Vec<MatchedAdvisory>>, Vec<String>)> {
+ let names: Vec<&str> = packages.iter().map(|p| p.name.as_str()).collect();
+
+ let (raw_advisories, unreachable_repos) =
+ match crate::packagist::fetch_security_advisories(&names).await {
+ Ok(a) => (a, vec![]),
+ Err(e) if ignore_unreachable => {
+ tracing::warn!("Packagist advisory fetch failed (ignored): {e}");
+ let unreachable = vec!["https://packagist.org".to_string()];
+ (BTreeMap::new(), unreachable)
+ }
+ Err(e) => return Err(e),
+ };
+
+ let matched = version_filter_advisories(&raw_advisories, packages);
+
+ Ok((matched, unreachable_repos))
+ }
+}
+
+/// Normalize single-pipe OR separators (`|`) in a version constraint string to
+/// double-pipe (`||`) so the constraint parser can handle both forms.
+///
+/// The Packagist security advisories API may return constraints with single `|`
+/// as the OR separator (e.g. `>=1.0,<1.5|>=2.0,<2.3`), but Mozart's
+/// `VersionConstraint::parse` expects `||`.
+///
+/// TODO: fix `mozart_semver::VersionConstraint::parse` to accept single `|` and remove this.
+fn normalize_or_separator(constraint: &str) -> String {
+ let bytes = constraint.as_bytes();
+ let mut result = String::with_capacity(constraint.len() + 4);
+ let mut i = 0;
+ while i < bytes.len() {
+ if bytes[i] == b'|' {
+ if i + 1 < bytes.len() && bytes[i + 1] == b'|' {
+ result.push_str("||");
+ i += 2;
+ } else {
+ result.push_str("||");
+ i += 1;
+ }
+ } else {
+ result.push(bytes[i] as char);
+ i += 1;
+ }
+ }
+ result
+}
+
+/// Filter raw advisories by installed package versions.
+///
+/// Mirrors the version-matching step inside Composer's repository advisory fetch.
+fn version_filter_advisories(
+ all_advisories: &BTreeMap<String, Vec<crate::packagist::SecurityAdvisory>>,
+ packages: &[PackageInfo],
+) -> BTreeMap<String, Vec<MatchedAdvisory>> {
+ let mut result: BTreeMap<String, Vec<MatchedAdvisory>> = BTreeMap::new();
+
+ for pkg in packages {
+ let Some(advisories) = all_advisories.get(&pkg.name) else {
+ continue;
+ };
+
+ let version_str = pkg
+ .version_normalized
+ .as_deref()
+ .unwrap_or(pkg.version.as_str());
+
+ let installed_ver = match mozart_semver::Version::parse(version_str) {
+ Ok(v) => v,
+ Err(_) => {
+ tracing::warn!(
+ "Could not parse version {:?} for package {:?}, skipping advisory matching",
+ version_str,
+ pkg.name
+ );
+ continue;
+ }
+ };
+
+ let mut matched: Vec<MatchedAdvisory> = Vec::new();
+
+ for advisory in advisories {
+ let normalized = normalize_or_separator(&advisory.affected_versions);
+ let constraint = match mozart_semver::VersionConstraint::parse(&normalized) {
+ Ok(c) => c,
+ Err(_) => {
+ tracing::warn!(
+ "Could not parse affected versions {:?} for advisory {:?}, skipping",
+ advisory.affected_versions,
+ advisory.advisory_id
+ );
+ continue;
+ }
+ };
+
+ if constraint.matches(&installed_ver) {
+ matched.push(MatchedAdvisory {
+ advisory: advisory.clone(),
+ installed_version: pkg.version.clone(),
+ });
+ }
+ }
+
+ if !matched.is_empty() {
+ result.insert(pkg.name.clone(), matched);
+ }
+ }
+
+ result
}