From ad62364cf99ad9524530987459a08daa916ead5c Mon Sep 17 00:00:00 2001 From: jarebear6expepjozn6rakjq5iczi3irqwphcvbswgkahd6b6twnxxid <1045813+jarebear6expepjozn6rakjq5iczi3irqwphcvb@users.noreply.github.com> Date: Wed, 20 Aug 2025 05:08:56 -0700 Subject: [PATCH 1/2] add authentication support for private repositories (#32) * add authentication support for private repositories support for ~/.netrc support for environment variable support for stdin * document authentication methods --- README.md | 32 ++++++++++++++++++++++++++++++++ vet | 30 +++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0cffb63..4838984 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,38 @@ Skip all interactive prompts and execute immediately. Use with caution. Display the help message. +\-n, \--netrc + +Use ~/.netrc credentials for authentication. + +\-t, \--token-stdin + +Set authentication token from standard input. + +#### Authentication + +`vet` can: + +- Read from a `~/.netrc` file. +``` +# Example ~/.netrc file to authenticate with GitHub private repositories +machine raw.githubusercontent.com +login api +password +``` + +- Detect and read a `$VET_TOKEN` from an environment variable into an `Authorization` token. +```bash +# Example setting a VET_TOKEN from an environment variable for private GitHub repository access +export VET_TOKEN= +``` + +- Read an `Authorization` token from standard input. +```bash +# Example setting a VET_TOKEN from standared input for private GitHub repository access +echo | ./vet --token-stdin https://example.com/private.sh +``` + ## Project Philosophy & Technical Decisions ### Bash 4+ is a Required Dependency diff --git a/vet b/vet index 3fbeee0..9a9215e 100755 --- a/vet +++ b/vet @@ -39,6 +39,9 @@ OPTIONS: -f, --force Skip all interactive prompts and execute immediately. Use with extreme caution in trusted environments. -h, --help Display this help message. + -n, --netrc Use ~/.netrc credentials for authentication. + -t, --token-stdin + Set authentication token from standard input. EOF } @@ -59,6 +62,20 @@ check_dependencies() { fi } +build_authentication() { + # Tell curl to check ~/.netrc for authentication credentials. + # wget checks for existence of ~/.netrc by default + NETRC="${NETRC:-}" + [[ ! "$NETRC" ]] && NETRC_ARG=() + if [[ "${DOWNLOAD_CMD[0]}" == "curl" ]]; then + NETRC_ARG=(-n) + fi + + # Build Authorization header, if token exists. + VET_TOKEN="${VET_TOKEN:-}" + [[ ! "$VET_TOKEN" ]] && AUTH_HEADER=() || AUTH_HEADER=(--header "Authorization: bearer $VET_TOKEN") +} + trap cleanup EXIT INT TERM FORCE_MODE=0 @@ -73,6 +90,16 @@ while [[ $# -gt 0 ]]; do usage exit 0 ;; + -n|--netrc) + NETRC=1 + shift + ;; + -t|--token-stdin) + if [[ ! -t 0 ]]; then + IFS= read -r VET_TOKEN + fi + shift + ;; --) shift break @@ -100,13 +127,14 @@ shift SCRIPT_ARGS=("$@") check_dependencies +build_authentication mkdir -p "$CACHE_DIR" TMPFILE=$(mktemp) || { log_error "Failed to create temporary file."; exit 1; } TMPFILE_DIFF=$(mktemp) || { log_error "Failed to create temporary diff file."; exit 1; } log_info "Downloading script from: $URL" -if ! "${DOWNLOAD_CMD[@]}" "$TMPFILE" "$URL"; then +if ! "${DOWNLOAD_CMD[@]}" "$TMPFILE" "$URL" "${AUTH_HEADER[@]}" "${NETRC_ARG[@]}"; then log_error "Download failed. Check URL and network connection." exit 1 fi From 611b5473b08ff6379f2c77a40725183405e074e0 Mon Sep 17 00:00:00 2001 From: Artem Lykhvar Date: Wed, 20 Aug 2025 16:26:43 +0300 Subject: [PATCH 2/2] feat(auth): Add authentication for private repositories Introduce an authentication system to fetch scripts from private sources like GitHub and GitLab. - Add `--auth-header` flag and `VET_AUTH_HEADER` env var to allow custom authentication headers (e.g., "PRIVATE-TOKEN: {}"). - Support providing tokens via `--token-stdin` (piping) and the `VET_TOKEN` environment variable, defaulting to "Authorization: Bearer {}". - Add support for `-n, --netrc`. --- README.md | 62 +++++++++++++++----- tests/vet.bats | 152 +++++++++++++++++++++++++++++++++++++++++++++++++ vet | 117 +++++++++++++++++++++++++++---------- 3 files changed, 289 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 4838984..9326c35 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,9 @@ vet https://example.com/setup.sh --user myuser --version latest # Non-interactive mode for trusted scripts in automated environments (e.g., CI/CD) vet --force https://my-trusted-internal-script.sh + +# Example setting a VET_AUTH_TOKEN from standared input for private repository access +echo | vet --token-stdin https://example.com/private.sh ``` #### Options @@ -141,28 +144,61 @@ Use ~/.netrc credentials for authentication. Set authentication token from standard input. -#### Authentication +\--auth-header \ -`vet` can: +Use a custom header template for token authentication. The template must contain {} as a placeholder for the token. This is the most flexible way to authenticate. Example: `--auth-header "PRIVATE-TOKEN: {}"` -- Read from a `~/.netrc` file. +### Authentication + +`vet` supports two main authentication methods: token-based (recommended for its flexibility) and `.netrc`. Token-based authentication will always be used if a token is provided. + +#### Token-Based Authentication +To support various services like GitHub, GitLab, and private artifact repositories, `vet` allows you to define a custom authentication header. This is a two-part system: you provide the header template and the token itself. + +#### 1. Providing the Header Template +You can specify the format of the authentication header using: +* The `--auth-header "TEMPLATE"` flag (takes highest precedence). +* The `VET_AUTH_HEADER` environment variable. + +The template must contain `{}` which will be replaced with your token. If you don't provide a template, `vet` will use a default of `"Authorization: Bearer {}"`. + +#### 2. Providing the Token + +You can provide the secret token using: +* The `--token-stdin` flag, by piping the token to `vet`. +* The `VET_AUTH_TOKEN` environment variable. +--- +#### Examples + +#### Example 1: Accessing a private GitLab repository +GitLab uses the `PRIVATE-TOKEN` header. We can provide our token from an environment variable and specify the header format with the flag. +```bash +# Your GitLab Personal Access Token +export VET_AUTH_TOKEN="glpat-123xyz..." + +# Use the --auth-header flag to specify GitLab's format +vet --auth-header "PRIVATE-TOKEN: {}" https://gitlab.com/api/v4/projects/.../raw +``` +#### Example 2: Accessing a private GitHub repository using the GitHub CLI +GitHub requires an `Authorization: token ...` header for classic PATs. We can securely pipe the token directly from the gh CLI. +```bash +# Pipe the token from `gh auth token` and provide the correct header template +gh auth token | vet --token-stdin --auth-header "Authorization: token {}" https://raw.githubusercontent.com/owner/private-repo/main/script.sh ``` +--- +#### `.netrc` File +As a fallback, `vet` can read credentials from a `~/.netrc` file if the `-n`, `--netrc` flag is used. This method will be ignored if `VET_AUTH_TOKEN` is set or `--token-stdin` is used. + +- Read from a `~/.netrc` file. +```bash # Example ~/.netrc file to authenticate with GitHub private repositories machine raw.githubusercontent.com login api password ``` - -- Detect and read a `$VET_TOKEN` from an environment variable into an `Authorization` token. -```bash -# Example setting a VET_TOKEN from an environment variable for private GitHub repository access -export VET_TOKEN= -``` - -- Read an `Authorization` token from standard input. +Then run with the flag: ```bash -# Example setting a VET_TOKEN from standared input for private GitHub repository access -echo | ./vet --token-stdin https://example.com/private.sh +vet -n https://raw.githubusercontent.com/owner/private-repo/main/script.sh ``` ## Project Philosophy & Technical Decisions diff --git a/tests/vet.bats b/tests/vet.bats index ce45085..c7e7f25 100644 --- a/tests/vet.bats +++ b/tests/vet.bats @@ -57,6 +57,31 @@ EOF create_mock "curl" "$script_content" } +create_downloader_mock() { + local downloader_name="$1" + local fixture_path="$2" + local args_log_file="$3" + + local script_content=$(cat < "${args_log_file}" + +output_file="" +while [ "\$#" -gt 0 ]; do + case "\$1" in + -o|--output) output_file="\$2"; shift ;; # curl + -O) output_file="\$2"; shift ;; # wget -qO + esac + shift +done +if [ -n "\$output_file" ]; then + cat '${fixture_path}' > "\$output_file" +fi +exit 0 +EOF +) + create_mock "$downloader_name" "$script_content" +} + @test "vet --help shows usage" { run "$VET_SCRIPT" --help assert_output --partial "USAGE:" @@ -124,3 +149,130 @@ EOF assert_failure assert_output --partial "Downloaded file is empty" } + +@test "vet handles --token-stdin when piped" { + local args_log="${TEST_DIR}/downloader_args.log" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + run bash -c "(echo 'secret-token'; echo 'y') | ${VET_SCRIPT} --token-stdin http://example.com/script.sh" + assert_success + + run grep -q -- "-H Authorization: Bearer secret-token" "$args_log" + assert_success "Expected to find bearer token header in curl args" +} + +@test "vet fails when --token-stdin receives no input" { + create_curl_mock "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" + + run ${VET_SCRIPT} --token-stdin http://example.com/script.sh < /dev/null + + assert_failure + assert_output --partial "Failed to read a non-empty token from stdin" +} + +@test "vet uses VET_AUTH_TOKEN environment variable for authentication" { + local args_log="${TEST_DIR}/downloader_args.log" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + export VET_AUTH_TOKEN="env-secret-token" + run bash -c "echo 'y' | ${VET_SCRIPT} http://example.com/script.sh" + + assert_success + + run grep -q -- "-H Authorization: Bearer env-secret-token" "$args_log" + assert_success "Expected to find VET_AUTH_TOKEN bearer header in curl args" +} + +@test "vet handles --netrc flag and shows compatibility warning" { + local args_log="${TEST_DIR}/downloader_args.log" + echo "machine example.com login user password pass" > "${HOME}/.netrc" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + run bash -c "echo 'y' | ${VET_SCRIPT} --netrc http://example.com/script.sh" + + assert_success + assert_output --partial "The --netrc flag is often incompatible" + run grep -q -- "-n" "$args_log" + assert_success "Expected to find -n flag in curl args" +} + +@test "vet prioritizes --token-stdin over VET_AUTH_TOKEN" { + local args_log="${TEST_DIR}/downloader_args.log" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + export VET_AUTH_TOKEN="env-token-should-be-ignored" + run bash -c "(echo 'stdin-token-is-king'; echo 'y') | ${VET_SCRIPT} --token-stdin http://example.com/script.sh" + + assert_success + + run grep -q -- "-H Authorization: Bearer stdin-token-is-king" "$args_log" + assert_success "Expected stdin token to be used" + + run grep -q "env-token-should-be-ignored" "$args_log" + assert_failure "Expected environment token to be ignored" +} + +@test "vet prioritizes VET_AUTH_TOKEN over --netrc" { + local args_log="${TEST_DIR}/downloader_args.log" + echo "machine example.com login user password pass" > "${HOME}/.netrc" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + export VET_AUTH_TOKEN="the-real-token" + run bash -c "echo 'y' | ${VET_SCRIPT} --netrc http://example.com/script.sh" + + assert_success + + run grep -q -- "-H Authorization: Bearer the-real-token" "$args_log" + assert_success "Expected VET_AUTH_TOKEN to be used" + + run grep -q -- "-n" "$args_log" + assert_failure "Expected -n flag to be ignored when VET_AUTH_TOKEN is present" +} + +@test "vet uses --auth-header with VET_AUTH_TOKEN for custom authentication" { + local args_log="${TEST_DIR}/downloader_args.log" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + export VET_AUTH_TOKEN="glpat-abcdef123456" + + run "$VET_SCRIPT" --force --auth-header "PRIVATE-TOKEN: {}" http://example.com/gitlab-script.sh + + assert_success + + run cat "$args_log" + assert_output --partial "-H PRIVATE-TOKEN: glpat-abcdef123456" + + refute_output --partial "Bearer" +} + +@test "vet handles piped token and confirmation in non-interactive mode" { + local args_log="${TEST_DIR}/downloader_args.log" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + run bash -c "(echo 'secret-token'; echo 'y') | ${VET_SCRIPT} --token-stdin http://example.com/script.sh" + + assert_success + assert_output --partial "Simple script executed successfully" + + run cat "$args_log" + assert_output --partial "-H Authorization: Bearer secret-token" +} + + +@test "vet switches to keyboard for prompts after piping token" { + local args_log="${TEST_DIR}/downloader_args.log" + create_downloader_mock "curl" "${BATS_TEST_DIRNAME}/fixtures/simple_success.sh" "$args_log" + + local keyboard_pipe="${TEST_DIR}/keyboard_pipe" + mkfifo "$keyboard_pipe" + + ( sleep 0.5 && echo 'y' > "$keyboard_pipe" ) & + + run script -q -c "echo 'secret-token' | ${VET_SCRIPT} --token-stdin http://example.com/script.sh" /dev/null < "$keyboard_pipe" + + assert_success "Script should succeed after user confirms with 'y'" + assert_output --partial "Simple script executed successfully" + + run cat "$args_log" + assert_output --partial "-H Authorization: Bearer secret-token" +} diff --git a/vet b/vet index 9a9215e..7275846 100755 --- a/vet +++ b/vet @@ -36,12 +36,23 @@ USAGE: vet [OPTIONS] [SCRIPT_ARGUMENTS...] OPTIONS: - -f, --force Skip all interactive prompts and execute immediately. - Use with extreme caution in trusted environments. - -h, --help Display this help message. - -n, --netrc Use ~/.netrc credentials for authentication. - -t, --token-stdin - Set authentication token from standard input. + -f, --force Skip all interactive prompts and execute immediately. + Use with extreme caution in trusted environments. + -h, --help Display this help message. + +AUTHENTICATION OPTIONS: + --auth-header Use a custom header template for token authentication. + The template must contain '{}' as a placeholder for the token. + Example: --auth-header "PRIVATE-TOKEN: {}" + Overrides VET_AUTH_HEADER environment variable. + -t, --token-stdin Read the authentication token from standard input. + Takes precedence over VET_AUTH_TOKEN environment variable. + -n, --netrc Use ~/.netrc credentials for authentication. + (Used only if token-based auth is not active). + +ENVIRONMENT VARIABLES: + VET_AUTH_TOKEN Authentication token. Used if --token-stdin is not set. + VET_AUTH_HEADER Authentication header template. Used if --auth-header is not set. EOF } @@ -53,32 +64,57 @@ cleanup() { check_dependencies() { if command -v curl &>/dev/null; then - DOWNLOAD_CMD=(curl -fsSL -o) + DOWNLOAD_CMD=(curl -fsSL) + OUTPUT_ARGS=(-o) elif command -v wget &>/dev/null; then - DOWNLOAD_CMD=(wget -qO) + DOWNLOAD_CMD=(wget -q) + OUTPUT_ARGS=(-O) else log_error "This tool requires either 'curl' or 'wget'." exit 1 fi } -build_authentication() { - # Tell curl to check ~/.netrc for authentication credentials. - # wget checks for existence of ~/.netrc by default - NETRC="${NETRC:-}" - [[ ! "$NETRC" ]] && NETRC_ARG=() - if [[ "${DOWNLOAD_CMD[0]}" == "curl" ]]; then - NETRC_ARG=(-n) +configure_authentication() { + if [ -n "${VET_AUTH_TOKEN:-}" ]; then + log_info "Using token for authentication." + + local template="${AUTH_HEADER_TPL:-${VET_AUTH_HEADER:-}}" + if [[ -z "$template" ]]; then + template="Authorization: Bearer {}" + log_info "Using default auth header template: ${template}" + fi + + if [[ "$template" != *"{}"* ]]; then + log_error "Authentication header template must contain the placeholder '{}'." + exit 1 + fi + local final_header="${template/'{}'/"$VET_AUTH_TOKEN"}" + + if [[ "${DOWNLOAD_CMD[0]}" == curl ]]; then + DOWNLOAD_CMD+=(-H "$final_header") + else + DOWNLOAD_CMD+=("--header=$final_header") + fi + return fi + if [ "$NETRC" -eq 1 ]; then + log_warn "The --netrc flag is often incompatible with modern, token-based APIs (e.g., GitHub, GitLab)." + log_info "For best results, use token authentication via --token-stdin or the VET_AUTH_TOKEN variable." - # Build Authorization header, if token exists. - VET_TOKEN="${VET_TOKEN:-}" - [[ ! "$VET_TOKEN" ]] && AUTH_HEADER=() || AUTH_HEADER=(--header "Authorization: bearer $VET_TOKEN") + if [[ "${DOWNLOAD_CMD[0]}" == curl ]]; then + DOWNLOAD_CMD+=(-n) + fi + return + fi } trap cleanup EXIT INT TERM FORCE_MODE=0 +NETRC=0 +TOKEN_STDIN=0 +AUTH_HEADER_TPL="" while [[ $# -gt 0 ]]; do case "$1" in @@ -90,14 +126,21 @@ while [[ $# -gt 0 ]]; do usage exit 0 ;; + --auth-header) + if [[ -z "${2-}" ]]; then + log_error "--auth-header requires a template argument." + usage + exit 1 + fi + AUTH_HEADER_TPL="$2" + shift 2 + ;; -n|--netrc) NETRC=1 shift ;; -t|--token-stdin) - if [[ ! -t 0 ]]; then - IFS= read -r VET_TOKEN - fi + TOKEN_STDIN=1 shift ;; --) @@ -115,6 +158,13 @@ while [[ $# -gt 0 ]]; do esac done +if [ "$TOKEN_STDIN" -eq 1 ]; then + if ! IFS= read -r VET_AUTH_TOKEN || [ -z "$VET_AUTH_TOKEN" ]; then + log_error "Failed to read a non-empty token from stdin." + exit 1 + fi +fi + URL="${1-}" if [ -z "$URL" ]; then @@ -127,15 +177,15 @@ shift SCRIPT_ARGS=("$@") check_dependencies -build_authentication +configure_authentication mkdir -p "$CACHE_DIR" TMPFILE=$(mktemp) || { log_error "Failed to create temporary file."; exit 1; } TMPFILE_DIFF=$(mktemp) || { log_error "Failed to create temporary diff file."; exit 1; } log_info "Downloading script from: $URL" -if ! "${DOWNLOAD_CMD[@]}" "$TMPFILE" "$URL" "${AUTH_HEADER[@]}" "${NETRC_ARG[@]}"; then - log_error "Download failed. Check URL and network connection." +if ! "${DOWNLOAD_CMD[@]}" "${OUTPUT_ARGS[@]}" "$TMPFILE" "$URL"; then + log_error "Download failed. Check URL, credentials, and network connection." exit 1 fi @@ -153,6 +203,15 @@ elif command -v batcat &>/dev/null; then BAT_CMD="batcat" fi +read_reply() { + if [[ "$TOKEN_STDIN" -eq 1 ]] && [[ -t 0 ]]; then + read -n 1 -r REPLY < /dev/tty + else + read -n 1 -r REPLY + fi + echo +} + # Generate a filesystem-safe cache key from the URL. CACHE_FILE_ID=$(echo -n "$URL" | sha256sum | awk '{print $1}') CACHE_FILE_PATH="${CACHE_DIR}/${CACHE_FILE_ID}.sh" @@ -163,7 +222,7 @@ if [ -f "$CACHE_FILE_PATH" ]; then elif [ "$FORCE_MODE" -eq 0 ]; then log_warn "Script has CHANGED since the last run." printf "[?] %sShow diff?%s [y/N] " "$C_BOLD" "$C_RESET" - read -n 1 -r REPLY; echo + read_reply if [[ "$REPLY" =~ ^[Yy]$ ]]; then less -U "$TMPFILE_DIFF" fi @@ -172,7 +231,7 @@ fi execute_script() { EXEC_EXIT_CODE=0 - bash "$TMPFILE" "${SCRIPT_ARGS[@]}" || EXEC_EXIT_CODE=$? + env -u VET_AUTH_TOKEN bash "$TMPFILE" "${SCRIPT_ARGS[@]}" || EXEC_EXIT_CODE=$? if [ $EXEC_EXIT_CODE -eq 0 ]; then cp "$TMPFILE" "$CACHE_FILE_PATH" log_info "Script cached for future comparison." @@ -193,7 +252,7 @@ if command -v shellcheck &>/dev/null; then if ! shellcheck "$TMPFILE"; then log_warn "ShellCheck found potential issues." printf "[?] %sContinue with review despite issues?%s [y/N] " "$C_BOLD" "$C_RESET" - read -n 1 -r REPLY; echo + read_reply if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then log_info "Execution aborted due to ShellCheck warnings." exit 1 @@ -207,7 +266,7 @@ fi if [ -t 0 ]; then # log_info "Displaying script for review. Press 'q' to exit viewer." printf "[?] %sDisplaying script for review?%s [y/N] " "$C_BOLD" "$C_RESET" - read -n 1 -r REPLY; echo + read_reply if [[ "$REPLY" =~ ^[Yy]$ ]]; then if [ -n "$BAT_CMD" ]; then "$BAT_CMD" -l sh --paging=always "$TMPFILE" @@ -216,7 +275,7 @@ if [ -t 0 ]; then fi fi printf "[?] %sExecute this script?%s [y/N] " "$C_BOLD" "$C_RESET" - read -n 1 -r REPLY; echo + read_reply else # Non-interactive session (e.g., CI/CD): Just print the script content. log_info "Running in non-interactive mode. Displaying script content:"