//! Refresh detection manifests from herdr's published catalog //! (`https://herdr.dev/agent-detection/index.toml`). Bundled manifests remain //! the offline fallback; downloads land in dingger's state dir where the //! detection engine picks them up on the next watcher start. use std::process::Command; use serde::Deserialize; use tracing::info; use detect_core::manifest_update::{remote_manifest_path, remote_store_dir}; use detect_core::{agent_label, Agent}; const DEFAULT_CATALOG_URL: &str = "https://herdr.dev/agent-detection/index.toml"; const CATALOG_URL_ENV: &str = "DINGGER_AGENT_DETECTION_MANIFEST_CATALOG_URL"; #[derive(Deserialize)] struct ManifestCatalog { #[serde(default)] agents: Vec, } #[derive(Deserialize)] struct ManifestCatalogAgent { id: String, path: String, } fn catalog_url() -> String { std::env::var(CATALOG_URL_ENV).unwrap_or_else(|_| DEFAULT_CATALOG_URL.to_string()) } fn fetch_text(url: &str) -> Result { let output = Command::new("curl") .args(["-fsSL", "--max-time", "30", url]) .output() .map_err(|err| format!("failed to run curl: {err}"))?; if !output.status.success() { return Err(format!("curl {url} failed: {}", output.status)); } String::from_utf8(output.stdout).map_err(|err| format!("non-utf8 response: {err}")) } pub fn update() -> Result<(), String> { let url = catalog_url(); let catalog_text = fetch_text(&url)?; let catalog: ManifestCatalog = toml::from_str(&catalog_text) .map_err(|err| format!("invalid catalog: {err}"))?; let base = url.rsplit_once('/').map(|(prefix, _)| prefix).unwrap_or(""); let store = remote_store_dir(); std::fs::create_dir_all(&store).map_err(|err| format!("mkdir failed: {err}"))?; let mut stored = 0usize; for entry in &catalog.agents { let Some(agent) = Agent::ALL.iter().copied().find(|a| agent_label(*a) == entry.id) else { continue; }; let manifest_url = format!("{base}/{}", entry.path); let content = match fetch_text(&manifest_url) { Ok(content) => content, Err(err) => { tracing::warn!(agent = %entry.id, %err, "skipping manifest"); continue; } }; std::fs::write(remote_manifest_path(agent), content) .map_err(|err| format!("write failed: {err}"))?; stored += 1; } info!(stored, "manifest update complete"); println!( "stored {stored} manifests into {}\nrestart `dingger watch` to pick them up", store.display() ); Ok(()) }