diff options
Diffstat (limited to 'crates/dinggerd/src/manifest_update.rs')
| -rw-r--r-- | crates/dinggerd/src/manifest_update.rs | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/crates/dinggerd/src/manifest_update.rs b/crates/dinggerd/src/manifest_update.rs new file mode 100644 index 0000000..b18a153 --- /dev/null +++ b/crates/dinggerd/src/manifest_update.rs @@ -0,0 +1,79 @@ +//! 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<ManifestCatalogAgent>, +} + +#[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<String, String> { + 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(()) +} |
