From cd7a9c8c83d78083233d42a950ed6d5dd6807929 Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Fri, 4 Sep 2026 14:05:14 -0400 Subject: [PATCH 1/3] fix(security): protect credentials in deployment scripts --- .github/scripts/test-credential-handling.sh | 121 +++++++++++++++++ .github/scripts/test-curl-oauth-live.sh | 71 ++++++++++ .../test-powershell-credential-handling.ps1 | 60 ++++++++ .github/workflows/container_sensor_pull.yml | 3 + .github/workflows/credential_handling.yml | 91 +++++++++++++ .../falcon-container-sensor-pull/README.md | 2 +- .../falcon-container-sensor-pull.sh | 29 ++-- .../test/test-curl-command.sh | 128 ++++++++++++++++++ bash/install/README.md | 2 +- bash/install/falcon-linux-install.sh | 76 +++++++---- bash/install/falcon-linux-uninstall.sh | 72 +++++----- bash/migrate/README.md | 5 +- bash/migrate/falcon-linux-migrate.sh | 74 ++++++---- powershell/install/falcon_windows_install.ps1 | 5 +- .../install/falcon_windows_uninstall.ps1 | 11 +- powershell/migrate/falcon_windows_migrate.ps1 | 10 +- 16 files changed, 640 insertions(+), 120 deletions(-) create mode 100644 .github/scripts/test-credential-handling.sh create mode 100644 .github/scripts/test-curl-oauth-live.sh create mode 100644 .github/scripts/test-powershell-credential-handling.ps1 create mode 100644 .github/workflows/credential_handling.yml create mode 100644 bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh diff --git a/.github/scripts/test-credential-handling.sh b/.github/scripts/test-credential-handling.sh new file mode 100644 index 0000000..155ffe7 --- /dev/null +++ b/.github/scripts/test-credential-handling.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +curl() { + printf '%s\n' "$@" >"$CURL_ARGS_FILE" + cat >"$CURL_STDIN_FILE" +} + +export CURL_ARGS_FILE CURL_STDIN_FILE + +test_curl_helper() { + local script=$1 token_mode=$2 + local helper + + helper=$(awk '/^curl_command\(\)/,/^}/' "$repo_root/$script") + [ -n "$helper" ] || fail "curl_command not found in $script" + eval "$helper" + + for old_curl in 0 1; do + CURL_ARGS_FILE="$work_dir/args" + CURL_STDIN_FILE="$work_dir/stdin" + proxy="" + export proxy + cs_falcon_oauth_token="REGRESSION_SECRET_TOKEN" + + if [ "$token_mode" = "argument" ]; then + curl_command "$cs_falcon_oauth_token" "https://api.example.invalid/resource" + else + curl_command "https://api.example.invalid/resource" + fi + + if grep -qF "$cs_falcon_oauth_token" "$CURL_ARGS_FILE"; then + fail "$script exposed the bearer token in curl arguments (old_curl=$old_curl)" + fi + grep -qF 'https://api.example.invalid/resource' "$CURL_ARGS_FILE" || + fail "$script did not pass the expected URL (old_curl=$old_curl)" + grep -qF "$cs_falcon_oauth_token" "$CURL_STDIN_FILE" || + fail "$script did not provide the bearer token through stdin (old_curl=$old_curl)" + grep -qF -- '--proto' "$CURL_ARGS_FILE" || + fail "$script did not restrict the request protocol (old_curl=$old_curl)" + grep -qF -- '--proto-redir' "$CURL_ARGS_FILE" || + fail "$script did not restrict the redirect protocol (old_curl=$old_curl)" + done +} + +test_curl_helper \ + bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh argument +test_curl_helper bash/install/falcon-linux-install.sh global +test_curl_helper bash/install/falcon-linux-uninstall.sh global +test_curl_helper bash/migrate/falcon-linux-migrate.sh global + +test_xtrace_guard() { + local script=$1 guard trace_file + + guard=$(awk '/^case \$- in$/,/^esac$/' "$repo_root/$script") + [ -n "$guard" ] || fail "xtrace guard not found in $script" + trace_file="$work_dir/xtrace" + + FALCON_CLIENT_SECRET="XTRACE_SECRET_SENTINEL" \ + bash -xc "$guard; : \"\$FALCON_CLIENT_SECRET\"" \ + >/dev/null 2>"$trace_file" + + if grep -qF 'XTRACE_SECRET_SENTINEL' "$trace_file"; then + fail "$script allowed a credential into bash xtrace output" + fi +} + +test_xtrace_guard bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh +test_xtrace_guard bash/install/falcon-linux-install.sh +test_xtrace_guard bash/install/falcon-linux-uninstall.sh +test_xtrace_guard bash/migrate/falcon-linux-migrate.sh + +test_hash_verification() { + local script=$1 helper test_file expected_sha + + helper=$(awk '/^verify_sha256\(\)/,/^}/' "$repo_root/$script") + [ -n "$helper" ] || fail "verify_sha256 not found in $script" + eval "$helper" + die() { exit 1; } + + test_file="$work_dir/installer" + printf '%s' 'verified installer content' >"$test_file" + expected_sha=$(openssl dgst -sha256 "$test_file" | awk '{ print $NF }') + verify_sha256 "$test_file" "$expected_sha" || + fail "$script rejected a valid installer hash" + + if (verify_sha256 "$test_file" '0000000000000000000000000000000000000000000000000000000000000000'); then + fail "$script accepted an invalid installer hash" + fi + [ ! -e "$test_file" ] || fail "$script retained an installer with an invalid hash" +} + +test_hash_verification bash/install/falcon-linux-install.sh +test_hash_verification bash/migrate/falcon-linux-migrate.sh + +if rg -n 'Invalid Access Token:.*\$cs_falcon_oauth_token|Failed to retrieve maintenance token\. Response:' \ + "$repo_root/bash" --glob '*.sh'; then + fail 'a Bash error path exposes a credential or raw maintenance-token response' +fi + +if rg -n 'curl .*X-aws-ec2-metadata-token:.*\$token' \ + "$repo_root/bash" --glob '*.sh'; then + fail 'an EC2 metadata token is exposed in curl arguments' +fi + +if rg -n '(Invoke-FalconAuth|GetToken) - \$content:|Retrieved maintenance token:|Starting .*parameters.*\$(Install|Uninstall)Params' \ + "$repo_root/powershell" --glob '*.ps1'; then + fail 'a PowerShell log statement exposes an authentication or installer token' +fi + +echo 'PASS: credential handling regression checks' diff --git a/.github/scripts/test-curl-oauth-live.sh b/.github/scripts/test-curl-oauth-live.sh new file mode 100644 index 0000000..cf7a314 --- /dev/null +++ b/.github/scripts/test-curl-oauth-live.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +set -eu + +work_dir=$(mktemp -d) +server_pids="" +cleanup() { + for pid in $server_pids; do + kill "$pid" 2>/dev/null || true + done + rm -rf "$work_dir" +} +trap cleanup EXIT + +start_capture_server() { + port=$1 output=$2 + python3 -c 'import http.server, socketserver, sys +socketserver.TCPServer.allow_reuse_address = True +class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + with open(sys.argv[1], "w") as output: + output.write(self.headers.get("Authorization", "")) + self.send_response(204) + self.end_headers() + def log_message(self, *args): + pass +http.server.HTTPServer(("127.0.0.1", int(sys.argv[2])), Handler).handle_request() +' "$output" "$port" & + last_server_pid=$! + server_pids="$server_pids $last_server_pid" +} + +token=CONTAINER_LIVE_TEST_TOKEN +curl --version | head -n 1 + +start_capture_server 28768 "$work_dir/header" +header_pid=$last_server_pid +sleep 1 +printf 'oauth2-bearer = "%s"\n' "$token" | + curl --silent --show-error -K- --url http://127.0.0.1:28768/test +wait "$header_pid" +[ "$(cat "$work_dir/header")" = "Bearer $token" ] || { + echo 'FAIL: OAuth configuration did not transmit the expected header' >&2 + exit 1 +} + +start_capture_server 28770 "$work_dir/redirect-header" +target_pid=$last_server_pid +python3 -c 'import http.server, socketserver +socketserver.TCPServer.allow_reuse_address = True +class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(302) + self.send_header("Location", "http://localhost:28770/target") + self.end_headers() + def log_message(self, *args): + pass +http.server.HTTPServer(("127.0.0.1", 28769), Handler).handle_request() +' & +redirect_pid=$! +server_pids="$server_pids $redirect_pid" +sleep 1 +printf 'oauth2-bearer = "%s"\n' "$token" | + curl --silent --show-error -L -K- --url http://127.0.0.1:28769/start +wait "$redirect_pid" "$target_pid" +[ ! -s "$work_dir/redirect-header" ] || { + echo 'FAIL: OAuth credential followed a cross-host redirect' >&2 + exit 1 +} + +echo 'PASS: OAuth stdin configuration and cross-host redirect protection' diff --git a/.github/scripts/test-powershell-credential-handling.ps1 b/.github/scripts/test-powershell-credential-handling.ps1 new file mode 100644 index 0000000..a1bdc8e --- /dev/null +++ b/.github/scripts/test-powershell-credential-handling.ps1 @@ -0,0 +1,60 @@ +$ErrorActionPreference = 'Stop' + +$RepositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '../..') +$Scripts = @( + 'powershell/install/falcon_windows_install.ps1' + 'powershell/install/falcon_windows_uninstall.ps1' + 'powershell/migrate/falcon_windows_migrate.ps1' +) +$SensitiveLogVariables = '(FalconAccessToken|FalconClientSecret|MaintenanceToken|ProvToken|InstallParams|UninstallParams)' +$Failures = [System.Collections.Generic.List[string]]::new() + +foreach ($RelativePath in $Scripts) { + $Path = Join-Path $RepositoryRoot $RelativePath + $Tokens = $null + $ParseErrors = $null + $Ast = [System.Management.Automation.Language.Parser]::ParseFile( + $Path, + [ref] $Tokens, + [ref] $ParseErrors + ) + + foreach ($ParseError in $ParseErrors) { + $Failures.Add("${RelativePath}:$($ParseError.Extent.StartLineNumber): parser error: $($ParseError.Message)") + } + + $LogCommands = $Ast.FindAll({ + param($Node) + $Node -is [System.Management.Automation.Language.CommandAst] -and + $Node.GetCommandName() -in @('Write-FalconLog', 'Write-VerboseLog') + }, $true) + + foreach ($Command in $LogCommands) { + $CommandText = $Command.Extent.Text + if ($CommandText -match "\`$$SensitiveLogVariables") { + $Failures.Add("${RelativePath}:$($Command.Extent.StartLineNumber): sensitive variable used in log command") + } + if ($CommandText -match 'Write-VerboseLog' -and + $CommandText -match '\$content' -and + $CommandText -match '(Invoke-FalconAuth|GetToken)') { + $Failures.Add("${RelativePath}:$($Command.Extent.StartLineNumber): sensitive API response used in verbose log") + } + } + + $DebugOffCommands = $Ast.FindAll({ + param($Node) + $Node -is [System.Management.Automation.Language.CommandAst] -and + $Node.GetCommandName() -eq 'Set-PSDebug' -and + $Node.Extent.Text -match '-Off' + }, $true) + if ($DebugOffCommands.Count -eq 0) { + $Failures.Add("${RelativePath}: PowerShell tracing is not disabled before credentials are processed") + } +} + +if ($Failures.Count -gt 0) { + $Failures | ForEach-Object { Write-Error $_ } + exit 1 +} + +Write-Output 'PASS: PowerShell credential logging and parser checks' diff --git a/.github/workflows/container_sensor_pull.yml b/.github/workflows/container_sensor_pull.yml index 7ec5b36..4fad84e 100644 --- a/.github/workflows/container_sensor_pull.yml +++ b/.github/workflows/container_sensor_pull.yml @@ -31,6 +31,9 @@ jobs: shellcheck --version shellcheck bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh + - name: Unit tests + run: sh bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh + container-test: name: Container Test needs: validate diff --git a/.github/workflows/credential_handling.yml b/.github/workflows/credential_handling.yml new file mode 100644 index 0000000..96d80dd --- /dev/null +++ b/.github/workflows/credential_handling.yml @@ -0,0 +1,91 @@ +name: "CI: credential handling" + +on: + push: + paths: + - '.github/scripts/test-credential-handling.sh' + - '.github/scripts/test-curl-oauth-live.sh' + - '.github/scripts/test-powershell-credential-handling.ps1' + - '.github/workflows/credential_handling.yml' + - 'bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh' + - 'bash/install/falcon-linux-install.sh' + - 'bash/install/falcon-linux-uninstall.sh' + - 'bash/migrate/falcon-linux-migrate.sh' + - 'powershell/install/falcon_windows_install.ps1' + - 'powershell/install/falcon_windows_uninstall.ps1' + - 'powershell/migrate/falcon_windows_migrate.ps1' + pull_request: + paths: + - '.github/scripts/test-credential-handling.sh' + - '.github/scripts/test-curl-oauth-live.sh' + - '.github/scripts/test-powershell-credential-handling.ps1' + - '.github/workflows/credential_handling.yml' + - 'bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh' + - 'bash/install/falcon-linux-install.sh' + - 'bash/install/falcon-linux-uninstall.sh' + - 'bash/migrate/falcon-linux-migrate.sh' + - 'powershell/install/falcon_windows_install.ps1' + - 'powershell/install/falcon_windows_uninstall.ps1' + - 'powershell/migrate/falcon_windows_migrate.ps1' + +permissions: + contents: read + +jobs: + test: + name: Prevent credential exposure + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Run credential handling regression tests + run: bash .github/scripts/test-credential-handling.sh + + - name: Run PowerShell credential handling regression tests + shell: pwsh + run: ./.github/scripts/test-powershell-credential-handling.ps1 + + - name: Test curl OAuth behavior + run: sh .github/scripts/test-curl-oauth-live.sh + + curl-container-compatibility: + name: curl compatibility (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: Ubuntu 22.04 + image: ubuntu:22.04 + setup: apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl python3 >/dev/null + - name: Debian 12 + image: debian:12-slim + setup: apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl python3 >/dev/null + - name: Alpine 3.20 + image: alpine:3.20 + setup: apk add --no-cache curl python3 >/dev/null + - name: AlmaLinux 9 + image: almalinux:9 + setup: dnf install -y -q python3 >/dev/null + - name: Amazon Linux 2023 + image: amazonlinux:2023 + setup: dnf install -y -q python3 >/dev/null + steps: + - name: Check out code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Test packaged curl + env: + CONTAINER_IMAGE: ${{ matrix.image }} + CONTAINER_SETUP: ${{ matrix.setup }} + run: | + docker run --rm \ + --volume "$PWD:/repo:ro" \ + --env CONTAINER_SETUP \ + "$CONTAINER_IMAGE" \ + sh -c '$CONTAINER_SETUP && sh /repo/.github/scripts/test-curl-oauth-live.sh' diff --git a/bash/containers/falcon-container-sensor-pull/README.md b/bash/containers/falcon-container-sensor-pull/README.md index 01ed774..dada9a8 100644 --- a/bash/containers/falcon-container-sensor-pull/README.md +++ b/bash/containers/falcon-container-sensor-pull/README.md @@ -52,7 +52,7 @@ CrowdStrike now provides unified images that work across all regions: ### Use cURL version 7.55.0 or later -We've identified a security concern related to cURL versions 7.54.1 and earlier. In these versions, request headers were set using the `-H` option, which allowed potential secrets to be exposed via the command line. In newer versions of cURL, versions 7.55.0 and later, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. **We recommend that you to upgrade cURL to version 7.55.0 or later**. If this is not possible, this script offers compatibility with the older method through the use of the `--allow-legacy-curl` optional command line flag. +OAuth credentials are supplied to cURL through its configuration input rather than command-line arguments. This also lets cURL treat the bearer token as an authentication credential and prevents it from forwarding the token when a redirect crosses to another host. **We recommend upgrading cURL to version 7.55.0 or later.** If this is not possible, `--allow-legacy-curl` bypasses the version check without reverting to command-line credential handling. To check your version of cURL, run the following command: `curl --version` diff --git a/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh b/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh index ecab4c0..d8556f3 100755 --- a/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh +++ b/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh @@ -1,4 +1,17 @@ #!/bin/bash + +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +falcon_client_secret=$FALCON_CLIENT_SECRET +unset FALCON_CLIENT_SECRET +FALCON_CLIENT_SECRET=$falcon_client_secret +unset falcon_client_secret + : <<'#DESCRIPTION#' File: falcon-container-sensor-pull.sh Description: Bash script to copy Falcon DaemonSet Sensor, Container Sensor, or Kubernetes Admission Controller images from CrowdStrike Container Registry. @@ -245,8 +258,8 @@ old_curl=$( if [ "$old_curl" -eq 0 ]; then if [ "${ALLOW_LEGACY_CURL}" != "true" ]; then echo """ -WARNING: Your version of curl does not support the ability to pass headers via stdin. -For security considerations, we strongly recommend upgrading to curl 7.55.0 or newer. +WARNING: Your version of curl is below the supported 7.55.0 baseline. +OAuth credentials remain protected from command-line exposure in compatibility mode. To bypass this warning, set the optional flag --allow-legacy-curl """ @@ -288,12 +301,9 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments local token="$1" - set -- "$@" - if [ "$old_curl" -eq 0 ]; then - curl -s -L -H "Authorization: Bearer ${token}" "$@" - else - echo "Authorization: Bearer ${token}" | curl -s -L -H @- "$@" - fi + shift + printf 'oauth2-bearer = "%s"\n' "$token" | + curl -s -L --proto '=https' --proto-redir '=https' -K- "$@" } fetch_tags() { @@ -875,8 +885,7 @@ docker_api_token=$(echo "$raw_docker_api_token" | json_value "token") ART_PASSWORD=$(echo "$docker_api_token" | sed 's/ *$//g' | sed 's/^ *//g') if [ -z "$ART_PASSWORD" ]; then - die "Failed to retrieve the CrowdStrike registry password. Response from API: -$raw_docker_api_token + die "Failed to retrieve the CrowdStrike registry password. Ensure the following: - Correct API Scopes assigned for sensor type: ${SENSOR_TYPE} diff --git a/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh b/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh new file mode 100644 index 0000000..e7df88f --- /dev/null +++ b/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh @@ -0,0 +1,128 @@ +#!/bin/sh +# Regression test for curl_command() argument handling. +# +# curl_command() takes the bearer token as its first parameter and must pass +# only the remaining parameters through to curl. If it does not consume that +# first parameter, curl receives the token as an extra positional parameter and +# treats it as a URL. +# +# This test loads curl_command() from the shipped script, replaces curl with a +# stub that records its argument vector, and checks that the token is never +# handed to curl as an argument. The token is supplied through curl's +# credential-aware OAuth configuration on stdin. +# +# Usage: sh test-curl-command.sh [path-to-falcon-container-sensor-pull.sh] + +# Re-run under every available shell. The script ships as POSIX sh and runs +# under whatever shell the operator has, so both dash and bash must pass. +if [ -z "${CURL_CMD_TEST_SHELL:-}" ]; then + overall=0 + for shell_bin in sh dash bash; do + command -v "$shell_bin" >/dev/null 2>&1 || continue + echo "=== shell under test: $shell_bin ===" + CURL_CMD_TEST_SHELL="$shell_bin" "$shell_bin" "$0" "$@" || overall=1 + echo + done + exit "$overall" +fi + +TEST_DIR=$(dirname "$0") +SCRIPT="${1:-$TEST_DIR/../falcon-container-sensor-pull.sh}" + +if [ ! -f "$SCRIPT" ]; then + echo "FATAL: cannot find script under test: $SCRIPT" >&2 + exit 1 +fi + +TMPDIR_TEST=$(mktemp -d) +ARGV_FILE="$TMPDIR_TEST/argv" +STDIN_FILE="$TMPDIR_TEST/stdin" +trap 'rm -rf "$TMPDIR_TEST"' EXIT + +PASS=0 +FAIL=0 + +pass() { + PASS=$((PASS + 1)) + echo " ok - $1" +} + +fail() { + FAIL=$((FAIL + 1)) + echo " NOT OK - $1" +} + +# A token shaped like the tokens the script really handles: dot-separated +# base64url segments. Every segment is short enough to be a valid DNS label, +# which is what makes an accidental positional argument reach the resolver. +TOKEN="eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJmYy1kZWFkYmVlZiJ9.c2lnbmF0dXJl" +URL="https://example.com/v2/tags/list" + +# Load the real curl_command() from the shipped script so the test tracks the +# code that ships, not a copy of it. +curl_command_source=$(sed -n '/^curl_command() {$/,/^}$/p' "$SCRIPT") +if [ -z "$curl_command_source" ]; then + echo "FATAL: could not extract curl_command() from $SCRIPT" >&2 + exit 1 +fi +eval "$curl_command_source" + +# Stub curl. Records each argument on its own line, and records stdin when the +# caller asks curl to read a header from it. +curl() { + : >"$ARGV_FILE" + : >"$STDIN_FILE" + for arg in "$@"; do + printf '%s\n' "$arg" >>"$ARGV_FILE" + if [ "$arg" = "-K-" ]; then + read_stdin=yes + fi + done + if [ "${read_stdin:-no}" = "yes" ]; then + cat >"$STDIN_FILE" + fi + read_stdin=no +} + +# The token must never be handed to curl in any argument. +assert_token_not_in_arguments() { + found=no + while IFS= read -r arg; do + case "$arg" in + *"$TOKEN"*) found=yes ;; + esac + done <"$ARGV_FILE" + + if [ "$found" = "yes" ]; then + fail "$1: token reached curl arguments" + echo " argv: $(tr '\n' ' ' <"$ARGV_FILE")" + else + pass "$1: token is absent from curl arguments" + fi +} + +assert_file_contains() { + if grep -qF "$2" "$3"; then + pass "$1" + else + fail "$1" + echo " contents: $(tr '\n' ' ' <"$3")" + fi +} + +assert_url_passed_through() { + assert_file_contains "$1: request URL reached curl" "$URL" "$ARGV_FILE" +} + +# Credential-aware OAuth configuration prevents forwarding the token when a +# redirect crosses to another host. +echo "case: OAuth credential over stdin (-K-)" +curl_command "$TOKEN" "$URL" +assert_token_not_in_arguments "OAuth config" +assert_url_passed_through "OAuth config" +assert_file_contains "OAuth config: token delivered over stdin" \ + "oauth2-bearer = \"$TOKEN\"" "$STDIN_FILE" + +echo +echo "passed: $PASS failed: $FAIL" +[ "$FAIL" -eq 0 ] diff --git a/bash/install/README.md b/bash/install/README.md index 3ed5fa1..9dc135d 100644 --- a/bash/install/README.md +++ b/bash/install/README.md @@ -9,7 +9,7 @@ environment variable. Consult the Environment Variables for each script for more ### Use cURL version 7.55.0 or newer -We have identified a security concern related to cURL versions prior to 7.55, which required request headers to be set using the `-H` option, thus allowing potential secrets to be exposed via the command line. In newer versions of cURL, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. Although our script offers compatibility with the older method by allowing you to set the environment variable `ALLOW_LEGACY_CURL=true`, we strongly urge you to upgrade cURL if your environment permits. +OAuth credentials are supplied to cURL through its configuration input rather than command-line arguments. This also lets cURL treat the bearer token as an authentication credential and prevents it from forwarding the token when a redirect crosses to another host. We strongly recommend cURL 7.55.0 or newer. If compatibility mode is required, `ALLOW_LEGACY_CURL=true` bypasses the version check without reverting to command-line credential handling. To check your version of cURL, run the following command: `curl --version` diff --git a/bash/install/falcon-linux-install.sh b/bash/install/falcon-linux-install.sh index 19213f1..22298b5 100755 --- a/bash/install/falcon-linux-install.sh +++ b/bash/install/falcon-linux-install.sh @@ -1,5 +1,21 @@ #!/bin/bash +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +falcon_client_secret=$FALCON_CLIENT_SECRET +falcon_access_token=$FALCON_ACCESS_TOKEN +falcon_provisioning_token=$FALCON_PROVISIONING_TOKEN +unset FALCON_CLIENT_SECRET FALCON_ACCESS_TOKEN FALCON_PROVISIONING_TOKEN +FALCON_CLIENT_SECRET=$falcon_client_secret +FALCON_ACCESS_TOKEN=$falcon_access_token +FALCON_PROVISIONING_TOKEN=$falcon_provisioning_token +unset falcon_client_secret falcon_access_token falcon_provisioning_token + print_usage() { cat </dev/null 2>&1; then + local_sha=$(sha256sum "$file" | awk '{ print $1 }') + else + local_sha=$(openssl dgst -sha256 "$file" | awk '{ print $NF }') + fi + if [ "$local_sha" != "$expected_sha" ]; then + rm -f "$file" + die "Downloaded sensor installer failed SHA-256 verification." + fi +} + cs_sensor_download() { local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer @@ -352,7 +382,7 @@ cs_sensor_download() { if echo "$existing_installers" | grep "authorization failed"; then die "Access denied: Please make sure that your Falcon API credentials allow sensor download (scope Sensor Download [read])" elif echo "$existing_installers" | grep "invalid bearer token"; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi sha_list=$(echo "$existing_installers" | json_value "sha256") @@ -376,6 +406,8 @@ cs_sensor_download() { handle_curl_error $? + verify_sha256 "$installer" "$sha" + echo "$installer" } @@ -467,9 +499,9 @@ aws_ssm_parameter() { token=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") api_endpoint="AmazonSSM.GetParameters" - iam_role="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/)" - aws_my_region="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" - _security_credentials="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" + iam_role="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/)" + aws_my_region="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" + _security_credentials="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" access_key_id="$(echo "$_security_credentials" | grep AccessKeyId | sed -e 's/ "AccessKeyId" : "//' -e 's/",$//')" access_key_secret="$(echo "$_security_credentials" | grep SecretAccessKey | sed -e 's/ "SecretAccessKey" : "//' -e 's/",$//')" security_token="$(echo "$_security_credentials" | grep Token | sed -e 's/ "Token" : "//' -e 's/",$//')" @@ -507,17 +539,16 @@ EOF ) response=$( - curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ - -x "$proxy" \ - -H "Authorization: AWS4-HMAC-SHA256 \ - Credential=$access_key_id/$date/$aws_my_region/ssm/aws4_request, \ - SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, \ - Signature=$signature" \ - -H "x-amz-security-token: $security_token" \ - -H "x-amz-target: $api_endpoint" \ - -H "content-type: application/x-amz-json-1.1" \ - -d "$request_data" \ - -H "x-amz-date: $datetime" + { + printf 'header = "Authorization: AWS4-HMAC-SHA256 Credential=%s/%s/%s/ssm/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=%s"\n' \ + "$access_key_id" "$date" "$aws_my_region" "$signature" + printf 'header = "x-amz-security-token: %s"\n' "$security_token" + printf 'header = "x-amz-target: %s"\n' "$api_endpoint" + printf 'header = "content-type: application/x-amz-json-1.1"\n' + printf 'header = "x-amz-date: %s"\n' "$datetime" + } | curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ + -x "$proxy" -K- \ + -d "$request_data" ) handle_curl_error $? if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$'; then @@ -634,8 +665,8 @@ old_curl=$( if [ "$old_curl" -eq 0 ]; then if [ "${ALLOW_LEGACY_CURL}" != "true" ]; then echo """ -WARNING: Your version of curl does not support the ability to pass headers via stdin. -For security considerations, we strongly recommend upgrading to curl 7.55.0 or newer. +WARNING: Your version of curl is below the supported 7.55.0 baseline. +OAuth credentials remain protected from command-line exposure in compatibility mode. To bypass this warning, set the environment variable ALLOW_LEGACY_CURL=true """ @@ -678,13 +709,8 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - set -- "$@" - - if [ "$old_curl" -eq 0 ]; then - curl -s -x "$proxy" -L -H "Authorization: Bearer ${cs_falcon_oauth_token}" "$@" - else - echo "Authorization: Bearer ${cs_falcon_oauth_token}" | curl -s -x "$proxy" -L -H @- "$@" - fi + printf 'oauth2-bearer = "%s"\n' "$cs_falcon_oauth_token" | + curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } check_aws_instance() { diff --git a/bash/install/falcon-linux-uninstall.sh b/bash/install/falcon-linux-uninstall.sh index 6b900fa..9e2dceb 100755 --- a/bash/install/falcon-linux-uninstall.sh +++ b/bash/install/falcon-linux-uninstall.sh @@ -1,5 +1,21 @@ #!/bin/bash +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +falcon_client_secret=$FALCON_CLIENT_SECRET +falcon_access_token=$FALCON_ACCESS_TOKEN +falcon_maintenance_token=$FALCON_MAINTENANCE_TOKEN +unset FALCON_CLIENT_SECRET FALCON_ACCESS_TOKEN FALCON_MAINTENANCE_TOKEN +FALCON_CLIENT_SECRET=$falcon_client_secret +FALCON_ACCESS_TOKEN=$falcon_access_token +FALCON_MAINTENANCE_TOKEN=$falcon_maintenance_token +unset falcon_client_secret falcon_access_token falcon_maintenance_token + print_usage() { cat </dev/null 2>&1; then - die "The 'curl' command is missing. Please install it before continuing. Aborting..." - fi - - version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" - - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -C; then - echo 0 - else - echo 1 - fi -) +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - set -- "$@" - - if [ "$old_curl" -eq 0 ]; then - curl -s -x "$proxy" -L -H "Authorization: Bearer ${cs_falcon_oauth_token}" "$@" - else - echo "Authorization: Bearer ${cs_falcon_oauth_token}" | curl -s -x "$proxy" -L -H @- "$@" - fi + printf 'oauth2-bearer = "%s"\n' "$cs_falcon_oauth_token" | + curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } handle_curl_error() { @@ -299,9 +298,9 @@ aws_ssm_parameter() { token=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") api_endpoint="AmazonSSM.GetParameters" - iam_role="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/)" - aws_my_region="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" - _security_credentials="$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" + iam_role="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/)" + aws_my_region="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)" + _security_credentials="$(printf 'header = "X-aws-ec2-metadata-token: %s"\n' "$token" | curl -s -K- http://169.254.169.254/latest/meta-data/iam/security-credentials/"$iam_role")" access_key_id="$(echo "$_security_credentials" | grep AccessKeyId | sed -e 's/ "AccessKeyId" : "//' -e 's/",$//')" access_key_secret="$(echo "$_security_credentials" | grep SecretAccessKey | sed -e 's/ "SecretAccessKey" : "//' -e 's/",$//')" security_token="$(echo "$_security_credentials" | grep Token | sed -e 's/ "Token" : "//' -e 's/",$//')" @@ -339,17 +338,16 @@ EOF ) response=$( - curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ - -x "$proxy" \ - -H "Authorization: AWS4-HMAC-SHA256 \ - Credential=$access_key_id/$date/$aws_my_region/ssm/aws4_request, \ - SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, \ - Signature=$signature" \ - -H "x-amz-security-token: $security_token" \ - -H "x-amz-target: $api_endpoint" \ - -H "content-type: application/x-amz-json-1.1" \ - -d "$request_data" \ - -H "x-amz-date: $datetime" + { + printf 'header = "Authorization: AWS4-HMAC-SHA256 Credential=%s/%s/%s/ssm/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=%s"\n' \ + "$access_key_id" "$date" "$aws_my_region" "$signature" + printf 'header = "x-amz-security-token: %s"\n' "$security_token" + printf 'header = "x-amz-target: %s"\n' "$api_endpoint" + printf 'header = "content-type: application/x-amz-json-1.1"\n' + printf 'header = "x-amz-date: %s"\n' "$datetime" + } | curl -s "https://ssm.$aws_my_region.amazonaws.com/" \ + -x "$proxy" -K- \ + -d "$request_data" ) handle_curl_error $? if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$'; then diff --git a/bash/migrate/README.md b/bash/migrate/README.md index 65884e4..68086a1 100644 --- a/bash/migrate/README.md +++ b/bash/migrate/README.md @@ -6,7 +6,7 @@ Bash script to migrate Falcon sensor from one CID to another through the Falcon ### Use cURL version 7.55.0 or newer -We have identified a security concern related to cURL versions prior to 7.55, which required request headers to be set using the `-H` option, thus allowing potential secrets to be exposed via the command line. In newer versions of cURL, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. Although our script offers compatibility with the older method by allowing you to set the environment variable `ALLOW_LEGACY_CURL=true`, we strongly urge you to upgrade cURL if your environment permits. +OAuth credentials are supplied to cURL through its configuration input rather than command-line arguments. This also lets cURL treat the bearer token as an authentication credential and prevents it from forwarding the token when a redirect crosses to another host. We strongly recommend cURL 7.55.0 or newer. To check your version of cURL, run the following command: `curl --version` @@ -183,9 +183,6 @@ Other Options This allows specifying the cloud region for unified sensors at installation time. Accepted values are [us-1|us-2|us-3|eu-1|us-gov-1|us-gov-2]. - - ALLOW_LEGACY_CURL (default: false) - To use the legacy version of curl; version < 7.55.0. - - USER_AGENT (default: unset) User agent string to append to the User-Agent header when making requests to the CrowdStrike API. diff --git a/bash/migrate/falcon-linux-migrate.sh b/bash/migrate/falcon-linux-migrate.sh index 258ecc7..bebcd67 100755 --- a/bash/migrate/falcon-linux-migrate.sh +++ b/bash/migrate/falcon-linux-migrate.sh @@ -1,4 +1,24 @@ #!/bin/bash + +case $- in + *x*) + set +x + printf '%s\n' 'WARNING: shell tracing disabled to protect credentials.' >&2 + ;; +esac + +old_falcon_client_secret=$OLD_FALCON_CLIENT_SECRET +new_falcon_client_secret=$NEW_FALCON_CLIENT_SECRET +falcon_access_token=$FALCON_ACCESS_TOKEN +falcon_maintenance_token=$FALCON_MAINTENANCE_TOKEN +falcon_provisioning_token=$FALCON_PROVISIONING_TOKEN +unset OLD_FALCON_CLIENT_SECRET NEW_FALCON_CLIENT_SECRET FALCON_ACCESS_TOKEN FALCON_MAINTENANCE_TOKEN FALCON_PROVISIONING_TOKEN +OLD_FALCON_CLIENT_SECRET=$old_falcon_client_secret +NEW_FALCON_CLIENT_SECRET=$new_falcon_client_secret +FALCON_ACCESS_TOKEN=$falcon_access_token +FALCON_MAINTENANCE_TOKEN=$falcon_maintenance_token +FALCON_PROVISIONING_TOKEN=$falcon_provisioning_token +unset old_falcon_client_secret new_falcon_client_secret falcon_access_token falcon_maintenance_token falcon_provisioning_token # # Bash script to migrate Falcon sensor to another falcon CID. # @@ -110,9 +130,6 @@ Other Options This allows specifying the cloud region for unified sensors at installation time. Accepted values are [us-1|us-2|us-3|eu-1|us-gov-1|us-gov-2]. - - ALLOW_LEGACY_CURL (default: false) - To use the legacy version of curl; version < 7.55.0. - - USER_AGENT (default: unset) User agent string to append to the User-Agent header when making requests to the CrowdStrike API. @@ -198,31 +215,14 @@ uninstall_sensor() { } # Shared functions -old_curl=$( - if ! command -v curl >/dev/null 2>&1; then - die "The 'curl' command is missing. Please install it before continuing. Aborting..." - fi - - version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" - - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -C; then - echo 0 - else - echo 1 - fi -) +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - set -- "$@" - - if [ "$old_curl" -eq 0 ]; then - curl -s -x "$proxy" -L -H "Authorization: Bearer ${cs_falcon_oauth_token}" "$@" - else - echo "Authorization: Bearer ${cs_falcon_oauth_token}" | curl -s -x "$proxy" -L -H @- "$@" - fi + printf 'oauth2-bearer = "%s"\n' "$cs_falcon_oauth_token" | + curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } handle_curl_error() { @@ -472,7 +472,7 @@ get_maintenance_token() { die "Retrieved empty maintenance token from API." fi else - die "Failed to retrieve maintenance token. Response: $response" + die "Failed to retrieve a maintenance token from the Falcon API." fi } @@ -574,7 +574,7 @@ cs_sensor_policy_version() { if echo "$sensor_update_policy" | grep "authorization failed"; then die "Access denied: Please make sure that your Falcon API credentials allow access to sensor update policies (scope Sensor update policies [read])" elif echo "$sensor_update_policy" | grep "invalid bearer token"; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi sensor_update_versions=$(echo "$sensor_update_policy" | json_value "sensor_version") @@ -598,6 +598,20 @@ cs_sensor_policy_version() { IFS=$oldIFS } +verify_sha256() { + local file="$1" expected_sha="$2" local_sha + + if command -v sha256sum >/dev/null 2>&1; then + local_sha=$(sha256sum "$file" | awk '{ print $1 }') + else + local_sha=$(openssl dgst -sha256 "$file" | awk '{ print $NF }') + fi + if [ "$local_sha" != "$expected_sha" ]; then + rm -f "$file" + die "Downloaded sensor installer failed SHA-256 verification." + fi +} + cs_sensor_download() { local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer @@ -621,7 +635,7 @@ cs_sensor_download() { if echo "$existing_installers" | grep "authorization failed"; then die "Access denied: Please make sure that your Falcon API credentials allow sensor download (scope Sensor Download [read])" elif echo "$existing_installers" | grep "invalid bearer token"; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi sha_list=$(echo "$existing_installers" | json_value "sha256") @@ -645,6 +659,8 @@ cs_sensor_download() { handle_curl_error $? + verify_sha256 "$installer" "$sha" + echo "$installer" } @@ -860,7 +876,7 @@ get_falcon_tags() { if echo "$response" | grep "authorization failed" >/dev/null; then die "Access denied: Please make sure your Falcon API credentials allow access to host data (scope Host [read])" elif echo "$response" | grep "invalid bearer token" >/dev/null; then - die "Invalid Access Token: $cs_falcon_oauth_token" + die "Invalid or expired Falcon access token." fi # Extract tags from response diff --git a/powershell/install/falcon_windows_install.ps1 b/powershell/install/falcon_windows_install.ps1 index e1ea7ff..7c38d39 100755 --- a/powershell/install/falcon_windows_install.ps1 +++ b/powershell/install/falcon_windows_install.ps1 @@ -135,6 +135,8 @@ param( [string] $UserAgent ) begin { + Set-PSDebug -Off + if ($PSVersionTable.PSVersion -lt '3.0') { throw "This script requires a miniumum PowerShell 3.0" } @@ -214,7 +216,6 @@ begin { try { $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-FalconAuth - $content:' if ([string]::IsNullOrEmpty($content.access_token)) { $message = 'Unable to authenticate to the CrowdStrike Falcon API. Please check your credentials and try again.' @@ -597,7 +598,7 @@ process { # Begin installation Write-FalconLog 'Installer' 'Installing Falcon Sensor...' - Write-FalconLog 'StartProcess' "Starting installer with parameters: '$InstallParams'" + Write-FalconLog 'StartProcess' 'Starting installer; command-line parameters omitted from the log because they may contain sensitive values' try { $process = (Start-Process -FilePath $LocalFile -ArgumentList $InstallParams -PassThru -ErrorAction SilentlyContinue) Write-FalconLog 'StartProcess' "Started '$LocalFile' ($($process.Id))" diff --git a/powershell/install/falcon_windows_uninstall.ps1 b/powershell/install/falcon_windows_uninstall.ps1 index a7fb5b2..916bb70 100755 --- a/powershell/install/falcon_windows_uninstall.ps1 +++ b/powershell/install/falcon_windows_uninstall.ps1 @@ -115,6 +115,8 @@ param( [string] $UserAgent ) begin { + Set-PSDebug -Off + if ($FalconAccessToken) { if ($FalconCloud -eq "autodiscover") { @@ -200,7 +202,6 @@ begin { try { $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-FalconAuth - $content:' if ([string]::IsNullOrEmpty($content.access_token)) { $Message = 'Unable to authenticate to the CrowdStrike Falcon API. Please check your credentials and try again.' @@ -529,7 +530,6 @@ process { try { $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Body $bodyJson -MaximumRedirection 0 $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'GetToken - $content:' if ($content.errors) { $Message = 'Failed to retrieve maintenance token: ' @@ -539,7 +539,7 @@ process { } else { $MaintenanceToken = $content.resources[0].uninstall_token - Write-FalconLog 'GetToken' "Retrieved maintenance token: $MaintenanceToken" + Write-FalconLog 'GetToken' 'Retrieved maintenance token' $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" } } @@ -576,9 +576,8 @@ process { if ($UninstallTool -eq 'standalone') { # Check if /uninstall parameter is present if ($UninstallParams -match '/?uninstall') { - $OriginalParams = $UninstallParams $UninstallParams = $UninstallParams -replace '/?uninstall\s*', '' -replace '^\s+|\s+$', '' - Write-FalconLog 'ParamValidation' "Removed '/uninstall' parameter for standalone uninstaller. Original: '$OriginalParams', Modified: '$UninstallParams'" + Write-FalconLog 'ParamValidation' "Removed '/uninstall' parameter for standalone uninstaller; parameter values omitted from the log" } # Ensure we have at least /quiet parameter @@ -590,7 +589,7 @@ process { # Begin uninstallation Write-FalconLog 'Uninstaller' 'Uninstalling the Falcon Sensor...' - Write-FalconLog 'StartProcess' "Starting uninstaller with parameters: '$UninstallParams'" + Write-FalconLog 'StartProcess' 'Starting uninstaller; command-line parameters omitted from the log because they may contain sensitive values' $UninstallerProcess = Start-Process -FilePath "$UninstallerPath" -ArgumentList $UninstallParams -PassThru -Wait $UninstallerProcessId = $UninstallerProcess.Id Write-FalconLog 'StartProcess' "Started '$UninstallerPath' ($UninstallerProcessId)" diff --git a/powershell/migrate/falcon_windows_migrate.ps1 b/powershell/migrate/falcon_windows_migrate.ps1 index 2f196ac..909a552 100644 --- a/powershell/migrate/falcon_windows_migrate.ps1 +++ b/powershell/migrate/falcon_windows_migrate.ps1 @@ -135,6 +135,8 @@ param( [string] $UserAgent ) +Set-PSDebug -Off + function Write-RecoveryCsv { param ( @@ -324,7 +326,6 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Headers $oldCloudHeaders -Body $bodyJson -MaximumRedirection 0 $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'GetToken - $content:' if ($content.errors) { $Message = 'Failed to retrieve maintenance token: ' @@ -334,7 +335,7 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst } else { $MaintenanceToken = $content.resources[0].uninstall_token - Write-FalconLog -Source 'Invoke-FalconUninstall' -Message "Retrieved maintenance token: $MaintenanceToken" + Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Retrieved maintenance token' $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" } } @@ -369,7 +370,7 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst # Begin uninstallation Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Uninstalling Falcon Sensor...' - Write-FalconLog -Source 'Invoke-FalconUninstall' -Message "Starting uninstaller with parameters: '$UninstallParams'" + Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Starting uninstaller; command-line parameters omitted from the log because they may contain sensitive values' $UninstallerProcess = Start-Process -FilePath "$UninstallerPath" -ArgumentList $UninstallParams -PassThru -Wait $UninstallerProcessId = $UninstallerProcess.Id Write-FalconLog -Source 'Invoke-FalconUninstall' -Message "Started '$UninstallerPath' ($UninstallerProcessId)" @@ -579,7 +580,7 @@ function Invoke-FalconInstall ([hashtable] $WebRequestParams, [string] $InstallP # Begin installation Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Installing Falcon Sensor..." - Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Starting installer '$LocalFile' with parameters '$InstallParams'" + Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Starting installer '$LocalFile'; command-line parameters omitted from the log because they may contain sensitive values" $process = (Start-Process -FilePath $LocalFile -ArgumentList $InstallParams -PassThru -ErrorAction SilentlyContinue) Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Started '$LocalFile' ($($process.Id))" @@ -1008,7 +1009,6 @@ function Invoke-FalconAuth([hashtable] $WebRequestParams, [string] $BaseUrl, [ha try { $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body $content = ConvertFrom-Json -InputObject $response.Content - Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-FalconAuth - $content:' if ([string]::IsNullOrEmpty($content.access_token)) { $message = 'Unable to authenticate to the CrowdStrike Falcon API. Please check your credentials and try again.' From 191306695d088307e6e178f545012c75da63626d Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Fri, 4 Sep 2026 15:14:50 -0400 Subject: [PATCH 2/3] fix(security): restore EL7 support and make the credential tests able to fail RHEL/CentOS 7 ships curl 7.29.0, which does not know oauth2-bearer. It does not reject the unknown configuration key either: it ignores it, exits 0, and sends the request with no credential at all. The previous commit therefore broke EL7 silently rather than loudly. Each script now detects the capability positively and falls back to a raw Authorization header when it is missing. The credential still travels on curl's configuration input in both modes, so it never reaches the command line. The gate moves from 7.55 to 7.33, the version that added oauth2-bearer, which also stops blocking curl 7.33 through 7.54 for no reason, and it is restored in the uninstall and migrate scripts where it had been dropped. Warning text now names what is actually unverified on very old curl, which is redirect handling, and names the control that bounds it. Two latent defects turned up while doing this. The command -v curl guard sat inside a command substitution, where die only exits the subshell and leaves the variable empty; in falcon-linux-uninstall.sh that guard also called die 39 lines before die was defined, so a host without curl got "die: command not found" instead of the message. Both are fixed. The credential handling suite could not fail. It called rg, which is not installed on ubuntu-latest, so "if rg ...; then fail; fi" read exit 127 as no match and three checks quietly did nothing. Replaced with grep behind a helper that inspects the exit status, so a missing or broken tool fails instead of passing. The mode loop now exercises both credential mechanisms rather than looping over a variable nothing read. The container matrix passed its setup command through a variable that the container shell expanded after parsing, turning && and > into literal arguments, which is why all five legs errored. A dependency script that detects the package manager replaces it, and Oracle Linux 7 and CentOS 7 legs are added, since the oldest curl exercised before this was 7.81. Also: aws_ssm_parameter no longer prints the decrypted response body on failure, since that body can carry the client secret; the live curl test runs on python2 and cannot hang; and the PowerShell test reports every collected failure instead of throwing on the first one. --- .github/scripts/install-test-deps.sh | 53 +++++ .github/scripts/test-credential-handling.sh | 120 +++++++---- .github/scripts/test-curl-oauth-live.sh | 194 ++++++++++++++---- .../test-powershell-credential-handling.ps1 | 7 +- .github/workflows/credential_handling.yml | 15 +- .../falcon-container-sensor-pull/README.md | 4 +- .../falcon-container-sensor-pull.sh | 47 +++-- .../test/test-curl-command.sh | 45 ++-- bash/install/README.md | 6 +- bash/install/falcon-linux-install.sh | 60 ++++-- bash/install/falcon-linux-uninstall.sh | 63 +++++- bash/migrate/README.md | 7 +- bash/migrate/falcon-linux-migrate.sh | 45 +++- 13 files changed, 510 insertions(+), 156 deletions(-) create mode 100644 .github/scripts/install-test-deps.sh diff --git a/.github/scripts/install-test-deps.sh b/.github/scripts/install-test-deps.sh new file mode 100644 index 0000000..3016bfa --- /dev/null +++ b/.github/scripts/install-test-deps.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Installs the packages that .github/scripts/test-curl-oauth-live.sh needs: +# curl and a python interpreter. Only missing packages are installed, so the +# script does nothing on images that already have both. RHEL/CentOS 7 images +# supply curl and python2 already, so no package manager runs there. + +set -eu + +have() { + command -v "$1" >/dev/null 2>&1 +} + +have_python() { + have python3 || have python2 || have python +} + +if have curl && have_python; then + echo 'curl and python are already present; no packages to install.' + exit 0 +fi + +if have apk; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python3" + # shellcheck disable=SC2086 # deliberate word splitting into package names + apk add --no-cache $packages +elif have dnf; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python3" + # shellcheck disable=SC2086 # deliberate word splitting into package names + dnf install -y -q $packages +elif have yum; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python" + # shellcheck disable=SC2086 # deliberate word splitting into package names + yum install -y -q $packages +elif have apt-get; then + packages="" + have curl || packages="$packages curl" + have_python || packages="$packages python3" + apt-get update -qq + # shellcheck disable=SC2086 # deliberate word splitting into package names + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $packages +else + echo 'ERROR: no supported package manager found (apk, dnf, yum, apt-get).' >&2 + exit 1 +fi + +echo 'Test dependencies are installed.' diff --git a/.github/scripts/test-credential-handling.sh b/.github/scripts/test-credential-handling.sh index 155ffe7..9a3db03 100644 --- a/.github/scripts/test-credential-handling.sh +++ b/.github/scripts/test-credential-handling.sh @@ -11,26 +11,56 @@ fail() { exit 1 } +# Report an absent or broken search tool as a failure instead of a pass. grep +# returns 0 for a match, 1 for no match, and 2 or more for an error. Only 1 is +# an acceptable result here. +assert_no_match() { + local description=$1 pattern=$2 tree=$3 include=$4 + local output status + + set +e + output=$(grep -rnE "$pattern" "$repo_root/$tree" --include="$include") + status=$? + set -e + + if [ "$status" -eq 0 ]; then + echo "$output" >&2 + fail "$description" + fi + if [ "$status" -ne 1 ]; then + fail "grep failed with status $status while checking: $description" + fi +} + curl() { printf '%s\n' "$@" >"$CURL_ARGS_FILE" cat >"$CURL_STDIN_FILE" } -export CURL_ARGS_FILE CURL_STDIN_FILE - test_curl_helper() { local script=$1 token_mode=$2 - local helper + local helper mode expected_key helper=$(awk '/^curl_command\(\)/,/^}/' "$repo_root/$script") [ -n "$helper" ] || fail "curl_command not found in $script" eval "$helper" - for old_curl in 0 1; do + # Mode 1 uses the oauth2-bearer configuration key. Mode 0 is the fallback + # for curl older than 7.33.0 and uses a raw Authorization header. Both must + # keep the credential on stdin. + for mode in 1 0; do + # shellcheck disable=SC2034 # read by the curl_command body under eval + curl_has_oauth2_bearer=$mode + if [ "$mode" -eq 1 ]; then + expected_key='oauth2-bearer = "REGRESSION_SECRET_TOKEN"' + else + expected_key='header = "Authorization: Bearer REGRESSION_SECRET_TOKEN"' + fi + CURL_ARGS_FILE="$work_dir/args" CURL_STDIN_FILE="$work_dir/stdin" + # shellcheck disable=SC2034 # read by the curl_command body under eval proxy="" - export proxy cs_falcon_oauth_token="REGRESSION_SECRET_TOKEN" if [ "$token_mode" = "argument" ]; then @@ -40,16 +70,18 @@ test_curl_helper() { fi if grep -qF "$cs_falcon_oauth_token" "$CURL_ARGS_FILE"; then - fail "$script exposed the bearer token in curl arguments (old_curl=$old_curl)" + fail "$script exposed the bearer token in curl arguments (mode=$mode)" fi grep -qF 'https://api.example.invalid/resource' "$CURL_ARGS_FILE" || - fail "$script did not pass the expected URL (old_curl=$old_curl)" + fail "$script did not pass the expected URL (mode=$mode)" grep -qF "$cs_falcon_oauth_token" "$CURL_STDIN_FILE" || - fail "$script did not provide the bearer token through stdin (old_curl=$old_curl)" + fail "$script did not provide the bearer token through stdin (mode=$mode)" + grep -qF "$expected_key" "$CURL_STDIN_FILE" || + fail "$script did not use the expected credential mechanism (mode=$mode)" grep -qF -- '--proto' "$CURL_ARGS_FILE" || - fail "$script did not restrict the request protocol (old_curl=$old_curl)" + fail "$script did not restrict the request protocol (mode=$mode)" grep -qF -- '--proto-redir' "$CURL_ARGS_FILE" || - fail "$script did not restrict the redirect protocol (old_curl=$old_curl)" + fail "$script did not restrict the redirect protocol (mode=$mode)" done } @@ -85,37 +117,51 @@ test_hash_verification() { helper=$(awk '/^verify_sha256\(\)/,/^}/' "$repo_root/$script") [ -n "$helper" ] || fail "verify_sha256 not found in $script" - eval "$helper" - die() { exit 1; } - - test_file="$work_dir/installer" - printf '%s' 'verified installer content' >"$test_file" - expected_sha=$(openssl dgst -sha256 "$test_file" | awk '{ print $NF }') - verify_sha256 "$test_file" "$expected_sha" || - fail "$script rejected a valid installer hash" - - if (verify_sha256 "$test_file" '0000000000000000000000000000000000000000000000000000000000000000'); then - fail "$script accepted an invalid installer hash" - fi - [ ! -e "$test_file" ] || fail "$script retained an installer with an invalid hash" + ( + eval "$helper" + # verify_sha256 calls die on a mismatch; keep the stub inside the subshell. + # shellcheck disable=SC2329 # invoked indirectly by verify_sha256 + die() { exit 1; } + + test_file="$work_dir/installer" + printf '%s' 'verified installer content' >"$test_file" + expected_sha=$(openssl dgst -sha256 "$test_file" | awk '{ print $NF }') + verify_sha256 "$test_file" "$expected_sha" || + fail "$script rejected a valid installer hash" + + if (verify_sha256 "$test_file" '0000000000000000000000000000000000000000000000000000000000000000'); then + fail "$script accepted an invalid installer hash" + fi + [ ! -e "$test_file" ] || fail "$script retained an installer with an invalid hash" + ) } test_hash_verification bash/install/falcon-linux-install.sh test_hash_verification bash/migrate/falcon-linux-migrate.sh -if rg -n 'Invalid Access Token:.*\$cs_falcon_oauth_token|Failed to retrieve maintenance token\. Response:' \ - "$repo_root/bash" --glob '*.sh'; then - fail 'a Bash error path exposes a credential or raw maintenance-token response' -fi - -if rg -n 'curl .*X-aws-ec2-metadata-token:.*\$token' \ - "$repo_root/bash" --glob '*.sh'; then - fail 'an EC2 metadata token is exposed in curl arguments' -fi - -if rg -n '(Invoke-FalconAuth|GetToken) - \$content:|Retrieved maintenance token:|Starting .*parameters.*\$(Install|Uninstall)Params' \ - "$repo_root/powershell" --glob '*.ps1'; then - fail 'a PowerShell log statement exposes an authentication or installer token' -fi +# The arguments below are grep patterns, not shell expansions. +# shellcheck disable=SC2016 +assert_no_match \ + 'a Bash error path exposes a credential or raw maintenance-token response' \ + 'Invalid Access Token:.*\$cs_falcon_oauth_token|Failed to retrieve maintenance token\. Response:' \ + bash '*.sh' + +# shellcheck disable=SC2016 +assert_no_match \ + 'an EC2 metadata token is exposed in curl arguments' \ + 'curl .*X-aws-ec2-metadata-token:.*\$token' \ + bash '*.sh' + +# shellcheck disable=SC2016 +assert_no_match \ + 'an AWS SSM Parameter Store error path prints the decrypted response body' \ + 'AWS SSM Parameter Store[^"]*\$response' \ + bash '*.sh' + +# shellcheck disable=SC2016 +assert_no_match \ + 'a PowerShell log statement exposes an authentication or installer token' \ + '(Invoke-FalconAuth|GetToken) - \$content:|Retrieved maintenance token:|Starting .*parameters.*\$(Install|Uninstall)Params' \ + powershell '*.ps1' echo 'PASS: credential handling regression checks' diff --git a/.github/scripts/test-curl-oauth-live.sh b/.github/scripts/test-curl-oauth-live.sh index cf7a314..fba80f2 100644 --- a/.github/scripts/test-curl-oauth-live.sh +++ b/.github/scripts/test-curl-oauth-live.sh @@ -1,9 +1,14 @@ #!/bin/sh +# Confirms how the installed curl handles an OAuth 2 bearer credential that +# arrives on its configuration input. The script adapts to the curl it finds, +# so it runs on RHEL/CentOS 7 (curl 7.29.0, python2) with no extra packages. + set -eu work_dir=$(mktemp -d) server_pids="" + cleanup() { for pid in $server_pids; do kill "$pid" 2>/dev/null || true @@ -12,60 +17,161 @@ cleanup() { } trap cleanup EXIT -start_capture_server() { - port=$1 output=$2 - python3 -c 'import http.server, socketserver, sys -socketserver.TCPServer.allow_reuse_address = True -class Handler(http.server.BaseHTTPRequestHandler): +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +python_bin="" +for candidate in python3 python2 python; do + if command -v "$candidate" >/dev/null 2>&1; then + python_bin=$candidate + break + fi +done +[ -n "$python_bin" ] || fail 'no python interpreter is available for the capture server' + +server_script="$work_dir/capture_server.py" +cat >"$server_script" <<'PY' +import sys + +try: + from http.server import BaseHTTPRequestHandler, HTTPServer +except ImportError: # Python 2, which is all that RHEL/CentOS 7 provides. + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +output_path = sys.argv[1] +port = int(sys.argv[2]) +redirect_to = sys.argv[3] if len(sys.argv) > 3 else "" + + +class Handler(BaseHTTPRequestHandler): def do_GET(self): - with open(sys.argv[1], "w") as output: - output.write(self.headers.get("Authorization", "")) - self.send_response(204) + # The marker shows that a request arrived. Without it, an empty capture + # file cannot show the difference between a credential that curl + # removed and a connection that never happened. + with open(output_path, "w") as handle: + handle.write("REQUEST_ARRIVED|auth=[%s]" + % self.headers.get("Authorization", "")) + if redirect_to: + self.send_response(302) + self.send_header("Location", redirect_to) + else: + self.send_response(204) self.end_headers() + def log_message(self, *args): pass -http.server.HTTPServer(("127.0.0.1", int(sys.argv[2])), Handler).handle_request() -' "$output" "$port" & - last_server_pid=$! - server_pids="$server_pids $last_server_pid" + + +HTTPServer.allow_reuse_address = True +# Bind every interface. curl can resolve "localhost" to ::1, and a server bound +# only to 127.0.0.1 refuses that connection and gives a false failure. +server = HTTPServer(("0.0.0.0", port), Handler) + +with open(output_path + ".ready", "w") as handle: + handle.write("ready") + +# Serve one request, then stop. The timeout makes sure the process always ends. +server.timeout = 20 +server.handle_request() +PY + +start_server() { + output=$1 + port=$2 + redirect_to=${3:-} + + rm -f "$output" "$output.ready" + "$python_bin" "$server_script" "$output" "$port" "$redirect_to" & + server_pids="$server_pids $!" +} + +wait_for_server() { + output=$1 + attempt=0 + + while [ "$attempt" -lt 15 ]; do + if [ -f "$output.ready" ]; then + return 0 + fi + sleep 1 + attempt=$((attempt + 1)) + done + fail "the capture server for $output did not start" +} + +assert_marker() { + file=$1 + expected=$2 + description=$3 + actual="" + + [ -s "$file" ] || fail "$description (no request reached the capture server)" + actual=$(cat "$file") + [ "$actual" = "$expected" ] || + fail "$description (expected '$expected', received '$actual')" +} + +auth_config_for() { + case $1 in + oauth2-bearer) printf 'oauth2-bearer = "%s"\n' "$token" ;; + header) printf 'header = "Authorization: Bearer %s"\n' "$token" ;; + *) fail "unknown credential mechanism: $1" ;; + esac } token=CONTAINER_LIVE_TEST_TOKEN curl --version | head -n 1 -start_capture_server 28768 "$work_dir/header" -header_pid=$last_server_pid -sleep 1 -printf 'oauth2-bearer = "%s"\n' "$token" | +# curl 7.33.0 added the oauth2-bearer option. Compare the version with awk +# because busybox sort has no -V option. +if curl --version | head -n 1 | + awk '{ split($2, v, "."); exit !(v[1] > 7 || (v[1] == 7 && v[2] >= 33)) }'; then + active_mode=oauth2-bearer +else + active_mode=header +fi +echo "Credential mechanism under test: $active_mode" + +# 1. The mechanism this curl uses must deliver the credential to the same host. +start_server "$work_dir/direct" 28768 +wait_for_server "$work_dir/direct" +auth_config_for "$active_mode" | curl --silent --show-error -K- --url http://127.0.0.1:28768/test -wait "$header_pid" -[ "$(cat "$work_dir/header")" = "Bearer $token" ] || { - echo 'FAIL: OAuth configuration did not transmit the expected header' >&2 - exit 1 -} +assert_marker "$work_dir/direct" "REQUEST_ARRIVED|auth=[Bearer $token]" \ + "the $active_mode mechanism did not transmit the credential" -start_capture_server 28770 "$work_dir/redirect-header" -target_pid=$last_server_pid -python3 -c 'import http.server, socketserver -socketserver.TCPServer.allow_reuse_address = True -class Handler(http.server.BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(302) - self.send_header("Location", "http://localhost:28770/target") - self.end_headers() - def log_message(self, *args): - pass -http.server.HTTPServer(("127.0.0.1", 28769), Handler).handle_request() -' & -redirect_pid=$! -server_pids="$server_pids $redirect_pid" -sleep 1 -printf 'oauth2-bearer = "%s"\n' "$token" | +# 2. curl must remove the credential when a redirect crosses to another host. +start_server "$work_dir/redirect" 28769 'http://localhost:28770/target' +start_server "$work_dir/target" 28770 +wait_for_server "$work_dir/redirect" +wait_for_server "$work_dir/target" +auth_config_for "$active_mode" | curl --silent --show-error -L -K- --url http://127.0.0.1:28769/start -wait "$redirect_pid" "$target_pid" -[ ! -s "$work_dir/redirect-header" ] || { - echo 'FAIL: OAuth credential followed a cross-host redirect' >&2 - exit 1 -} +assert_marker "$work_dir/redirect" "REQUEST_ARRIVED|auth=[Bearer $token]" \ + 'the first request of the redirect chain did not carry the credential' +assert_marker "$work_dir/target" 'REQUEST_ARRIVED|auth=[]' \ + "the $active_mode credential crossed a redirect to another host" + +# 3. On a modern curl, also confirm that the older fallback still works. This +# keeps the fallback path tested on the machines that do not need it. +if [ "$active_mode" = "oauth2-bearer" ]; then + start_server "$work_dir/fallback" 28771 + wait_for_server "$work_dir/fallback" + auth_config_for header | + curl --silent --show-error -K- --url http://127.0.0.1:28771/test + assert_marker "$work_dir/fallback" "REQUEST_ARRIVED|auth=[Bearer $token]" \ + 'the raw-header fallback did not transmit the credential' +fi + +# 4. A stray bare argument must not become a request. This is the guard that +# stops a leaked credential from reaching a name server. +if auth_config_for "$active_mode" | + curl --silent --show-error --proto '=https' --proto-redir '=https' -K- \ + "$token" >/dev/null 2>"$work_dir/proto-error"; then + fail 'the protocol guard accepted a bare argument as a request' +fi +echo "Protocol guard rejected a bare argument: $(cat "$work_dir/proto-error")" -echo 'PASS: OAuth stdin configuration and cross-host redirect protection' +echo "PASS: curl credential handling ($active_mode mode)" diff --git a/.github/scripts/test-powershell-credential-handling.ps1 b/.github/scripts/test-powershell-credential-handling.ps1 index a1bdc8e..05899aa 100644 --- a/.github/scripts/test-powershell-credential-handling.ps1 +++ b/.github/scripts/test-powershell-credential-handling.ps1 @@ -53,7 +53,12 @@ foreach ($RelativePath in $Scripts) { } if ($Failures.Count -gt 0) { - $Failures | ForEach-Object { Write-Error $_ } + # -ErrorAction Continue overrides $ErrorActionPreference = 'Stop' for these + # calls. Without it the first Write-Error throws, and only one of the + # collected failures is ever reported. + foreach ($Failure in $Failures) { + Write-Error $Failure -ErrorAction Continue + } exit 1 } diff --git a/.github/workflows/credential_handling.yml b/.github/workflows/credential_handling.yml index 96d80dd..911e65b 100644 --- a/.github/workflows/credential_handling.yml +++ b/.github/workflows/credential_handling.yml @@ -3,6 +3,7 @@ name: "CI: credential handling" on: push: paths: + - '.github/scripts/install-test-deps.sh' - '.github/scripts/test-credential-handling.sh' - '.github/scripts/test-curl-oauth-live.sh' - '.github/scripts/test-powershell-credential-handling.ps1' @@ -16,6 +17,7 @@ on: - 'powershell/migrate/falcon_windows_migrate.ps1' pull_request: paths: + - '.github/scripts/install-test-deps.sh' - '.github/scripts/test-credential-handling.sh' - '.github/scripts/test-curl-oauth-live.sh' - '.github/scripts/test-powershell-credential-handling.ps1' @@ -58,21 +60,20 @@ jobs: fail-fast: false matrix: include: + - name: Oracle Linux 7 + image: oraclelinux:7 + - name: CentOS 7 + image: centos:7 - name: Ubuntu 22.04 image: ubuntu:22.04 - setup: apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl python3 >/dev/null - name: Debian 12 image: debian:12-slim - setup: apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl python3 >/dev/null - name: Alpine 3.20 image: alpine:3.20 - setup: apk add --no-cache curl python3 >/dev/null - name: AlmaLinux 9 image: almalinux:9 - setup: dnf install -y -q python3 >/dev/null - name: Amazon Linux 2023 image: amazonlinux:2023 - setup: dnf install -y -q python3 >/dev/null steps: - name: Check out code uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -82,10 +83,8 @@ jobs: - name: Test packaged curl env: CONTAINER_IMAGE: ${{ matrix.image }} - CONTAINER_SETUP: ${{ matrix.setup }} run: | docker run --rm \ --volume "$PWD:/repo:ro" \ - --env CONTAINER_SETUP \ "$CONTAINER_IMAGE" \ - sh -c '$CONTAINER_SETUP && sh /repo/.github/scripts/test-curl-oauth-live.sh' + sh -c 'sh /repo/.github/scripts/install-test-deps.sh && sh /repo/.github/scripts/test-curl-oauth-live.sh' diff --git a/bash/containers/falcon-container-sensor-pull/README.md b/bash/containers/falcon-container-sensor-pull/README.md index dada9a8..9482086 100644 --- a/bash/containers/falcon-container-sensor-pull/README.md +++ b/bash/containers/falcon-container-sensor-pull/README.md @@ -50,9 +50,9 @@ CrowdStrike now provides unified images that work across all regions: ## Security recommendations -### Use cURL version 7.55.0 or later +### Use cURL version 7.33.0 or later -OAuth credentials are supplied to cURL through its configuration input rather than command-line arguments. This also lets cURL treat the bearer token as an authentication credential and prevents it from forwarding the token when a redirect crosses to another host. **We recommend upgrading cURL to version 7.55.0 or later.** If this is not possible, `--allow-legacy-curl` bypasses the version check without reverting to command-line credential handling. +OAuth credentials go to cURL through its configuration input instead of command-line arguments, so the credential never appears in the process list. cURL 7.33.0 and later accept the credential as an OAuth 2 bearer token, which also lets cURL remove the credential when a redirect crosses to another host. On older cURL the script can send the same credential as a raw `Authorization` header — still through the configuration input, still off the command line — but it cannot confirm the redirect behavior of a cURL that old. The script restricts every request and redirect to HTTPS. To accept this and continue on an older cURL, use `--allow-legacy-curl`. To check your version of cURL, run the following command: `curl --version` diff --git a/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh b/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh index d8556f3..f80bff0 100755 --- a/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh +++ b/bash/containers/falcon-container-sensor-pull/falcon-container-sensor-pull.sh @@ -241,27 +241,39 @@ while [ $# != 0 ]; do shift done -# Check if curl is greater or equal to 7.55 -old_curl=$( +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi + +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" + minimum="7.33" - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -c >/dev/null 2>&1; then - echo 0 - else + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then echo 1 + else + echo 0 fi ) -# Old curl print warning message -if [ "$old_curl" -eq 0 ]; then - if [ "${ALLOW_LEGACY_CURL}" != "true" ]; then +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then echo """ -WARNING: Your version of curl is below the supported 7.55.0 baseline. -OAuth credentials remain protected from command-line exposure in compatibility mode. +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. + +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. -To bypass this warning, set the optional flag --allow-legacy-curl +To accept this and continue, set the optional flag --allow-legacy-curl """ exit 1 fi @@ -300,9 +312,14 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - local token="$1" + local token="$1" auth_config shift - printf 'oauth2-bearer = "%s"\n' "$token" | + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$token") + else + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$token") + fi + printf '%s\n' "$auth_config" | curl -s -L --proto '=https' --proto-redir '=https' -K- "$@" } diff --git a/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh b/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh index e7df88f..0a79df7 100644 --- a/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh +++ b/bash/containers/falcon-container-sensor-pull/test/test-curl-command.sh @@ -8,8 +8,8 @@ # # This test loads curl_command() from the shipped script, replaces curl with a # stub that records its argument vector, and checks that the token is never -# handed to curl as an argument. The token is supplied through curl's -# credential-aware OAuth configuration on stdin. +# handed to curl as an argument. The token always travels on curl's +# configuration input, in both credential mechanisms that the script supports. # # Usage: sh test-curl-command.sh [path-to-falcon-container-sensor-pull.sh] @@ -102,7 +102,8 @@ assert_token_not_in_arguments() { } assert_file_contains() { - if grep -qF "$2" "$3"; then + # Use -- so that a pattern starting with a dash is not read as an option. + if grep -qF -- "$2" "$3"; then pass "$1" else fail "$1" @@ -114,15 +115,35 @@ assert_url_passed_through() { assert_file_contains "$1: request URL reached curl" "$URL" "$ARGV_FILE" } -# Credential-aware OAuth configuration prevents forwarding the token when a -# redirect crosses to another host. -echo "case: OAuth credential over stdin (-K-)" -curl_command "$TOKEN" "$URL" -assert_token_not_in_arguments "OAuth config" -assert_url_passed_through "OAuth config" -assert_file_contains "OAuth config: token delivered over stdin" \ - "oauth2-bearer = \"$TOKEN\"" "$STDIN_FILE" +# curl_command() picks its credential mechanism from curl_has_oauth2_bearer. +# Mode 1 uses the oauth2-bearer configuration key, which curl 7.33.0 added. +# Mode 0 is the fallback for older curl and sends a raw Authorization header. +# Both must keep the token on stdin and off the argument vector. +for mode in 1 0; do + # shellcheck disable=SC2034 # read by the curl_command body loaded with eval + curl_has_oauth2_bearer=$mode + if [ "$mode" -eq 1 ]; then + label="oauth2-bearer config" + expected="oauth2-bearer = \"$TOKEN\"" + else + label="raw header fallback" + expected="header = \"Authorization: Bearer $TOKEN\"" + fi + + echo "case: $label (curl_has_oauth2_bearer=$mode)" + curl_command "$TOKEN" "$URL" + assert_token_not_in_arguments "$label" + assert_url_passed_through "$label" + assert_file_contains "$label: token delivered over stdin" \ + "$expected" "$STDIN_FILE" + assert_file_contains "$label: curl reads the configuration from stdin" \ + "-K-" "$ARGV_FILE" + assert_file_contains "$label: request protocol restricted" \ + "--proto" "$ARGV_FILE" + assert_file_contains "$label: redirect protocol restricted" \ + "--proto-redir" "$ARGV_FILE" + echo +done -echo echo "passed: $PASS failed: $FAIL" [ "$FAIL" -eq 0 ] diff --git a/bash/install/README.md b/bash/install/README.md index 9dc135d..913fe41 100644 --- a/bash/install/README.md +++ b/bash/install/README.md @@ -7,9 +7,9 @@ environment variable. Consult the Environment Variables for each script for more ## Security Recommendations -### Use cURL version 7.55.0 or newer +### Use cURL version 7.33.0 or newer -OAuth credentials are supplied to cURL through its configuration input rather than command-line arguments. This also lets cURL treat the bearer token as an authentication credential and prevents it from forwarding the token when a redirect crosses to another host. We strongly recommend cURL 7.55.0 or newer. If compatibility mode is required, `ALLOW_LEGACY_CURL=true` bypasses the version check without reverting to command-line credential handling. +OAuth credentials go to cURL through its configuration input instead of command-line arguments, so the credential never appears in the process list. cURL 7.33.0 and newer accept the credential as an OAuth 2 bearer token, which also lets cURL remove the credential when a redirect crosses to another host. On older cURL the script can send the same credential as a raw `Authorization` header — still through the configuration input, still off the command line — but it cannot confirm the redirect behavior of a cURL that old. The script restricts every request and redirect to HTTPS. To accept this and continue on an older cURL, set `ALLOW_LEGACY_CURL=true`. To check your version of cURL, run the following command: `curl --version` @@ -182,7 +182,7 @@ Other Options The path to download the falcon sensor to. - ALLOW_LEGACY_CURL (default: false) - To use the legacy version of curl; version < 7.55.0. + To continue on a version of curl older than 7.33.0. - GET_ACCESS_TOKEN (default: false) Prints an access token and exits. diff --git a/bash/install/falcon-linux-install.sh b/bash/install/falcon-linux-install.sh index 22298b5..e8bd9d0 100755 --- a/bash/install/falcon-linux-install.sh +++ b/bash/install/falcon-linux-install.sh @@ -98,7 +98,7 @@ Other Options The path to download the falcon sensor to. - ALLOW_LEGACY_CURL (default: false) - To use the legacy version of curl; version < 7.55.0. + To continue on a version of curl older than 7.33.0. - GET_ACCESS_TOKEN (default: false) Prints an access token and exits. @@ -551,10 +551,12 @@ EOF -d "$request_data" ) handle_curl_error $? - if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" - elif ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" + if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$' || + ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then + # The response body holds the decrypted parameter value, so report only + # the error message that AWS returns and never the body itself. + ssm_error=$(echo "$response" | json_value "message" 1) + die "Unexpected response from AWS SSM Parameter Store for parameter '$param_name'.${ssm_error:+ AWS reported:$ssm_error}" fi echo "$response" } @@ -644,31 +646,39 @@ cs_cloud() { esac } -# Check if curl is greater or equal to 7.55 -old_curl=$( - if ! command -v curl >/dev/null 2>&1; then - die "The 'curl' command is missing. Please install it before continuing. Aborting..." - fi +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( version=$(curl --version | head -n 1 | awk '{ print $2 }') - minimum="7.55" + minimum="7.33" - # Check if the version is less than the minimum - if printf "%s\n" "$version" "$minimum" | sort -V -C; then - echo 0 - else + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then echo 1 + else + echo 0 fi ) -# Old curl print warning message -if [ "$old_curl" -eq 0 ]; then - if [ "${ALLOW_LEGACY_CURL}" != "true" ]; then +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then echo """ -WARNING: Your version of curl is below the supported 7.55.0 baseline. -OAuth credentials remain protected from command-line exposure in compatibility mode. +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. + +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. -To bypass this warning, set the environment variable ALLOW_LEGACY_CURL=true +To accept this and continue, set the environment variable ALLOW_LEGACY_CURL=true """ exit 1 fi @@ -709,7 +719,13 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - printf 'oauth2-bearer = "%s"\n' "$cs_falcon_oauth_token" | + local auth_config + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$cs_falcon_oauth_token") + else + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$cs_falcon_oauth_token") + fi + printf '%s\n' "$auth_config" | curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } diff --git a/bash/install/falcon-linux-uninstall.sh b/bash/install/falcon-linux-uninstall.sh index 9e2dceb..2644102 100755 --- a/bash/install/falcon-linux-uninstall.sh +++ b/bash/install/falcon-linux-uninstall.sh @@ -65,6 +65,9 @@ Other Options: - FALCON_APP (default: unset) The proxy port for the sensor to use when communicating with CrowdStrike. + - ALLOW_LEGACY_CURL (default: false) + To continue on a version of curl older than 7.33.0. + - USER_AGENT (default: unset) User agent string to append to the User-Agent header when making requests to the CrowdStrike API. @@ -243,13 +246,15 @@ get_maintenance_token() { fi } -if ! command -v curl >/dev/null 2>&1; then - die "The 'curl' command is missing. Please install it before continuing. Aborting..." -fi - curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - printf 'oauth2-bearer = "%s"\n' "$cs_falcon_oauth_token" | + local auth_config + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$cs_falcon_oauth_token") + else + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$cs_falcon_oauth_token") + fi + printf '%s\n' "$auth_config" | curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } @@ -287,6 +292,44 @@ die() { exit 1 } +if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." +fi + +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( + version=$(curl --version | head -n 1 | awk '{ print $2 }') + minimum="7.33" + + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then + echo 1 + else + echo 0 + fi +) + +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then + echo """ +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. + +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. + +To accept this and continue, set the environment variable ALLOW_LEGACY_CURL=true +""" + exit 1 + fi +fi + aws_ssm_parameter() { local param_name="$1" @@ -350,10 +393,12 @@ EOF -d "$request_data" ) handle_curl_error $? - if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" - elif ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then - die "Unexpected response from AWS SSM Parameter Store: $response" + if ! echo "$response" | grep -q '^.*"InvalidParameters":\[\].*$' || + ! echo "$response" | grep -q '^.*'"${param_name}"'.*$'; then + # The response body holds the decrypted parameter value, so report only + # the error message that AWS returns and never the body itself. + ssm_error=$(echo "$response" | json_value "message" 1) + die "Unexpected response from AWS SSM Parameter Store for parameter '$param_name'.${ssm_error:+ AWS reported:$ssm_error}" fi echo "$response" } diff --git a/bash/migrate/README.md b/bash/migrate/README.md index 68086a1..8bc2fba 100644 --- a/bash/migrate/README.md +++ b/bash/migrate/README.md @@ -4,9 +4,9 @@ Bash script to migrate Falcon sensor from one CID to another through the Falcon ## Security Recommendations -### Use cURL version 7.55.0 or newer +### Use cURL version 7.33.0 or newer -OAuth credentials are supplied to cURL through its configuration input rather than command-line arguments. This also lets cURL treat the bearer token as an authentication credential and prevents it from forwarding the token when a redirect crosses to another host. We strongly recommend cURL 7.55.0 or newer. +OAuth credentials go to cURL through its configuration input instead of command-line arguments, so the credential never appears in the process list. cURL 7.33.0 and newer accept the credential as an OAuth 2 bearer token, which also lets cURL remove the credential when a redirect crosses to another host. On older cURL the script can send the same credential as a raw `Authorization` header — still through the configuration input, still off the command line — but it cannot confirm the redirect behavior of a cURL that old. The script restricts every request and redirect to HTTPS. To accept this and continue on an older cURL, set `ALLOW_LEGACY_CURL=true`. To check your version of cURL, run the following command: `curl --version` @@ -183,6 +183,9 @@ Other Options This allows specifying the cloud region for unified sensors at installation time. Accepted values are [us-1|us-2|us-3|eu-1|us-gov-1|us-gov-2]. + - ALLOW_LEGACY_CURL (default: false) + To continue on a version of curl older than 7.33.0. + - USER_AGENT (default: unset) User agent string to append to the User-Agent header when making requests to the CrowdStrike API. diff --git a/bash/migrate/falcon-linux-migrate.sh b/bash/migrate/falcon-linux-migrate.sh index bebcd67..930f314 100755 --- a/bash/migrate/falcon-linux-migrate.sh +++ b/bash/migrate/falcon-linux-migrate.sh @@ -130,6 +130,9 @@ Other Options This allows specifying the cloud region for unified sensors at installation time. Accepted values are [us-1|us-2|us-3|eu-1|us-gov-1|us-gov-2]. + - ALLOW_LEGACY_CURL (default: false) + To continue on a version of curl older than 7.33.0. + - USER_AGENT (default: unset) User agent string to append to the User-Agent header when making requests to the CrowdStrike API. @@ -219,9 +222,49 @@ if ! command -v curl >/dev/null 2>&1; then die "The 'curl' command is missing. Please install it before continuing. Aborting..." fi +# curl 7.33.0 added the oauth2-bearer option. Older versions ignore the option +# without an error, which sends the request with no credential at all. +curl_has_oauth2_bearer=$( + version=$(curl --version | head -n 1 | awk '{ print $2 }') + minimum="7.33" + + # sort -C succeeds when the input is already in order, so print the minimum + # first. The check then also accepts a version equal to the minimum. + if printf "%s\n" "$minimum" "$version" | sort -V -C; then + echo 1 + else + echo 0 + fi +) + +if [ "$curl_has_oauth2_bearer" -eq 0 ]; then + if [ "${ALLOW_LEGACY_CURL:-false}" != "true" ]; then + echo """ +WARNING: Your version of curl is older than 7.33.0 and cannot use the +oauth2-bearer option. The script can instead send the credential as a raw +Authorization header. The credential still travels on the curl configuration +input and stays off the command line either way. + +What is not verified on curl this old is redirect handling: the script cannot +confirm that your curl removes the credential when a redirect crosses to +another host. The script restricts every request and redirect to HTTPS, so the +credential can only ever go to an HTTPS host. + +To accept this and continue, set the environment variable ALLOW_LEGACY_CURL=true +""" + exit 1 + fi +fi + curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - printf 'oauth2-bearer = "%s"\n' "$cs_falcon_oauth_token" | + local auth_config + if [ "$curl_has_oauth2_bearer" -eq 1 ]; then + auth_config=$(printf 'oauth2-bearer = "%s"' "$cs_falcon_oauth_token") + else + auth_config=$(printf 'header = "Authorization: Bearer %s"' "$cs_falcon_oauth_token") + fi + printf '%s\n' "$auth_config" | curl -s -x "$proxy" -L --proto '=https' --proto-redir '=https' -K- "$@" } From d5dd243f7bbf23f67221f413a430ddadb6e37c8f Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Fri, 4 Sep 2026 15:18:50 -0400 Subject: [PATCH 3/3] fix(ci): keep test and CI helper scripts out of release assets The release job swept the whole tree for *.sh and *.ps1, so every test and CI helper script added on this branch would have been uploaded as a release asset and listed in checksum.txt. That is five extra files, including one named install-test-deps.sh, which is exactly the sort of thing someone would mistake for something they should run. Restricting the search to bash, powershell and systemd, and skipping bash/**/test/, brings the asset list back to the same 8 scripts and 3 zips that a release produced before. Verified by running the whole step in a container and diffing the result against the file list on main. --- .github/workflows/release.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75a16dc..daceba6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,8 +31,14 @@ jobs: zip -r ../../systemd_podman.zip * popd - # Create a list of files to upload - list_of_files=$(find . -type f \( -name "*.sh" -o -name "*.ps1" -o -name "*.zip" \)) + # Create a list of files to upload. Restrict the search to the + # directories that hold shipped artifacts, so CI helper scripts under + # .github/scripts and unit tests under bash/**/test/ stay out of the + # release assets and out of checksum.txt. + list_of_files=$( + find ./bash ./powershell ./systemd -type f \( -name "*.sh" -o -name "*.ps1" \) -not -path '*/test/*' + find . -maxdepth 1 -type f -name "*.zip" + ) # Create checksum.txt for file in $list_of_files; do