sbom_walker/visitors/
store.rs

1use crate::{
2    discover::DiscoveredSbom,
3    model::metadata::SourceMetadata,
4    retrieve::{RetrievalContext, RetrievedSbom, RetrievedVisitor},
5    source::Source,
6    validation::{ValidatedSbom, ValidatedVisitor, ValidationContext},
7};
8use anyhow::Context;
9use sequoia_openpgp::{Cert, armor::Kind, serialize::SerializeInto};
10use std::{
11    io::{ErrorKind, Write},
12    path::{Path, PathBuf},
13};
14use tokio::fs;
15use walker_common::{
16    retrieve::RetrievalError,
17    store::{Document, StoreError, store_document},
18    utils::openpgp::PublicKey,
19    validate::ValidationError,
20};
21
22pub const DIR_METADATA: &str = "metadata";
23
24/// Stores all data so that it can be used as a [`crate::source::Source`] later.
25#[non_exhaustive]
26pub struct StoreVisitor {
27    /// the output base
28    pub base: PathBuf,
29
30    /// whether to set the file modification timestamps
31    pub no_timestamps: bool,
32
33    /// whether to store additional metadata (like the etag) using extended attributes
34    #[cfg(any(target_os = "linux", target_os = "macos"))]
35    pub no_xattrs: bool,
36}
37
38impl StoreVisitor {
39    pub fn new(base: impl Into<PathBuf>) -> Self {
40        Self {
41            base: base.into(),
42            no_timestamps: false,
43            #[cfg(any(target_os = "linux", target_os = "macos"))]
44            no_xattrs: false,
45        }
46    }
47
48    pub fn no_timestamps(mut self, no_timestamps: bool) -> Self {
49        self.no_timestamps = no_timestamps;
50        self
51    }
52
53    #[cfg(any(target_os = "linux", target_os = "macos"))]
54    pub fn no_xattrs(mut self, no_xattrs: bool) -> Self {
55        self.no_xattrs = no_xattrs;
56        self
57    }
58}
59
60#[derive(Debug, thiserror::Error)]
61pub enum StoreRetrievedError<S: Source> {
62    #[error(transparent)]
63    Store(#[from] StoreError),
64    #[error(transparent)]
65    Retrieval(#[from] RetrievalError<DiscoveredSbom, S>),
66}
67
68#[derive(Debug, thiserror::Error)]
69pub enum StoreValidatedError<S: Source> {
70    #[error(transparent)]
71    Store(#[from] StoreError),
72    #[error(transparent)]
73    Validation(#[from] ValidationError<S>),
74}
75
76impl<S: Source> RetrievedVisitor<S> for StoreVisitor {
77    type Error = StoreRetrievedError<S>;
78    type Context = ();
79
80    async fn visit_context(
81        &self,
82        context: &RetrievalContext<'_>,
83    ) -> Result<Self::Context, Self::Error> {
84        self.store_provider_metadata(context.metadata).await?;
85        self.store_keys(context.keys).await?;
86        Ok(())
87    }
88
89    async fn visit_sbom(
90        &self,
91        _context: &Self::Context,
92        result: Result<RetrievedSbom, RetrievalError<DiscoveredSbom, S>>,
93    ) -> Result<(), Self::Error> {
94        self.store(&result?).await?;
95        Ok(())
96    }
97}
98
99impl<S: Source> ValidatedVisitor<S> for StoreVisitor {
100    type Error = StoreValidatedError<S>;
101    type Context = ();
102
103    async fn visit_context(
104        &self,
105        context: &ValidationContext<'_>,
106    ) -> Result<Self::Context, Self::Error> {
107        self.store_provider_metadata(context.metadata).await?;
108        self.store_keys(context.retrieval.keys).await?;
109        Ok(())
110    }
111
112    async fn visit_sbom(
113        &self,
114        _context: &Self::Context,
115        result: Result<ValidatedSbom, ValidationError<S>>,
116    ) -> Result<(), Self::Error> {
117        self.store(&result?.retrieved).await?;
118        Ok(())
119    }
120}
121
122impl StoreVisitor {
123    async fn store_provider_metadata(&self, metadata: &SourceMetadata) -> Result<(), StoreError> {
124        let metadir = self.base.join(DIR_METADATA);
125
126        fs::create_dir(&metadir)
127            .await
128            .or_else(|err| match err.kind() {
129                ErrorKind::AlreadyExists => Ok(()),
130                _ => Err(err),
131            })
132            .with_context(|| format!("Failed to create metadata directory: {}", metadir.display()))
133            .map_err(StoreError::Io)?;
134
135        let file = metadir.join("metadata.json");
136        let mut out = std::fs::File::create(&file)
137            .with_context(|| {
138                format!(
139                    "Unable to open provider metadata file for writing: {}",
140                    file.display()
141                )
142            })
143            .map_err(StoreError::Io)?;
144        serde_json::to_writer_pretty(&mut out, metadata)
145            .context("Failed serializing provider metadata")
146            .map_err(StoreError::Io)?;
147        Ok(())
148    }
149
150    async fn store_keys(&self, keys: &[PublicKey]) -> Result<(), StoreError> {
151        let metadata = self.base.join(DIR_METADATA).join("keys");
152        fs::create_dir(&metadata)
153            .await
154            // ignore if the directory already exists
155            .or_else(|err| match err.kind() {
156                ErrorKind::AlreadyExists => Ok(()),
157                _ => Err(err),
158            })
159            .with_context(|| {
160                format!(
161                    "Failed to create metadata directory: {}",
162                    metadata.display()
163                )
164            })
165            .map_err(StoreError::Io)?;
166
167        for cert in keys.iter().flat_map(|k| &k.certs) {
168            log::info!("Storing key: {}", cert.fingerprint());
169            self.store_cert(cert, &metadata).await?;
170        }
171
172        Ok(())
173    }
174
175    async fn store_cert(&self, cert: &Cert, path: &Path) -> Result<(), StoreError> {
176        let name = path.join(format!("{}.txt", cert.fingerprint().to_hex()));
177
178        let data = Self::serialize_key(cert).map_err(StoreError::SerializeKey)?;
179
180        fs::write(&name, data)
181            .await
182            .with_context(|| format!("Failed to store key: {}", name.display()))
183            .map_err(StoreError::Io)?;
184        Ok(())
185    }
186
187    fn serialize_key(cert: &Cert) -> Result<Vec<u8>, anyhow::Error> {
188        let mut writer = sequoia_openpgp::armor::Writer::new(Vec::new(), Kind::PublicKey)?;
189        writer.write_all(&cert.to_vec()?)?;
190        Ok(writer.finalize()?)
191    }
192
193    async fn store(&self, sbom: &RetrievedSbom) -> Result<(), StoreError> {
194        log::info!(
195            "Storing: {} (modified: {:?})",
196            sbom.url,
197            sbom.metadata.last_modification
198        );
199
200        let file = PathBuf::from(sbom.url.path())
201            .file_name()
202            .map(|file| self.base.join(file))
203            .ok_or_else(|| StoreError::Filename(sbom.url.to_string()))?;
204
205        log::debug!("Writing {}", file.display());
206
207        store_document(
208            &file,
209            Document {
210                data: &sbom.data,
211                changed: sbom.modified,
212                metadata: &sbom.metadata,
213                sha256: &sbom.sha256,
214                sha512: &sbom.sha512,
215                signature: &sbom.signature,
216                no_timestamps: self.no_timestamps,
217                #[cfg(any(target_os = "linux", target_os = "macos"))]
218                no_xattrs: self.no_xattrs,
219            },
220        )
221        .await?;
222
223        Ok(())
224    }
225}