1use std::path::{Path, PathBuf};
19
20use anyhow::{bail, Context, Result};
21use chrono::{DateTime, Utc};
22use holger_update_bundle::container::load_bundle_verified;
23use holger_update_bundle::core::BundleCore;
24use holger_update_bundle::digest::sha256_hex;
25use holger_update_bundle::receipt::ImportReceipt;
26use holger_update_bundle::rollback::{AcceptedEntry, AcceptedState, Admissibility, RollbackStore};
27use holger_update_bundle::schema::{CORE_JSON, PAYLOAD_PREFIX};
28use holger_update_bundle::time::TrustedTime;
29use holger_update_bundle::verify::{verify_member_map_with_time, TargetSpec};
30use holger_update_bundle::{atomic_write, TrustPolicy};
31use serde::{Deserialize, Serialize};
32
33use crate::db::{InstalledIntel, LocalIntelDb, INSTALLED_SCHEMA};
34use crate::set::AdvisorySet;
35use crate::{ADVISORIES_REL, INTEL_PRODUCT};
36
37pub struct ImportRequest<'a> {
39 pub bundle: &'a Path,
40 pub policy: &'a TrustPolicy,
41 pub db: &'a LocalIntelDb,
42 pub first_import: bool,
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct ImportRecord {
50 pub bundle_id: String,
51 pub sequence: u64,
52 pub supplier: String,
53 pub snapshot_at: DateTime<Utc>,
54 pub candidate_digest: String,
55 pub bundle_sha256: String,
56 pub advisories: u64,
57 pub outcome: String,
59}
60
61impl ImportRecord {
62 pub fn detail(&self) -> String {
64 format!(
65 "bundle_id={} seq={} snapshot={} digest={} advisories={} outcome={}",
66 self.bundle_id,
67 self.sequence,
68 self.snapshot_at.format("%Y-%m-%d"),
69 self.candidate_digest,
70 self.advisories,
71 self.outcome
72 )
73 }
74}
75
76pub trait ImportAudit {
79 fn ready(&self) -> Result<()>;
82 fn record(&self, record: &ImportRecord) -> Result<()>;
84}
85
86#[derive(Debug, Clone)]
88pub struct ImportOutcome {
89 pub record: ImportRecord,
90 pub installed: InstalledIntel,
91 pub stored_bundle: PathBuf,
92 pub receipt: PathBuf,
93 pub checks: Vec<String>,
95}
96
97struct Lock(PathBuf);
99
100impl Lock {
101 fn acquire(path: PathBuf) -> Result<Self> {
102 match std::fs::OpenOptions::new()
103 .write(true)
104 .create_new(true)
105 .open(&path)
106 {
107 Ok(mut f) => {
108 use std::io::Write;
109 let _ = writeln!(f, "pid={}", std::process::id());
110 Ok(Lock(path))
111 }
112 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => bail!(
113 "intel import refused: {} exists, so another import is running or one was \
114 killed. Check no import runs, then remove the lock.",
115 path.display()
116 ),
117 Err(e) => Err(e).with_context(|| format!("create {}", path.display())),
118 }
119 }
120}
121
122impl Drop for Lock {
123 fn drop(&mut self) {
124 let _ = std::fs::remove_file(&self.0);
125 }
126}
127
128fn safe_name(s: &str) -> String {
129 let out: String = s
130 .chars()
131 .map(|c| {
132 if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
133 c
134 } else {
135 '_'
136 }
137 })
138 .collect();
139 if out.is_empty() || out == "." || out == ".." {
140 "_".into()
141 } else {
142 out
143 }
144}
145
146pub fn import(
148 req: &ImportRequest<'_>,
149 time: &dyn TrustedTime,
150 audit: &dyn ImportAudit,
151) -> Result<ImportOutcome> {
152 let db = req.db;
153 std::fs::create_dir_all(db.root()).with_context(|| format!("mkdir {}", db.root().display()))?;
154 let _lock = Lock::acquire(db.lock_path())?;
155
156 let raw = std::fs::read(req.bundle)
157 .with_context(|| format!("read bundle {}", req.bundle.display()))?;
158 let staged = db.root().join(".intel-import.staged.znippy");
159 std::fs::write(&staged, &raw).with_context(|| format!("stage {}", staged.display()))?;
160 let loaded = load_bundle_verified(&staged, req.policy);
161 let _ = std::fs::remove_file(&staged);
162 let members = loaded.map_err(|e| anyhow::anyhow!("intel bundle verification failed: {e}"))?;
163
164 let core = BundleCore::from_json_bytes(
165 members
166 .get(CORE_JSON)
167 .ok_or_else(|| anyhow::anyhow!("intel bundle has no core.json"))?,
168 )?;
169 if core.product != INTEL_PRODUCT {
170 bail!(
171 "intel import refused: the bundle's product is {:?}, not {INTEL_PRODUCT:?}. Import \
172 update bundles with `bundle import`.",
173 core.product
174 );
175 }
176
177 let rollback = RollbackStore::open(db.state_dir());
178 let floor = rollback.floor(&core.supplier, &core.product)?;
179 let state = floor
180 .accepted()
181 .cloned()
182 .unwrap_or_else(AcceptedState::empty);
183 let target = TargetSpec {
184 product: Some(INTEL_PRODUCT.to_string()),
185 model: None,
186 revision: None,
187 };
188 let report = verify_member_map_with_time(&members, req.policy, &target, Some(&state), time)
189 .map_err(|e| anyhow::anyhow!("intel bundle verification failed: {e}"))?;
190 let admissibility = floor
191 .check_admissible(&core, &report.candidate_digest, req.first_import)
192 .map_err(|e| anyhow::anyhow!("intel import refused: {e}"))?;
193
194 let payload_path = format!("{PAYLOAD_PREFIX}{ADVISORIES_REL}");
195 let payload = members
196 .get(&payload_path)
197 .ok_or_else(|| anyhow::anyhow!("intel bundle has no {payload_path}"))?;
198 let payload_sha = sha256_hex(payload);
199 if core.policy_result.advisory_snapshot_digest.as_deref() != Some(payload_sha.as_str()) {
200 bail!("intel bundle's advisory_snapshot_digest does not name its own advisory set");
201 }
202 let set = AdvisorySet::from_json_bytes(payload)?;
203
204 audit.ready().map_err(|e| {
205 anyhow::anyhow!(
206 "intel import refused: the audit log is off or could not be opened ({e:#}), and an \
207 import that leaves no audit record is not allowed"
208 )
209 })?;
210
211 let outcome = match admissibility {
212 Admissibility::New => "new",
213 Admissibility::IdempotentReimport => "idempotent-reimport",
214 };
215 let live_before = db.installed().ok().flatten();
216 let gen_file = LocalIntelDb::generation_file(core.sequence);
217 let gen_path = db.stage_generation(core.sequence, payload)?;
218 let stored = db
219 .bundles_dir()
220 .join(format!("{}.holger.znippy", safe_name(&core.bundle_id)));
221 let bundle_was_there = stored.exists();
222 if !bundle_was_there {
223 atomic_write(&stored, &raw).map_err(|e| anyhow::anyhow!("store intel bundle: {e}"))?;
224 }
225 let undo = || {
226 if live_before.as_ref().map(|m| m.advisories_file.as_str()) != Some(gen_file.as_str()) {
227 let _ = std::fs::remove_file(&gen_path);
228 }
229 if !bundle_was_there {
230 let _ = std::fs::remove_file(&stored);
231 }
232 };
233
234 let record = ImportRecord {
235 bundle_id: core.bundle_id.clone(),
236 sequence: core.sequence,
237 supplier: core.supplier.clone(),
238 snapshot_at: core.created_at,
239 candidate_digest: report.candidate_digest.clone(),
240 bundle_sha256: sha256_hex(&raw),
241 advisories: set.advisories.len() as u64,
242 outcome: outcome.to_string(),
243 };
244 if let Err(e) = audit.record(&record) {
245 undo();
246 bail!(
247 "intel import refused: its audit record could not be written ({e:#}). No audit record \
248 means no import; the advisory database and the anti-rollback floor are unchanged."
249 );
250 }
251
252 let now = time.now().unwrap_or_else(|_| Utc::now());
253 rollback.record(
254 &core.supplier,
255 &core.product,
256 &AcceptedEntry {
257 sequence: core.sequence,
258 bundle_id: core.bundle_id.clone(),
259 core_digest: report.candidate_digest.clone(),
260 accepted_at: now,
261 },
262 )?;
263 let installed = InstalledIntel {
264 schema: INSTALLED_SCHEMA.to_string(),
265 bundle_id: core.bundle_id.clone(),
266 sequence: core.sequence,
267 supplier: core.supplier.clone(),
268 snapshot_at: core.created_at,
269 imported_at: now,
270 candidate_digest: report.candidate_digest.clone(),
271 release_signer_fingerprint: report.release_signer_fingerprint.clone(),
272 advisories_file: gen_file,
273 advisories_sha256: payload_sha,
274 advisories: set.advisories.len() as u64,
275 };
276 db.activate(&installed)?;
277
278 let receipt = ImportReceipt {
279 bundle_id: core.bundle_id.clone(),
280 sequence: core.sequence,
281 supplier: core.supplier.clone(),
282 product: core.product.clone(),
283 release_version: core.release_version.clone(),
284 candidate_digest: report.candidate_digest.clone(),
285 release_signer_fingerprint: report.release_signer_fingerprint.clone(),
286 imported_at: now,
287 outcome: outcome.to_string(),
288 bundle_sha256: record.bundle_sha256.clone(),
289 };
290 std::fs::create_dir_all(db.receipts_dir()).ok();
291 let receipt_path = db
292 .receipts_dir()
293 .join(format!("{}.json", safe_name(&core.bundle_id)));
294 std::fs::write(&receipt_path, serde_json::to_vec_pretty(&receipt)?)?;
295
296 Ok(ImportOutcome {
297 record,
298 installed,
299 stored_bundle: stored,
300 receipt: receipt_path,
301 checks: report.checks,
302 })
303}