Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ commands, `--json` output and exit codes. Each release's notes start with its se

## [Unreleased]

### Fixed

- Usage errors no longer list `--api-url` or `--profile` in the `Usage:` line when only
`HODEISHIELD_API_URL` or `HODEISHIELD_PROFILE` sets them. The variables still work the same and
`--help` still names them.
([#201](https://github.com/Hodeitek/hodeishield-cli/issues/201))

## [0.5.0] - 2026-10-15

### Added
Expand Down
12 changes: 5 additions & 7 deletions crates/hodeishield-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,22 @@ Exit codes:
#[derive(Debug, Parser)]
#[command(name = "hodeishield", version, propagate_version = true, after_help = AFTER_HELP)]
pub struct Cli {
/// Configuration profile to use.
/// Configuration profile to use. [env: HODEISHIELD_PROFILE]
// Read from the environment in `commands::run`, not by clap: clap would list the flag in every
// usage line while the variable is set.
#[arg(
long,
global = true,
help_heading = "Global options",
env = "HODEISHIELD_PROFILE",
hide_env_values = true,
value_name = "NAME"
)]
pub profile: Option<String>,

/// Base URL of the API, overriding the profile.
/// Base URL of the API, overriding the profile. [env: HODEISHIELD_API_URL]
#[arg(
long,
global = true,
help_heading = "Global options",
env = "HODEISHIELD_API_URL",
hide_env_values = true,
value_name = "URL"
)]
pub api_url: Option<String>,
Expand Down Expand Up @@ -149,7 +147,7 @@ With --device, you get a short code to type in on any other device instead.
On the consent screen you choose the one tenant the CLI may read; the token covers only that \
tenant, and only reads. The scopes asked for are the ones the CLI's commands need (see \
`config set oauth_scopes` to ask for fewer). Revoke the access with `hodeishield logout`, or in the \
app under Account → Application access.";
app under Account → Applications.";

#[derive(Debug, Args)]
pub struct LoginArgs {
Expand Down
9 changes: 7 additions & 2 deletions crates/hodeishield-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,16 @@ impl Context {
}
}

/// A flag's environment fallback; an empty or blank value counts as unset.
fn env_value(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.trim().is_empty())
}

pub fn run(cli: Cli, out: &mut dyn Write) -> Result<()> {
let ctx = Context {
overrides: Overrides {
profile: cli.profile,
api_url: cli.api_url,
profile: cli.profile.or_else(|| env_value("HODEISHIELD_PROFILE")),
api_url: cli.api_url.or_else(|| env_value("HODEISHIELD_API_URL")),
},
json: cli.json,
verbose: cli.verbose,
Expand Down
4 changes: 2 additions & 2 deletions crates/hodeishield-cli/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ fn unstored_notice(revoked: &Result<()>) -> String {
Err(revocation) => format!(
"The session could not be stored on this machine, and the app did not confirm \
revoking the tokens issued for it ({}). Revoke the CLI's access in the app under \
\"Application access\".",
Account → Applications.",
clean(&revocation.message)
),
}
Expand Down Expand Up @@ -1321,7 +1321,7 @@ mod tests {
fn the_notice_for_unrevoked_tokens_points_to_the_app() {
let notice = unstored_notice(&Err(Failure::general("the app refused to revoke")));
assert!(
notice.contains("did not confirm") && notice.contains("Application access"),
notice.contains("did not confirm") && notice.contains("Account → Applications"),
"{notice}"
);
}
Expand Down
44 changes: 44 additions & 0 deletions crates/hodeishield-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,50 @@ fn assert_no_key(output: &std::process::Output) {
);
}

/// A variable that only sets a global option must not turn that option into part of the usage line.
#[test]
fn usage_line_does_not_list_options_set_by_the_environment() {
let env = Env::new();
let output = env
.cmd()
.env("HODEISHIELD_API_URL", "http://127.0.0.1:9")
.env("HODEISHIELD_PROFILE", "default")
.args(["vendors", "get"])
.output()
.expect("run");
let stderr = String::from_utf8_lossy(&output.stderr);
let usage = stderr
.lines()
.find(|l| l.starts_with("Usage:"))
.expect(&stderr);
assert!(
!usage.contains("--api-url") && !usage.contains("--profile"),
"{usage}"
);
}

/// The flag wins over the variable, and the variable still selects the profile when there is no flag.
#[test]
fn profile_flag_beats_the_environment() {
let env = Env::new();
let missing = "Profile `does-not-exist` is not defined";
let with_flag = env
.cmd()
.env("HODEISHIELD_PROFILE", "does-not-exist")
.args(["--profile", "default", "config", "show"])
.output()
.expect("run");
assert!(with_flag.status.success(), "{with_flag:?}");
assert!(!String::from_utf8_lossy(&with_flag.stderr).contains(missing));
let without_flag = env
.cmd()
.env("HODEISHIELD_PROFILE", "does-not-exist")
.args(["config", "show"])
.output()
.expect("run");
assert!(String::from_utf8_lossy(&without_flag.stderr).contains(missing));
}

/// `--help` names the variables but never prints their values, which end up in terminals and CI logs.
#[test]
fn help_does_not_print_the_values_of_the_environment() {
Expand Down
3 changes: 3 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ feature-depth = 1
# (.github/scripts/advisory-exceptions.py), and cargo audit ignores exactly the same list.
[advisories]
ignore = [
{ id = "RUSTSEC-2026-0370", reason = "Unmaintained notice, not a vulnerability: the code of zvariant 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." },
{ id = "RUSTSEC-2026-0371", reason = "Unmaintained notice, not a vulnerability: the code of zbus_names 4.3.4 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." },
{ id = "RUSTSEC-2026-0372", reason = "Unmaintained notice, not a vulnerability: the code of zvariant_derive 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31." },
]

# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
Expand Down
17 changes: 17 additions & 0 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# OSV-Scanner configuration (read by the scan in .github/workflows/security-tests.yml).
# Same acceptances, reasons and review date as [advisories] ignore in deny.toml.

[[IgnoredVulns]]
id = "RUSTSEC-2026-0370"
ignoreUntil = 2027-01-31
reason = "Unmaintained notice, not a vulnerability: the code of zvariant 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31."

[[IgnoredVulns]]
id = "RUSTSEC-2026-0371"
ignoreUntil = 2027-01-31
reason = "Unmaintained notice, not a vulnerability: the code of zbus_names 4.3.4 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31."

[[IgnoredVulns]]
id = "RUSTSEC-2026-0372"
ignoreUntil = 2027-01-31
reason = "Unmaintained notice, not a vulnerability: the code of zvariant_derive 5.15.0 was merged into zbus 6.0.0. Reached only through zbus 5 <- secret-service 5.2.0 <- zbus-secret-service-keyring-store 1.0.1, the Linux Secret Service keychain backend. The latest release of both secret-service and the keyring store still depend on zbus 5, so there is no upgrade yet. Review by 2027-01-31."
Loading