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

nordisk/holger

holger-cli/src/cli.rs

//! The command line. One command per console action (`table.rs` says which RPC each runs).

use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum};

/// holger-cli: the thin gRPC client of a Holger server.
///
/// Every console action is a command here, over the same gRPC services the console calls.
/// It dials only the server named by --at. The credential comes from --token-file,
/// $HOLGER_TOKEN or the one `holger-cli login` stored for that server (0600).
#[derive(Parser, Debug)]
#[command(name = "holger-cli", version, about, long_about = None)]
pub struct Cli {
    /// The server's gRPC door: host:port (TLS, or plain gRPC when it names this machine), http://host:port or https://host:port.
    #[arg(long, global = true, env = "HOLGER_AT", default_value = crate::conn::DEFAULT_AT)]
    pub at: String,
    /// A file holding the credential to act with (the box's static bearer or a passkey
    /// session token).
    #[arg(long, global = true)]
    pub token_file: Option<PathBuf>,
    /// The CA (PEM) an https:// door's certificate is checked against. Absent: the public
    /// web roots compiled into this program.
    #[arg(long, global = true)]
    pub ca: Option<PathBuf>,
    /// Dial this address (host:port) for --at's name, as curl's --connect-to: the TLS
    /// name and the routed name stay --at's. For a front reached before its name resolves.
    #[arg(long, global = true)]
    pub connect: Option<String>,
    #[command(subcommand)]
    pub command: Command,
}

/// An artifact's coordinate.
#[derive(Args, Debug, Clone)]
pub struct Coord {
    /// The repository.
    #[arg(long, short = 'r')]
    pub repository: String,
    /// The artifact's name (a file name for raw, a package name otherwise).
    #[arg(long)]
    pub name: String,
    #[arg(long, default_value = "")]
    pub version: String,
    /// groupId for Maven; empty elsewhere.
    #[arg(long, default_value = "")]
    pub namespace: String,
}

