aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/manifest_update.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/src/manifest_update.rs')
-rw-r--r--crates/dinggerd/src/manifest_update.rs79
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(())
+}