Skip to content
Closed
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,13 @@ bundle-js:
@echo "✓ bundle-js tool built"
@echo "To bundle a JavaScript file: ./bundle-js <input-file> [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)
Expand Down
201 changes: 115 additions & 86 deletions actions/setup/sh/install_threat_detect_binary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 --<flag> 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.
Expand All @@ -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() {
Expand All @@ -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"
Expand All @@ -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}\"..."
Expand All @@ -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}\"..."
Expand All @@ -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 --<flag> 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
74 changes: 74 additions & 0 deletions actions/setup/sh/install_threat_detect_binary_test.sh
Original file line number Diff line number Diff line change
@@ -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!"
13 changes: 13 additions & 0 deletions pkg/workflow/runs_on_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading