diff --git a/.github/workflows/shell-quality.yml b/.github/workflows/shell-quality.yml index bd30eb2..9ae27a0 100644 --- a/.github/workflows/shell-quality.yml +++ b/.github/workflows/shell-quality.yml @@ -44,6 +44,7 @@ jobs: scripts/health-check.sh \ scripts/tool-health-check.sh \ tests/test-installer-functions.sh \ + tests/test-shell-functions.sh \ tests/docker-ubuntu-smoke.sh \ macos/defaults.sh \ macos/dock.sh \ @@ -70,6 +71,7 @@ jobs: scripts/health-check.sh \ scripts/tool-health-check.sh \ tests/test-installer-functions.sh \ + tests/test-shell-functions.sh \ tests/docker-ubuntu-smoke.sh \ macos/defaults.sh \ macos/dock.sh \ @@ -170,6 +172,7 @@ jobs: fi export SHELL="$shell_bin" + timeout 30 "$shell_bin" tests/test-shell-functions.sh ./install.sh --minimal mkdir -p "$HOME/.local/bin" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c032ff8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security policy + +## Reporting a vulnerability + +Use [GitHub private vulnerability reporting](https://github.com/alsd4git/dotfiles/security/advisories/new). +Do not disclose exploit details or sensitive data in public issues or pull requests. + +Include the affected commit, operating system, shell, expected impact and minimal +reproduction steps. Use synthetic examples and redact screenshots and logs. +If your own credential was exposed, revoke or rotate it. Report credentials you +do not control promptly and privately; do not use or attempt to revoke them. + +## Versions and maintenance + +Check the current default branch, `main`, before reporting an issue. Fixes, when +provided, target that branch; older revisions do not receive separate backports. +Reports are handled on a best-effort basis without guaranteed response or +remediation times. + +## Safe reproduction + +Use disposable home directories or containers and the repository's test harnesses. +Do not run installers, uninstallers, package bootstraps or macOS defaults changes +against a real user account merely to demonstrate an issue. Never include SSH +private keys, tokens, credential stores, private hostnames or unredacted shell +history in a report. Issues in third-party software should also be reported to +the relevant upstream maintainer. diff --git a/docs/linux.md b/docs/linux.md index afa4660..a8a02eb 100644 --- a/docs/linux.md +++ b/docs/linux.md @@ -391,4 +391,6 @@ Run the full disposable Ubuntu 24.04 Bash/Zsh lifecycle test when Docker is avai ./tests/docker-ubuntu-smoke.sh ``` -The container test mounts the repository read-only and verifies install, interactive shell loading, health checks, and uninstall in isolated home directories. +The container test mounts the repository read-only and verifies install, interactive shell loading, health checks, and uninstall in isolated home directories. It also runs `tests/test-shell-functions.sh` in Bash and Zsh, checking Watchtower arguments with a simulated Docker command and `netuse` exit codes without updating real containers. + +Watchtower uses `${DOCKER_CONFIG:-$HOME/.docker}/config.json` when readable, mounted read-only. Configurations referencing `credsStore` or `credHelpers` produce a warning: mounting the JSON alone does not make host credential helpers or keychains available inside Watchtower. Use a dedicated Docker configuration suitable for the container if private-registry authentication requires it; never commit registry credentials. diff --git a/general/.functions b/general/.functions index dc363ac..45b0226 100644 --- a/general/.functions +++ b/general/.functions @@ -41,11 +41,15 @@ function watchtower_run_once() { return 1 fi - local docker_config="$HOME/.docker/config.json" + local docker_config="${DOCKER_CONFIG:-$HOME/.docker}/config.json" local -a docker_args docker_args=(run --rm -v /var/run/docker.sock:/var/run/docker.sock) if [ -r "$docker_config" ]; then + # Warn conservatively on helper keys, including multiline JSON. + if grep -Eq '"(credsStore|credHelpers)"' "$docker_config"; then + printf '%s\n' 'Warning: Docker config references credential helpers. Mounting the config alone does not provide the host keychain or helper binaries to Watchtower; private-registry authentication may fail. Use a dedicated DOCKER_CONFIG with credentials usable inside the container.' >&2 + fi docker_args+=(-v "$docker_config:/config.json:ro") fi @@ -147,20 +151,21 @@ function netuse() { echo -e "${dim}Interface: $iface | Refresh: ${interval}s${nc}" echo "" - local tmpdir fifo cmd_pid + local tmpdir fifo cmd_pid done_marker cmd_status tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/netuse.XXXXXX") || return 1 fifo="$tmpdir/output" + done_marker="__NETUSE_DONE_${tmpdir}__" if ! mkfifo "$fifo"; then rmdir "$tmpdir" 2>/dev/null || true return 1 fi ( - "$@" 2>&1 | while IFS= read -r line; do - printf "\r\033[K${green}[CMD]${nc} %s\n" "$line" - done - echo "__DONE__" - ) >"$fifo" & + # Keep exit/exec inside the command from bypassing the completion marker. + if ("$@"); then cmd_status=0; else cmd_status=$?; fi + printf '\n%s\n' "$done_marker" + exit "$cmd_status" + ) >"$fifo" 2>&1 & cmd_pid=$! exec 3<"$fifo" @@ -172,10 +177,10 @@ function netuse() { while $running; do if read -t "$interval" -r line <&3; then - if [[ "$line" == "__DONE__" ]]; then + if [[ "$line" == "$done_marker" ]]; then running=false else - echo "$line" + printf "\r\033[K${green}[CMD]${nc} %s\n" "$line" fi fi @@ -200,7 +205,7 @@ function netuse() { done exec 3<&- - wait "$cmd_pid" 2>/dev/null || true + if wait "$cmd_pid"; then cmd_status=0; else cmd_status=$?; fi rm -f -- "$fifo" rmdir "$tmpdir" 2>/dev/null || true @@ -215,6 +220,7 @@ function netuse() { if ((total_time > 0)); then echo -e " Media: $(_hu_speed $((total_rx / total_time)))" fi + return "$cmd_status" } # ===== Notes and Future Enhancements ===== diff --git a/tests/docker-ubuntu-smoke.sh b/tests/docker-ubuntu-smoke.sh index 16502cc..efe6bef 100755 --- a/tests/docker-ubuntu-smoke.sh +++ b/tests/docker-ubuntu-smoke.sh @@ -14,6 +14,7 @@ docker run --rm \ apt-get install -y --no-install-recommends ca-certificates git zsh >/dev/null for shell_bin in /bin/bash /usr/bin/zsh; do + timeout 30 "$shell_bin" /workspace/tests/test-shell-functions.sh test_home=$(mktemp -d /tmp/dotfiles-home.XXXXXX) export HOME="$test_home" export SHELL="$shell_bin" diff --git a/tests/test-shell-functions.sh b/tests/test-shell-functions.sh new file mode 100644 index 0000000..230cecd --- /dev/null +++ b/tests/test-shell-functions.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Run with Bash or Zsh. Docker calls are always simulated. + +set -eu +repo_root=$(cd "$(dirname "$0")/.." && pwd) +test_root=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-shell-tests.XXXXXX") +cleanup() { + rm -f "$test_root/config dir/config.json" "$test_root/calls" "$test_root/warnings" "$test_root/netuse" + rmdir "$test_root/config dir" "$test_root" +} +trap cleanup EXIT +# shellcheck source=general/.functions +source "$repo_root/general/.functions" + +docker() { + printf '<%s>\n' "$@" >"$test_root/calls" + return "${docker_result:-0}" +} + +export DOCKER_CONFIG="$test_root/config dir" +mkdir -p "$DOCKER_CONFIG" +watchtower_run_once 'container with spaces' 2>"$test_root/warnings" +if grep -Fq '/config.json:ro' "$test_root/calls"; then exit 1; fi +grep -Fxq '' "$test_root/calls" +test ! -s "$test_root/warnings" + +printf '%s\n' '{"auths":{}}' >"$DOCKER_CONFIG/config.json" +watchtower_run_once 2>"$test_root/warnings" +grep -Fxq "<$DOCKER_CONFIG/config.json:/config.json:ro>" "$test_root/calls" +test ! -s "$test_root/warnings" + +for config_fixture in '{"credsStore":"desktop"}' '{"credHelpers":{"registry.example":"helper"}}' '{ +"credsStore" +: "desktop" +}'; do + printf '%s\n' "$config_fixture" >"$DOCKER_CONFIG/config.json" + watchtower_run_once 2>"$test_root/warnings" + grep -Fq 'credential helpers' "$test_root/warnings" + if grep -Fq 'registry.example' "$test_root/warnings"; then exit 1; fi +done +docker_result=17 +actual_result=0 +watchtower_run_once 2>"$test_root/warnings" || actual_result=$? +test "$actual_result" -eq 17 + +if [ "$(uname -s)" = Linux ]; then + netuse --iface lo true >"$test_root/netuse" + fail_command() { + printf '%s\n' '__DONE__' + printf 'stderr without newline' >&2 + return 23 + } + actual_result=0 + netuse --iface lo fail_command >"$test_root/netuse" || actual_result=$? + test "$actual_result" -eq 23 + grep -Fq '__DONE__' "$test_root/netuse" + grep -Fq 'stderr without newline' "$test_root/netuse" + grep -Fq 'Riepilogo' "$test_root/netuse" + + for expected_result in 0 7; do + actual_result=0 + netuse --iface lo exit "$expected_result" >"$test_root/netuse" || actual_result=$? + test "$actual_result" -eq "$expected_result" + grep -Fq 'Riepilogo' "$test_root/netuse" + done + + actual_result=0 + netuse --iface lo exec sh -c 'printf "exec output\n"; exit 29' >"$test_root/netuse" || actual_result=$? + test "$actual_result" -eq 29 + grep -Fq 'exec output' "$test_root/netuse" + grep -Fq 'Riepilogo' "$test_root/netuse" +else + printf '%s\n' 'netuse runtime tests skipped: Linux /proc required' +fi +printf '%s\n' 'Shell function tests passed'