Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/cli/src/sync_cli.rs

//! `holger-server sync` — the sync protocol by hand (Holger v2.0 H4,
//! `server_lib::sync`): the USB half of every verb the server's doors and its
//! `sync.tick` job run.
//!
//! | verb | side | what |
//! |---|---|---|
//! | `key` | both | this Holger's sync name and public key (minted on first use) |
//! | `invalidate` | upstream | issue an invalidation; the package is held here at once |
//! | `manifest --out <stick>` | upstream | the signed manifest for a stick |
//! | `apply --manifest <file> --ack-out <stick>` | downstream | verify, hold, sign the ack onto the stick |
//! | `ack --ack <file>` | upstream | take a downstream's ack from the stick |
//! | `board` | upstream | the board's sync data (JSON), the alarms due raised first |
//! | `pull` | downstream (`online`) | one pull from every upstream with a `url`, and its ack |

use std::path::PathBuf;
use std::sync::Arc;

use anyhow::{Context, Result};
use clap::{Args, Subcommand};

use server_lib::sync::SyncHub;

#[derive(Args)]
pub struct SyncArgs {
    #[command(subcommand)]
    pub verb: SyncVerb,
}

#[derive(Subcommand)]
pub enum SyncVerb {
    /// Print this Holger's sync name and public key (minted on first use).
    Key {
        #[arg(short, long)]
        config: PathBuf,
    },
    /// Print this downstream's WireGuard public key (minted on first use): what its upstream
    /// declares as `via: Wireguard(key: ..)` (lane HOL-WGDS).
    #[cfg(feature = "downstream-wg")]
    WgKey {
        #[arg(short, long)]
        config: PathBuf,
    },
    /// Issue an invalidation (upstream): every downstream holds the package at its next sync.
    Invalidate {
        #[arg(short, long)]
        config: PathBuf,
        /// The coordinate (`name@version`, a file path) as this Holger's intake record names it.
        #[arg(long)]
        coordinate: String,
        /// The bytes it names; absent: the bytes this Holger holds at the coordinate.
        #[arg(long)]
        sha256: Option<String>,
        /// cve-after, malware, pulled or policy.
        #[arg(long)]
        reason: String,
        /// One sentence: the CVE, the scanner hit, the publisher's notice.
        #[arg(long, default_value = "")]
        detail: String,
    },
    /// Write the signed manifest for a stick (upstream).
    Manifest {
        #[arg(short, long)]
        config: PathBuf,
        /// The stick's directory.
        #[arg(long)]
        out: PathBuf,
        /// The packages added after this generation (0: all).
        #[arg(long, default_value_t = 0)]
        since: u64,
    },
    /// Apply a signed manifest from the stick (downstream) and write the signed ack.
    Apply {
        #[arg(short, long)]
        config: PathBuf,
        #[arg(long)]
        manifest: PathBuf,
        /// Where the ack goes (the stick); it is always kept in the outbox too.
        #[arg(long)]
        ack_out: Option<PathBuf>,
    },
    /// Take a downstream's signed ack from the stick (upstream).
    Ack {
        #[arg(short, long)]
        config: PathBuf,
        #[arg(long)]
        ack: PathBuf,
    },
    /// Print the board's sync data (upstream), JSON.
    Board {
        #[arg(short, long)]
        config: PathBuf,
    },
    /// Pull once from every upstream with a `url`, and post the acks (downstream).
    #[cfg(all(feature = "online", feature = "fetch"))]
    Pull {
        #[arg(short, long)]
        config: PathBuf,
    },
}

fn os_user() -> String {
    std::env::var("USER")
        .or_else(|_| std::env::var("LOGNAME"))
        .unwrap_or_else(|_| "local".into())
}

/// The hub over `config`'s intake chain (the same one the server builds).
/// The chain is returned too: the hub holds it weakly, so the caller keeps it alive.
fn hub(config: &PathBuf) -> Result<(Arc<server_lib::promote::intake::IntakeChain>, Arc<SyncHub>)> {
    let mut holger = server_lib::read_ron_config(config)
        .with_context(|| format!("load config {}", config.display()))?;
    if holger.resolved_network() == server_lib::egress::NetworkMode::Offline {
        server_lib::egress::set_offline();
    }
    holger.instantiate_backends()?;
    let audit = holger.audit_log()?;
    let chain = holger.intake_chain(audit)?.ok_or_else(|| {
        anyhow::anyhow!(
            "config {} declares no intake chain (`promotion.intake`)",
            config.display()
        )
    })?;
    let hub = chain.sync().ok_or_else(|| {
        anyhow::anyhow!(
            "{}",
            server_lib::errcode::SYNC_NOT_CONFIGURED.tag(format_args!(
                "config {} has no `sync` block",
                config.display()
            ))
        )
    })?;
    Ok((chain, hub))
}