/// An artifact's id, without a repository (the intake chain knows where it is).
#[derive(Args, Debug, Clone)]
pub struct Id {
    #[arg(long)]
    pub name: String,
    #[arg(long, default_value = "")]
    pub version: String,
    #[arg(long, default_value = "")]
    pub namespace: String,
}

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Store the credential for --at (read from stdin, checked with the server first).
    Login,
    /// Forget the credential stored for --at.
    Logout,
    /// The server is up: its status, version and uptime.
    Health,
    /// Who the server says you are, and your role (globally, or on --repository).
    Whoami {
        #[arg(long, default_value = "")]
        repository: String,
    },
    /// Every repository the server serves.
    Repos,
    /// What this server is: its profile and the modules it loaded.
    Profile,
    /// The admin view: disks, twin, growth, clock, plugins, audit, logs (admin).
    Snapshot {
        #[arg(long, default_value_t = 50)]
        log_lines: u32,
    },
    /// The estate board's rows and links (admin read).
    Board,
    /// The artifact flow holger draws of its RON: the SVG, or with `--json` the model it is drawn from (admin read).
    /// `--open main` unfolds main, `--open all` draws the full RON; none draws the machines only.
    Flow {
        #[arg(long)]
        json: bool,
        #[arg(long, default_value = "")]
        open: String,
    },
    /// The Security page: the intake chain, scanners, approvals, what is held, who reads, sync signatures, security events.
    Security,
    /// One repository's artifacts.
    #[command(subcommand)]
    Artifacts(ArtifactsCmd),
    /// A repository's archive.
    #[command(subcommand)]
    Archive(ArchiveCmd),
    /// Find artifacts by name, version, checksum, path or property.
    Search(SearchArgs),
    /// An artifact's SBOM.
    #[command(subcommand)]
    Sbom(SbomCmd),
    /// An artifact's properties.
    #[command(subcommand)]
    Property(PropertyCmd),
    /// Promote an artifact from one repository to another.
    Promote {
        #[arg(long)]
        from: String,
        #[arg(long)]
        to: String,
        #[command(flatten)]
        id: Id,
        /// Queue it as a job instead of waiting.
        #[arg(long)]
        r#async: bool,
    },
    /// The intake chain: its queues, an artifact's state and evidence, and the verbs.
    #[command(subcommand)]
    Intake(IntakeCmd),
    /// The audit plane.
    #[command(subcommand)]
    Audit(AuditCmd),
    /// A one-time migration from Nexus or Artifactory (online builds only).
    #[command(subcommand)]
    Migration(MigrationCmd),
    /// Copy what one repository has and another lacks.
    Replicate {
        #[arg(long)]
        from: String,
        #[arg(long)]
        to: String,
        /// Copy (without it: a dry run that only counts).
        #[arg(long)]
        execute: bool,
        #[arg(long, default_value_t = 0)]
        limit: i32,
        #[arg(long)]
        r#async: bool,
    },
    /// The job queue (admin).
    Jobs {
        #[arg(long, default_value = "")]
        kind: String,
        #[arg(long, default_value = "")]
        state: String,
        #[arg(long, default_value_t = 50)]
        limit: i32,
    },
    /// The door admission's book (admin).
    #[command(subcommand)]
    Admission(AdmissionCmd),
    /// One repository's meters.
    Usage {
        #[arg(long, short = 'r')]
        repository: String,
    },
    /// Where more room is bought (a metered server).
    #[command(subcommand)]
    Room(RoomCmd),
    /// The repositories you have here.
    #[command(subcommand)]
    Account(AccountCmd),
    /// Repository tokens for package clients.
    #[command(subcommand)]
    Token(TokenCmd),
    /// Signed airgap bundles.
    #[command(subcommand)]
    Bundle(BundleCmd),
    /// Verify a signed bundle against a trust policy, offline (no server is dialled).
    Verify(holger_update_bundle::cli::VerifyArgs),
    /// The courier: transfer artifacts between registries, archives and directories (no
    /// server is dialled unless the route names one).
    Agent(holger_agent_cli::AgentArgs),
    /// Signed intel bundles (the advisory database the scans read).
    #[command(subcommand)]
    Intel(IntelCmd),
    /// This server's downstreams (sync): the signed invite a downstream is handed.
    #[command(subcommand)]
    Downstream(DownstreamCmd),
    /// The invite that enrols this box's FIRST admin key (admin, once).
    FirstKey {
        /// The name the first admin is enrolled under (empty: theirs to choose).
        #[arg(long, default_value = "")]
        display: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum DownstreamCmd {
    /// The signed invite of enrolled downstream --name, written to --out (0600): the file its
    /// RON names in `sync.upstreams[].invite`. A `via: Front` downstream gets a direct invite
    /// (this server's sync door, no tunnel); a WireGuard one the front's (with the link's
    /// pre-shared key). Signed with this server's sync key (admin, audited).
    Invite {
        /// The enrolled downstream.
        #[arg(long)]
        name: String,
        /// The door the downstream reaches this server at (`http(s)://host:port`). Absent:
        /// the server's own sync door.
        #[arg(long, default_value = "")]
        door: String,
        /// A WireGuard downstream: the front's downstream key (absent: the front's own).
        #[arg(long, default_value = "")]
        hub_key: String,
        /// A WireGuard downstream: the front's endpoint, ip:port (absent: the front's own).
        #[arg(long, default_value = "")]
        endpoint: String,
        /// Where the invite is written (refused if it exists).
        #[arg(long)]
        out: PathBuf,
    },
}

#[derive(Subcommand, Debug)]
pub enum ArtifactsCmd {
    /// A page of artifacts.
    List {
        #[arg(long, short = 'r')]
        repository: String,
        #[arg(long, default_value = "")]
        filter: String,
        #[arg(long, default_value_t = 100)]
        limit: i32,
        #[arg(long, default_value = "")]
        page_token: String,
    },
    /// Download an artifact to --out ("-" for stdout).
    Fetch {
        #[command(flatten)]
        coord: Coord,
        #[arg(long)]
        out: PathBuf,
    },
    /// Upload --file as an artifact.
    Put {
        #[command(flatten)]
        coord: Coord,
        #[arg(long)]
        file: PathBuf,
        #[arg(long)]
        r#async: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum ArchiveCmd {
    /// The files in a repository's archive.
    Files {
        #[arg(long, short = 'r')]
        repository: String,
        #[arg(long, default_value = "")]
        prefix: String,
    },
    /// The archive's size, entries and dates.
    Info {
        #[arg(long, short = 'r')]
        repository: String,
    },
}

#[derive(Args, Debug)]
pub struct SearchArgs {
    #[arg(long, default_value = "")]
    pub name: String,
    #[arg(long, default_value = "")]
    pub namespace: String,
    #[arg(long, default_value = "")]
    pub version: String,
    #[arg(long, default_value = "")]
    pub checksum: String,
    #[arg(long, default_value = "")]
    pub path: String,
    /// Only these repositories (repeatable).
    #[arg(long = "repository")]
    pub repositories: Vec<String>,
    /// key=value (repeatable).
    #[arg(long = "property")]
    pub properties: Vec<String>,
    #[arg(long, default_value_t = 100)]
    pub limit: i32,
}

#[derive(Subcommand, Debug)]
pub enum SbomCmd {
    /// Attach --file as the artifact's SBOM.
    Attach {
        #[command(flatten)]
        coord: Coord,
        #[arg(long)]
        file: PathBuf,
    },
    /// The artifact's SBOM, to --out ("-" for stdout).
    Fetch {
        #[command(flatten)]
        coord: Coord,
        #[arg(long, default_value = "-")]
        out: PathBuf,
    },
}

#[derive(Subcommand, Debug)]
pub enum PropertyCmd {
    /// The artifact's properties.
    Get {
        #[command(flatten)]
        coord: Coord,
    },
    /// Set a property (repeat --value for a list).
    Set {
        #[command(flatten)]
        coord: Coord,
        #[arg(long)]
        key: String,
        #[arg(long = "value")]
        values: Vec<String>,
    },
}

#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum Verb {
    Advance,
    Quarantine,
    Release,
    Block,
}

#[derive(Subcommand, Debug)]
pub enum IntakeCmd {
    /// A queue of the intake chain (e.g. quarantine, approval).
    List {
        #[arg(long, default_value = "")]
        queue: String,
        #[arg(long, default_value_t = 100)]
        limit: u32,
    },
    /// An artifact's state.
    State {
        #[command(flatten)]
        id: Id,
    },
    /// An artifact's stored evidence.
    Evidence {
        #[command(flatten)]
        id: Id,
    },
    /// Move an artifact: advance, quarantine, release or block.
    Advance {
        #[command(flatten)]
        id: Id,
        #[arg(long, value_enum, default_value = "advance")]
        verb: Verb,
        #[arg(long, default_value = "")]
        to_state: String,
        #[arg(long, default_value = "")]
        reason: String,
    },
    /// The dependency sets: what each pull brought in, read and judged as one set.
    Sets {
        /// Only the sets in this status (resolving, held, refused, awaiting-approval,
        /// approved, available). Absent: every set you may read.
        #[arg(long, default_value = "")]
        status: String,
    },
    /// One dependency set (`set <id>`), or judge it: `set approve <id>`, `set refuse <id> --reason`.
    Set(SetCmd),
}

/// `intake set <id>` shows a set; `intake set approve|refuse <id>` judges it.
#[derive(Args, Debug)]
#[command(args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)]
pub struct SetCmd {
    /// The set's id (`intake sets` lists them).
    #[arg(required = true)]
    pub id: Option<String>,
    #[command(subcommand)]
    pub verb: Option<SetVerb>,
}

#[derive(Subcommand, Debug)]
pub enum SetVerb {
    /// Your vote on every member waiting for approval (each member needs its own approvers;
    /// the importer never approves).
    Approve {
        id: String,
        /// The set's `revision` as `holger intake set <id>` showed it: the vote is refused
        /// when the set changed since (SEC-REVIEW D4).
        #[arg(long)]
        revision: String,
        #[arg(long, default_value = "")]
        reason: String,
    },
    /// Block every waiting member, with the reason.
    Refuse {
        id: String,
        /// The set's `revision` as `holger intake set <id>` showed it.
        #[arg(long)]
        revision: String,
        #[arg(long)]
        reason: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum AuditCmd {
    /// The trail of the artifact with this sha256.
    Trail {
        #[arg(long)]
        sha256: String,
    },
    /// What a person downloaded: the audit log's download rows (Auditor or Admin).
    Downloads {
        /// Who downloaded (`anonymous` for open reads). Absent: anybody.
        #[arg(long, default_value = "")]
        who: String,
        /// The repository. Absent: every repository.
        #[arg(short = 'r', long, default_value = "")]
        repository: String,
        /// From (inclusive), nanoseconds since the Unix epoch. Absent: open.
        #[arg(long, default_value_t = 0)]
        since: i64,
        /// To (exclusive), nanoseconds since the Unix epoch. Absent: open.
        #[arg(long, default_value_t = 0)]
        until: i64,
        /// At most this many rows, the newest that match (0: the server's 1000).
        #[arg(long, default_value_t = 0)]
        limit: u64,
    },
    /// Filter and page every record: by actor, repository, artifact, action, address and time.
    Query {
        #[command(flatten)]
        filter: AuditFilterArgs,
        /// Matching records to skip (the previous answer's next_offset).
        #[arg(long, default_value_t = 0)]
        offset: u64,
        /// Page size (0: the server's 100; at most 1000).
        #[arg(long, default_value_t = 0)]
        limit: u32,
    },
    /// Count the records per repository, action and day, or per the keys given.
    Aggregate {
        #[command(flatten)]
        filter: AuditFilterArgs,
        /// repo, action, day, ident, client, status (repeat or comma-separate).
        #[arg(long, value_delimiter = ',')]
        by: Vec<String>,
        /// Count only what was served (2xx).
        #[arg(long)]
        ok_only: bool,
    },
    /// The selected records as one Arrow IPC file.
    Export {
        #[command(flatten)]
        filter: AuditFilterArgs,
        /// Where the Arrow IPC file goes ("-": stdout).
        #[arg(long)]
        out: PathBuf,
        /// Only records from this chain seq on (the previous answer's next_seq).
        #[arg(long, default_value_t = 0)]
        since_seq: u64,
        /// At most this many rows (0: the server's 50000).
        #[arg(long, default_value_t = 0)]
        limit: u32,
    },
    /// Walk the whole hash chain on the server and name the first break.
    Verify,
}

/// The audit filter every audit read takes; every flag optional.
#[derive(Args, Debug, Clone, Default)]
pub struct AuditFilterArgs {
    /// The actor (`anonymous` for open reads).
    #[arg(long, default_value = "")]
    pub who: String,
    /// The repository.
    #[arg(short = 'r', long, default_value = "")]
    pub repository: String,
    /// A prefix of the artifact (`acme/`, `acme/widget@`).
    #[arg(long, default_value = "")]
    pub artifact: String,
    /// download, upload, delete, list, promote, entitlement, purge, scan, token, other.
    #[arg(long, default_value = "")]
    pub action: String,
    /// The client address, with or without a port.
    #[arg(long, default_value = "")]
    pub source_ip: String,
    /// The exact status (0: any).
    #[arg(long, default_value_t = 0)]
    pub status: u32,
    /// From (inclusive), nanoseconds since the Unix epoch.
    #[arg(long, default_value_t = 0)]
    pub since: i64,
    /// To (exclusive), nanoseconds since the Unix epoch.
    #[arg(long, default_value_t = 0)]
    pub until: i64,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum SourceKind {
    Nexus,
    Artifactory,
}

#[derive(Args, Debug)]
pub struct Source {
    #[arg(long, value_enum)]
    pub kind: SourceKind,
    #[arg(long)]
    pub base_url: String,
    #[arg(long, default_value = "")]
    pub username: String,
    #[arg(long, default_value = "")]
    pub repository: String,
    /// Read the source's password from stdin (never from the command line).
    #[arg(long)]
    pub password_stdin: bool,
}

#[derive(Subcommand, Debug)]
pub enum MigrationCmd {
    /// What the source holds.
    Discover(Source),
    /// Start the migration; prints its job id.
    Start {
        #[command(flatten)]
        source: Source,
        #[arg(long, default_value_t = 4)]
        concurrency: i32,
    },
    /// Follow a migration's progress until it ends.
    Watch {
        #[arg(long)]
        job: String,
    },
    /// Cancel a migration.
    Cancel {
        #[arg(long)]
        job: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum AdmissionCmd {
    /// Who is taking the door, consumer by consumer.
    Usage {
        #[arg(long, default_value_t = 50)]
        limit: i32,
    },
    /// What the door would admit for this consumer and work.
    Plan {
        #[arg(long)]
        consumer: String,
        #[arg(long, default_value = "")]
        work: String,
        #[arg(long, default_value_t = 0)]
        size_hint: u64,
    },
}

#[derive(Subcommand, Debug)]
pub enum RoomCmd {
    /// Where more room for this repository is bought.
    Show {
        #[arg(long, short = 'r')]
        repository: String,
    },
    /// The shop's page for a private estate.
    PrivateEstate,
}

#[derive(Subcommand, Debug)]
pub enum AccountCmd {
    /// The repositories you have here, and their client setup.
    Mine,
    /// Make a private repository (admin).
    Create {
        #[arg(long)]
        name: String,
        #[arg(long)]
        format: String,
    },
    /// Give a person access to a repository.
    Grant {
        #[arg(long, short = 'r')]
        repository: String,
        #[arg(long)]
        person: String,
        /// read, write or none.
        #[arg(long)]
        access: String,
    },
}

#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum Scope {
    Read,
    ReadWrite,
}

#[derive(Subcommand, Debug)]
pub enum TokenCmd {
    /// Mint a repository token (a passkey session, stepped up). Prints the secret ONCE.
    Mint {
        #[arg(long, default_value = "")]
        label: String,
        #[arg(long = "repository", required = true)]
        repositories: Vec<String>,
        #[arg(long, value_enum, default_value = "read")]
        scope: Scope,
        /// Days until it expires (0: the default of 90).
        #[arg(long, default_value_t = 0)]
        days: u32,
    },
    /// The tokens your credential may see.
    List,
    /// Revoke a token by its id.
    Revoke {
        #[arg(long)]
        id: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum IntelCmd {
    /// Import a signed intel bundle: verified, audited first, then the live advisory database.
    Import {
        #[arg(long)]
        file: PathBuf,
        // No `--first-import` (lane HOL-CDS, NCSC gap 2): an absent floor is established only by
        // an admin's authorization on the secure side (`holger bundle allow-first-import`).
    },
}

#[derive(Subcommand, Debug)]
#[allow(clippy::large_enum_variant)] // clap's argument structs; boxing them buys nothing here
pub enum BundleCmd {
    /// The local, offline verbs (no server is dialled): prepare, approve, finalize,
    /// inspect and extract a bundle.
    #[command(flatten)]
    Tool(holger_update_bundle::cli::BundleTool),
    /// The signed airgap export of the approved set, written as a candidate directory
    /// (admin). Approve and release-sign it with `holger-bundle`, as any bundle.
    Export {
        /// The candidate directory to write (must not exist).
        #[arg(long)]
        out: PathBuf,
        #[arg(long)]
        bundle_id: String,
        #[arg(long)]
        sequence: u64,
        #[arg(long)]
        supplier: String,
        #[arg(long)]
        product: String,
        #[arg(long)]
        release_version: String,
        #[arg(long, default_value_t = 30)]
        expires_days: u32,
        /// Only these artifacts (repeatable). Absent: every AVAILABLE artifact.
        #[arg(long = "artifact")]
        artifacts: Vec<String>,
    },
    /// Authorize the FIRST import of a vendor and product at a sequence (admin, audited, used
    /// once): with no anti-rollback floor yet, the server refuses every bundle until an admin
    /// on the secure side says which one sets it (`holger-import-9`).
    AllowFirstImport {
        /// The bundle's vendor (its `supplier`).
        #[arg(long)]
        vendor: String,
        #[arg(long)]
        product: String,
        /// The sequence that establishes the floor.
        #[arg(long)]
        sequence: u64,
    },
    /// Import a signed bundle through the intake chain.
    Import {
        #[arg(long)]
        file: PathBuf,
        // No `--first-import` (lane HOL-CDS, NCSC gap 2): an absent floor is established only by
        // an admin's authorization on the secure side (`holger bundle allow-first-import`).
    },
}