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`).
},
}