fn tagged(e: server_lib::sync::SyncFailure) -> anyhow::Error {
    anyhow::anyhow!("{}", e.tagged())
}

/// `holger-server sync`.
pub fn run(a: SyncArgs) -> Result<()> {
    match a.verb {
        SyncVerb::Key { config } => {
            let (_chain, h) = hub(&config)?;
            println!("{} {}", h.name(), h.public_key());
        }
        #[cfg(feature = "downstream-wg")]
        SyncVerb::WgKey { config } => {
            let holger = server_lib::read_ron_config(&config)?;
            let id = server_lib::sync::tunnel::identity(&holger.data_root()).map_err(tagged)?;
            println!("{}", id.public_b64);
        }
        SyncVerb::Invalidate {
            config,
            coordinate,
            sha256,
            reason,
            detail,
        } => {
            let (_chain, h) = hub(&config)?;
            let reason = server_lib::sync::protocol::Reason::parse(&reason).ok_or_else(|| {
                anyhow::anyhow!(
                    "{}",
                    server_lib::errcode::SYNC_MALFORMED
                        .tag("--reason is one of cve-after, malware, pulled, policy")
                )
            })?;
            let i = h
                .invalidate(
                    &os_user(),
                    "local",
                    &coordinate,
                    sha256.as_deref(),
                    reason,
                    &detail,
                )
                .map_err(tagged)?;
            println!(
                "{} {} generation {}: {} ({}) sha256 {}",
                if i.new {
                    "INVALIDATED"
                } else {
                    "ALREADY INVALIDATED"
                },
                i.invalidation.id,
                i.invalidation.generation,
                i.invalidation.coordinate,
                i.invalidation.reason,
                i.invalidation.sha256.as_deref().unwrap_or("any")
            );
            for c in &i.held_here {
                println!("  HELD here {c}");
            }
        }
        SyncVerb::Manifest { config, out, since } => {
            let (_chain, h) = hub(&config)?;
            let m = h.usb_manifest(since).map_err(tagged)?;
            let path = h.write_manifest(&m, &out).map_err(tagged)?;
            println!("MANIFEST {}", path.display());
        }
        SyncVerb::Apply {
            config,
            manifest,
            ack_out,
        } => {
            let (_chain, h) = hub(&config)?;
            let bytes =
                std::fs::read(&manifest).with_context(|| format!("read {}", manifest.display()))?;
            let a = h
                .apply(&bytes, "usb", &os_user(), "local")
                .map_err(tagged)?;
            println!(
                "APPLIED ({}) '{}' generation {}: {} package(s) added",
                a.admission, a.upstream, a.generation, a.added
            );
            for c in &a.quarantined {
                println!("  HELD {c}");
            }
            for c in &a.already_held {
                println!("  ALREADY HELD {c}");
            }
            #[cfg(feature = "fetch")]
            {
                println!("  ack: {}", a.ack_path.display());
                if let Some(dir) = ack_out {
                    let p = h.write_ack(&a.ack, &dir).map_err(tagged)?;
                    println!("  ACK {}", p.display());
                }
            }
            // A one-way Holger signs no receipt: nothing goes back over the diode.
            #[cfg(not(feature = "fetch"))]
            {
                let _ = ack_out;
                println!("  no receipt: this Holger is one-way (a build without `fetch`), so it signs no ack");
            }
        }
        SyncVerb::Ack { config, ack } => {
            let (_chain, h) = hub(&config)?;
            let bytes = std::fs::read(&ack).with_context(|| format!("read {}", ack.display()))?;
            let out = h.import_ack(&bytes, "usb", "local").map_err(tagged)?;
            println!(
                "ACK '{}' generation {}: {} invalidation(s) reached now",
                out.downstream,
                out.generation,
                out.reached_now.len()
            );
            for id in &out.cleared {
                println!("  ALARM CLEARED {id}");
            }
        }
        SyncVerb::Board { config } => {
            let (_chain, h) = hub(&config)?;
            let b = h
                .board(server_lib::sync::protocol::now_secs())
                .map_err(tagged)?;
            println!("{}", serde_json::to_string_pretty(&b)?);
        }
        #[cfg(all(feature = "online", feature = "fetch"))]
        SyncVerb::Pull { config } => {
            let (_chain, h) = hub(&config)?;
            let link = server_lib::sync::HttpLink::new();
            for u in h.config().upstreams.iter().filter(|u| u.url.is_some()) {
                let a = h.pull_once(&link, u).map_err(tagged)?;
                println!(
                    "PULLED ({}) '{}' generation {}: {} held, ack {}",
                    a.admission,
                    a.upstream,
                    a.generation,
                    a.quarantined.len(),
                    if a.ack_sent {
                        "sent"
                    } else {
                        "kept in the outbox"
                    }
                );
            }
        }
    }
    Ok(())
}