csaf_walker/visitors/
store.rs

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