diff --git a/.github/scripts/proxy-tls-test.sh b/.github/scripts/proxy-tls-test.sh index 145a0dd..a4fbf2d 100755 --- a/.github/scripts/proxy-tls-test.sh +++ b/.github/scripts/proxy-tls-test.sh @@ -2,33 +2,47 @@ # 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) +if [ "$OS" = windows ]; then SCRIPTS=$(cygpath -m "$SCRIPTS"); fi # handed to python.exe HOST=api.test API_PORT=8443 @@ -37,11 +51,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 @@ -56,19 +71,47 @@ 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" 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 +191,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 +219,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/CHANGELOG.md b/CHANGELOG.md index 55a75f3..450f204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,50 @@ 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 + `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)) + +### 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)) +- 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 ### Added @@ -381,7 +425,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 8d4e54b..d4c8a8b 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 @@ -46,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 | |---|---| @@ -56,14 +61,22 @@ 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. +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 -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 @@ -77,7 +90,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. @@ -110,6 +124,35 @@ 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 -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) +``` + +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: + +- **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 @@ -179,7 +222,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 ``` @@ -193,7 +236,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 \ @@ -229,15 +272,31 @@ 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.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 ``` +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.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.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 +``` + 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). @@ -268,23 +327,23 @@ 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 -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 @@ -313,6 +372,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 ``` @@ -329,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). @@ -351,10 +415,11 @@ 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 → 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 @@ -373,7 +438,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 @@ -675,6 +740,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; @@ -751,8 +820,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 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/cli.rs b/crates/hodeishield-cli/src/cli.rs index 3731181..0e80849 100644 --- a/crates/hodeishield-cli/src/cli.rs +++ b/crates/hodeishield-cli/src/cli.rs @@ -37,22 +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", 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", value_name = "URL" )] pub api_url: Option, @@ -147,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 { @@ -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. 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/src/commands/session.rs b/crates/hodeishield-cli/src/commands/session.rs index 1a133f8..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) ), } @@ -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); @@ -1311,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}" ); } diff --git a/crates/hodeishield-cli/src/failure.rs b/crates/hodeishield-cli/src/failure.rs index 4af27a5..d05db28 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,20 @@ 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); diff --git a/crates/hodeishield-cli/tests/cli.rs b/crates/hodeishield-cli/tests/cli.rs index 4fb7f15..fbd1223 100644 --- a/crates/hodeishield-cli/tests/cli.rs +++ b/crates/hodeishield-cli/tests/cli.rs @@ -108,6 +108,71 @@ 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() { + 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(); 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/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/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 diff --git a/docs/verifying-releases.md b/docs/verifying-releases.md index 0bb181b..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,9 +46,12 @@ From the release page, download: ## 2. Check the checksum ```sh -sha256sum -c SHA256SUMS --ignore-missing +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 +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. @@ -62,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 @@ -72,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 @@ -89,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 @@ -102,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 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."