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#[non_exhaustive]
26pub struct StoreVisitor {
27 pub base: PathBuf,
29
30 pub no_timestamps: bool,
32
33 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 .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}