Skip to main content

provenant/parsers/
rpm_mariner_manifest.rs

1//! Parser for RPM Mariner container manifest files.
2//!
3//! Extracts package metadata from `container-manifest-2` files which contain
4//! installed RPM package information in Mariner distroless containers.
5//!
6//! # Supported Formats
7//! - `container-manifest-2` - RPM Mariner distroless package manifest
8//!
9//! # Key Features
10//! - Installed package identification
11//! - Version and architecture metadata
12//! - Checksum information
13//!
14//! # Implementation Notes
15//! - Format: Tab-separated text with package metadata
16//! - One package per line
17//! - Spec: https://github.com/microsoft/marinara/
18
19use crate::models::{DatasourceId, PackageType};
20use std::fs;
21use std::path::Path;
22
23use crate::parser_warn as warn;
24
25use crate::models::PackageData;
26
27use super::PackageParser;
28
29const PACKAGE_TYPE: PackageType = PackageType::Rpm;
30
31fn default_package_data() -> PackageData {
32    PackageData {
33        package_type: Some(PACKAGE_TYPE),
34        namespace: Some("mariner".to_string()),
35        datasource_id: Some(DatasourceId::RpmMarinerManifest),
36        ..Default::default()
37    }
38}
39
40/// Parser for RPM Mariner container manifest files
41pub struct RpmMarinerManifestParser;
42
43impl PackageParser for RpmMarinerManifestParser {
44    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
45
46    fn is_match(path: &Path) -> bool {
47        path.to_str()
48            .is_some_and(|p| p.ends_with("/var/lib/rpmmanifest/container-manifest-2"))
49    }
50
51    fn extract_packages(path: &Path) -> Vec<PackageData> {
52        let content = match fs::read_to_string(path) {
53            Ok(c) => c,
54            Err(e) => {
55                warn!("Failed to read RPM Mariner manifest {:?}: {}", path, e);
56                return vec![default_package_data()];
57            }
58        };
59
60        parse_rpm_mariner_manifest(&content)
61    }
62}
63
64pub(crate) fn parse_rpm_mariner_manifest(content: &str) -> Vec<PackageData> {
65    let mut packages = Vec::new();
66
67    for line in content.lines() {
68        // Only trim whitespace, not tabs
69        let line = line.trim_matches(|c: char| c.is_whitespace() && c != '\t');
70        if line.is_empty() {
71            continue;
72        }
73
74        // Split by tabs
75        let parts: Vec<&str> = line.split('\t').collect();
76
77        // According to Python reference, manifest_attributes are:
78        // ["name", "version", "n1", "n2", "party", "n3", "n4", "arch", "checksum_algo", "filename"]
79        // We only care about name, version, arch, and filename
80
81        if parts.len() < 10 {
82            warn!(
83                "Invalid RPM Mariner manifest line (expected 10 fields): {}",
84                line
85            );
86            continue;
87        }
88
89        let name = parts[0];
90        let version = parts[1];
91        let arch = parts[7];
92        let filename = parts[9];
93
94        let qualifiers = if arch.is_empty() {
95            None
96        } else {
97            let mut quals = std::collections::HashMap::new();
98            quals.insert("arch".to_string(), arch.to_string());
99            Some(quals)
100        };
101
102        let extra_data = if filename.is_empty() {
103            None
104        } else {
105            let mut extra = std::collections::HashMap::new();
106            extra.insert(
107                "filename".to_string(),
108                serde_json::Value::String(filename.to_string()),
109            );
110            Some(extra)
111        };
112
113        packages.push(PackageData {
114            package_type: Some(PACKAGE_TYPE),
115            namespace: Some("mariner".to_string()),
116            name: if name.is_empty() {
117                None
118            } else {
119                Some(name.to_string())
120            },
121            version: if version.is_empty() {
122                None
123            } else {
124                Some(version.to_string())
125            },
126            qualifiers,
127            datasource_id: Some(DatasourceId::RpmMarinerManifest),
128            extra_data,
129            ..Default::default()
130        });
131    }
132
133    if packages.is_empty() {
134        packages.push(default_package_data());
135    }
136
137    packages
138}
139
140crate::register_parser!(
141    "RPM Mariner distroless package manifest",
142    &["*var/lib/rpmmanifest/container-manifest-2"],
143    "rpm",
144    "",
145    Some("https://github.com/microsoft/marinara/"),
146);