1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use std::{collections::BTreeSet, io::Write, path::PathBuf, process::Command};
use crate::{Entry, LayerBuilder};
use anyhow::{Context, Result};
use tar::{EntryType, Header};
impl<W: Write> LayerBuilder<W> {
/// Add an RPM manifest to the image.
/// This copies the format of [AzureLinux](https://github.com/microsoft/azurelinux/blob/64ef81a5b9c855fceaa63006a3f42603386a2c7e/toolkit/docs/how_it_works/5_misc.md?plain=1#L154),
/// which is already supported by Trivy/Syft/Qualys and more.
pub(crate) fn write_rpm_manifest<'b>(
&mut self,
entries: impl IntoIterator<Item = &'b Entry>,
) -> Result<()> {
// Check if rpm is available
if Command::new("rpm").arg("--version").output().is_err() {
return Ok(());
}
// determine the owning packages of the files with
// rpm --query --file --queryformat "%{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{BUILDTIME}\t%{VENDOR}\t%{EPOCH}\t%{SIZE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM}\n" [file]
// we should filter out "no owning package" lines, keeping only the ones with a valid package name
let output = Command::new("rpm")
.arg("--query")
.arg("--file")
.arg("--queryformat")
.arg("%{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{BUILDTIME}\t%{VENDOR}\t%{EPOCH}\t%{SIZE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM}\n")
.args(entries.into_iter().map(|e| e.source.as_os_str()))
.output()?;
// don't check for success as here as rpm returns 1 if no package is found
let stdout = String::from_utf8_lossy(&output.stdout);
let lines = stdout
.lines()
.filter(|line| !line.contains("not owned by"))
.collect::<Vec<_>>();
if !lines.is_empty() {
let data = lines.join("\n").into_bytes();
log::debug!("Adding RPM manifest with {} entries", lines.len());
self.0.insert(
PathBuf::from("./var/lib/rpmmanifest/container-manifest-2"),
Box::new(move |writer| {
let mut header = Header::new_gnu();
header.set_entry_type(EntryType::file());
header.set_path("./var/lib/rpmmanifest/container-manifest-2")?;
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_uid(0);
header.set_gid(0);
header.set_cksum();
writer.append(&header, &*data)?;
Ok(())
}),
);
}
Ok(())
}
/// Detect RPM license files from any files we are adding to the image,
/// and add them too.
pub(crate) fn add_rpm_license_files<'a>(
&mut self,
entries: impl IntoIterator<Item = &'a Entry>,
) -> Result<()> {
// Check if rpm is available
if Command::new("rpm").arg("--version").output().is_err() {
return Ok(());
}
let license_files = entries
.into_iter()
.map(|entry| {
// For each entry, run `rpm --query --licensefiles --file <file>`
// to find the license file, if it exists.
let output = Command::new("rpm")
.arg("--query")
.arg("--licensefiles")
.arg("--file")
.arg(&entry.source)
.output()
.with_context(|| {
format!(
"failed to run rpm --query --license --file for {}",
entry.source.display()
)
})?;
// Do nothing on failure - not all files are RPM packages
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout
.lines()
.map(|line| line.to_string())
.collect::<Vec<_>>())
} else {
log::trace!(
"Failed to run rpm --query --license --file for {}: {}",
entry.source.display(),
String::from_utf8_lossy(&output.stderr)
);
Ok(vec![])
}
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<BTreeSet<_>>();
if !license_files.is_empty() {
log::debug!("Adding RPM license files: {license_files:?}");
for file in license_files {
self.add_file(file);
}
}
Ok(())
}
}