Skip to main content

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