From 59be8d1b3f29131524bc7af2c41d64eaa216b220 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 13 Aug 2026 10:00:53 +0200 Subject: [PATCH] fix: harden license installer sources --- licenses/Makefile | 1 + licenses/README.md | 31 +++- licenses/check/action.yml | 4 +- licenses/checksums.sha256 | 5 + licenses/install | 269 ++++++++++++++++++++++++--- licenses/setup/action.yml | 4 +- licenses/test.sh | 379 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 659 insertions(+), 34 deletions(-) create mode 100644 licenses/checksums.sha256 create mode 100755 licenses/test.sh diff --git a/licenses/Makefile b/licenses/Makefile index 39d6ca4..498ece9 100644 --- a/licenses/Makefile +++ b/licenses/Makefile @@ -28,6 +28,7 @@ test: .bin/shellcheck .bin/shfmt # runs all automated tests echo running tests ... find . -type f | grep -v node_modules | grep -v '^\.\/\.bin\/' | grep -v '\.json$$' | grep -v '\.yml$$' | xargs grep -l '^\#!\/' | xargs .bin/shellcheck .bin/shfmt --diff . + ./test.sh .bin/shellcheck: Makefile echo installing Shellcheck ... diff --git a/licenses/README.md b/licenses/README.md index 8db11e7..d02302d 100644 --- a/licenses/README.md +++ b/licenses/README.md @@ -6,10 +6,39 @@ print the affected libraries and licenses and exit with error code 1. ### Installation +```sh +ORY_CI_INSTALL_REF=<40-character-ory-ci-commit-sha> +curl --fail --location \ + "https://raw.githubusercontent.com/ory/ci/${ORY_CI_INSTALL_REF}/licenses/install" | + sh ``` -curl https://raw.githubusercontent.com/ory/ci/master/licenses/install | sh + +Calling the installer without arguments remains supported and installs the +license assets from its built-in, pinned revision. + +Standalone consumers should pin both the installer itself and its asset source +to the same full commit SHA: + +```sh +ORY_CI_REF=<40-character-ory-ci-commit-sha> +curl --fail --location \ + "https://raw.githubusercontent.com/ory/ci/${ORY_CI_REF}/licenses/install" | + sh -s -- --source-ref "${ORY_CI_REF}" ``` +Composite actions and local development can install the files bundled in a +checkout instead. The source directory must contain `checksums.sha256` and the +five license assets next to it: + +```sh +sh licenses/install --source-dir "$(pwd)/licenses" +``` + +`--source-ref` and `--source-dir` are mutually exclusive. In either explicit +mode, every asset is staged, checked against the selected source's SHA-256 +manifest, and only then moved into `.bin`. `--full-install` can be combined with +either source option to install both language templates. + ### Usage To check licenses, run this in your repo: diff --git a/licenses/check/action.yml b/licenses/check/action.yml index 2babf8d..a8b0470 100644 --- a/licenses/check/action.yml +++ b/licenses/check/action.yml @@ -4,8 +4,8 @@ runs: steps: - name: Install license scanner run: | - ORY_CI_ROOT="${{ github.action_path }}/../.." \ - sh "${{ github.action_path }}/../install" + sh "$GITHUB_ACTION_PATH/../install" \ + --source-dir "$GITHUB_ACTION_PATH/.." shell: bash - name: Check licenses run: .bin/licenses diff --git a/licenses/checksums.sha256 b/licenses/checksums.sha256 new file mode 100644 index 0000000..bf6c799 --- /dev/null +++ b/licenses/checksums.sha256 @@ -0,0 +1,5 @@ +05aec4e6acd68e3b2649937d7e02d219c46d15d2f9a88f9bd4c584a934feeef8 license-engine.sh +2ca46be9e72520bfd7203d54bcf1533b963e07fcc8c5e54355a0d7e7a2a0bcca licenses +4e30b5e88b559a449835c0fc7e20f21b91eb580227b3637070cb61c9b322793c list-licenses +4dde5952bf6c8adad4479bff57233ea929c04ea92e2d106c4d573c19dfdde10e license-template-go.tpl +2b4af23297359dee7d6fa0483b95a31bd7c1e474ab88273e77e7072ab16fc6ae license-template-node.json diff --git a/licenses/install b/licenses/install index 8acafc0..7e40417 100755 --- a/licenses/install +++ b/licenses/install @@ -1,61 +1,209 @@ #!/bin/sh set -e -ORY_CI_REF=${ORY_CI_REF:-747531b2acc027b7308a27722efb2dc6d43377db} +DEFAULT_SOURCE_REF=747531b2acc027b7308a27722efb2dc6d43377db -install_asset() { - source_path=$1 - destination=$2 - mode=$3 - if [ -n "${ORY_CI_ROOT:-}" ]; then - install -m "$mode" "${ORY_CI_ROOT}/licenses/${source_path}" "$destination" - return +FULL_INSTALL=false +SOURCE_DIR= +SOURCE_REF= +SOURCE_MODE=default +STAGING_DIR= +PREPARED_ASSETS= +ROLLBACK_ASSETS= +SHA256_TOOL= +COMMIT_STARTED=false +INSTALL_COMPLETE=false +BIN_DIR_CREATED=false + +die() { + echo "licenses/install: $*" >&2 + exit 1 +} + +usage() { + cat >&2 <<'EOF' +Usage: licenses/install [--full-install] [--source-ref <40-hex SHA> | --source-dir ] +EOF +} + +cleanup() { + set +e + if [ "$COMMIT_STARTED" = true ] && [ "$INSTALL_COMPLETE" = false ] && [ -f "$ROLLBACK_ASSETS" ]; then + while IFS='|' read -r destination backup_path existed; do + rm -f "$destination" + if [ "$existed" = true ]; then + mv -f "$backup_path" "$destination" + fi + done <"$ROLLBACK_ASSETS" + fi + if [ -n "$PREPARED_ASSETS" ] && [ -f "$PREPARED_ASSETS" ]; then + while IFS='|' read -r _source_path _destination _mode prepared_path; do + [ -z "$prepared_path" ] || rm -f "$prepared_path" + done <"$PREPARED_ASSETS" + fi + if [ -n "$STAGING_DIR" ] && [ -d "$STAGING_DIR" ]; then + rm -rf "$STAGING_DIR" + fi + if [ "$BIN_DIR_CREATED" = true ]; then + rmdir .bin 2>/dev/null fi - curl --fail --location --silent --show-error \ - "https://raw.githubusercontent.com/ory/ci/${ORY_CI_REF}/licenses/${source_path}" \ - -o "$destination" - chmod "$mode" "$destination" } -# Default mono-install to false -FULL_INSTALL=false +trap cleanup 0 +trap 'exit 1' 1 2 15 -# Parse command line arguments while [ "$#" -gt 0 ]; do case "$1" in --full-install) FULL_INSTALL=true shift ;; + --source-ref) + [ "$#" -ge 2 ] && [ -n "$2" ] || { + usage + die "--source-ref requires a value" + } + [ -z "$SOURCE_REF" ] || die "--source-ref may only be specified once" + [ -z "$SOURCE_DIR" ] || die "--source-ref and --source-dir are mutually exclusive" + SOURCE_REF=$(printf '%s' "$2" | tr 'A-F' 'a-f') + shift 2 + ;; + --source-dir) + [ "$#" -ge 2 ] && [ -n "$2" ] || { + usage + die "--source-dir requires a value" + } + [ -z "$SOURCE_DIR" ] || die "--source-dir may only be specified once" + [ -z "$SOURCE_REF" ] || die "--source-ref and --source-dir are mutually exclusive" + SOURCE_DIR=$2 + shift 2 + ;; *) - echo "Unknown parameter: $1" - exit 1 + usage + die "unknown parameter: $1" ;; esac done -{ - echo - echo "Installing license checker engine ..." - echo -} 2>/dev/null +if [ -n "$SOURCE_REF" ]; then + [ "${#SOURCE_REF}" -eq 40 ] || die "--source-ref must be a full 40-character commit SHA" + case "$SOURCE_REF" in + *[!0-9a-f]*) die "--source-ref must contain only hexadecimal characters" ;; + esac + SOURCE_MODE=ref +elif [ -n "$SOURCE_DIR" ]; then + [ -d "$SOURCE_DIR" ] || die "source directory does not exist: $SOURCE_DIR" + SOURCE_MODE=dir +else + SOURCE_REF=$DEFAULT_SOURCE_REF +fi + +if command -v sha256sum >/dev/null 2>&1; then + SHA256_TOOL=sha256sum +elif command -v shasum >/dev/null 2>&1; then + SHA256_TOOL=shasum +elif command -v openssl >/dev/null 2>&1; then + SHA256_TOOL=openssl +else + die "a SHA-256 implementation is required (sha256sum, shasum, or openssl)" +fi + +sha256_file() { + case "$SHA256_TOOL" in + sha256sum) + sha256sum "$1" | awk '{print $1}' + ;; + shasum) + shasum -a 256 "$1" | awk '{print $1}' + ;; + openssl) + openssl dgst -sha256 -r "$1" | awk '{print $1}' + ;; + esac +} + +STAGING_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ory-ci-licenses.XXXXXX") || die "unable to create staging directory" +ASSET_LIST="$STAGING_DIR/assets" +PREPARED_ASSETS="$STAGING_DIR/prepared-assets" +ROLLBACK_ASSETS="$STAGING_DIR/rollback-assets" +CHECKSUMS_FILE="$STAGING_DIR/checksums.sha256" +: >"$ASSET_LIST" +: >"$PREPARED_ASSETS" +: >"$ROLLBACK_ASSETS" + +write_default_checksums() { + cat >"$CHECKSUMS_FILE" <<'EOF' +05aec4e6acd68e3b2649937d7e02d219c46d15d2f9a88f9bd4c584a934feeef8 license-engine.sh +2ca46be9e72520bfd7203d54bcf1533b963e07fcc8c5e54355a0d7e7a2a0bcca licenses +4e30b5e88b559a449835c0fc7e20f21b91eb580227b3637070cb61c9b322793c list-licenses +4dde5952bf6c8adad4479bff57233ea929c04ea92e2d106c4d573c19dfdde10e license-template-go.tpl +2b4af23297359dee7d6fa0483b95a31bd7c1e474ab88273e77e7072ab16fc6ae license-template-node.json +EOF +} + +download() { + url=$1 + destination=$2 + curl --fail --location --silent --show-error "$url" -o "$destination" +} + +case "$SOURCE_MODE" in +default) + write_default_checksums + ;; +ref) + download \ + "https://raw.githubusercontent.com/ory/ci/${SOURCE_REF}/licenses/checksums.sha256" \ + "$CHECKSUMS_FILE" + ;; +dir) + [ -f "$SOURCE_DIR/checksums.sha256" ] || die "checksum manifest not found: $SOURCE_DIR/checksums.sha256" + install -m 0600 "$SOURCE_DIR/checksums.sha256" "$CHECKSUMS_FILE" + ;; +esac + +manifest_checksum() { + asset=$1 + found= + while IFS=' ' read -r checksum filename extra; do + [ "$filename" = "$asset" ] || continue + [ -z "$extra" ] || die "malformed checksum entry for $asset" + [ -z "$found" ] || die "duplicate checksum entry for $asset" + found=$checksum + done <"$CHECKSUMS_FILE" + + [ -n "$found" ] || die "checksum missing for $asset" + [ "${#found}" -eq 64 ] || die "malformed checksum for $asset" + case "$found" in + *[!0-9a-f]*) die "malformed checksum for $asset" ;; + esac + printf '%s\n' "$found" +} + +add_asset() { + printf '%s|%s|%s\n' "$1" "$2" "$3" >>"$ASSET_LIST" +} + +add_asset license-engine.sh .bin/license-engine.sh 0755 +add_asset licenses .bin/licenses 0755 +add_asset list-licenses .bin/list-licenses 0755 if [ ! -d .bin ]; then mkdir .bin + BIN_DIR_CREATED=true fi -install_asset license-engine.sh .bin/license-engine.sh 0755 -install_asset licenses .bin/licenses 0755 -install_asset list-licenses .bin/list-licenses 0755 - if [ "$FULL_INSTALL" = true ] || [ -f go.mod ]; then { echo echo "Installing the Go license checker ..." echo } 2>/dev/null - GOBIN="$(pwd)/.bin" go install github.com/google/go-licenses/v2@v2.0.1 - install_asset license-template-go.tpl .bin/license-template-go.tpl 0644 + GO_BIN_DIR="$STAGING_DIR/go-bin" + mkdir "$GO_BIN_DIR" + GOBIN="$GO_BIN_DIR" go install github.com/google/go-licenses/v2@v2.0.1 + [ -f "$GO_BIN_DIR/go-licenses" ] || die "go-licenses installation did not produce a binary" + add_asset license-template-go.tpl .bin/license-template-go.tpl 0644 fi if [ "$FULL_INSTALL" = true ] || [ -f package.json ]; then @@ -64,5 +212,68 @@ if [ "$FULL_INSTALL" = true ] || [ -f package.json ]; then echo "Installing the Node license checker ..." echo } 2>/dev/null - install_asset license-template-node.json .bin/license-template-node.json 0644 + add_asset license-template-node.json .bin/license-template-node.json 0644 fi + +{ + echo + echo "Installing license checker engine ..." + echo +} 2>/dev/null + +# Fetch and verify every requested asset before preparing any destination. +while IFS='|' read -r source_path _destination _mode; do + staged_path="$STAGING_DIR/$source_path" + case "$SOURCE_MODE" in + dir) + [ -f "$SOURCE_DIR/$source_path" ] || die "source asset not found: $SOURCE_DIR/$source_path" + install -m 0600 "$SOURCE_DIR/$source_path" "$staged_path" + ;; + *) + download \ + "https://raw.githubusercontent.com/ory/ci/${SOURCE_REF}/licenses/${source_path}" \ + "$staged_path" + ;; + esac + + expected=$(manifest_checksum "$source_path") + actual=$(sha256_file "$staged_path") || die "unable to checksum $source_path" + [ "$actual" = "$expected" ] || die "checksum mismatch for $source_path" +done <"$ASSET_LIST" + +# Prepare every destination before replacing any existing installed asset. +while IFS='|' read -r source_path destination mode; do + prepared_path=$(mktemp "${destination}.tmp.XXXXXX") || die "unable to prepare $destination" + printf '%s|%s|%s|%s\n' "$source_path" "$destination" "$mode" "$prepared_path" >>"$PREPARED_ASSETS" + install -m "$mode" "$STAGING_DIR/$source_path" "$prepared_path" + + expected=$(manifest_checksum "$source_path") + actual=$(sha256_file "$prepared_path") || die "unable to checksum prepared $source_path" + [ "$actual" = "$expected" ] || die "checksum mismatch while preparing $source_path" +done <"$ASSET_LIST" + +if [ -n "${GO_BIN_DIR:-}" ]; then + prepared_path=$(mktemp ".bin/go-licenses.tmp.XXXXXX") || die "unable to prepare .bin/go-licenses" + printf '%s|%s|%s|%s\n' generated .bin/go-licenses 0755 "$prepared_path" >>"$PREPARED_ASSETS" + install -m 0755 "$GO_BIN_DIR/go-licenses" "$prepared_path" +fi + +# Snapshot existing destinations so a failed final rename can roll back the +# complete install set. +rollback_index=0 +while IFS='|' read -r _source_path destination _mode _prepared_path; do + rollback_index=$((rollback_index + 1)) + backup_path="$STAGING_DIR/backup-$rollback_index" + if [ -e "$destination" ]; then + cp -p "$destination" "$backup_path" + printf '%s|%s|true\n' "$destination" "$backup_path" >>"$ROLLBACK_ASSETS" + else + printf '%s||false\n' "$destination" >>"$ROLLBACK_ASSETS" + fi +done <"$PREPARED_ASSETS" + +COMMIT_STARTED=true +while IFS='|' read -r _source_path destination _mode prepared_path; do + mv -f "$prepared_path" "$destination" +done <"$PREPARED_ASSETS" +INSTALL_COMPLETE=true diff --git a/licenses/setup/action.yml b/licenses/setup/action.yml index 9d9430e..49100e9 100644 --- a/licenses/setup/action.yml +++ b/licenses/setup/action.yml @@ -26,6 +26,6 @@ runs: shell: bash - name: Install license scanner run: | - ORY_CI_ROOT="${{ github.action_path }}/../.." \ - sh "${{ github.action_path }}/../install" + sh "$GITHUB_ACTION_PATH/../install" \ + --source-dir "$GITHUB_ACTION_PATH/.." shell: bash diff --git a/licenses/test.sh b/licenses/test.sh new file mode 100755 index 0000000..d8ddb00 --- /dev/null +++ b/licenses/test.sh @@ -0,0 +1,379 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +LICENSE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +INSTALLER="$LICENSE_DIR/install" +DEFAULT_SOURCE_REF=747531b2acc027b7308a27722efb2dc6d43377db +TEST_SOURCE_REF=0123456789abcdef0123456789abcdef01234567 +TEST_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/ory-ci-license-tests.XXXXXX") + +cleanup() { + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_file_equals() { + expected=$1 + actual=$2 + cmp -s "$expected" "$actual" || fail "$actual does not match $expected" +} + +file_mode() { + file=$1 + if stat -c '%a' "$file" >/dev/null 2>&1; then + stat -c '%a' "$file" + else + stat -f '%Lp' "$file" + fi +} + +assert_mode() { + expected=$1 + file=$2 + actual=$(file_mode "$file") + [ "$actual" = "$expected" ] || fail "$file mode is $actual, expected $expected" +} + +assert_core_install() { + target=$1 + for asset in license-engine.sh licenses list-licenses; do + assert_file_equals "$LICENSE_DIR/$asset" "$target/.bin/$asset" + assert_mode 755 "$target/.bin/$asset" + done +} + +assert_all_assets() { + target=$1 + assert_core_install "$target" + for asset in license-template-go.tpl license-template-node.json; do + assert_file_equals "$LICENSE_DIR/$asset" "$target/.bin/$asset" + assert_mode 644 "$target/.bin/$asset" + done +} + +make_fake_go() { + bin_dir=$1 + mkdir -p "$bin_dir" + cat >"$bin_dir/go" <<'EOF' +#!/bin/sh +set -e +[ "$1" = install ] +mkdir -p "$GOBIN" +: >"$GOBIN/go-licenses" +chmod 0755 "$GOBIN/go-licenses" +EOF + chmod +x "$bin_dir/go" +} + +make_fake_curl() { + bin_dir=$1 + mkdir -p "$bin_dir" + cat >"$bin_dir/curl" <<'EOF' +#!/bin/sh +set -e + +url= +destination= +while [ "$#" -gt 0 ]; do + case "$1" in + https://*) url=$1 ;; + -o) + shift + destination=$1 + ;; + esac + shift +done + +[ -n "$url" ] && [ -n "$destination" ] +case "$url" in +*"/${EXPECTED_REF}/licenses/"*) ;; +*) + echo "unexpected URL: $url" >&2 + exit 91 + ;; +esac + +asset=${url##*/} +printf '%s\n' "$url" >>"$CURL_LOG" +[ "${FAIL_DOWNLOAD_ASSET:-}" != "$asset" ] || exit 92 +cp "$TEST_SOURCE_DIR/$asset" "$destination" +if [ "${CORRUPT_ASSET:-}" = "$asset" ]; then + printf 'corrupt\n' >>"$destination" +fi +EOF + chmod +x "$bin_dir/curl" +} + +seed_existing_core_assets() { + target=$1 + mkdir -p "$target/.bin" + for asset in license-engine.sh licenses list-licenses; do + printf 'existing-%s\n' "$asset" >"$target/.bin/$asset" + done +} + +assert_existing_core_assets() { + target=$1 + for asset in license-engine.sh licenses list-licenses; do + expected="existing-$asset" + actual=$(sed -n '1p' "$target/.bin/$asset") + [ "$actual" = "$expected" ] || fail "$asset changed after a failed install" + done +} + +run_local_source_test() { + target="$TEST_ROOT/local-source" + fake_bin="$TEST_ROOT/local-source-bin" + mkdir -p "$target" + make_fake_go "$fake_bin" + ( + cd "$target" + PATH="$fake_bin:$PATH" "$INSTALLER" --full-install --source-dir "$LICENSE_DIR" >/dev/null + ) + assert_all_assets "$target" +} + +run_source_ref_test() { + target="$TEST_ROOT/source-ref" + fake_bin="$TEST_ROOT/source-ref-bin" + curl_log="$TEST_ROOT/source-ref-curl.log" + mkdir -p "$target" + make_fake_curl "$fake_bin" + : >"$curl_log" + ( + cd "$target" + EXPECTED_REF="$TEST_SOURCE_REF" \ + TEST_SOURCE_DIR="$LICENSE_DIR" \ + CURL_LOG="$curl_log" \ + PATH="$fake_bin:$PATH" \ + "$INSTALLER" --source-ref "$TEST_SOURCE_REF" >/dev/null + ) + assert_core_install "$target" + [ "$(wc -l <"$curl_log" | tr -d ' ')" = 4 ] || fail "source-ref mode made an unexpected number of downloads" + grep -F "/$TEST_SOURCE_REF/licenses/checksums.sha256" "$curl_log" >/dev/null || fail "source-ref manifest URL was not immutable" +} + +run_default_source_test() { + target="$TEST_ROOT/default-source" + fake_bin="$TEST_ROOT/default-source-bin" + curl_log="$TEST_ROOT/default-source-curl.log" + mkdir -p "$target" + make_fake_curl "$fake_bin" + : >"$curl_log" + ( + cd "$target" + EXPECTED_REF="$DEFAULT_SOURCE_REF" \ + TEST_SOURCE_DIR="$LICENSE_DIR" \ + CURL_LOG="$curl_log" \ + PATH="$fake_bin:$PATH" \ + "$INSTALLER" >/dev/null + ) + assert_core_install "$target" + [ "$(wc -l <"$curl_log" | tr -d ' ')" = 3 ] || fail "default mode did not preserve its three asset downloads" + if grep -F 'checksums.sha256' "$curl_log" >/dev/null; then + fail "default mode unexpectedly downloaded a manifest absent from its pinned revision" + fi +} + +run_argument_validation_test() { + target="$TEST_ROOT/arguments" + fake_bin="$TEST_ROOT/arguments-bin" + curl_marker="$TEST_ROOT/curl-called" + mkdir -p "$target" "$fake_bin" + cat >"$fake_bin/curl" </dev/null 2>&1 + ); then + fail "invalid arguments succeeded: $*" + fi + [ ! -e "$curl_marker" ] || fail "invalid arguments reached the downloader: $*" + } + + assert_invalid --source-ref + assert_invalid --source-ref abc123 + assert_invalid --source-ref zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz + assert_invalid --source-dir + assert_invalid --source-dir "$LICENSE_DIR" --source-ref "$TEST_SOURCE_REF" + assert_invalid --source-ref "$TEST_SOURCE_REF" --source-ref "$TEST_SOURCE_REF" + assert_invalid --unknown +} + +run_local_checksum_failure_test() { + target="$TEST_ROOT/local-checksum-failure" + source_dir="$TEST_ROOT/corrupt-source" + mkdir -p "$target" "$source_dir" + for asset in checksums.sha256 license-engine.sh licenses list-licenses license-template-go.tpl license-template-node.json; do + cp "$LICENSE_DIR/$asset" "$source_dir/$asset" + done + printf 'corrupt\n' >>"$source_dir/licenses" + seed_existing_core_assets "$target" + if ( + cd "$target" + "$INSTALLER" --source-dir "$source_dir" >/dev/null 2>&1 + ); then + fail "corrupt local asset passed checksum verification" + fi + assert_existing_core_assets "$target" +} + +run_remote_failure_tests() { + fake_bin="$TEST_ROOT/remote-failure-bin" + make_fake_curl "$fake_bin" + + for failure_mode in checksum download; do + target="$TEST_ROOT/remote-$failure_mode-failure" + curl_log="$TEST_ROOT/remote-$failure_mode-curl.log" + mkdir -p "$target" + seed_existing_core_assets "$target" + : >"$curl_log" + + failure_env=() + if [ "$failure_mode" = checksum ]; then + failure_env=(CORRUPT_ASSET=licenses) + else + failure_env=(FAIL_DOWNLOAD_ASSET=licenses) + fi + + if ( + cd "$target" + env \ + EXPECTED_REF="$TEST_SOURCE_REF" \ + TEST_SOURCE_DIR="$LICENSE_DIR" \ + CURL_LOG="$curl_log" \ + PATH="$fake_bin:$PATH" \ + "${failure_env[@]}" \ + "$INSTALLER" --source-ref "$TEST_SOURCE_REF" >/dev/null 2>&1 + ); then + fail "remote $failure_mode failure unexpectedly succeeded" + fi + assert_existing_core_assets "$target" + done +} + +run_installation_failure_test() { + target="$TEST_ROOT/installation-failure" + fake_bin="$TEST_ROOT/installation-failure-bin" + mv_marker="$TEST_ROOT/mv-failed" + real_mv=$(command -v mv) + mkdir -p "$target" "$fake_bin" + seed_existing_core_assets "$target" + cat >"$fake_bin/mv" <<'EOF' +#!/bin/sh +set -e +destination=$3 +if [ ! -e "$MV_FAIL_MARKER" ] && [ "$destination" = .bin/licenses ]; then + touch "$MV_FAIL_MARKER" + exit 93 +fi +exec "$REAL_MV" "$@" +EOF + chmod +x "$fake_bin/mv" + + if ( + cd "$target" + MV_FAIL_MARKER="$mv_marker" \ + REAL_MV="$real_mv" \ + PATH="$fake_bin:$PATH" \ + "$INSTALLER" --source-dir "$LICENSE_DIR" >/dev/null 2>&1 + ); then + fail "installation failure unexpectedly succeeded" + fi + [ -e "$mv_marker" ] || fail "installation failure was not exercised" + assert_existing_core_assets "$target" + if find "$target/.bin" -name '*.tmp.*' -print | grep . >/dev/null; then + fail "installation failure left prepared files behind" + fi +} + +make_isolated_tool_path() { + bin_dir=$1 + shift + mkdir -p "$bin_dir" + for tool in "$@"; do + tool_path=$(command -v "$tool") || fail "required test tool not found: $tool" + ln -s "$tool_path" "$bin_dir/$tool" + done +} + +run_checksum_backend_tests() { + for backend in shasum openssl; do + target="$TEST_ROOT/checksum-$backend" + tool_bin="$TEST_ROOT/checksum-$backend-bin" + mkdir -p "$target" + make_isolated_tool_path "$tool_bin" awk install mkdir mktemp mv rm "$backend" + ( + cd "$target" + PATH="$tool_bin" /bin/sh "$INSTALLER" --source-dir "$LICENSE_DIR" >/dev/null + ) + assert_core_install "$target" + done + + empty_bin="$TEST_ROOT/no-checksum-bin" + mkdir -p "$empty_bin" + if PATH="$empty_bin" /bin/sh "$INSTALLER" --source-dir "$LICENSE_DIR" >"$TEST_ROOT/no-checksum.log" 2>&1; then + fail "installer succeeded without a SHA-256 backend" + fi + grep -F 'a SHA-256 implementation is required' "$TEST_ROOT/no-checksum.log" >/dev/null || fail "missing checksum backend error was unclear" +} + +run_license_behavior_tests() { + printf '"example","MIT"\n' | "$LICENSE_DIR/license-engine.sh" >"$TEST_ROOT/allowed-license.log" + grep -F 'Licenses are okay.' "$TEST_ROOT/allowed-license.log" >/dev/null || fail "allowed license was rejected" + + printf '"github.com/ory-corp/cloud/service","Proprietary"\n' | + "$LICENSE_DIR/license-engine.sh" >"$TEST_ROOT/approved-module.log" + grep -F 'Licenses are okay.' "$TEST_ROOT/approved-module.log" >/dev/null || fail "approved module was rejected" + + if printf '"example","GPL-3.0"\n' | "$LICENSE_DIR/license-engine.sh" >"$TEST_ROOT/unknown-license.log" 2>&1; then + fail "unknown license was accepted" + fi + grep -F 'Unknown licenses found!' "$TEST_ROOT/unknown-license.log" >/dev/null || fail "unknown license failure was unclear" + grep -F '"example","GPL-3.0"' "$TEST_ROOT/unknown-license.log" >/dev/null || fail "unknown license details were omitted" +} + +run_action_hardening_tests() { + uses_count=0 + while IFS= read -r line; do + uses_count=$((uses_count + 1)) + ref=${line##*@} + ref=${ref%% *} + [[ "$ref" =~ ^[0-9a-f]{40}$ ]] || fail "mutable nested action reference: $line" + done < <(grep -hE '^[[:space:]]*(-[[:space:]]+)?uses:' "$LICENSE_DIR"/*/action.yml) + [ "$uses_count" -gt 0 ] || fail "no nested license actions were checked" + + for action in "$LICENSE_DIR/check/action.yml" "$LICENSE_DIR/setup/action.yml"; do + grep -F 'GITHUB_ACTION_PATH' "$action" >/dev/null || fail "$action does not use GITHUB_ACTION_PATH" + grep -F -- '--source-dir' "$action" >/dev/null || fail "$action does not select its bundled source directory" + if grep -E 'ORY_CI_(ROOT|REF)|raw\.githubusercontent\.com' "$action" >/dev/null; then + fail "$action still uses an ambient or remote installer source" + fi + done +} + +run_local_source_test +run_source_ref_test +run_default_source_test +run_argument_validation_test +run_local_checksum_failure_test +run_remote_failure_tests +run_installation_failure_test +run_checksum_backend_tests +run_license_behavior_tests +run_action_hardening_tests + +echo "All license tests passed."