From b9c488787751728ecd0ad6b626c357cf7cda30c2 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:33:16 +0200 Subject: [PATCH 01/23] Run the proxy and TLS inspection test on macOS and Windows The proxy-tls workflow becomes a matrix over Linux, macOS and Windows. Each runner uses the native mitmproxy release (pinned by checksum), and the test script puts the proxy's CA in the system store the way an administrator would: security add-trusted-cert on macOS, Import-Certificate on Windows. SSL_CERT_FILE is asserted to have no effect there, as the README states. Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- .github/scripts/proxy-tls-test.sh | 83 ++++++++++++++++++++++++------- .github/workflows/proxy-tls.yml | 69 ++++++++++++++++++------- README.md | 4 +- 3 files changed, 119 insertions(+), 37 deletions(-) diff --git a/.github/scripts/proxy-tls-test.sh b/.github/scripts/proxy-tls-test.sh index 145a0dd..e17d30a 100755 --- a/.github/scripts/proxy-tls-test.sh +++ b/.github/scripts/proxy-tls-test.sh @@ -2,30 +2,43 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2026 Hodeitek S.L. # -# Proves the README's "Networking: proxies and TLS inspection" claims on Linux, end to end, with a -# real TLS-inspecting proxy (mitmproxy) and a fake API, all on this machine: +# Proves the README's "Networking: proxies and TLS inspection" claims on Linux, macOS and Windows, +# end to end, with a real TLS-inspecting proxy (mitmproxy) and a fake API, all on this machine: # # 1. HTTPS_PROXY set, proxy CA not trusted -> exit 7, "error: Could not reach the API:", certificate # 2. proxy CA in the system store -> works, and the proxy saw the request -# 3. proxy CA only in SSL_CERT_FILE -> works +# 3. proxy CA only in SSL_CERT_FILE -> works on Linux; no effect on macOS and +# Windows (same failure as case 1) # 4. NO_PROXY covers the host -> the proxy is not used # 5. lower-case https_proxy -> works like HTTPS_PROXY # 6. --verbose -> the request line goes to stderr only # -# The API is reached as api.test:8443, mapped to 127.0.0.1 in /etc/hosts, because the CLI never uses +# The API is reached as api.test:8443, mapped to 127.0.0.1 in the hosts file, because the CLI never uses # a proxy for a loopback URL. The fake API's certificate comes from one throwaway CA that only the # proxy trusts; the proxy re-signs with another throwaway CA, the one the CLI must trust. # -# This is meant to run in CI (GitHub Actions): it edits /etc/hosts and the system certificate store, -# and case 1 is a deliberate failure. It refuses to run elsewhere. +# The CA goes into the system store the way an administrator would do it: update-ca-certificates +# on Linux, `security add-trusted-cert` on macOS, Import-Certificate on Windows. On macOS and +# Windows the fake API's CA goes there too (case 4), since SSL_CERT_FILE is not read there. +# +# This is meant to run in CI (GitHub Actions): it edits the hosts file and the system certificate +# store, and case 1 is a deliberate failure. It refuses to run elsewhere. On Windows it runs in Git +# Bash, as an administrator. # # Usage: proxy-tls-test.sh PATH_TO_HODEISHIELD PATH_TO_MITMPROXY_DIR set -euo pipefail if [ "${GITHUB_ACTIONS:-}" != "true" ]; then - echo "proxy-tls-test.sh edits /etc/hosts and the system certificate store: it only runs in GitHub Actions." >&2 + echo "proxy-tls-test.sh edits the hosts file and the system certificate store: it only runs in GitHub Actions." >&2 exit 2 fi +export MSYS_NO_PATHCONV=1 # Git Bash on Windows: keep arguments such as -subj /CN=... as they are +case "$(uname -s)" in + Linux) OS=linux ;; + Darwin) OS=macos ;; + MINGW* | MSYS* | CYGWIN*) OS=windows ;; + *) echo "unsupported system: $(uname -s)" >&2; exit 2 ;; +esac BIN=$(realpath "${1:?usage: proxy-tls-test.sh HODEISHIELD MITMPROXY_DIR}") MITM=$(realpath "${2:?usage: proxy-tls-test.sh HODEISHIELD MITMPROXY_DIR}") SCRIPTS=$(cd "$(dirname "$0")" && pwd) @@ -37,11 +50,12 @@ API_URL="https://$HOST:$API_PORT" REQ_RE="GET https://$HOST:$API_PORT/v1/vendors" WORK=$(mktemp -d) +if [ "$OS" = windows ]; then WORK=$(cygpath -m "$WORK"); fi # C:/... form, understood by native programs PIDS=() # shellcheck disable=SC2329 # invoked by the EXIT trap cleanup() { - for pid in "${PIDS[@]}"; do kill "$pid" 2> /dev/null || true; done - rm -rf "$WORK" + for pid in ${PIDS[@]+"${PIDS[@]}"}; do kill "$pid" 2> /dev/null || true; done + rm -rf "$WORK" || true # on Windows a file can still be open for a moment } trap cleanup EXIT @@ -65,10 +79,37 @@ cat "$WORK/proxy-ca.key" "$WORK/proxy-ca.crt" > "$WORK/confdir/mitmproxy-ca.pem" cp "$WORK/proxy-ca.crt" "$WORK/confdir/mitmproxy-ca-cert.pem" # --- Name, fake API, proxy -------------------------------------------------------------------- -echo "127.0.0.1 $HOST" | sudo tee -a /etc/hosts > /dev/null +SUDO=sudo +HOSTS_FILE=/etc/hosts +PY=python3 +if [ "$OS" = windows ]; then + SUDO= # the runner account is already an administrator + HOSTS_FILE=/c/Windows/System32/drivers/etc/hosts + PY=python +fi +echo "127.0.0.1 $HOST" | $SUDO tee -a "$HOSTS_FILE" > /dev/null +if [ "$OS" = macos ]; then sudo dscacheutil -flushcache; fi +if [ "$OS" = windows ]; then ipconfig /flushdns > /dev/null; fi + +# Puts a CA certificate (PEM) into the system store, the way an administrator would. +trust_ca() { + case "$OS" in + linux) + sudo cp "$1" "/usr/local/share/ca-certificates/hodeishield-test-$2.crt" + sudo update-ca-certificates > /dev/null + ;; + macos) + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$1" + ;; + windows) + powershell.exe -NoProfile -Command \ + "Import-Certificate -FilePath '$1' -CertStoreLocation Cert:\\LocalMachine\\Root | Out-Null" + ;; + esac +} : > "$WORK/api.log" -python3 -I "$SCRIPTS/proxy-tls-fake-api.py" "$WORK/api.crt" "$WORK/api.key" "$API_PORT" "$WORK/api.log" & +"$PY" -I "$SCRIPTS/proxy-tls-fake-api.py" "$WORK/api.crt" "$WORK/api.key" "$API_PORT" "$WORK/api.log" & PIDS+=($!) PYTHONUNBUFFERED=1 "$MITM/mitmdump" --set "confdir=$WORK/confdir" \ @@ -148,16 +189,23 @@ else fi # --- Case 3: the proxy's CA only in SSL_CERT_FILE (run before it enters the system store) ----- -p0=$(proxy_seen) +# Linux reads the variable; macOS and Windows use the system store only (README), so there it +# must fail like case 1. +p0=$(proxy_seen) a0=$(api_seen) cli HTTPS_PROXY="$PROXY" SSL_CERT_FILE="$WORK/proxy-ca.crt" -- vendors list --json -if works && wait_proxy_above "$p0"; then - record 3 "proxy CA only in SSL_CERT_FILE: works through the proxy" PASS "exit $rc" +if [ "$OS" = linux ]; then + desc="proxy CA only in SSL_CERT_FILE: works through the proxy" + if works && wait_proxy_above "$p0"; then ok=1; else ok=0; fi else - record 3 "proxy CA only in SSL_CERT_FILE: works through the proxy" FAIL "exit $rc" - show_failure 3 + desc="proxy CA only in SSL_CERT_FILE: no effect here, exit 7, certificate error" + sleep 1 + if [ "$rc" -eq 7 ] && [[ "$(cat "$WORK/err")" == "error: Could not reach the API:"* ]] \ + && [ "$(proxy_seen)" -eq "$p0" ] && [ "$(api_seen)" -eq "$a0" ]; then ok=1; else ok=0; fi fi +if [ "$ok" -eq 1 ]; then record 3 "$desc" PASS "exit $rc"; else record 3 "$desc" FAIL "exit $rc"; show_failure 3; fi # --- Case 4: NO_PROXY covers the host: direct, the proxy never sees it ------------------------ +if [ "$OS" != linux ]; then trust_ca "$WORK/upstream-ca.crt" upstream; fi p0=$(proxy_seen) a0=$(api_seen) cli HTTPS_PROXY="$PROXY" NO_PROXY="$HOST" SSL_CERT_FILE="$WORK/upstream-ca.crt" -- vendors list --json sleep 1 @@ -169,8 +217,7 @@ else fi # --- Case 2: the proxy's CA in the system store ------------------------------------------------ -sudo cp "$WORK/proxy-ca.crt" /usr/local/share/ca-certificates/hodeishield-test-proxy-ca.crt -sudo update-ca-certificates > /dev/null +trust_ca "$WORK/proxy-ca.crt" proxy p0=$(proxy_seen) cli HTTPS_PROXY="$PROXY" -- vendors list --json if works && wait_proxy_above "$p0"; then diff --git a/.github/workflows/proxy-tls.yml b/.github/workflows/proxy-tls.yml index fa89115..89d8b48 100644 --- a/.github/workflows/proxy-tls.yml +++ b/.github/workflows/proxy-tls.yml @@ -1,7 +1,8 @@ name: Proxy and TLS inspection -# Checks the README's "Networking: proxies and TLS inspection" claims on Linux against a real -# TLS-inspecting proxy (mitmproxy) and a fake API, both on the runner, with throwaway certificates: +# Checks the README's "Networking: proxies and TLS inspection" claims on Linux, macOS and Windows +# against a real TLS-inspecting proxy (mitmproxy) and a fake API, both on the runner, with +# throwaway certificates: # nothing secret is used and nothing leaves the machine except the download of mitmproxy. Case 1 # is a deliberate certificate failure, so the script is only meant to run here (see # .github/scripts/proxy-tls-test.sh). @@ -34,19 +35,42 @@ env: jobs: proxy-tls: - name: proxy-tls - runs-on: ubuntu-latest - timeout-minutes: 15 + name: proxy-tls (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 20 permissions: contents: read + strategy: + fail-fast: false + matrix: + # Official mitmproxy releases, pinned by version and checksum. Bumped by hand. + # The download site publishes no checksum file, so each SHA-256 is the one of the + # release archive as downloaded when it was pinned (trust on first use). mitmproxy + # is only a test tool here and is never shipped. When bumping, record the new + # archives' SHA-256 the same way. + include: + - os: ubuntu-latest + mitm_archive: mitmproxy-12.2.3-linux-x86_64.tar.gz + mitm_sha256: 2e95286b618fa6fd33e5e62a78c2e5112571d85f42ec2bac29b97ee242bdb5c5 + mitm_dir: mitmproxy + bin: target/debug/hodeishield + - os: macos-latest + mitm_archive: mitmproxy-12.2.3-macos-arm64.tar.gz + mitm_sha256: 0a09ee3b82569e8985aff8186e4792618b8e5d0c766098db093d09a87d4b013a + mitm_dir: mitmproxy/mitmproxy.app/Contents/MacOS + bin: target/debug/hodeishield + - os: windows-latest + mitm_archive: mitmproxy-12.2.3-windows-x86_64.zip + mitm_sha256: 04a01ea95ae96df75058a893e774957d294e69012dab1f4e256ce2b0c6725483 + mitm_dir: mitmproxy + bin: target/debug/hodeishield.exe + defaults: + run: + shell: bash env: - # Official Linux release of mitmproxy, pinned by version and checksum. Bumped by hand. - # The download site publishes no checksum file, so the SHA-256 is the one of the - # release archive as downloaded when it was pinned (trust on first use). mitmproxy - # is only a test tool here and is never shipped. When bumping, record the new - # archive's SHA-256 the same way. MITMPROXY_VERSION: "12.2.3" - MITMPROXY_SHA256: 2e95286b618fa6fd33e5e62a78c2e5112571d85f42ec2bac29b97ee242bdb5c5 + MITM_ARCHIVE: ${{ matrix.mitm_archive }} + MITM_SHA256: ${{ matrix.mitm_sha256 }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -67,12 +91,23 @@ jobs: - name: Install mitmproxy run: | set -euo pipefail - archive="mitmproxy-${MITMPROXY_VERSION}-linux-x86_64.tar.gz" - curl --proto '=https' --tlsv1.2 -fsSL -o "$RUNNER_TEMP/$archive" \ - "https://downloads.mitmproxy.org/${MITMPROXY_VERSION}/${archive}" - echo "${MITMPROXY_SHA256} $RUNNER_TEMP/$archive" | sha256sum -c - + curl --proto '=https' --tlsv1.2 -fsSL -o "$RUNNER_TEMP/$MITM_ARCHIVE" \ + "https://downloads.mitmproxy.org/${MITMPROXY_VERSION}/${MITM_ARCHIVE}" + if command -v sha256sum > /dev/null; then sum=sha256sum; else sum="shasum -a 256"; fi + echo "${MITM_SHA256} $RUNNER_TEMP/$MITM_ARCHIVE" | $sum -c - mkdir "$RUNNER_TEMP/mitmproxy" - tar -xzf "$RUNNER_TEMP/$archive" -C "$RUNNER_TEMP/mitmproxy" + case "$MITM_ARCHIVE" in + *.zip) 7z x -y "-o$RUNNER_TEMP/mitmproxy" "$RUNNER_TEMP/$MITM_ARCHIVE" > /dev/null ;; + *) tar -xzf "$RUNNER_TEMP/$MITM_ARCHIVE" -C "$RUNNER_TEMP/mitmproxy" ;; + esac + + # macOS ships LibreSSL as `openssl`; the script needs OpenSSL 3 (req -addext). + - name: Use Homebrew OpenSSL on macOS + if: runner.os == 'macOS' + run: echo "$(brew --prefix openssl@3)/bin" >> "$GITHUB_PATH" - name: Run the proxy cases - run: bash .github/scripts/proxy-tls-test.sh target/debug/hodeishield "$RUNNER_TEMP/mitmproxy" + env: + BIN: ${{ matrix.bin }} + MITM_DIR: ${{ matrix.mitm_dir }} + run: bash .github/scripts/proxy-tls-test.sh "$BIN" "${RUNNER_TEMP//\\//}/$MITM_DIR" diff --git a/README.md b/README.md index 8d4e54b..280898d 100644 --- a/README.md +++ b/README.md @@ -751,8 +751,8 @@ message that starts with `Could not reach the API:` (or `Could not reach the app followed by the cause. A certificate error there usually means the proxy's CA is missing from the store described above. -This behaviour comes from the CLI's HTTP library. It is verified on Linux in CI against a -TLS-inspecting proxy; macOS and Windows have not been verified yet. +This behaviour comes from the CLI's HTTP library. It is verified in CI on Linux, macOS and Windows +against a TLS-inspecting proxy whose CA is installed in the system store. ## Repository layout From 38d1aac09399ace14e00e031b076df6b75a35e1e Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:39:36 +0200 Subject: [PATCH 02/23] Write the test certificate's extensions to a file, which openssl on Windows can open Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- .github/scripts/proxy-tls-test.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/proxy-tls-test.sh b/.github/scripts/proxy-tls-test.sh index e17d30a..26ec601 100755 --- a/.github/scripts/proxy-tls-test.sh +++ b/.github/scripts/proxy-tls-test.sh @@ -70,9 +70,10 @@ make_ca proxy # the "corporate" CA the proxy re-signs with; what the CLI must openssl req -newkey rsa:2048 -nodes -subj "/CN=$HOST" \ -keyout "$WORK/api.key" -out "$WORK/api.csr" 2> /dev/null +# A file, not <(...): a native openssl.exe on Windows cannot open /dev/fd paths. +printf 'subjectAltName=DNS:%s\nbasicConstraints=CA:FALSE\n' "$HOST" > "$WORK/api.ext" openssl x509 -req -in "$WORK/api.csr" -CA "$WORK/upstream-ca.crt" -CAkey "$WORK/upstream-ca.key" \ - -CAcreateserial -days 2 -out "$WORK/api.crt" \ - -extfile <(printf 'subjectAltName=DNS:%s\nbasicConstraints=CA:FALSE\n' "$HOST") 2> /dev/null + -CAcreateserial -days 2 -out "$WORK/api.crt" -extfile "$WORK/api.ext" 2> /dev/null mkdir "$WORK/confdir" cat "$WORK/proxy-ca.key" "$WORK/proxy-ca.crt" > "$WORK/confdir/mitmproxy-ca.pem" From 5c866e3c8a890ce130dcc4c822b13b92669821f6 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:45:03 +0200 Subject: [PATCH 03/23] Hand python on Windows the scripts folder in a form it can open Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- .github/scripts/proxy-tls-test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/proxy-tls-test.sh b/.github/scripts/proxy-tls-test.sh index 26ec601..a4fbf2d 100755 --- a/.github/scripts/proxy-tls-test.sh +++ b/.github/scripts/proxy-tls-test.sh @@ -42,6 +42,7 @@ esac BIN=$(realpath "${1:?usage: proxy-tls-test.sh HODEISHIELD MITMPROXY_DIR}") MITM=$(realpath "${2:?usage: proxy-tls-test.sh HODEISHIELD MITMPROXY_DIR}") SCRIPTS=$(cd "$(dirname "$0")" && pwd) +if [ "$OS" = windows ]; then SCRIPTS=$(cygpath -m "$SCRIPTS"); fi # handed to python.exe HOST=api.test API_PORT=8443 From df423ad0ec9b9ca35f71e651e5000f16b5416079 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:18:00 +0200 Subject: [PATCH 04/23] Test that whoami labels a user with no name by the email Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- crates/hodeishield-cli/src/commands/session.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/hodeishield-cli/src/commands/session.rs b/crates/hodeishield-cli/src/commands/session.rs index 1a133f8..d868ac1 100644 --- a/crates/hodeishield-cli/src/commands/session.rs +++ b/crates/hodeishield-cli/src/commands/session.rs @@ -1008,6 +1008,16 @@ mod tests { } } + #[test] + fn whoami_shows_the_email_when_the_api_gives_no_name() { + let (text, printed) = whoami_of_a_sign_in( + Some(r#"{"id":"u-1","name":null,"email":"ana@acme.example"}"#), + None, + ); + assert!(text.contains("User ana@acme.example\n"), "{text}"); + assert_eq!(printed["user"], "ana@acme.example"); + } + #[test] fn whoami_uses_the_id_when_the_api_gives_no_name_or_email() { let (text, _) = whoami_of_a_sign_in(Some(r#"{"id":"u-1","name":null}"#), None); From 87ed6fdfba7bed5e8b0d165b79f4ba4cec6cd129 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:20:15 +0200 Subject: [PATCH 05/23] Document the Linux prerequisites to install and verify, and a checksum command BusyBox runs Closes #195 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 10 +++++++++- docs/verifying-releases.md | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 280898d..19ebea6 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ section), and put `hodeishield` (`hodeishield.exe` on Windows) somewhere on your statically linked and run on any distribution. Homebrew and Scoop packages (next section) and a [container image](#container-image) are also available. +On Linux you need `curl` (or another way to download), `tar`, `gzip` and a CA certificate bundle +(the `ca-certificates` package). Minimal Debian, Ubuntu and Alpine images do not include `curl` or +the bundle: install them first, for example `apt-get install curl ca-certificates` or +`apk add curl ca-certificates`. Verifying a download also needs +[`cosign`](https://docs.sigstore.dev/cosign/system_config/installation/) v3 (or v2.4 or newer) and, +for the provenance check, [`slsa-verifier`](https://github.com/slsa-framework/slsa-verifier#installation); +neither is part of a distribution's default install. + From source, with the Rust toolchain installed (pin the release tag you want): ```sh @@ -229,7 +237,7 @@ Each release has a `SHA256SUMS` file, a Sigstore bundle (`*.sigstore.json`) for `SHA256SUMS`, and SLSA provenance (`hodeishield.intoto.jsonl`). In short: ```sh -sha256sum --ignore-missing -c SHA256SUMS +grep hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz SHA256SUMS | sha256sum -c - cosign verify-blob hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz \ --bundle hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz.sigstore.json \ diff --git a/docs/verifying-releases.md b/docs/verifying-releases.md index 0bb181b..51c8516 100644 --- a/docs/verifying-releases.md +++ b/docs/verifying-releases.md @@ -46,9 +46,12 @@ From the release page, download: ## 2. Check the checksum ```sh -sha256sum -c SHA256SUMS --ignore-missing +grep hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz SHA256SUMS | sha256sum -c - ``` +Checking only the line of your archive works with GNU `sha256sum` and with the BusyBox one on +Alpine, which has no `--ignore-missing`. On macOS, use `shasum -a 256 -c` in place of `sha256sum -c`. + This confirms the archive you downloaded matches the checksum the release workflow recorded. It does **not** by itself prove who built it — that's what the next two steps are for. From a93d075ba4104ff56ff109b65b8b6cfca6270781 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:20:25 +0200 Subject: [PATCH 06/23] Hint at the CA certificates package when the system has none Closes #196 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- crates/hodeishield-cli/src/auth/oauth.rs | 11 ++++++----- crates/hodeishield-cli/src/failure.rs | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/crates/hodeishield-cli/src/auth/oauth.rs b/crates/hodeishield-cli/src/auth/oauth.rs index 8600518..7159e4e 100644 --- a/crates/hodeishield-cli/src/auth/oauth.rs +++ b/crates/hodeishield-cli/src/auth/oauth.rs @@ -7,7 +7,7 @@ //! //! No endpoint is written here: every URL comes from the metadata the app publishes. -use crate::failure::{Failure, Kind, Result, error_chain}; +use crate::failure::{Failure, Kind, Result, ca_hint, error_chain}; use base64::Engine as _; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use hodeishield_api::v1::operations; @@ -102,10 +102,11 @@ pub fn http_client(app_url: &Url) -> Result { .https_only(!hodeishield_api::is_loopback_host(app_url)) .build() .map_err(|e| { - Failure::general(format!( - "Cannot start the HTTP client: {}.", - error_chain(&e) - )) + let chain = error_chain(&e); + ca_hint( + Failure::general(format!("Cannot start the HTTP client: {chain}.")), + &chain, + ) }) } diff --git a/crates/hodeishield-cli/src/failure.rs b/crates/hodeishield-cli/src/failure.rs index 4af27a5..28d4e4b 100644 --- a/crates/hodeishield-cli/src/failure.rs +++ b/crates/hodeishield-cli/src/failure.rs @@ -260,7 +260,10 @@ pub fn from_api(error: hodeishield_api::Error, context: &ApiContext<'_>) -> Fail failure.request_id = request_id; failure } - Error::Transport(e) => Failure::new(Kind::Unavailable, format!("Could not reach the API: {}.", error_chain(&e))), + Error::Transport(e) => { + let chain = error_chain(&e); + ca_hint(Failure::new(Kind::Unavailable, format!("Could not reach the API: {chain}.")), &chain) + } Error::Decode { operation, source } => Failure::general(format!( "The answer to {operation} does not match the API description this CLI was built from: {source}." )) @@ -373,6 +376,16 @@ pub fn error_chain(error: &dyn std::error::Error) -> String { text } +/// Adds the hint for a machine with no CA certificates, which the HTTP client reports only as +/// "No CA certificates were loaded from the system". +pub fn ca_hint(failure: Failure, chain: &str) -> Failure { + if chain.contains("No CA certificates were loaded") { + failure.hint("Install the system CA certificates package (for example `ca-certificates`).") + } else { + failure + } +} + /// Where the credential of a call came from, for the hint on a 401. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CredentialSource { @@ -425,6 +438,14 @@ mod tests { assert_eq!(sentence_body("version 1.2 is old"), "version 1.2 is old"); } + #[test] + fn a_missing_ca_bundle_gets_a_hint_and_other_errors_do_not() { + let chain = "builder error: unexpected error: No CA certificates were loaded from the system"; + let hinted = ca_hint(Failure::general("x"), chain); + assert!(hinted.hint.as_deref().is_some_and(|h| h.contains("ca-certificates"))); + assert!(ca_hint(Failure::general("x"), "dns error").hint.is_none()); + } + #[test] fn message_text_never_marks_a_closed_pipe() { assert!(!Failure::general("Broken pipe").closed_pipe); From afb5a826419c9635749d9eb8e622c3168ec5fcb1 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:20:25 +0200 Subject: [PATCH 07/23] Document how to uninstall the archive and install.sh installs Closes #197 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 19ebea6..2765ec6 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,22 @@ sudo dnf install ./hodeishield--1.x86_64.rpm # Fedora, RHEL (arm64: Upgrading is the same command with the newer package; `sudo apt remove hodeishield` or `sudo dnf remove hodeishield` uninstalls it. +### Uninstall an archive or install.sh install + +Nothing else is installed on the machine, so remove the files you copied. `install.sh` puts the +binary in `/usr/local/bin` or `~/.local/bin` (or the `--dir` you gave) and the man pages in +`share/man/man1` next to it: + +```sh +hodeishield logout # revokes the sign-in and removes the stored token +rm "$(command -v hodeishield)" # the binary +rm -f /usr/local/share/man/man1/hodeishield*.1 # man pages (~/.local/share/man/man1 for ~/.local/bin) +rm -f "$(hodeishield config path)" # optional: the settings +``` + +Run `hodeishield logout` first; with the binary gone it can no longer do it. Remove the completion +files too if you copied them. + ### Windows installer (MSI) From 0.3.0, each release also publishes From 0748cc20cf27d081f116ac6d35e7f5095a83baa7 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:20:26 +0200 Subject: [PATCH 08/23] Point servers and CI from the Quick start to the API key Closes #198 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2765ec6..338b956 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ hodeishield login # sign in through the browser and pick a tenant hodeishield vendors list # your third-party inventory ``` +On a server, in a container or in CI there is usually no keychain and `login` stops with an error: use a +[tenant API key](#with-a-tenant-api-key-scripts-ci-servers) instead. + Verify the download before you run it: see [Install](#install) and [Verify a download](#verify-a-download). Read your third-party inventory, supply-chain alerts, risk register, compliance posture, evidence and From 3ba2c6443da2e811ba6c06c938d0224ae1de5a10 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:20:36 +0200 Subject: [PATCH 09/23] Make the README and config set help match the login option, config keys and revocation screen Closes #199 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 9 +++++++-- crates/hodeishield-cli/src/cli.rs | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 338b956..01325e1 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,7 @@ leaves those fields null and says why on standard error. ```sh hodeishield login # opens the browser; the app redirects back to 127.0.0.1 hodeishield login --device # no browser here: approve a short code from another device +hodeishield login --no-browser # print the sign-in URL instead of opening a browser hodeishield whoami hodeishield logout # revokes the token at the app and removes it from the keychain ``` @@ -381,7 +382,7 @@ the system keychain is not available in this session; set `HODEISHIELD_API_KEY` automated sessions. With the key set, the keychain is not read at all. To cut the CLI's access from elsewhere, revoke it in [the app](https://app.hodeishield.com) under -**Account → Application access**. Signing out of the web app does not cut it. +**Account → Applications**. Signing out of the web app does not cut it. ### Servers, containers and remote sessions @@ -400,7 +401,7 @@ a code. You have two options, in this order of preference: - **How long.** At most 90 days from the sign-in, however often the session is refreshed, and at most 30 days after the CLI last refreshed it. It ends earlier with `hodeishield logout`, when you change or reset your password, when an administrator revokes it, when you are removed from - the tenant, or when you revoke the access in the app under **Account › Applications**. + the tenant, or when you revoke the access in the app under **Account → Applications**. - **Reuse is detected.** Each refresh replaces the refresh token. If someone refreshes with a copy of your file, the next refresh by your own CLI is seen as a reuse of an old token and ends the session for both. An unexpected "sign in again" is a reason to revoke the access in @@ -702,6 +703,10 @@ hodeishield --profile default vendors list # or pick one per command | `HODEISHIELD_TOKEN_STORE` | Where sign-in tokens are kept: `keychain` (the default) or `plaintext-file`, an [opt-in](#servers-containers-and-remote-sessions); takes precedence over `token_store` in the configuration file | | `HODEISHIELD_NO_UPDATE_CHECK` | Any non-empty value turns off the [new-version notice](#new-version-notice) | +`config set` and `config unset` take `api_url`, `app_url`, `oauth_client_id` and `oauth_scopes` +(per profile), and `update_check` (`true` or `false`) and `token_store` (`keychain` or +`plaintext-file`), which are global. + The configuration file holds URLs and sign-in settings only, never a credential. `update_check` and `token_store` are global settings, not per profile. An older CLI refuses a configuration file that contains `token_store`; use the environment variable there. URLs must use HTTPS; diff --git a/crates/hodeishield-cli/src/cli.rs b/crates/hodeishield-cli/src/cli.rs index 3731181..ec228d4 100644 --- a/crates/hodeishield-cli/src/cli.rs +++ b/crates/hodeishield-cli/src/cli.rs @@ -567,12 +567,12 @@ pub enum ConfigCommand { Path, /// Show the settings in effect for the profile. Show, - /// Set a key of the profile: api_url, app_url, oauth_client_id, oauth_scopes. + /// Set a key: api_url, app_url, oauth_client_id, oauth_scopes (per profile), update_check, token_store (global). Set { /// The key to set. #[arg(value_parser = PossibleValuesParser::new(crate::config::KEYS))] key: String, - /// The value. For oauth_scopes, a space-separated list. + /// The value. For oauth_scopes, a space-separated list; update_check takes true or false; token_store takes keychain or plaintext-file. value: String, }, /// Remove a key from the profile, going back to the default. From 625a0ab684564e093adcd828b9ac9f9b41800097 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:33:46 +0200 Subject: [PATCH 10/23] Add the Windows steps to check a download and to get Scoop and cosign Closes #195 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 01325e1..3940b2b 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ scoop bucket add hodeishield https://github.com/Hodeitek/scoop-hodeishield scoop install hodeishield # Windows, per user ``` -`scoop bucket add` needs Git: on a fresh Scoop, run `scoop install git` first. Scoop's own installer +Scoop itself must be installed first (see [scoop.sh](https://scoop.sh)); on a clean Windows machine +`scoop` is not a known command. `scoop bucket add` needs Git: on a fresh Scoop, run `scoop install git` first. Scoop's own installer refuses an elevated (administrator) shell unless it is run with `-RunAsAdmin`; installing per user, from a normal shell, is the recommended way and needs no administrator rights. @@ -265,6 +266,21 @@ cosign verify-blob hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz \ --certificate-github-workflow-trigger push ``` +On Windows, PowerShell has no `sha256sum`: compare the hash of the zip or MSI with its line in +`SHA256SUMS`. `cosign` for Windows is a `cosign-windows-amd64.exe` download on its +[release page](https://github.com/sigstore/cosign/releases) (`winget install sigstore.cosign` may +fail with a missing-source error), and the same `verify-blob` command works with Windows paths: + +```powershell +(Get-FileHash .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower() +Select-String hodeishield-0.5.0-x86_64-pc-windows-msvc.zip SHA256SUMS # the two hashes must match +cosign verify-blob .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip ` + --bundle .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip.sigstore.json ` + --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.0 ` + --certificate-oidc-issuer https://token.actions.githubusercontent.com ` + --certificate-github-workflow-trigger push +``` + A valid signature proves the file was built by this repository's release workflow from exactly that tag, with no long-lived key involved; put your version in the identity rather than matching any tag. The full steps, including the provenance check with `slsa-verifier`, are in [docs/verifying-releases.md](docs/verifying-releases.md). From aa36aec3e9b414021b2813a26809dccad4e21f52 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:33:46 +0200 Subject: [PATCH 11/23] Document what is left after an uninstall on Windows and how to remove the zip Closes #197 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 3940b2b..91b83df 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,17 @@ rm -f "$(hodeishield config path)" # optional: the settings Run `hodeishield logout` first; with the binary gone it can no longer do it. Remove the completion files too if you copied them. +On Windows, `hodeishield logout` first removes the sign-in from Credential Manager, then: + +- **Zip:** delete the folder you unpacked it into, and remove it from your `PATH` if you added it + (the zip never changes `PATH`). +- **MSI:** uninstall from **Settings → Apps** or with `msiexec /x`. This removes the program folder, + the `PATH` entry and the Apps entry. +- **Scoop:** `scoop uninstall hodeishield`, which removes the program and its shim. + +None of these remove the per-user settings in `%APPDATA%\Hodeitek\HodeiShield\config`, one folder +in each user profile that ran `config set`; delete it if you want them gone. + ### Windows installer (MSI) From 0.3.0, each release also publishes From 65ca1cc897d6df0784160a1eb39c683ca467c0c7 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:33:46 +0200 Subject: [PATCH 12/23] Say that login and logout exit 1 when the keychain is out of reach Closes #200 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 91b83df..675d28c 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,8 @@ the file store below is an explicit choice, never a fallback. The keychain can also be out of reach in a session that has one on the machine, such as an SSH or other remote logon on Windows. With no API key set, commands then stop with exit code 3 and say that the system keychain is not available in this session; set `HODEISHIELD_API_KEY` in remote or -automated sessions. With the key set, the keychain is not read at all. +automated sessions. `login` and `logout` exit 1 in that case, not 3. With the key set, the keychain +is not read at all. To cut the CLI's access from elsewhere, revoke it in [the app](https://app.hodeishield.com) under **Account → Applications**. Signing out of the web app does not cut it. From c0423d882098e1494f13cbded9714c51701c7806 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:44:04 +0200 Subject: [PATCH 13/23] Format the CA hint and its test Refs #196 Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- crates/hodeishield-cli/src/failure.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/hodeishield-cli/src/failure.rs b/crates/hodeishield-cli/src/failure.rs index 28d4e4b..d05db28 100644 --- a/crates/hodeishield-cli/src/failure.rs +++ b/crates/hodeishield-cli/src/failure.rs @@ -440,9 +440,15 @@ mod tests { #[test] fn a_missing_ca_bundle_gets_a_hint_and_other_errors_do_not() { - let chain = "builder error: unexpected error: No CA certificates were loaded from the system"; + let chain = + "builder error: unexpected error: No CA certificates were loaded from the system"; let hinted = ca_hint(Failure::general("x"), chain); - assert!(hinted.hint.as_deref().is_some_and(|h| h.contains("ca-certificates"))); + assert!( + hinted + .hint + .as_deref() + .is_some_and(|h| h.contains("ca-certificates")) + ); assert!(ca_hint(Failure::general("x"), "dns error").hint.is_none()); } From e24c93add4b16eb742551d617c33c3846928c50b Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:55:24 +0200 Subject: [PATCH 14/23] Keep the values of environment variables out of --help Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- crates/hodeishield-cli/src/cli.rs | 2 ++ crates/hodeishield-cli/tests/cli.rs | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/crates/hodeishield-cli/src/cli.rs b/crates/hodeishield-cli/src/cli.rs index ec228d4..0505f85 100644 --- a/crates/hodeishield-cli/src/cli.rs +++ b/crates/hodeishield-cli/src/cli.rs @@ -43,6 +43,7 @@ pub struct Cli { global = true, help_heading = "Global options", env = "HODEISHIELD_PROFILE", + hide_env_values = true, value_name = "NAME" )] pub profile: Option, @@ -53,6 +54,7 @@ pub struct Cli { global = true, help_heading = "Global options", env = "HODEISHIELD_API_URL", + hide_env_values = true, value_name = "URL" )] pub api_url: Option, diff --git a/crates/hodeishield-cli/tests/cli.rs b/crates/hodeishield-cli/tests/cli.rs index 4fb7f15..15bc171 100644 --- a/crates/hodeishield-cli/tests/cli.rs +++ b/crates/hodeishield-cli/tests/cli.rs @@ -108,6 +108,27 @@ fn assert_no_key(output: &std::process::Output) { ); } +/// `--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() { + let env = Env::new(); + let output = env + .cmd() + .env("HODEISHIELD_API_URL", "https://api-from-env.example.test") + .env("HODEISHIELD_PROFILE", "profile-from-env") + .env("HODEISHIELD_API_KEY", KEY) + .arg("--help") + .output() + .expect("run"); + let text = String::from_utf8_lossy(&output.stdout); + assert!(text.contains("[env: HODEISHIELD_API_URL]"), "{text}"); + assert!( + !text.contains("api-from-env") && !text.contains("profile-from-env"), + "{text}" + ); + assert_no_key(&output); +} + #[test] fn vendors_list_prints_a_table_and_the_next_page() { let env = Env::new(); From 35edc3b4ec1b3e424fd42ed64655840eefab9442 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:58:27 +0200 Subject: [PATCH 15/23] Remove the settings before the binary on uninstall, and name the MSI check and the Applications screen everywhere Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 9 ++++++--- docs/user-information.md | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 675d28c..39ea8ad 100644 --- a/README.md +++ b/README.md @@ -130,13 +130,15 @@ binary in `/usr/local/bin` or `~/.local/bin` (or the `--dir` you gave) and the m ```sh hodeishield logout # revokes the sign-in and removes the stored token +rm -f "$(hodeishield config path)" # optional: the settings rm "$(command -v hodeishield)" # the binary rm -f /usr/local/share/man/man1/hodeishield*.1 # man pages (~/.local/share/man/man1 for ~/.local/bin) -rm -f "$(hodeishield config path)" # optional: the settings ``` -Run `hodeishield logout` first; with the binary gone it can no longer do it. Remove the completion -files too if you copied them. +Run `hodeishield logout` and `config path` first; with the binary gone neither can run. If you +installed completions from `hodeishield completions`, remove the file where you saved it, usually +`~/.local/share/bash-completion/completions/hodeishield` (bash), `_hodeishield` in a directory of +your `fpath` (zsh) or `~/.config/fish/completions/hodeishield.fish` (fish). On Windows, `hodeishield logout` first removes the sign-in from Credential Manager, then: @@ -285,6 +287,7 @@ fail with a missing-source error), and the same `verify-blob` command works with ```powershell (Get-FileHash .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower() Select-String hodeishield-0.5.0-x86_64-pc-windows-msvc.zip SHA256SUMS # the two hashes must match +# the same two lines with .msi for the MSI cosign verify-blob .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip ` --bundle .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip.sigstore.json ` --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.0 ` diff --git a/docs/user-information.md b/docs/user-information.md index 3f91895..51bcb56 100644 --- a/docs/user-information.md +++ b/docs/user-information.md @@ -89,7 +89,7 @@ logged, HTTPS only, no redirects followed, signed releases) is in has no option to skip certificate verification: install your organisation's CA in that store ([Networking](../README.md#networking-proxies-and-tls-inspection)). - **Cut access you no longer need**: `hodeishield logout`, or revoke the CLI's access in the app - under **Account → Application access**; revoke an API key in the app as soon as it is not needed + under **Account → Applications**; revoke an API key in the app as soon as it is not needed or may have been exposed. Known limitations: @@ -141,7 +141,7 @@ To stop using `hodeishield` on a machine and leave nothing behind: 1. **Sign out of every profile you signed in to**: `hodeishield logout`, and `hodeishield logout --profile ` for each other profile. This revokes the token at the app and removes it from the system keychain. If the CLI warns that the app did not confirm the - revocation, revoke the CLI's access in the app under **Account → Application access**. + revocation, revoke the CLI's access in the app under **Account → Applications**. 2. **Revoke the tenant API keys** the machine used, in the app, and remove them from the environment and secret stores that held them. 3. **Delete the configuration file and the update-check record.** `hodeishield config path` prints @@ -169,7 +169,7 @@ To stop using `hodeishield` on a machine and leave nothing behind: | `cargo install` | `cargo uninstall hodeishield-cli` | If the CLI is already gone and a sign-in was left in the keychain, revoke the access in the app -(**Account → Application access**), then delete the entry, one per profile, whose service is +(**Account → Applications**), then delete the entry, one per profile, whose service is `hodeishield-cli` and whose account is the profile name: on macOS with Keychain Access or `security delete-generic-password -s hodeishield-cli -a `, on Windows in Credential Manager (*Windows Credentials*, named `.hodeishield-cli`), and on Linux with From 45347fdb30ca270f1bb027078680ce127e88aca4 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:22:54 +0200 Subject: [PATCH 16/23] Name the Applications screen the same way in login help and the session notice Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- crates/hodeishield-cli/src/cli.rs | 2 +- crates/hodeishield-cli/src/commands/session.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/hodeishield-cli/src/cli.rs b/crates/hodeishield-cli/src/cli.rs index 0505f85..555dcb1 100644 --- a/crates/hodeishield-cli/src/cli.rs +++ b/crates/hodeishield-cli/src/cli.rs @@ -149,7 +149,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 { diff --git a/crates/hodeishield-cli/src/commands/session.rs b/crates/hodeishield-cli/src/commands/session.rs index d868ac1..f9591ea 100644 --- a/crates/hodeishield-cli/src/commands/session.rs +++ b/crates/hodeishield-cli/src/commands/session.rs @@ -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) ), } @@ -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}" ); } From cd5f7939f1e83534b4af228f806bbf9eed40f58d Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:05:21 +0200 Subject: [PATCH 17/23] Keep --api-url and --profile out of the usage line when only the environment sets them Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- CHANGELOG.md | 7 ++++ crates/hodeishield-cli/src/cli.rs | 10 ++--- crates/hodeishield-cli/src/commands/mod.rs | 9 ++++- crates/hodeishield-cli/tests/cli.rs | 44 ++++++++++++++++++++++ 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a75f3..5cd955e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/hodeishield-cli/src/cli.rs b/crates/hodeishield-cli/src/cli.rs index 555dcb1..0e80849 100644 --- a/crates/hodeishield-cli/src/cli.rs +++ b/crates/hodeishield-cli/src/cli.rs @@ -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, - /// 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, diff --git a/crates/hodeishield-cli/src/commands/mod.rs b/crates/hodeishield-cli/src/commands/mod.rs index a585398..528b091 100644 --- a/crates/hodeishield-cli/src/commands/mod.rs +++ b/crates/hodeishield-cli/src/commands/mod.rs @@ -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 { + 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, diff --git a/crates/hodeishield-cli/tests/cli.rs b/crates/hodeishield-cli/tests/cli.rs index 15bc171..fbd1223 100644 --- a/crates/hodeishield-cli/tests/cli.rs +++ b/crates/hodeishield-cli/tests/cli.rs @@ -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() { From 80d2a8f042f25b58b9213aa7ac0308a5209acf64 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:13:30 +0200 Subject: [PATCH 18/23] Accept the zbus unmaintained notices until secret-service moves to zbus 6 RUSTSEC-2026-0370, 0371 and 0372 say zvariant, zbus_names and zvariant_derive were merged into zbus 6.0.0. They are reached only through the Linux Secret Service keychain backend, and the latest secret-service and zbus-secret-service-keyring-store still use zbus 5. Accepted in deny.toml (which cargo audit also reads) and in osv-scanner.toml, with a review date of 2027-01-31. Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- deny.toml | 3 +++ osv-scanner.toml | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 osv-scanner.toml diff --git a/deny.toml b/deny.toml index 6132258..c7b30e4 100644 --- a/deny.toml +++ b/deny.toml @@ -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 diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 0000000..b25f1f9 --- /dev/null +++ b/osv-scanner.toml @@ -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." From 4e6b6beff62a30fc720e8675a34832e586d88838 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:32:19 +0200 Subject: [PATCH 19/23] Prepare 0.5.1: version and changelog Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- CHANGELOG.md | 35 ++++++++++++++++++++++++++++++++++- Cargo.lock | 6 +++--- Cargo.toml | 2 +- README.md | 28 ++++++++++++++-------------- docs/release-signing.md | 2 +- docs/verifying-releases.md | 24 ++++++++++++------------ 6 files changed, 65 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd955e..5655bb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ commands, `--json` output and exit codes. Each release's notes start with its se ## [Unreleased] +## [0.5.1] - 2026-10-18 + +### Changed + +- When the system has no CA certificates, the error for a request that cannot start or reach the API + now suggests installing the system CA certificates package (for example `ca-certificates`). The + HTTP client reports that case only as "No CA certificates were loaded from the system". + ([#196](https://github.com/Hodeitek/hodeishield-cli/issues/196)) +- `--help` no longer prints the values of `HODEISHIELD_API_URL` and `HODEISHIELD_PROFILE` when they + are set in the environment. +- The `login` help, the session notice and the README name the screen where an app's access is + revoked "Account → Applications", as the web app does, and the README and `config set --help` + match the code on `login --no-browser` and the configuration keys. + ([#199](https://github.com/Hodeitek/hodeishield-cli/issues/199)) +- CI now runs the proxy and TLS inspection test on macOS and Windows as well as Linux. + ### Fixed - Usage errors no longer list `--api-url` or `--profile` in the `Usage:` line when only @@ -13,6 +29,22 @@ commands, `--json` output and exit codes. Each release's notes start with its se `--help` still names them. ([#201](https://github.com/Hodeitek/hodeishield-cli/issues/201)) +### Documentation + +- The README lists the Linux prerequisites to install and to verify a download, gives a checksum + command that BusyBox also runs, and has Windows steps to check a download and to get Scoop and + cosign. + ([#195](https://github.com/Hodeitek/hodeishield-cli/issues/195)) +- The README says how to uninstall the archive and `install.sh` installs, in which order (remove the + settings before the binary), what an uninstall leaves behind on Windows, how to remove the zip + and how to check the MSI. + ([#197](https://github.com/Hodeitek/hodeishield-cli/issues/197)) +- The Quick start points servers and CI from the interactive sign-in to the tenant API key. + ([#198](https://github.com/Hodeitek/hodeishield-cli/issues/198)) +- The README says that `login` and `logout` exit with code 1 when the system keychain cannot be + reached. + ([#200](https://github.com/Hodeitek/hodeishield-cli/issues/200)) + ## [0.5.0] - 2026-10-15 ### Added @@ -388,7 +420,8 @@ commands, `--json` output and exit codes. Each release's notes start with its se - Profiles (`config`), shell completions, and release archives for Linux, macOS and Windows signed with Sigstore and with SLSA provenance. -[Unreleased]: https://github.com/Hodeitek/hodeishield-cli/compare/v0.5.0...HEAD +[Unreleased]: https://github.com/Hodeitek/hodeishield-cli/compare/v0.5.1...HEAD +[0.5.1]: https://github.com/Hodeitek/hodeishield-cli/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/Hodeitek/hodeishield-cli/compare/v0.4.4...v0.5.0 [0.4.4]: https://github.com/Hodeitek/hodeishield-cli/compare/v0.4.3...v0.4.4 [0.4.3]: https://github.com/Hodeitek/hodeishield-cli/compare/v0.4.2...v0.4.3 diff --git a/Cargo.lock b/Cargo.lock index bf518b6..41a39ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -970,7 +970,7 @@ dependencies = [ [[package]] name = "hodeishield-api" -version = "0.5.0" +version = "0.5.1" dependencies = [ "mockito", "reqwest", @@ -983,7 +983,7 @@ dependencies = [ [[package]] name = "hodeishield-cli" -version = "0.5.0" +version = "0.5.1" dependencies = [ "apple-native-keyring-store", "assert_cmd", @@ -3067,7 +3067,7 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xtask" -version = "0.5.0" +version = "0.5.1" dependencies = [ "clap", "clap_mangen", diff --git a/Cargo.toml b/Cargo.toml index 8ae10c8..b011531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ default-members = ["crates/hodeishield-api", "crates/hodeishield-cli"] [workspace.package] authors = ["Hodeitek S.L. "] -version = "0.5.0" +version = "0.5.1" edition = "2024" rust-version = "1.98" license = "Apache-2.0" diff --git a/README.md b/README.md index 39ea8ad..31bebba 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ section), and put `hodeishield` (`hodeishield.exe` on Windows) somewhere on your | Windows x86_64 | `hodeishield--x86_64-pc-windows-msvc.zip` | | Windows arm64 | `hodeishield--aarch64-pc-windows-msvc.zip` (from 0.4.0) | -`` has no leading `v`: release `v0.5.0` ships `hodeishield-0.5.0-…`. The Linux binaries are +`` has no leading `v`: release `v0.5.1` ships `hodeishield-0.5.1-…`. The Linux binaries are statically linked and run on any distribution. Homebrew and Scoop packages (next section) and a [container image](#container-image) are also available. @@ -74,7 +74,7 @@ neither is part of a distribution's default install. From source, with the Rust toolchain installed (pin the release tag you want): ```sh -cargo install --locked --git https://github.com/Hodeitek/hodeishield-cli --tag v0.5.0 hodeishield-cli +cargo install --locked --git https://github.com/Hodeitek/hodeishield-cli --tag v0.5.1 hodeishield-cli ``` ### Homebrew and Scoop @@ -220,7 +220,7 @@ exact version, or better its digest. The image carries no documentation: the 0.4.2, as `USER-INFORMATION.md` in its archives. ```sh -v=0.5.0 # the version you want +v=0.5.1 # the version you want docker pull ghcr.io/hodeitek/hodeishield-cli:$v docker run --rm -e HODEISHIELD_API_KEY ghcr.io/hodeitek/hodeishield-cli:$v vendors list ``` @@ -234,7 +234,7 @@ The image is signed by digest with cosign (keyless) and carries the release's Cy signed attestation. Verify both before you rely on the image: ```sh -v=0.5.0 +v=0.5.1 image=ghcr.io/hodeitek/hodeishield-cli:$v cosign verify $image \ --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/container-image.yml@refs/tags/v$v \ @@ -270,11 +270,11 @@ Each release has a `SHA256SUMS` file, a Sigstore bundle (`*.sigstore.json`) for `SHA256SUMS`, and SLSA provenance (`hodeishield.intoto.jsonl`). In short: ```sh -grep hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz SHA256SUMS | sha256sum -c - +grep hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz SHA256SUMS | sha256sum -c - -cosign verify-blob hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz \ - --bundle hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz.sigstore.json \ - --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.0 \ +cosign verify-blob hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz \ + --bundle hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz.sigstore.json \ + --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.1 \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-github-workflow-trigger push ``` @@ -285,12 +285,12 @@ On Windows, PowerShell has no `sha256sum`: compare the hash of the zip or MSI wi fail with a missing-source error), and the same `verify-blob` command works with Windows paths: ```powershell -(Get-FileHash .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower() -Select-String hodeishield-0.5.0-x86_64-pc-windows-msvc.zip SHA256SUMS # the two hashes must match +(Get-FileHash .\hodeishield-0.5.1-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower() +Select-String hodeishield-0.5.1-x86_64-pc-windows-msvc.zip SHA256SUMS # the two hashes must match # the same two lines with .msi for the MSI -cosign verify-blob .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip ` - --bundle .\hodeishield-0.5.0-x86_64-pc-windows-msvc.zip.sigstore.json ` - --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.0 ` +cosign verify-blob .\hodeishield-0.5.1-x86_64-pc-windows-msvc.zip ` + --bundle .\hodeishield-0.5.1-x86_64-pc-windows-msvc.zip.sigstore.json ` + --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.1 ` --certificate-oidc-issuer https://token.actions.githubusercontent.com ` --certificate-github-workflow-trigger push ``` @@ -325,7 +325,7 @@ on the binary, so the block still applies. A download with `curl` does not set i place: ```sh -curl -LO https://github.com/Hodeitek/hodeishield-cli/releases/download/v0.5.0/hodeishield-0.5.0-universal-apple-darwin.tar.gz +curl -LO https://github.com/Hodeitek/hodeishield-cli/releases/download/v0.5.1/hodeishield-0.5.1-universal-apple-darwin.tar.gz ``` **Windows, before 0.2.0.** Unblock the archive before unpacking it, so the executable does not diff --git a/docs/release-signing.md b/docs/release-signing.md index e47d5a8..8b2e917 100644 --- a/docs/release-signing.md +++ b/docs/release-signing.md @@ -80,7 +80,7 @@ an RFC 3161 timestamp from `http://timestamp.acs.microsoft.com`. - **Switch.** The repository variable `WINDOWS_SIGNING_ENABLED` turns the signing on, and only for a run started by a pushed `v*` tag. -The signatures of every signed release so far (0.2.0 to 0.5.0) carry: +The signatures of every signed release so far (0.2.0 to 0.5.1) carry: | Field | Value | |---|---| diff --git a/docs/verifying-releases.md b/docs/verifying-releases.md index 51c8516..38344ed 100644 --- a/docs/verifying-releases.md +++ b/docs/verifying-releases.md @@ -30,14 +30,14 @@ From 0.4.2, every archive, `.deb`, `.rpm` and MSI also contains `USER-INFORMATIO too. The `.deb` and `.rpm` install it in `/usr/share/doc/hodeishield/`, the MSI next to `hodeishield.exe`. -The steps below use `v0.5.0` and `hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz` +The steps below use `v0.5.1` and `hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz` as examples — substitute the actual version and archive you downloaded. ## 1. Download the files From the release page, download: -- the archive you want (e.g. `hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz`) +- the archive you want (e.g. `hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz`) - `SHA256SUMS` - the matching `.sigstore.json` - `SHA256SUMS.sigstore.json` @@ -46,7 +46,7 @@ From the release page, download: ## 2. Check the checksum ```sh -grep hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz SHA256SUMS | sha256sum -c - +grep hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz SHA256SUMS | sha256sum -c - ``` Checking only the line of your archive works with GNU `sha256sum` and with the BusyBox one on @@ -65,7 +65,7 @@ in the format cosign v3 reads by default. Older cosign versions cannot read them ```sh cosign verify-blob \ --bundle SHA256SUMS.sigstore.json \ - --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.0 \ + --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.1 \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-github-workflow-trigger push \ SHA256SUMS @@ -75,11 +75,11 @@ Repeat for the archive itself, using its own `.sigstore.json`: ```sh cosign verify-blob \ - --bundle hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz.sigstore.json \ - --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.0 \ + --bundle hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz.sigstore.json \ + --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.1 \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-github-workflow-trigger push \ - hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz + hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz ``` A successful verification confirms the file was signed by the @@ -92,11 +92,11 @@ The SBOM is verified the same way, with its own bundle: ```sh cosign verify-blob \ - --bundle hodeishield-0.5.0-x86_64-unknown-linux-musl.cdx.json.sigstore.json \ - --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.0 \ + --bundle hodeishield-0.5.1-x86_64-unknown-linux-musl.cdx.json.sigstore.json \ + --certificate-identity https://github.com/Hodeitek/hodeishield-cli/.github/workflows/release.yml@refs/tags/v0.5.1 \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-github-workflow-trigger push \ - hodeishield-0.5.0-x86_64-unknown-linux-musl.cdx.json + hodeishield-0.5.1-x86_64-unknown-linux-musl.cdx.json ``` ## 4. Verify SLSA build provenance @@ -105,10 +105,10 @@ Requires [`slsa-verifier`](https://github.com/slsa-framework/slsa-verifier). ```sh slsa-verifier verify-artifact \ - hodeishield-0.5.0-x86_64-unknown-linux-musl.tar.gz \ + hodeishield-0.5.1-x86_64-unknown-linux-musl.tar.gz \ --provenance-path hodeishield.intoto.jsonl \ --source-uri github.com/Hodeitek/hodeishield-cli \ - --source-tag v0.5.0 + --source-tag v0.5.1 ``` `hodeishield.intoto.jsonl` is attached to the release and covers every From cff019f369a1979b7021a29490439e9f3b15f42e Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:57:12 +0200 Subject: [PATCH 20/23] Point the Install section to the unblock steps, and give the Windows steps for every version Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 39ea8ad..a249e67 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,9 @@ endpoint agents from a terminal or a script. Download the archive for your platform from the [latest release](https://github.com/Hodeitek/hodeishield-cli/releases/latest), **verify it** (next -section), and put `hodeishield` (`hodeishield.exe` on Windows) somewhere on your `PATH`. +section), and put `hodeishield` (`hodeishield.exe` on Windows) somewhere on your `PATH`. If macOS or +Windows blocks the downloaded binary, see +[If macOS or Windows blocks the binary](#if-macos-or-windows-blocks-the-binary). | Platform | Archive | |---|---| @@ -328,20 +330,20 @@ place: curl -LO https://github.com/Hodeitek/hodeishield-cli/releases/download/v0.5.0/hodeishield-0.5.0-universal-apple-darwin.tar.gz ``` -**Windows, before 0.2.0.** Unblock the archive before unpacking it, so the executable does not -inherit the download mark: +**Windows.** SmartScreen can warn about a signed file that is still new to it, and versions before +0.2.0 are not signed at all. To avoid the warning, unblock the archive before unpacking it, so the +executable does not inherit the download mark: ```powershell -Unblock-File .\hodeishield-0.1.1-x86_64-pc-windows-msvc.zip -Expand-Archive .\hodeishield-0.1.1-x86_64-pc-windows-msvc.zip +Unblock-File .\hodeishield--x86_64-pc-windows-msvc.zip +Expand-Archive .\hodeishield--x86_64-pc-windows-msvc.zip ``` If you already unpacked it, run `Unblock-File` on `hodeishield.exe` instead. Windows PowerShell 5.1's `Expand-Archive` does not copy the download mark to the unpacked files, so unblocking matters when you unpack with File Explorer's "Extract All" or another tool that carries the mark over. -SmartScreen can also warn about a signed file that is still new to it; in either case, if it shows -"Windows protected your PC", choose **More info → Run anyway** («Más información → Ejecutar de todos -modos» on a Spanish system). +If SmartScreen still shows "Windows protected your PC", choose **More info → Run anyway** («Más +información → Ejecutar de todos modos» on a Spanish system). ## Authenticate From 3df4e7b4e5255cc4fb0f4f3db2c9bf3ea9ece0cc Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:13:26 +0200 Subject: [PATCH 21/23] Add the unblock-steps change to the 0.5.1 changelog Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5655bb4..730dabb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,9 @@ commands, `--json` output and exit codes. Each release's notes start with its se - The README says that `login` and `logout` exit with code 1 when the system keychain cannot be reached. ([#200](https://github.com/Hodeitek/hodeishield-cli/issues/200)) +- The Install section links to the steps for a binary that macOS or Windows blocks, and the Windows + steps (`Unblock-File`, SmartScreen's "More info → Run anyway") apply to every version. + ([#16](https://github.com/Hodeitek/hodeishield-cli/issues/16)) ## [0.5.0] - 2026-10-15 From 277b9cb89c9a9f1fdd7e9245c2844b5b18521929 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:34:50 +0200 Subject: [PATCH 22/23] Say that two profiles of the same user, tenant and flow share one sign-in Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a249e67..4b57376 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,10 @@ hodeishield login --profile client-b A profile that has not been defined is refused (`Profile ... is not defined`). +Two profiles signed in as the same user, to the same tenant, with the same flow (browser or +`--device`) share one sign-in at the app: logging out of either one signs out both. Profiles on +different tenants, or one signed in with the browser and the other with `--device`, are independent. + `login` first checks that the selected token store is available and stops before opening the browser or showing a code if it is not. On a machine without a keychain, see [Servers, containers and remote sessions](#servers-containers-and-remote-sessions). From 44fba33fcb44d0910835786985a70c53c004cd27 Mon Sep 17 00:00:00 2001 From: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:39:28 +0200 Subject: [PATCH 23/23] Add the shared sign-in note to the 0.5.1 changelog Signed-off-by: d0ubl3g <48954024+d0ubl3g@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 730dabb..450f204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ commands, `--json` output and exit codes. Each release's notes start with its se - The Install section links to the steps for a binary that macOS or Windows blocks, and the Windows steps (`Unblock-File`, SmartScreen's "More info → Run anyway") apply to every version. ([#16](https://github.com/Hodeitek/hodeishield-cli/issues/16)) +- The README says that two profiles signed in as the same user, to the same tenant, with the same + flow share one sign-in, so logging out of either one signs out both. ## [0.5.0] - 2026-10-15