diff --git a/Makefile b/Makefile index b3e6675b343..87d5bc6eb36 100644 --- a/Makefile +++ b/Makefile @@ -482,12 +482,13 @@ bundle-js: @echo "✓ bundle-js tool built" @echo "To bundle a JavaScript file: ./bundle-js [output-file]" -# Run Bash script tests (check-stale-lock-files, check-workflow-drift) +# Run Bash script tests .PHONY: test-scripts test-scripts: build @echo "Running Bash script tests..." bash scripts/check-stale-lock-files_test.sh bash scripts/check-workflow-drift_test.sh ./$(BINARY_NAME) + bash actions/setup/sh/install_threat_detect_binary_test.sh @echo "✓ All Bash script tests passed" # Test all code (Go, JavaScript, wasm golden, and shell scripts) diff --git a/actions/setup/sh/install_threat_detect_binary.sh b/actions/setup/sh/install_threat_detect_binary.sh index 102a3f67bc5..46a7cc131b2 100755 --- a/actions/setup/sh/install_threat_detect_binary.sh +++ b/actions/setup/sh/install_threat_detect_binary.sh @@ -15,6 +15,8 @@ set +o histexpand # # Platform support: # - Linux (x64, arm64): Downloads pre-built binary +# - macOS (x64, arm64): Downloads pre-built binary +# Note: macOS binaries are unsigned and not notarized. # # Security features: # - Downloads directly from GitHub releases @@ -25,35 +27,11 @@ set -euo pipefail # Configuration THREAT_DETECT_REPO="github/gh-aw-threat-detection" -THREAT_DETECT_INSTALL_DIR="/usr/local/bin" +THREAT_DETECT_DEFAULT_INSTALL_DIR="/usr/local/bin" +THREAT_DETECT_INSTALL_DIR="${THREAT_DETECT_DEFAULT_INSTALL_DIR}" THREAT_DETECT_INSTALL_NAME="threat-detect" - -# Parse arguments: treat the first non-flag argument as VERSION, all -- arguments as flags. THREAT_DETECT_VERSION="" ROOTLESS=false -for arg in "$@"; do - case "$arg" in - --rootless) ROOTLESS=true ;; - --*) echo "WARNING: Unknown flag: $arg" >&2 ;; - *) - if [ -z "$THREAT_DETECT_VERSION" ]; then - THREAT_DETECT_VERSION="$arg" - fi - ;; - esac -done - -if [ -z "$THREAT_DETECT_VERSION" ]; then - echo "ERROR: threat-detect version is required" - echo "Usage: $0 VERSION [--rootless]" - exit 1 -fi - -# In rootless mode, install into the user's home directory instead of /usr/local/bin -# so that ARC/DinD runners with allowPrivilegeEscalation: false can run without sudo. -if [ "$ROOTLESS" = "true" ]; then - THREAT_DETECT_INSTALL_DIR="${HOME}/.local/bin" -fi # maybe_sudo runs a command with sudo unless --rootless was specified. # In rootless mode, sudo is not available or needed. @@ -65,23 +43,31 @@ maybe_sudo() { fi } -# Rootless mode preflight: create and verify write access to the install directory. -if [ "$ROOTLESS" = "true" ]; then - if ! { mkdir -p "${THREAT_DETECT_INSTALL_DIR}" && [ -w "${THREAT_DETECT_INSTALL_DIR}" ]; }; then - echo "ERROR: --rootless could not create a writable install directory at ${THREAT_DETECT_INSTALL_DIR}" >&2 - exit 1 - fi -fi - -# Detect OS and architecture -OS="$(uname -s)" -ARCH="$(uname -m)" - -echo "Installing threat-detect with checksum verification (version: ${THREAT_DETECT_VERSION}, os: ${OS}, arch: ${ARCH})" - -# Download URLs -BASE_URL="https://github.com/${THREAT_DETECT_REPO}/releases/download/${THREAT_DETECT_VERSION}" -CHECKSUMS_URL="${BASE_URL}/checksums.txt" +resolve_binary_name() { + local os="$1" + local arch="$2" + + case "$os" in + Linux) + case "$arch" in + x86_64|amd64) echo "threat-detect-linux-amd64" ;; + aarch64|arm64) echo "threat-detect-linux-arm64" ;; + *) echo "ERROR: Unsupported Linux architecture: ${arch}" >&2; return 1 ;; + esac + ;; + Darwin) + case "$arch" in + x86_64) echo "threat-detect-darwin-x64" ;; + arm64) echo "threat-detect-darwin-arm64" ;; + *) echo "ERROR: Unsupported macOS architecture: ${arch}" >&2; return 1 ;; + esac + ;; + *) + echo "ERROR: Unsupported operating system: ${os}" >&2 + return 1 + ;; + esac +} # Platform-portable SHA256 function sha256_hash() { @@ -96,14 +82,6 @@ sha256_hash() { fi } -# Create temp directory -TEMP_DIR=$(mktemp -d) -trap 'rm -rf "$TEMP_DIR"' EXIT - -# Download checksums -echo "Downloading checksums from \"${CHECKSUMS_URL}\"..." -curl -fsSL --retry 5 --retry-delay 10 --retry-max-time 180 -o "${TEMP_DIR}/checksums.txt" "${CHECKSUMS_URL}" - verify_checksum() { local file="$1" local fname="$2" @@ -130,13 +108,8 @@ verify_checksum() { } install_linux_binary() { - # Determine binary name based on architecture local binary_name - case "$ARCH" in - x86_64|amd64) binary_name="threat-detect-linux-amd64" ;; - aarch64|arm64) binary_name="threat-detect-linux-arm64" ;; - *) echo "ERROR: Unsupported Linux architecture: ${ARCH}"; exit 1 ;; - esac + binary_name="$(resolve_binary_name "Linux" "${ARCH}")" local binary_url="${BASE_URL}/${binary_name}" echo "Downloading binary from \"${binary_url}\"..." @@ -151,13 +124,8 @@ install_linux_binary() { } install_darwin_binary() { - # Determine binary name based on architecture local binary_name - case "$ARCH" in - x86_64) binary_name="threat-detect-darwin-x64" ;; - arm64) binary_name="threat-detect-darwin-arm64" ;; - *) echo "ERROR: Unsupported macOS architecture: ${ARCH}"; exit 1 ;; - esac + binary_name="$(resolve_binary_name "Darwin" "${ARCH}")" local binary_url="${BASE_URL}/${binary_name}" echo "Downloading binary from \"${binary_url}\"..." @@ -171,30 +139,91 @@ install_darwin_binary() { maybe_sudo mv "${TEMP_DIR}/${binary_name}" "${THREAT_DETECT_INSTALL_DIR}/${THREAT_DETECT_INSTALL_NAME}" } -case "$OS" in - Linux) - install_linux_binary - ;; - Darwin) - install_darwin_binary - ;; - *) - echo "ERROR: Unsupported operating system: ${OS}" +main() { + THREAT_DETECT_VERSION="" + ROOTLESS=false + THREAT_DETECT_INSTALL_DIR="${THREAT_DETECT_DEFAULT_INSTALL_DIR}" + + # Parse arguments: treat the first non-flag argument as VERSION, all -- arguments as flags. + for arg in "$@"; do + case "$arg" in + --rootless) ROOTLESS=true ;; + --*) echo "WARNING: Unknown flag: $arg" >&2 ;; + *) + if [ -z "$THREAT_DETECT_VERSION" ]; then + THREAT_DETECT_VERSION="$arg" + fi + ;; + esac + done + + if [ -z "$THREAT_DETECT_VERSION" ]; then + echo "ERROR: threat-detect version is required" + echo "Usage: $0 VERSION [--rootless]" exit 1 - ;; -esac - -# In rootless mode, add the install dir to PATH for subsequent steps. -if [ "$ROOTLESS" = "true" ]; then - if [ -n "${GITHUB_PATH:-}" ]; then - echo "${THREAT_DETECT_INSTALL_DIR}" >> "${GITHUB_PATH}" - echo " Exported ${THREAT_DETECT_INSTALL_DIR} to GITHUB_PATH" - else - echo " GITHUB_PATH not set — binary installed at ${THREAT_DETECT_INSTALL_DIR}/${THREAT_DETECT_INSTALL_NAME}" fi -fi -# Verify installation -"${THREAT_DETECT_INSTALL_DIR}/${THREAT_DETECT_INSTALL_NAME}" --version + # In rootless mode, install into the user's home directory instead of /usr/local/bin + # so that ARC/DinD runners with allowPrivilegeEscalation: false can run without sudo. + if [ "$ROOTLESS" = "true" ]; then + THREAT_DETECT_INSTALL_DIR="${HOME}/.local/bin" + fi + + # Rootless mode preflight: create and verify write access to the install directory. + if [ "$ROOTLESS" = "true" ]; then + if ! { mkdir -p "${THREAT_DETECT_INSTALL_DIR}" && [ -w "${THREAT_DETECT_INSTALL_DIR}" ]; }; then + echo "ERROR: --rootless could not create a writable install directory at ${THREAT_DETECT_INSTALL_DIR}" >&2 + exit 1 + fi + fi + + # Detect OS and architecture + OS="$(uname -s)" + ARCH="$(uname -m)" + + echo "Installing threat-detect with checksum verification (version: ${THREAT_DETECT_VERSION}, os: ${OS}, arch: ${ARCH})" + + # Download URLs + BASE_URL="https://github.com/${THREAT_DETECT_REPO}/releases/download/${THREAT_DETECT_VERSION}" + CHECKSUMS_URL="${BASE_URL}/checksums.txt" + + # Create temp directory + TEMP_DIR=$(mktemp -d) + trap 'rm -rf "$TEMP_DIR"' EXIT + + # Download checksums + echo "Downloading checksums from \"${CHECKSUMS_URL}\"..." + curl -fsSL --retry 5 --retry-delay 10 --retry-max-time 180 -o "${TEMP_DIR}/checksums.txt" "${CHECKSUMS_URL}" + + case "$OS" in + Linux) + install_linux_binary + ;; + Darwin) + install_darwin_binary + ;; + *) + echo "ERROR: Unsupported operating system: ${OS}" + exit 1 + ;; + esac + + # In rootless mode, add the install dir to PATH for subsequent steps. + if [ "$ROOTLESS" = "true" ]; then + if [ -n "${GITHUB_PATH:-}" ]; then + echo "${THREAT_DETECT_INSTALL_DIR}" >> "${GITHUB_PATH}" + echo " Exported ${THREAT_DETECT_INSTALL_DIR} to GITHUB_PATH" + else + echo " GITHUB_PATH not set — binary installed at ${THREAT_DETECT_INSTALL_DIR}/${THREAT_DETECT_INSTALL_NAME}" + fi + fi + + # Verify installation + "${THREAT_DETECT_INSTALL_DIR}/${THREAT_DETECT_INSTALL_NAME}" --version -echo "✓ threat-detect installation complete" + echo "✓ threat-detect installation complete" +} + +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + main "$@" +fi diff --git a/actions/setup/sh/install_threat_detect_binary_test.sh b/actions/setup/sh/install_threat_detect_binary_test.sh new file mode 100644 index 00000000000..3f244647ab2 --- /dev/null +++ b/actions/setup/sh/install_threat_detect_binary_test.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set +o histexpand + +# Tests for install_threat_detect_binary.sh platform resolver. +# Run: bash actions/setup/sh/install_threat_detect_binary_test.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=actions/setup/sh/install_threat_detect_binary.sh +source "${SCRIPT_DIR}/install_threat_detect_binary.sh" +set +e + +TESTS_PASSED=0 +TESTS_FAILED=0 + +pass() { echo "PASS: $1"; TESTS_PASSED=$((TESTS_PASSED + 1)); } +fail() { echo "FAIL: $1"; echo " $2"; TESTS_FAILED=$((TESTS_FAILED + 1)); } + +assert_maps_to() { + local os="$1" + local arch="$2" + local expected="$3" + local result + + if result="$(resolve_binary_name "$os" "$arch" 2>&1)"; then + if [ "$result" = "$expected" ]; then + pass "${os} ${arch} -> ${expected}" + else + fail "${os} ${arch} did not map to ${expected}" "got: ${result}" + fi + else + fail "${os} ${arch} unexpectedly failed" "got: ${result}" + fi +} + +assert_fails_with() { + local os="$1" + local arch="$2" + local expected_msg="$3" + local result + local exit_code + + result="$(resolve_binary_name "$os" "$arch" 2>&1)" + exit_code=$? + + if [ "$exit_code" -ne 0 ] && echo "$result" | grep -q "$expected_msg"; then + pass "${os} ${arch} -> expected error" + else + fail "${os} ${arch} did not fail as expected" "exit=${exit_code}, output=${result}" + fi +} + +echo "Running install_threat_detect_binary.sh tests..." +echo + +assert_maps_to "Linux" "x86_64" "threat-detect-linux-amd64" +assert_maps_to "Linux" "amd64" "threat-detect-linux-amd64" +assert_maps_to "Linux" "aarch64" "threat-detect-linux-arm64" +assert_maps_to "Linux" "arm64" "threat-detect-linux-arm64" +assert_maps_to "Darwin" "x86_64" "threat-detect-darwin-x64" +assert_maps_to "Darwin" "arm64" "threat-detect-darwin-arm64" + +assert_fails_with "Darwin" "aarch64" "Unsupported macOS architecture" +assert_fails_with "Linux" "s390x" "Unsupported Linux architecture" +assert_fails_with "Windows_NT" "x86_64" "Unsupported operating system" + +echo +echo "Tests passed: $TESTS_PASSED" +echo "Tests failed: $TESTS_FAILED" + +if [ "$TESTS_FAILED" -gt 0 ]; then + exit 1 +fi + +echo "All tests passed!" diff --git a/pkg/workflow/runs_on_validation_test.go b/pkg/workflow/runs_on_validation_test.go index e61f523c83b..adda97489f3 100644 --- a/pkg/workflow/runs_on_validation_test.go +++ b/pkg/workflow/runs_on_validation_test.go @@ -153,6 +153,19 @@ func TestValidateRunsOn(t *testing.T) { errorInMsg: "safe-outputs.threat-detection.runs-on", description: "threat-detection runs-on labels containing macos runner should be rejected", }, + { + name: "macos string in safe-outputs.threat-detection.runs-on", + frontmatter: map[string]any{ + "safe-outputs": map[string]any{ + "threat-detection": map[string]any{ + "runs-on": "macos-latest", + }, + }, + }, + wantErr: true, + errorInMsg: "safe-outputs.threat-detection.runs-on", + description: "threat-detection runs-on string set to macos-latest should be rejected", + }, } for _, tt := range tests {