From a0311fc4980c35293da9d4ef5e98f89b3e89c55e Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Tue, 8 Sep 2026 17:06:57 -0400 Subject: [PATCH] feat: add redacted FALCON_DEBUG mode for bash and PowerShell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support currently has to reach for `bash -x` or `Set-PSDebug -Trace` to diagnose a failing install, and both print credentials. This adds an opt-in debug mode that answers the questions support actually asks, without ever printing a value that is not known to be safe. bash gets `FALCON_DEBUG=1` or `--debug`; PowerShell gets `-FalconDebug` or `$env:FALCON_DEBUG`. Markers cover the detected OS, architecture, kernel and package manager; the exact FQL sensor query filter and how many installers matched; which installer was selected, its size and SHA-256 check; the API route, HTTP status and curl exit code for every call; the resolved sensor update policy version; and the installed sensor version and AID. The filter is the point of the whole feature. "No sensor found for OS" and "why did it pick that version" are unanswerable without seeing the query that was sent, and it is assembled from four separately derived pieces. Redaction is fail-closed. Only keys on a fixed allow-list keep their value; everything else becomes `[DROPPED]`, and a bare token with no key is dropped entirely. Anything user-supplied that could hold a secret is reported as presence only, under a distinct `_set` key — `provisioning_token` is not allow-listed, so a future line that prints it still drops. Customer data is reported as counts, not contents. In PowerShell, values that can contain a space are passed through `-Pairs` rather than a joined string. An FQL filter can hold a multi-word policy name, and any scheme that splits a joined string would either mangle it or, worse, glue a bare word onto an allow-listed value and print it. Debug adds no request of its own. `curl_command` already dumps headers and buffers the body, so the marker reports the status and exit code it has computed; the curl invocation is untouched. The exit code is reported inside `handle_curl_error`, which receives it on every failure path, so no call site captures `$?` and none of the `||` chains change. Neither language re-enables tracing. The `set +x` guard and `Set-PSDebug -Off` both stay, and no curl or Invoke-WebRequest gains a verbose flag. The PowerShell catch blocks now record a status code instead of serializing the whole exception object into the on-disk log. READMEs document the new mode with sample output from real runs, and stop recommending `bash -x` and `Set-PSDebug -Trace` for support. --- .../falcon-container-sensor-pull/README.md | 33 +++ .../falcon-container-sensor-pull.sh | 139 +++++++++++-- bash/install/README.md | 71 ++++++- bash/install/falcon-linux-install.sh | 178 +++++++++++++++- bash/install/falcon-linux-uninstall.sh | 154 +++++++++++++- bash/migrate/README.md | 41 +++- bash/migrate/falcon-linux-migrate.sh | 194 ++++++++++++++++-- powershell/install/README.md | 69 +++++-- powershell/install/falcon_windows_install.ps1 | 162 ++++++++++++++- .../install/falcon_windows_uninstall.ps1 | 126 +++++++++++- powershell/migrate/README.md | 78 ++++--- powershell/migrate/falcon_windows_migrate.ps1 | 177 +++++++++++++++- 12 files changed, 1282 insertions(+), 140 deletions(-) diff --git a/bash/containers/falcon-container-sensor-pull/README.md b/bash/containers/falcon-container-sensor-pull/README.md index 51f2266..1796358 100644 --- a/bash/containers/falcon-container-sensor-pull/README.md +++ b/bash/containers/falcon-container-sensor-pull/README.md @@ -15,6 +15,7 @@ Please refer to the [Deprecation](DEPRECATION.md) document for more information - [Prerequisites](#prerequisites) - [Auto-Discovery of Falcon Cloud Region](#auto-discovery-of-falcon-cloud-region) - [Usage](#usage) +- [Troubleshooting](#troubleshooting) ## Multi-Architecture Support :rocket: @@ -135,6 +136,10 @@ Optional Flags: --get-cid Get the CID assigned to the API Credentials --list-tags List all tags available for the selected sensor type and platform, sorted in ascending order --allow-legacy-curl Deprecated. Accepted and ignored; no longer needed + --debug Print redacted progress markers to stderr (or set FALCON_DEBUG=1). + Step name, HTTP status, cloud/region and curl exit only; values are + dropped unless the key is on a fixed allow-list, so secrets cannot + appear. Do not use bash -x for support; it prints credentials. Internal Flags: --internal-build-stage (Internal only) Falcon Build Stage [release|stage] (Default: release) @@ -166,6 +171,7 @@ Help Options: | `--get-cid` | N/A | `None` | Get the CID assigned to the API Credentials. | | `--list-tags` | `$LISTTAGS` | `False` (Optional) | List all tags available for the selected sensor | | `--allow-legacy-curl` | `$ALLOW_LEGACY_CURL` | `False` (Optional) | Deprecated. Accepted and ignored; no longer needed | +| `--debug` | `$FALCON_DEBUG` | `unset` (Optional) | Print redacted progress markers to stderr. Step, HTTP status, cloud/region and curl exit only; values are dropped unless the key is allow-listed, so secrets cannot appear. Do not use `bash -x` for support. | | `-h`, `--help` | N/A | `None` | Display help message | --- @@ -438,3 +444,30 @@ The following example will pull the `falcon-sensor` image for the `x86_64` platf --type falcon-sensor \ --platform x86_64 ``` + +--- + +## Troubleshooting + +Use the redacted debug mode. It prints, to stderr: the detected OS, architecture, +kernel and package manager; the exact sensor query filter and how many installers +matched; which installer was selected, its size and SHA-256 check; the API route, +HTTP status and curl exit code for every call; and the installed sensor version +and AID. Values are dropped unless the key is on a fixed allow-list, so +credentials cannot appear in the output you send to support. + +```shell +./falcon-container-sensor-pull.sh \ +--client-id \ +--client-secret \ +--type falcon-sensor \ +--debug +``` + +`FALCON_DEBUG=1` does the same thing, which is useful when the script runs from a +pipe or a job where you cannot add a flag. + +Do **not** use `bash -x` for support. It prints every expanded command, including +`client_secret`, access tokens, the registry password and `Authorization` +headers. This script turns tracing off at startup and warns when it does, but a +trace enabled before that point can still expose credentials. 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 484eee3..8c482cc 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 @@ -12,6 +12,54 @@ unset FALCON_CLIENT_SECRET FALCON_CLIENT_SECRET=$falcon_client_secret unset falcon_client_secret +# Opt-in redacted debug. Never re-enable set -x around credential paths. +falcon_debug_enabled() { + case "${FALCON_DEBUG:-}" in + 1 | true) return 0 ;; + *) return 1 ;; + esac +} + +# Allow-list. Only known-safe keys keep their value; everything else is dropped, +# so a future debug line cannot leak a secret by accident. +falcon_debug_filter() { + printf '%s\n' "$@" | awk ' + BEGIN { + split("step source error stage \ + cloud old_cloud new_cloud region region_hint sensor_cloud \ + http_status curl_exit exit_code path filter sort \ + os os_version os_arch os_family kernel pkg_manager distro_id run_as \ + count index decrement version sensor_version policy_version file_type sha \ + installer bytes sha_verify billing backend apd aid cid_source \ + tags_count grouping_tags_count sensor_type param registry repository tag \ + client_id_set client_secret_set access_token_set member_cid_set \ + provisioning_token_set maintenance_token_set proxy_set policy_name_set \ + tags_set grouping_tags_set", safe, " ") + for (i in safe) { ok[safe[i]] = 1 } + } + { + eq = index($0, "=") + if (eq < 2) { next } + key = substr($0, 1, eq - 1) + printf " %s=%s", key, (key in ok) ? substr($0, eq + 1) : "[DROPPED]" + } + ' +} + +falcon_debug() { + falcon_debug_enabled || return 0 + local falcon_debug_label + falcon_debug_label=$1 + shift + printf 'FALCON_DEBUG: %s%s\n' "$falcon_debug_label" "$(falcon_debug_filter "$@")" >&2 +} + +# Last HTTP status from a curl --dump-header file. Status only — no header dump. +falcon_debug_http_status() { + [ -f "$1" ] || return 0 + grep -i '^HTTP/' "$1" 2>/dev/null | tail -n 1 | awk '{print $2}' +} + : <<'#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. @@ -72,6 +120,11 @@ Optional Flags: --get-cid Get the CID assigned to the API Credentials --list-tags List all tags available for the selected sensor type and platform, sorted in ascending order --allow-legacy-curl Deprecated. Accepted and ignored; no longer needed + --debug Print redacted progress markers to stderr (or set FALCON_DEBUG=1). + Sensor type, resolved registry/repository/tag, how many tags matched, + the API route, HTTP status and curl exit code. Values are dropped + unless the key is on a fixed allow-list, so secrets cannot appear. + Do not use bash -x for support; it prints credentials. Internal Flags: --internal-build-stage (Internal only) Falcon Build Stage [release|stage] (Default: release) @@ -202,6 +255,9 @@ while [ $# != 0 ]; do ALLOW_LEGACY_CURL=true fi ;; + --debug) + FALCON_DEBUG=1 + ;; -n | --node) if [ -n "${1}" ]; then deprecated "-n|--node" @@ -243,6 +299,10 @@ while [ $# != 0 ]; do shift done +falcon_debug start "version=$VERSION" "cloud=${FALCON_CLOUD:-unset}" "sensor_type=${SENSOR_TYPE:-unset}" \ + "client_id_set=$([ -n "${FALCON_CLIENT_ID}" ] && echo yes || echo no)" \ + "client_secret_set=$([ -n "${FALCON_CLIENT_SECRET}" ] && echo yes || echo no)" + if ! command -v curl >/dev/null 2>&1; then die "The 'curl' command is missing. Please install it before continuing. Aborting..." fi @@ -255,6 +315,8 @@ fi handle_curl_error() { local err_msg + falcon_debug handle_curl_error "curl_exit=$1" + if [ "$1" = "28" ]; then err_msg="Operation timed out (exit code 28). If using a proxy, please check your proxy settings." die "$err_msg" @@ -284,7 +346,7 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - local token="$1" escaped_token auth_config headers body status hint old_host new_host arg rc + local token="$1" escaped_token auth_config headers body status hint old_host new_host arg rc req_path shift # The configuration value must be quoted, because it holds a space and a # colon. curl processes backslash escapes inside a quoted value, so a @@ -292,6 +354,23 @@ curl_command() { escaped_token=$(printf '%s' "$token" | sed 's/\\/\\\\/g; s/"/\\"/g') auth_config=$(printf 'header = "Authorization: Bearer %s"' "$escaped_token") + # API route only, for the debug marker. The query string is dropped: it can + # carry an installer id, and the route alone identifies the call. + req_path="" + for arg in "$@"; do + case "$arg" in + https://*) + req_path=${arg#https://} + case "$req_path" in + */*) req_path=/${req_path#*/} ;; + *) req_path=/ ;; + esac + req_path=${req_path%%\?*} + break + ;; + esac + done + headers=$(mktemp) body=$(mktemp) # No -L: the bearer token must never cross a redirect hop. The body is held @@ -304,6 +383,7 @@ curl_command() { # Re-issue against that region instead of following Location. The registry is # a different host, so its URLs are never rewritten. status=$(awk '/^HTTP\//{s=$2} END{print s}' "$headers") + falcon_debug curl_command "path=$req_path" "http_status=$status" "curl_exit=$rc" case "$status" in 301 | 302 | 307 | 308) hint=$(grep -i ^x-cs-region: "$headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g') @@ -325,6 +405,7 @@ curl_command() { printf '%s\n' "$auth_config" | curl -s --proto '=https' -K- "$@" >"$body" rc=$? + falcon_debug curl_command "step=region_retry" "path=$req_path" "region=$hint" "curl_exit=$rc" fi fi ;; @@ -336,6 +417,7 @@ curl_command() { } fetch_tags() { + falcon_debug fetch_tags "step=registry_token" bearer_result=$(echo "-u $ART_USERNAME:$ART_PASSWORD" | curl -s --proto '=https' \ "https://$cs_registry/v2/token?account=$ART_USERNAME&scope=repository:$registry_opts/$repository_name:pull&service=$cs_registry" -K-) || handle_curl_error $? @@ -430,13 +512,26 @@ is_multi_arch() { fi } +# Runs a container tool command with set -e off just long enough to capture its +# real exit code for the debug marker, then returns that code unchanged. +run_container_cmd() { + local step="$1" rc + shift + set +e + "$@" + rc=$? + set -e + falcon_debug "$step" "exit_code=$rc" + return "$rc" +} + pull_image() { local image_path="$1" local platform_override="$2" if [ -n "$platform_override" ]; then - "$CONTAINER_TOOL" pull --platform "$platform_override" "$image_path" + run_container_cmd pull_image "$CONTAINER_TOOL" pull --platform "$platform_override" "$image_path" else - "$CONTAINER_TOOL" pull "$image_path" + run_container_cmd pull_image "$CONTAINER_TOOL" pull "$image_path" fi } @@ -447,18 +542,18 @@ copy_image() { if [ "$multi_arch_copy" = "true" ]; then case "${CONTAINER_TOOL}" in *skopeo) - "$CONTAINER_TOOL" copy --all "docker://$source_path" "docker://$destination_path" + run_container_cmd copy_image "$CONTAINER_TOOL" copy --all "docker://$source_path" "docker://$destination_path" ;; *podman) - "$CONTAINER_TOOL" manifest create --all "$destination_path" "$source_path" >/dev/null - "$CONTAINER_TOOL" manifest push --all "$destination_path" - "$CONTAINER_TOOL" manifest rm "$destination_path" >/dev/null + run_container_cmd copy_image "$CONTAINER_TOOL" manifest create --all "$destination_path" "$source_path" >/dev/null && + run_container_cmd copy_image "$CONTAINER_TOOL" manifest push --all "$destination_path" && + "$CONTAINER_TOOL" manifest rm "$destination_path" >/dev/null ;; *docker) if ! "$CONTAINER_TOOL" buildx version >/dev/null 2>&1; then die "Docker buildx is not installed/enabled. Please install/enable buildx before continuing." else - "$CONTAINER_TOOL" buildx imagetools create --tag "$destination_path" "$source_path" + run_container_cmd copy_image "$CONTAINER_TOOL" buildx imagetools create --tag "$destination_path" "$source_path" fi ;; *) @@ -467,8 +562,8 @@ copy_image() { esac else # Copy the image to the desired registry - "$CONTAINER_TOOL" tag "$source_path" "$destination_path" - "$CONTAINER_TOOL" push "$destination_path" + run_container_cmd copy_image "$CONTAINER_TOOL" tag "$source_path" "$destination_path" && + run_container_cmd copy_image "$CONTAINER_TOOL" push "$destination_path" fi } @@ -594,6 +689,7 @@ match_sensor_version() { local all_tags local matched_tags local version_pattern + local chosen # Get all available tags by properly parsing JSON output from list_tags all_tags=$(extract_raw_tags) @@ -601,7 +697,9 @@ match_sensor_version() { if [ -z "$requested_version" ]; then # If no version specified, get the latest version if [ -n "$all_tags" ]; then - echo "$all_tags" | sort -V | tail -1 + chosen=$(echo "$all_tags" | sort -V | tail -1) + falcon_debug match_sensor_version "count=$(echo "$all_tags" | grep -c .)" "tag=$chosen" + echo "$chosen" return 0 else return 1 @@ -613,7 +711,9 @@ match_sensor_version() { matched_tags=$(echo "$all_tags" | grep -E "$version_pattern") if [ -n "$matched_tags" ]; then - echo "$matched_tags" | sort -V | tail -1 + chosen=$(echo "$matched_tags" | sort -V | tail -1) + falcon_debug match_sensor_version "count=$(echo "$matched_tags" | grep -c .)" "tag=$chosen" + echo "$chosen" return 0 fi @@ -635,7 +735,9 @@ match_sensor_version() { fi if [ -n "$matched_tags" ]; then - echo "$matched_tags" | sort -V | tail -1 + chosen=$(echo "$matched_tags" | sort -V | tail -1) + falcon_debug match_sensor_version "count=$(echo "$matched_tags" | grep -c .)" "tag=$chosen" + echo "$chosen" return 0 fi @@ -735,7 +837,9 @@ cs_falcon_oauth_token=$( auth_payload="client_id=$FALCON_CLIENT_ID&client_secret=$FALCON_CLIENT_SECRET" + falcon_debug oauth2_token "step=request" "cloud=${FALCON_CLOUD:-unset}" token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "$response_headers") || handle_curl_error $? + falcon_debug oauth2_token "step=response" "http_status=$(falcon_debug_http_status "$response_headers")" "cloud=${FALCON_CLOUD:-unset}" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') if [ -z "$token" ]; then # Wrong region: retry against the x-cs-region hint instead of following @@ -749,7 +853,9 @@ cs_falcon_oauth_token=$( # Separate file: --dump-header truncates, and region_hint below # still needs the original response. retry_headers=$(mktemp) + falcon_debug oauth2_token "step=retry" "region=$hinted" token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers") || handle_curl_error $? + falcon_debug oauth2_token "step=retry_response" "http_status=$(falcon_debug_http_status "$retry_headers")" "region=$hinted" rm -f "$retry_headers" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') fi @@ -762,6 +868,7 @@ cs_falcon_oauth_token=$( ) region_hint=$(grep -i ^x-cs-region: "$response_headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g') +falcon_debug oauth2_token "region_hint=${region_hint:-none}" "cloud=${FALCON_CLOUD:-unset}" rm "${response_headers}" if [ "${FALCON_CLOUD}" != "${region_hint}" ] && [ -n "${region_hint}" ]; then @@ -1030,6 +1137,8 @@ fi #Construct full image path FULLIMAGEPATH="${REPOSITORY}:${LATESTSENSOR}" +falcon_debug main "registry=$cs_registry" "repository=$REPOSITORY" "tag=$LATESTSENSOR" + if [ "$GETIMAGEPATH" ]; then echo "${FULLIMAGEPATH}" exit 0 @@ -1065,7 +1174,7 @@ if [ "$(is_multi_arch "$FULLIMAGEPATH")" = "true" ]; then if [ -n "$SENSOR_PLATFORM" ]; then # If Skopeo is being used, the platform must be overridden if grep -qw "skopeo" "$CONTAINER_TOOL"; then - "$CONTAINER_TOOL" copy --override-arch "$(platform_override)" --override-os linux "docker://$FULLIMAGEPATH" "docker://$COPYPATH" + run_container_cmd skopeo_copy "$CONTAINER_TOOL" copy --override-arch "$(platform_override)" --override-os linux "docker://$FULLIMAGEPATH" "docker://$COPYPATH" else # Podman/Docker can pull the specific platform pf_override="linux/$(platform_override)" @@ -1094,7 +1203,7 @@ You can either: else # Handle non-multi-arch images if grep -qw "skopeo" "$CONTAINER_TOOL"; then - "$CONTAINER_TOOL" copy "docker://$FULLIMAGEPATH" "docker://$COPYPATH" + run_container_cmd skopeo_copy "$CONTAINER_TOOL" copy "docker://$FULLIMAGEPATH" "docker://$COPYPATH" else pull_image "$FULLIMAGEPATH" diff --git a/bash/install/README.md b/bash/install/README.md index 950429a..92ab6d3 100644 --- a/bash/install/README.md +++ b/bash/install/README.md @@ -101,7 +101,7 @@ The installer is AWS SSM aware, if `FALCON_CLIENT_ID` and `FALCON_CLIENT_SECRET` ## Install Script ```terminal -Usage: falcon-linux-install.sh [-h|--help] +Usage: falcon-linux-install.sh [-h|--help|--debug] Installs and configures the CrowdStrike Falcon Sensor for Linux. Version: 1.13.0 @@ -195,9 +195,18 @@ Other Options User agent string to append to the User-Agent header when making requests to the CrowdStrike API. -This script recognizes the following argument: + - FALCON_DEBUG (default: unset) + Print redacted progress markers to stderr: step name, HTTP status, + cloud/region and curl exit code. Values are dropped unless the key is + on a fixed allow-list, so secrets cannot appear. Do not use bash -x + for support; it prints credentials. + Accepted values are ['1', 'true']. + +This script recognizes the following arguments: -h, --help Print this help message and exit. + --debug + Same as FALCON_DEBUG=1. ``` ### Usage @@ -256,7 +265,7 @@ curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.13.0/bas ## Uninstall Script ```terminal -Usage: falcon-linux-uninstall.sh [-h|--help] +Usage: falcon-linux-uninstall.sh [-h|--help|--debug] Uninstalls the CrowdStrike Falcon Sensor from Linux operating systems. Version: 1.13.0 @@ -306,9 +315,18 @@ Other Options: User agent string to append to the User-Agent header when making requests to the CrowdStrike API. -This script recognizes the following argument: + - FALCON_DEBUG (default: unset) + Print redacted progress markers to stderr: step name, HTTP status, + cloud/region and curl exit code. Values are dropped unless the key is + on a fixed allow-list, so secrets cannot appear. Do not use bash -x + for support; it prints credentials. + Accepted values are ['1', 'true']. + +This script recognizes the following arguments: -h, --help Print this help message and exit. + --debug + Same as FALCON_DEBUG=1. ``` ### Usage @@ -336,14 +354,51 @@ curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.13.0/bas ## Troubleshooting -To troubleshoot installation issues, run the script by using `bash -x`: +Use the redacted debug mode. It prints, to stderr: the detected OS, architecture, +kernel and package manager; the exact sensor query filter and how many installers +matched; which installer was selected, its size and SHA-256 check; the API route, +HTTP status and curl exit code for every call; and the installed sensor version +and AID. Values are dropped unless the key is on a fixed allow-list, so +credentials cannot appear in the output you send to support. + +```bash +FALCON_DEBUG=1 ./falcon-linux-install.sh +``` +Sample output from a real install (values from a live run, credentials never appear): + +``` +FALCON_DEBUG: start version=1.13.0 cloud=us-2 client_id_set=yes access_token_set=no member_cid_set=no proxy_set=no +FALCON_DEBUG: start step=environment os=Ubuntu os_version=22 os_arch=x86_64 kernel=6.8.0-1066-gcp run_as=root pkg_manager=apt policy_name_set=no decrement=0 +FALCON_DEBUG: oauth2_token step=response http_status=201 cloud=us-2 +FALCON_DEBUG: cs_sensor_download step=query filter=os:"Ubuntu"+os_version:"*22*"+architectures:"x86_64" sort=version|desc decrement=0 +FALCON_DEBUG: curl_command path=/sensors/combined/installers/v3 http_status=200 curl_exit=0 +FALCON_DEBUG: cs_sensor_download step=matched count=23 +FALCON_DEBUG: cs_sensor_download step=selected index=1 file_type=deb sha=455353061160 +FALCON_DEBUG: cs_sensor_download step=downloaded installer=/tmp/tmp.B6yKufQ9Ys/falcon-sensor.deb bytes=71715968 +FALCON_DEBUG: cs_sensor_download step=verified sha_verify=ok +FALCON_DEBUG: cs_sensor_register step=configure cid_source=api provisioning_token_set=no tags_count=0 apd=unset proxy_set=no billing=unset backend=unset sensor_cloud=unset +FALCON_DEBUG: main step=installed version=8.10.19402.0 aid=none +``` + +`aid=none` right after an install is normal: registration completes asynchronously +once the sensor reaches the cloud. Markers go to stderr, and a progress line can +share a line with one, so match them with `grep FALCON_DEBUG` rather than `grep +'^FALCON_DEBUG'`. + + +or pass the flag: ```bash -bash -x falcon-linux-install.sh +./falcon-linux-install.sh --debug ``` -or +or over a pipe: ```bash -curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.13.0/bash/install/falcon-linux-install.sh | bash -x +curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.13.0/bash/install/falcon-linux-install.sh | FALCON_DEBUG=1 bash ``` + +Do **not** use `bash -x` for support. It prints every expanded command, including +`client_secret`, access tokens, provisioning tokens and `Authorization` headers. +These scripts turn tracing off at startup and warn when they do, but a trace +enabled before that point can still expose credentials. diff --git a/bash/install/falcon-linux-install.sh b/bash/install/falcon-linux-install.sh index 2da06de..2f3f5ed 100755 --- a/bash/install/falcon-linux-install.sh +++ b/bash/install/falcon-linux-install.sh @@ -16,10 +16,75 @@ FALCON_ACCESS_TOKEN=$falcon_access_token FALCON_PROVISIONING_TOKEN=$falcon_provisioning_token unset falcon_client_secret falcon_access_token falcon_provisioning_token +# Opt-in redacted debug. Never re-enable set -x around credential paths. +falcon_debug_enabled() { + case "${FALCON_DEBUG:-}" in + 1 | true) return 0 ;; + *) return 1 ;; + esac +} + +# Allow-list. Only known-safe keys keep their value; everything else is dropped, +# so a future debug line cannot leak a secret by accident. +falcon_debug_filter() { + printf '%s\n' "$@" | awk ' + BEGIN { + split("step source error stage \ + cloud old_cloud new_cloud region region_hint sensor_cloud \ + http_status curl_exit exit_code path filter sort \ + os os_version os_arch os_family kernel pkg_manager distro_id run_as \ + count index decrement version sensor_version policy_version file_type sha \ + installer bytes sha_verify billing backend apd aid cid_source \ + tags_count grouping_tags_count sensor_type param registry repository tag \ + client_id_set client_secret_set access_token_set member_cid_set \ + provisioning_token_set maintenance_token_set proxy_set policy_name_set \ + tags_set grouping_tags_set", safe, " ") + for (i in safe) { ok[safe[i]] = 1 } + } + { + eq = index($0, "=") + if (eq < 2) { next } + key = substr($0, 1, eq - 1) + printf " %s=%s", key, (key in ok) ? substr($0, eq + 1) : "[DROPPED]" + } + ' +} + +falcon_debug() { + falcon_debug_enabled || return 0 + local falcon_debug_label + falcon_debug_label=$1 + shift + printf 'FALCON_DEBUG: %s%s\n' "$falcon_debug_label" "$(falcon_debug_filter "$@")" >&2 +} + +# Last HTTP status from a curl --dump-header file. Status only — no header dump. +falcon_debug_http_status() { + [ -f "$1" ] || return 0 + grep -i '^HTTP/' "$1" 2>/dev/null | tail -n 1 | awk '{print $2}' +} + +# Mirrors the selection order in os_install_package / remove_package. +falcon_debug_pkg_manager() { + if type dnf >/dev/null 2>&1; then + echo dnf + elif type yum >/dev/null 2>&1; then + echo yum + elif type zypper >/dev/null 2>&1; then + echo zypper + elif type apt-get >/dev/null 2>&1; then + echo apt + elif type rpm >/dev/null 2>&1; then + echo rpm + else + echo unknown + fi +} + print_usage() { cat </dev/null)" "run_as=$(id -un 2>/dev/null)" \ + "pkg_manager=$(falcon_debug_pkg_manager)" \ + "policy_name_set=$([ -n "${FALCON_SENSOR_UPDATE_POLICY_NAME}" ] && echo yes || echo no)" \ + "decrement=${cs_falcon_sensor_version_dec:-0}" if [ "$GET_ACCESS_TOKEN" = "true" ]; then get_oauth_token echo "$cs_falcon_oauth_token" @@ -165,6 +261,16 @@ main() { else echo 'Falcon Sensor installed successfully.' fi + # Authoritative version and AID, read back from the installed sensor. + # aid=none is normal right after install: registration completes + # asynchronously once the sensor reaches the cloud. + if [ -x /opt/CrowdStrike/falconctl ]; then + local installed_version installed_aid + installed_version=$(/opt/CrowdStrike/falconctl -g --version 2>/dev/null | sed -n 's/.*version *= *\([0-9][0-9.]*\).*/\1/p') + installed_aid=$(/opt/CrowdStrike/falconctl -g --aid 2>/dev/null | sed -n 's/.*aid="*\([0-9a-fA-F]\{8,\}\)"*.*/\1/p') + falcon_debug main "step=installed" \ + "version=${installed_version:-unknown}" "aid=${installed_aid:-none}" + fi } cs_sensor_register() { @@ -216,6 +322,17 @@ cs_sensor_register() { cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_cloud" fi # run the configuration command + # Option names only. cs_falcon_args holds --provisioning-token and --cid, + # so it must never be printed. + falcon_debug cs_sensor_register "step=configure" \ + "cid_source=${cs_falcon_cid_source:-api}" \ + "provisioning_token_set=$([ -n "${cs_falcon_token}" ] && echo yes || echo no)" \ + "tags_count=$(printf '%s\n' "${FALCON_TAGS}" | awk -F, '{print ($0=="")?0:NF}')" \ + "apd=${cs_falcon_apd:-unset}" \ + "proxy_set=$([ -n "${FALCON_APH}${FALCON_APP}" ] && echo yes || echo no)" \ + "billing=${cs_falcon_billing:-unset}" \ + "backend=${cs_falcon_backend:-unset}" \ + "sensor_cloud=${cs_falcon_sensor_cloud:-unset}" # shellcheck disable=SC2086 /opt/CrowdStrike/falconctl -s -f ${cs_falcon_args} >/dev/null } @@ -371,7 +488,7 @@ verify_sha256() { } cs_sensor_download() { - local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer + local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer sensor_filter if [ -n "$cs_sensor_policy_name" ]; then cs_sensor_version=$(cs_sensor_policy_version "$cs_sensor_policy_name") @@ -383,9 +500,12 @@ cs_sensor_download() { fi fi + sensor_filter="os:\"$cs_os_name\"$cs_os_version_filter$cs_api_version_filter$cs_os_arch_filter" + # The single most useful line when no sensor is found or the wrong one is. + falcon_debug cs_sensor_download "step=query" "filter=$sensor_filter" "sort=version|desc" "decrement=$cs_falcon_sensor_version_dec" existing_installers=$( curl_command -G "https://$(cs_cloud)/sensors/combined/installers/v3?sort=version|desc" \ - --data-urlencode "filter=os:\"$cs_os_name\"$cs_os_version_filter$cs_api_version_filter$cs_os_arch_filter" + --data-urlencode "filter=$sensor_filter" ) || handle_curl_error $? if echo "$existing_installers" | grep "authorization failed"; then @@ -395,6 +515,7 @@ cs_sensor_download() { fi sha_list=$(echo "$existing_installers" | json_value "sha256") + falcon_debug cs_sensor_download "step=matched" "count=$(echo "$sha_list" | grep -c .)" if [ -z "$sha_list" ]; then die "No sensor found for OS: $cs_os_name, Version: $cs_os_version. Either the OS or the OS version is not yet supported." fi @@ -411,9 +532,16 @@ cs_sensor_download() { installer="${destination_dir}/falcon-sensor.${file_type}" + # json_value matches any key containing the name, so "version" would also + # match os_version. The sha identifies the build unambiguously instead. + falcon_debug cs_sensor_download "step=selected" "index=$INDEX" "file_type=$file_type" "sha=$(printf '%.12s' "$sha")" + curl_command "https://$(cs_cloud)/sensors/entities/download-installer/v3?id=$sha" -o "${installer}" || handle_curl_error $? + falcon_debug cs_sensor_download "step=downloaded" "installer=$installer" "bytes=$(wc -c <"$installer" 2>/dev/null | tr -d ' ')" + verify_sha256 "$installer" "$sha" + falcon_debug cs_sensor_download "step=verified" "sha_verify=ok" echo "$installer" } @@ -498,6 +626,8 @@ os_install_package() { aws_ssm_parameter() { local param_name="$1" imds_err + falcon_debug aws_ssm_parameter "step=request" "param=$param_name" + hmac_sha256() { key="$1" data="$2" @@ -546,6 +676,7 @@ $request_dgst EOF ) + falcon_debug aws_ssm_parameter "step=request" "param=$param_name" "region=${aws_my_region:-unset}" response=$( { 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' \ @@ -667,6 +798,8 @@ fi handle_curl_error() { local err_msg + falcon_debug handle_curl_error "curl_exit=$1" + # Failed to download the file to destination if [ "$1" -eq 23 ]; then err_msg="Failed writing received data to disk/destination (exit code 23). Please check the destination path and permissions." @@ -698,13 +831,30 @@ handle_curl_error() { curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - local escaped_token auth_config headers body status hint old_host new_host arg rc + local escaped_token auth_config headers body status hint old_host new_host arg rc req_path # The configuration value must be quoted, because it holds a space and a # colon. curl processes backslash escapes inside a quoted value, so a # backslash or a double quote in the token has to be escaped first. escaped_token=$(printf '%s' "$cs_falcon_oauth_token" | sed 's/\\/\\\\/g; s/"/\\"/g') auth_config=$(printf 'header = "Authorization: Bearer %s"' "$escaped_token") + # API route only, for the debug marker. The query string is dropped: it can + # carry an installer id, and the route alone identifies the call. + req_path="" + for arg in "$@"; do + case "$arg" in + https://*) + req_path=${arg#https://} + case "$req_path" in + */*) req_path=/${req_path#*/} ;; + *) req_path=/ ;; + esac + req_path=${req_path%%\?*} + break + ;; + esac + done + headers=$(mktemp) body=$(mktemp) # No -L: the bearer token must never cross a redirect hop. The body is held @@ -717,6 +867,7 @@ curl_command() { # Re-issue against that region instead of following Location. Take the last # status line, because a proxy CONNECT dumps one of its own first. status=$(awk '/^HTTP\//{s=$2} END{print s}' "$headers") + falcon_debug curl_command "path=$req_path" "http_status=$status" "curl_exit=$rc" case "$status" in 301 | 302 | 307 | 308) hint=$(grep -i ^x-cs-region: "$headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g') @@ -738,6 +889,7 @@ curl_command() { printf '%s\n' "$auth_config" | curl -s -x "$proxy" --proto '=https' -K- "$@" >"$body" rc=$? + falcon_debug curl_command "step=region_retry" "path=$req_path" "region=$hint" "curl_exit=$rc" fi fi ;; @@ -821,11 +973,14 @@ get_oauth_token() { cs_falcon_oauth_token=$( if [ -n "$FALCON_ACCESS_TOKEN" ]; then + falcon_debug oauth2_token "source=access_token" "cloud=${cs_falcon_cloud:-unset}" token=$FALCON_ACCESS_TOKEN else auth_payload="client_id=$cs_falcon_client_id&client_secret=$cs_falcon_client_secret" + falcon_debug oauth2_token "step=request" "cloud=${cs_falcon_cloud:-unset}" token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "${response_headers}") || handle_curl_error $? + falcon_debug oauth2_token "step=response" "http_status=$(falcon_debug_http_status "${response_headers}")" "cloud=${cs_falcon_cloud:-unset}" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') if [ -z "$token" ]; then @@ -840,7 +995,9 @@ get_oauth_token() { # Separate file: --dump-header truncates, and region_hint below # still needs the original response. retry_headers=$(mktemp) + falcon_debug oauth2_token "step=retry" "region=$hinted" token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers") || handle_curl_error $? + falcon_debug oauth2_token "step=retry_response" "http_status=$(falcon_debug_http_status "$retry_headers")" "region=$hinted" rm -f "$retry_headers" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') fi @@ -855,6 +1012,7 @@ get_oauth_token() { if [ -z "$FALCON_ACCESS_TOKEN" ]; then region_hint=$(grep -i ^x-cs-region: "$response_headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g') + falcon_debug oauth2_token "region_hint=${region_hint:-none}" "cloud=${cs_falcon_cloud:-unset}" if [ -z "${FALCON_CLOUD}" ]; then if [ -z "${region_hint}" ]; then diff --git a/bash/install/falcon-linux-uninstall.sh b/bash/install/falcon-linux-uninstall.sh index 6c5d6e9..2cf9261 100755 --- a/bash/install/falcon-linux-uninstall.sh +++ b/bash/install/falcon-linux-uninstall.sh @@ -16,10 +16,75 @@ FALCON_ACCESS_TOKEN=$falcon_access_token FALCON_MAINTENANCE_TOKEN=$falcon_maintenance_token unset falcon_client_secret falcon_access_token falcon_maintenance_token +# Opt-in redacted debug. Never re-enable set -x around credential paths. +falcon_debug_enabled() { + case "${FALCON_DEBUG:-}" in + 1 | true) return 0 ;; + *) return 1 ;; + esac +} + +# Allow-list. Only known-safe keys keep their value; everything else is dropped, +# so a future debug line cannot leak a secret by accident. +falcon_debug_filter() { + printf '%s\n' "$@" | awk ' + BEGIN { + split("step source error stage \ + cloud old_cloud new_cloud region region_hint sensor_cloud \ + http_status curl_exit exit_code path filter sort \ + os os_version os_arch os_family kernel pkg_manager distro_id run_as \ + count index decrement version sensor_version policy_version file_type sha \ + installer bytes sha_verify billing backend apd aid cid_source \ + tags_count grouping_tags_count sensor_type param registry repository tag \ + client_id_set client_secret_set access_token_set member_cid_set \ + provisioning_token_set maintenance_token_set proxy_set policy_name_set \ + tags_set grouping_tags_set", safe, " ") + for (i in safe) { ok[safe[i]] = 1 } + } + { + eq = index($0, "=") + if (eq < 2) { next } + key = substr($0, 1, eq - 1) + printf " %s=%s", key, (key in ok) ? substr($0, eq + 1) : "[DROPPED]" + } + ' +} + +falcon_debug() { + falcon_debug_enabled || return 0 + local falcon_debug_label + falcon_debug_label=$1 + shift + printf 'FALCON_DEBUG: %s%s\n' "$falcon_debug_label" "$(falcon_debug_filter "$@")" >&2 +} + +# Last HTTP status from a curl --dump-header file. Status only — no header dump. +falcon_debug_http_status() { + [ -f "$1" ] || return 0 + grep -i '^HTTP/' "$1" 2>/dev/null | tail -n 1 | awk '{print $2}' +} + +# Mirrors the selection order in os_install_package / remove_package. +falcon_debug_pkg_manager() { + if type dnf >/dev/null 2>&1; then + echo dnf + elif type yum >/dev/null 2>&1; then + echo yum + elif type zypper >/dev/null 2>&1; then + echo zypper + elif type apt-get >/dev/null 2>&1; then + echo apt + elif type rpm >/dev/null 2>&1; then + echo rpm + else + echo unknown + fi +} + print_usage() { cat </dev/null | tr -d '"' | head -n 1)" \ + "os_version=$(awk -F= '/^VERSION_ID=/{print $2}' /etc/*release 2>/dev/null | tr -d '"' | head -n 1)" \ + "os_arch=$(uname -m)" "kernel=$(uname -r 2>/dev/null)" "run_as=$(id -un 2>/dev/null)" \ + "pkg_manager=$(falcon_debug_pkg_manager)" if [ "$GET_ACCESS_TOKEN" = "true" ]; then get_oauth_token echo "$cs_falcon_oauth_token" @@ -101,6 +196,7 @@ main() { cs_maintenance_token="" if [ -n "$FALCON_MAINTENANCE_TOKEN" ]; then cs_maintenance_token="$FALCON_MAINTENANCE_TOKEN" + falcon_debug maintenance_token "source=env" "maintenance_token_set=yes" elif [ -n "$FALCON_CLIENT_ID" ] && [ -n "$FALCON_CLIENT_SECRET" ] && [ -n "$aid" ]; then get_oauth_token get_maintenance_token @@ -165,12 +261,16 @@ cs_sensor_remove() { fi } + falcon_debug cs_sensor_remove "step=start" "pkg_manager=$(falcon_debug_pkg_manager)" + # Handle maintenance protection if [ -n "$cs_maintenance_token" ]; then # shellcheck disable=SC2086 if ! /opt/CrowdStrike/falconctl -s -f --maintenance-token=${cs_maintenance_token} >/dev/null 2>&1; then + falcon_debug cs_sensor_remove "step=maintenance_protection" "error=apply_failed" die "Failed to apply maintenance token. Uninstallation may fail." fi + falcon_debug cs_sensor_remove "step=maintenance_protection" "maintenance_token_set=yes" fi # Check for package manager lock prior to uninstallation @@ -182,6 +282,7 @@ cs_sensor_remove() { removal_exit_code=$? set -e + falcon_debug cs_sensor_remove "step=removed" "exit_code=$removal_exit_code" if [ "$removal_exit_code" -ne 0 ]; then die "Failed to remove falcon-sensor package (exit code $removal_exit_code). This may indicate that tamper protection is enabled on the sensor. Please provide FALCON_MAINTENANCE_TOKEN or set FALCON_CLIENT_ID and FALCON_CLIENT_SECRET to retrieve a maintenance token via the API." fi @@ -189,12 +290,15 @@ cs_sensor_remove() { cs_remove_host_from_console() { if [ -z "$aid" ]; then + falcon_debug cs_remove_host_from_console "step=skipped" "aid=none" echo 'Unable to find AID. Skipping host removal from console.' else + falcon_debug cs_remove_host_from_console "step=request" "aid=$aid" payload="{\"ids\": [\"$aid\"]}" url="https://$(cs_cloud)/devices/entities/devices-actions/v2?action_name=hide_host" curl_command -X "POST" -H "Content-Type: application/json" -d "$payload" "$url" >/dev/null || handle_curl_error $? + falcon_debug cs_remove_host_from_console "step=done" fi } @@ -214,6 +318,7 @@ cs_cloud() { cs_sensor_installed() { if ! test -f /opt/CrowdStrike/falconctl; then + falcon_debug cs_sensor_installed "step=already_uninstalled" echo "Falcon sensor is already uninstalled." && exit 0 fi # Get AID if FALCON_REMOVE_HOST is set to true or if we need to get a maintenance token @@ -237,22 +342,42 @@ get_maintenance_token() { if echo "$response" | grep -q "\"uninstall_token\""; then cs_maintenance_token=$(echo "$response" | json_value "uninstall_token" 1 | sed 's/ *$//g' | sed 's/^ *//g') if [ -z "$cs_maintenance_token" ]; then + falcon_debug maintenance_token "source=api" "error=empty_token" die "Retrieved empty maintenance token from API." fi + falcon_debug maintenance_token "source=api" "maintenance_token_set=yes" else + falcon_debug maintenance_token "source=api" "error=no_token_in_response" die "Failed to retrieve a maintenance token from the Falcon API." fi } curl_command() { # Dash does not support arrays, so we have to pass the args as separate arguments - local escaped_token auth_config headers body status hint old_host new_host arg rc + local escaped_token auth_config headers body status hint old_host new_host arg rc req_path # The configuration value must be quoted, because it holds a space and a # colon. curl processes backslash escapes inside a quoted value, so a # backslash or a double quote in the token has to be escaped first. escaped_token=$(printf '%s' "$cs_falcon_oauth_token" | sed 's/\\/\\\\/g; s/"/\\"/g') auth_config=$(printf 'header = "Authorization: Bearer %s"' "$escaped_token") + # API route only, for the debug marker. The query string is dropped: it can + # carry an installer id, and the route alone identifies the call. + req_path="" + for arg in "$@"; do + case "$arg" in + https://*) + req_path=${arg#https://} + case "$req_path" in + */*) req_path=/${req_path#*/} ;; + *) req_path=/ ;; + esac + req_path=${req_path%%\?*} + break + ;; + esac + done + headers=$(mktemp) body=$(mktemp) # No -L: the bearer token must never cross a redirect hop. The body is held @@ -265,6 +390,7 @@ curl_command() { # Re-issue against that region instead of following Location. Take the last # status line, because a proxy CONNECT dumps one of its own first. status=$(awk '/^HTTP\//{s=$2} END{print s}' "$headers") + falcon_debug curl_command "path=$req_path" "http_status=$status" "curl_exit=$rc" case "$status" in 301 | 302 | 307 | 308) hint=$(grep -i ^x-cs-region: "$headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g') @@ -286,6 +412,7 @@ curl_command() { printf '%s\n' "$auth_config" | curl -s -x "$proxy" --proto '=https' -K- "$@" >"$body" rc=$? + falcon_debug curl_command "step=region_retry" "path=$req_path" "region=$hint" "curl_exit=$rc" fi fi ;; @@ -297,6 +424,7 @@ curl_command() { } handle_curl_error() { + falcon_debug handle_curl_error "curl_exit=$1" if [ "$1" = "28" ]; then err_msg="Operation timed out (exit code 28)." if [ -n "$proxy" ]; then @@ -341,6 +469,8 @@ fi aws_ssm_parameter() { local param_name="$1" imds_err + falcon_debug aws_ssm_parameter "step=request" "param=$param_name" + hmac_sha256() { key="$1" data="$2" @@ -389,6 +519,7 @@ $request_dgst EOF ) + falcon_debug aws_ssm_parameter "step=request" "param=$param_name" "region=${aws_my_region:-unset}" response=$( { 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' \ @@ -484,11 +615,14 @@ get_oauth_token() { cs_falcon_oauth_token=$( if [ -n "$FALCON_ACCESS_TOKEN" ]; then + falcon_debug oauth2_token "source=access_token" "cloud=${cs_falcon_cloud:-unset}" token=$FALCON_ACCESS_TOKEN else auth_payload="client_id=$cs_falcon_client_id&client_secret=$cs_falcon_client_secret" + falcon_debug oauth2_token "step=request" "cloud=${cs_falcon_cloud:-unset}" token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "${response_headers}") || handle_curl_error $? + falcon_debug oauth2_token "step=response" "http_status=$(falcon_debug_http_status "${response_headers}")" "cloud=${cs_falcon_cloud:-unset}" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') if [ -z "$token" ]; then @@ -503,7 +637,9 @@ get_oauth_token() { # Separate file: --dump-header truncates, and region_hint below # still needs the original response. retry_headers=$(mktemp) + falcon_debug oauth2_token "step=retry" "region=$hinted" token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers") || handle_curl_error $? + falcon_debug oauth2_token "step=retry_response" "http_status=$(falcon_debug_http_status "$retry_headers")" "region=$hinted" rm -f "$retry_headers" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') fi @@ -518,6 +654,7 @@ get_oauth_token() { if [ -z "$FALCON_ACCESS_TOKEN" ]; then region_hint=$(grep -i ^x-cs-region: "$response_headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g') + falcon_debug oauth2_token "region_hint=${region_hint:-none}" "cloud=${cs_falcon_cloud:-unset}" if [ -z "${FALCON_CLOUD}" ]; then if [ -z "${region_hint}" ]; then @@ -537,6 +674,7 @@ get_oauth_token() { get_aid() { aid="$(/opt/CrowdStrike/falconctl -g --aid | awk -F '"' '{print $2}')" + falcon_debug get_aid "aid=${aid:-none}" } #------Start of the script------# diff --git a/bash/migrate/README.md b/bash/migrate/README.md index 2b2459d..007c0f0 100644 --- a/bash/migrate/README.md +++ b/bash/migrate/README.md @@ -79,7 +79,7 @@ export [OLD|NEW]FALCON_CLOUD="us-gov-1" ## Usage ```terminal -Usage: falcon-linux-migrate.sh [-h|--help] +Usage: falcon-linux-migrate.sh [-h|--help|--debug] Migrates the Falcon sensor to another Falcon CID. Version: 1.13.0 @@ -188,9 +188,18 @@ Other Options User agent string to append to the User-Agent header when making requests to the CrowdStrike API. -This script recognizes the following argument: + - FALCON_DEBUG (default: unset) + Print redacted progress markers to stderr: step name, HTTP status, + cloud/region and curl exit code. Values are dropped unless the key is + on a fixed allow-list, so secrets cannot appear. Do not use bash -x + for support; it prints credentials. + Accepted values are ['1', 'true']. + +This script recognizes the following arguments: -h, --help Print this help message and exit. + --debug + Same as FALCON_DEBUG=1. ``` ### Examples @@ -246,18 +255,38 @@ curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.13.0/bas ## Troubleshooting -To troubleshoot migration issues, you can run the script with `bash -x` for detailed output: +Use the redacted debug mode. It prints, to stderr: the detected OS, architecture, +kernel and package manager; both clouds and which credentials are present; the +exact sensor query filter and how many installers matched; the old AID before +removal and the new AID after registration; how many sensor and Falcon grouping +tags were migrated; and whether the run resumed from the tag recovery file or +started fresh. Note there is no automatic rollback on failure — the recovery file +only preserves the old AID and tags so a retry can reapply them. Values are +dropped unless the key is on a fixed allow-list, so neither the old nor the new +CID credentials can appear in the output you send to support. + +```bash +FALCON_DEBUG=1 ./falcon-linux-migrate.sh +``` + +or pass the flag: ```bash -bash -x falcon-linux-migrate.sh +./falcon-linux-migrate.sh --debug ``` -or +or over a pipe: ```bash -curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.13.0/bash/migrate/falcon-linux-migrate.sh | bash -x +curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.13.0/bash/migrate/falcon-linux-migrate.sh | FALCON_DEBUG=1 bash ``` +Do **not** use `bash -x` for support. It prints every expanded command, including +`OLD_FALCON_CLIENT_SECRET`, `NEW_FALCON_CLIENT_SECRET`, access tokens, +maintenance tokens and `Authorization` headers. This script turns tracing off at +startup and warns when it does, but a trace enabled before that point can still +expose credentials. + The script creates a log file at the location specified by `LOG_PATH` (defaults to `/tmp`) with the name format `falcon_migration_YYYYMMDD_HHMMSS.log`. This log contains detailed information about each step of the migration process. If the migration process is interrupted, the script creates a recovery file at `$LOG_PATH/falcon_migration_recovery.csv` that contains information about the previous sensor's AID and tags. When rerunning the script, it will detect this file and attempt to continue the migration process. diff --git a/bash/migrate/falcon-linux-migrate.sh b/bash/migrate/falcon-linux-migrate.sh index 30496b0..06168d3 100755 --- a/bash/migrate/falcon-linux-migrate.sh +++ b/bash/migrate/falcon-linux-migrate.sh @@ -19,6 +19,71 @@ 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 + +# Opt-in redacted debug. Never re-enable set -x around credential paths. +falcon_debug_enabled() { + case "${FALCON_DEBUG:-}" in + 1 | true) return 0 ;; + *) return 1 ;; + esac +} + +# Allow-list. Only known-safe keys keep their value; everything else is dropped, +# so a future debug line cannot leak a secret by accident. +falcon_debug_filter() { + printf '%s\n' "$@" | awk ' + BEGIN { + split("step source error stage \ + cloud old_cloud new_cloud region region_hint sensor_cloud \ + http_status curl_exit exit_code path filter sort \ + os os_version os_arch os_family kernel pkg_manager distro_id run_as \ + count index decrement version sensor_version policy_version file_type sha \ + installer bytes sha_verify billing backend apd aid cid_source \ + tags_count grouping_tags_count sensor_type param registry repository tag \ + client_id_set client_secret_set access_token_set member_cid_set \ + provisioning_token_set maintenance_token_set proxy_set policy_name_set \ + tags_set grouping_tags_set", safe, " ") + for (i in safe) { ok[safe[i]] = 1 } + } + { + eq = index($0, "=") + if (eq < 2) { next } + key = substr($0, 1, eq - 1) + printf " %s=%s", key, (key in ok) ? substr($0, eq + 1) : "[DROPPED]" + } + ' +} + +falcon_debug() { + falcon_debug_enabled || return 0 + local falcon_debug_label + falcon_debug_label=$1 + shift + printf 'FALCON_DEBUG: %s%s\n' "$falcon_debug_label" "$(falcon_debug_filter "$@")" >&2 +} + +# Last HTTP status from a curl --dump-header file. Status only — no header dump. +falcon_debug_http_status() { + [ -f "$1" ] || return 0 + grep -i '^HTTP/' "$1" 2>/dev/null | tail -n 1 | awk '{print $2}' +} + +# Mirrors the selection order in os_install_package / remove_package. +falcon_debug_pkg_manager() { + if type dnf >/dev/null 2>&1; then + echo dnf + elif type yum >/dev/null 2>&1; then + echo yum + elif type zypper >/dev/null 2>&1; then + echo zypper + elif type apt-get >/dev/null 2>&1; then + echo apt + elif type rpm >/dev/null 2>&1; then + echo rpm + else + echo unknown + fi +} # # Bash script to migrate Falcon sensor to another falcon CID. # @@ -28,7 +93,7 @@ VERSION="1.13.0" print_usage() { cat <"$body" rc=$? + falcon_debug curl_command "step=region_retry" "path=$req_path" "region=$hint" "curl_exit=$rc" fi fi ;; @@ -279,6 +382,7 @@ curl_command() { } handle_curl_error() { + falcon_debug handle_curl_error "curl_exit=$1" if [ "$1" = "28" ]; then err_msg="Operation timed out (exit code 28)." if [ -n "$proxy" ]; then @@ -401,6 +505,7 @@ get_oauth_token() { cs_falcon_oauth_token=$( if [ -n "$FALCON_ACCESS_TOKEN" ]; then + falcon_debug oauth2_token "source=access_token" "cloud=${cs_falcon_cloud:-unset}" token=$FALCON_ACCESS_TOKEN else # Build the auth request payload, adding member_cid if specified @@ -410,7 +515,9 @@ get_oauth_token() { auth_payload="${auth_payload}&member_cid=${cs_falcon_member_cid}" fi + falcon_debug oauth2_token "step=request" "cloud=${cs_falcon_cloud:-unset}" token_result=$(echo "$auth_payload" | oauth_token_request "$(cs_cloud)" "${response_headers}") || handle_curl_error $? + falcon_debug oauth2_token "step=response" "http_status=$(falcon_debug_http_status "${response_headers}")" "cloud=${cs_falcon_cloud:-unset}" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') if [ -z "$token" ]; then @@ -425,7 +532,9 @@ get_oauth_token() { # Separate file: --dump-header truncates, and region_hint below # still needs the original response. retry_headers=$(mktemp) + falcon_debug oauth2_token "step=retry" "region=$hinted" token_result=$(echo "$auth_payload" | oauth_token_request "$retry_host" "$retry_headers") || handle_curl_error $? + falcon_debug oauth2_token "step=retry_response" "http_status=$(falcon_debug_http_status "$retry_headers")" "region=$hinted" rm -f "$retry_headers" token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') fi @@ -440,6 +549,7 @@ get_oauth_token() { if [ -z "$FALCON_ACCESS_TOKEN" ]; then region_hint=$(grep -i ^x-cs-region: "$response_headers" | head -n 1 | tr '[:upper:]' '[:lower:]' | tr -d '\r' | sed 's/^x-cs-region: //g') + falcon_debug oauth2_token "region_hint=${region_hint:-none}" "cloud=${cs_falcon_cloud:-unset}" if [ -z "${FALCON_CLOUD}" ]; then if [ -z "${region_hint}" ]; then @@ -598,6 +708,17 @@ cs_sensor_register() { cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_cloud" fi # run the configuration command + # Option names only. cs_falcon_args holds --provisioning-token and --cid, + # so it must never be printed. + falcon_debug cs_sensor_register "step=configure" \ + "cid_source=${cs_falcon_cid_source:-api}" \ + "provisioning_token_set=$([ -n "${cs_falcon_token}" ] && echo yes || echo no)" \ + "tags_count=$(printf '%s\n' "${FALCON_TAGS}" | awk -F, '{print ($0=="")?0:NF}')" \ + "apd=${cs_falcon_apd:-unset}" \ + "proxy_set=$([ -n "${FALCON_APH}${FALCON_APP}" ] && echo yes || echo no)" \ + "billing=${cs_falcon_billing:-unset}" \ + "backend=${cs_falcon_backend:-unset}" \ + "sensor_cloud=${cs_falcon_sensor_cloud:-unset}" # shellcheck disable=SC2086 /opt/CrowdStrike/falconctl -s -f ${cs_falcon_args} >/dev/null } @@ -634,7 +755,7 @@ cs_sensor_install() { } cs_sensor_policy_version() { - local cs_policy_name="$1" sensor_update_policy sensor_update_versions + local cs_policy_name="$1" sensor_update_policy sensor_update_versions chosen_version sensor_update_policy=$( curl_command -G "https://$(cs_cloud)/policy/combined/sensor-update/v2" \ @@ -658,14 +779,16 @@ cs_sensor_policy_version() { set -- $sensor_update_versions if [ "$(echo "$sensor_update_versions" | wc -w)" -gt 1 ]; then if [ "$cs_os_arch" = "aarch64" ]; then - echo "$2" + chosen_version="$2" else - echo "$1" + chosen_version="$1" fi else - echo "$1" + chosen_version="$1" fi IFS=$oldIFS + falcon_debug cs_sensor_policy_version "policy_version=$chosen_version" + echo "$chosen_version" } # Compare the downloaded installer against the SHA-256 that the API supplied. @@ -696,7 +819,7 @@ verify_sha256() { } cs_sensor_download() { - local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer + local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer sensor_filter if [ -n "$cs_sensor_policy_name" ]; then cs_sensor_version=$(cs_sensor_policy_version "$cs_sensor_policy_name") @@ -708,9 +831,12 @@ cs_sensor_download() { fi fi + sensor_filter="os:\"$cs_os_name\"$cs_os_version_filter$cs_api_version_filter$cs_os_arch_filter" + # The single most useful line when no sensor is found or the wrong one is. + falcon_debug cs_sensor_download "step=query" "filter=$sensor_filter" "sort=version|desc" "decrement=$cs_falcon_sensor_version_dec" existing_installers=$( curl_command -G "https://$(cs_cloud)/sensors/combined/installers/v3?sort=version|desc" \ - --data-urlencode "filter=os:\"$cs_os_name\"$cs_os_version_filter$cs_api_version_filter$cs_os_arch_filter" + --data-urlencode "filter=$sensor_filter" ) || handle_curl_error $? if echo "$existing_installers" | grep "authorization failed"; then @@ -720,6 +846,7 @@ cs_sensor_download() { fi sha_list=$(echo "$existing_installers" | json_value "sha256") + falcon_debug cs_sensor_download "step=matched" "count=$(echo "$sha_list" | grep -c .)" if [ -z "$sha_list" ]; then die "No sensor found for OS: $cs_os_name, Version: $cs_os_version. Either the OS or the OS version is not yet supported." fi @@ -736,9 +863,16 @@ cs_sensor_download() { installer="${destination_dir}/falcon-sensor.${file_type}" + # json_value matches any key containing the name, so "version" would also + # match os_version. The sha identifies the build unambiguously instead. + falcon_debug cs_sensor_download "step=selected" "index=$INDEX" "file_type=$file_type" "sha=$(printf '%.12s' "$sha")" + curl_command "https://$(cs_cloud)/sensors/entities/download-installer/v3?id=$sha" -o "${installer}" || handle_curl_error $? + falcon_debug cs_sensor_download "step=downloaded" "installer=$installer" "bytes=$(wc -c <"$installer" 2>/dev/null | tr -d ' ')" + verify_sha256 "$installer" "$sha" + falcon_debug cs_sensor_download "step=verified" "sha_verify=ok" echo "$installer" } @@ -1425,6 +1559,18 @@ if [ -n "$FALCON_SENSOR_CLOUD" ]; then fi main() { + falcon_debug start "version=$VERSION" "old_cloud=${OLD_FALCON_CLOUD:-unset}" "new_cloud=${NEW_FALCON_CLOUD:-unset}" \ + "proxy_set=$([ -n "${proxy}" ] && echo yes || echo no)" + # OS detection drives the sensor query filter, so a mis-detected distro is + # the usual cause of "no sensor found for OS". + falcon_debug start "step=environment" \ + "os=$cs_os_name" "os_version=${cs_os_version:-unset}" "os_arch=$cs_os_arch" \ + "kernel=$(uname -r 2>/dev/null)" "run_as=$(id -un 2>/dev/null)" \ + "pkg_manager=$(falcon_debug_pkg_manager)" \ + "policy_name_set=$([ -n "${FALCON_SENSOR_UPDATE_POLICY_NAME}" ] && echo yes || echo no)" \ + "tags_set=$([ -n "${FALCON_TAGS}" ] && echo yes || echo no)" \ + "grouping_tags_set=$([ -n "${FALCON_GROUPING_TAGS}" ] && echo yes || echo no)" \ + "decrement=${cs_falcon_sensor_version_dec:-0}" # Start of migration touch "$log_file" echo "Migration file created at: $log_file" @@ -1432,6 +1578,10 @@ main() { # auth with old credentials log "INFO" "Authenticating to old CID..." + falcon_debug main "stage=old" \ + "client_id_set=$([ -n "${OLD_FALCON_CLIENT_ID}" ] && echo yes || echo no)" \ + "client_secret_set=$([ -n "${OLD_FALCON_CLIENT_SECRET}" ] && echo yes || echo no)" \ + "member_cid_set=$([ -n "${OLD_FALCON_MEMBER_CID}" ] && echo yes || echo no)" authenticate_to_falcon "$OLD_FALCON_CLIENT_ID" "$OLD_FALCON_CLIENT_SECRET" "$OLD_FALCON_CLOUD" "$OLD_FALCON_MEMBER_CID" # Check if we are in recovery mode @@ -1446,6 +1596,9 @@ main() { recovery_mode=false fi fi + # There is no automatic rollback-on-failure; this only reports whether a + # previous attempt's recovery file was picked up or this is a fresh run. + falcon_debug main "step=recovery" "stage=$([ "$recovery_mode" = "true" ] && echo recovery || echo fresh)" # Get the AID if not in recovery mode if [ "$recovery_mode" = false ]; then @@ -1480,6 +1633,9 @@ main() { fi fi fi + falcon_debug main "step=old_aid" "aid=${old_aid:-none}" \ + "tags_count=$(printf '%s\n' "${sensor_tags}" | awk -F, '{print ($0=="")?0:NF}')" \ + "grouping_tags_count=$(printf '%s\n' "${falcon_tags}" | awk -F, '{print ($0=="")?0:NF}')" # Uninstall sensor log "INFO" "Uninstalling old Falcon sensor..." @@ -1488,6 +1644,10 @@ main() { # Install new sensor # auth with new credentials log "INFO" "Authenticating to new CID..." + falcon_debug main "stage=new" \ + "client_id_set=$([ -n "${NEW_FALCON_CLIENT_ID}" ] && echo yes || echo no)" \ + "client_secret_set=$([ -n "${NEW_FALCON_CLIENT_SECRET}" ] && echo yes || echo no)" \ + "member_cid_set=$([ -n "${NEW_FALCON_MEMBER_CID}" ] && echo yes || echo no)" authenticate_to_falcon "$NEW_FALCON_CLIENT_ID" "$NEW_FALCON_CLIENT_SECRET" "$NEW_FALCON_CLOUD" "$NEW_FALCON_CID" "$NEW_FALCON_MEMBER_CID" log "INFO" "Checking if tags need to be migrated..." @@ -1506,6 +1666,12 @@ main() { log "INFO" "Installing Falcon sensor to new CID..." install_sensor | tee -a "$log_file" + # Authoritative AID, read back after registration. aid=none is normal + # right after install: registration completes asynchronously. + local new_aid + new_aid=$(get_aid) + falcon_debug main "step=installed" "aid=${new_aid:-none}" + # Set Falcon grouping tags if needed if ! set_falcon_grouping_tags "$migrate_tags" "$falcon_tags"; then log "WARNING" "There was an issue setting the Falcon grouping tags" diff --git a/powershell/install/README.md b/powershell/install/README.md index 9b616e3..1708334 100644 --- a/powershell/install/README.md +++ b/powershell/install/README.md @@ -114,6 +114,13 @@ By default, the Falcon sensor for Windows automatically attempts to use any avai This parameter forces the sensor to skip those attempts and ignore any proxy configuration, including Windows Proxy Auto Detection. .PARAMETER Verbose Enable verbose logging +.PARAMETER FalconDebug +Print redacted progress markers: detected OS and PowerShell version, the exact sensor +query filter, how many installers matched and which was chosen, the API route and HTTP +status for every call, and the sensor version installed plus the AID (that version is the one resolved from +the policy or query, not re-read from the binary). Values are dropped +unless the key is on a fixed allow-list, so secrets cannot appear. Also honors `$env:FALCON_DEBUG=1`. +Do not use `Set-PSDebug -Trace` or the common `-Debug` parameter for support; they print credentials. #> ``` @@ -176,6 +183,13 @@ The proxy host for the sensor to use when communicating with CrowdStrike [defaul The proxy port for the sensor to use when communicating with CrowdStrike [default: $null] .PARAMETER Verbose Enable verbose logging +.PARAMETER FalconDebug +Print redacted progress markers: detected OS and PowerShell version, the exact sensor +query filter, how many installers matched and which was chosen, the API route and HTTP +status for every call, and the sensor version installed plus the AID (that version is the one resolved from +the policy or query, not re-read from the binary). Values are dropped +unless the key is on a fixed allow-list, so secrets cannot appear. Also honors `$env:FALCON_DEBUG=1`. +Do not use `Set-PSDebug -Trace` or the common `-Debug` parameter for support; they print credentials. #> ``` @@ -195,30 +209,47 @@ Basic example that will uninstall the sensor with the provided maintenance token ## Troubleshooting -To assist in troubleshooting the installation scripts, you can try the following: +Use the redacted debug mode. It prints the detected OS and PowerShell version, the +exact sensor query filter, how many installers matched and which was chosen, the API +route and HTTP status for every call, and the sensor version installed plus the AID +(the version is the one resolved from the policy or query, not re-read from the binary). +Values are dropped unless the key is on a fixed allow-list, so credentials cannot +appear in the output you send to support. -- Use the `-Verbose` parameter to enable verbose logging. +```pwsh +.\falcon_windows_install.ps1 -FalconDebug -FalconClientId -FalconClientSecret -ProvToken +``` - > Note: This will display additional logging in the console, as well as in the log file. +Sample output from a real install on Windows PowerShell 5.1 (values from a live run, +credentials never appear): - Example: +``` +FALCON_DEBUG: start version=1.13.0 (PowerShell 5.1.20348.5499 Desktop) cloud=us-2 client_id_set=yes client_secret_set=yes access_token_set=no member_cid_set=no proxy_set=no policy_name_set=no +FALCON_DEBUG: environment os=windows os_version=10.0.20348.0 os_arch=AMD64 run_as=admin +FALCON_DEBUG: Invoke-FalconAuth step=response http_status=201 cloud=us-2 +FALCON_DEBUG: GetPolicy step=query path=/policy/combined/sensor-update/v2 filter=platform_name:'Windows'+name.raw:'platform_default' +FALCON_DEBUG: GetPolicy step=resolved policy_version=8.10.21405 +FALCON_DEBUG: GetInstaller step=query path=/sensors/combined/installers/v3 filter=platform:'windows'+version:'8.10.21405' sort=none +FALCON_DEBUG: GetInstaller step=matched count=1 +FALCON_DEBUG: GetInstaller step=selected index=0 file_type=exe sha=338d1b7f2508 +FALCON_DEBUG: DownloadFile step=downloaded installer=C:\Windows\Temp\FalconSensor_Windows.exe bytes=131724104 +FALCON_DEBUG: Installer step=configure cid_source=api provisioning_token_set=no tags_count=0 proxy_set=no +FALCON_DEBUG: InstallerProcess step=installed version=8.10.21405 aid=61c06e35b3104b99a371be2d6943d2e7 +``` - ```pwsh - .\falcon_windows_install.ps1 -Verbose -FalconClientId -FalconClientSecret -ProvToken - ``` -- For a more detailed approach, you can use `Set-PSDebug -Trace`. This cmdlet offers three trace levels (0-2): +`$env:FALCON_DEBUG = '1'` does the same thing, which is useful when the script runs +from a job where you cannot add a parameter. - - 0 : Turn script block logging off. (Equivalent to -Off) - - 1 : Turn script block logging on. (Equivalent to -On) - - 2 : Turn script block logging on and generate a trace of all commands in a script block and the arguments they were used with. - > Similar to the output of `set -x` in bash. Very noisy but contains a lot of useful information. +`-Verbose` still enables the script's own operational logging in the console and the +log file. It is not a replacement for `-FalconDebug`. - Example: +> Note: debug markers go to the console via `Write-Host`, so they are not written to +> the script's log file. A `Start-Transcript` session does capture them, so stop any +> transcript first if you do not want the markers on disk. - ```pwsh - Set-PSDebug -Trace 2 - .\falcon_windows_install.ps1 -FalconClientId -FalconClientSecret -ProvToken - # To turn off tracing - Set-PSDebug -Trace 0 - ``` +Do **not** use `Set-PSDebug -Trace` or the common `-Debug` parameter for support. +Tracing prints every statement with its arguments, including `FalconClientSecret`, +`ProvToken`, access tokens, `Authorization` headers and the OAuth request body. These +scripts call `Set-PSDebug -Off` on entry, but that runs after PowerShell binds the +parameters, so a trace started beforehand can still expose the values you passed in. diff --git a/powershell/install/falcon_windows_install.ps1 b/powershell/install/falcon_windows_install.ps1 index 34b9de5..e739254 100755 --- a/powershell/install/falcon_windows_install.ps1 +++ b/powershell/install/falcon_windows_install.ps1 @@ -55,6 +55,13 @@ This parameter forces the sensor to skip those attempts and ignore any proxy con User agent string to append to the User-Agent header when making requests to the CrowdStrike API. .PARAMETER Verbose Enable verbose logging +.PARAMETER FalconDebug +Print redacted progress markers: detected OS and PowerShell version, the exact sensor +query filter, how many installers matched and which was chosen, the API route and HTTP +status for every call, and the sensor version installed plus the AID (that version is the one resolved from +the policy or query, not re-read from the binary). Values are dropped +unless the key is on a fixed allow-list, so secrets cannot appear. Also honors `$env:FALCON_DEBUG=1`. +Do not use `Set-PSDebug -Trace` or the common `-Debug` parameter for support; they print credentials. .EXAMPLE PS>.\falcon_windows_install.ps1 -FalconClientId -FalconClientSecret @@ -74,6 +81,10 @@ Updated 2021-10-22 to include 'sensor_version' property when matching policy to [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DeleteInstaller')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DeleteScript')] +# Read inside Test-FalconDebugEnabled, which the rule does not follow. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'FalconDebug')] +# Debug markers must stay out of the pipeline and out of the on-disk log. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param( [Parameter(Position = 1)] [ValidateSet('autodiscover', 'us-1', 'us-2', 'us-3', 'eu-1', 'us-gov-1', 'us-gov-2')] @@ -132,7 +143,10 @@ param( [string] $FalconAccessToken, [Parameter(Position = 19)] - [string] $UserAgent + [string] $UserAgent, + + [Parameter(Position = 20)] + [switch] $FalconDebug ) begin { Set-PSDebug -Off @@ -155,6 +169,8 @@ begin { } else { $BaseUserAgent } + # PSEdition is absent on PowerShell 3/4; Desktop is the only edition they had. + $PSEditionValue = if ($PSVersionTable.PSEdition) { $PSVersionTable.PSEdition } else { 'Desktop' } function Write-FalconLog ([string] $Source, [string] $Message, [bool] $stdout = $true) { $Content = @(Get-Date -Format 'yyyy-MM-dd hh:MM:ss') @@ -192,6 +208,70 @@ begin { Write-FalconLog -Source 'VERBOSE' -Message $message -stdout $false } + function Test-FalconDebugEnabled { + if ($FalconDebug) { return $true } + if ($env:FALCON_DEBUG -match '^(1|true)\z') { return $true } + return $false + } + + # Allow-list, the single decision point for both marker paths. Only known-safe + # keys keep their value; everything else is dropped, so a future debug line + # cannot leak a secret by accident. + function Protect-FalconDebugPair([string] $Key, [string] $Value) { + $SafeKeys = @( + 'step', 'source', 'error', 'stage', + 'cloud', 'old_cloud', 'new_cloud', 'region', 'region_hint', 'sensor_cloud', + 'http_status', 'curl_exit', 'exit_code', 'path', 'filter', 'sort', + 'os', 'os_version', 'os_arch', 'os_family', 'kernel', 'pkg_manager', 'distro_id', 'run_as', + 'count', 'index', 'decrement', 'version', 'sensor_version', 'policy_version', 'file_type', 'sha', + 'installer', 'bytes', 'sha_verify', 'billing', 'backend', 'apd', 'aid', 'cid_source', + 'tags_count', 'grouping_tags_count', 'sensor_type', 'param', 'registry', 'repository', 'tag', + 'client_id_set', 'client_secret_set', 'access_token_set', 'member_cid_set', + 'provisioning_token_set', 'maintenance_token_set', 'proxy_set', 'policy_name_set', + 'tags_set', 'grouping_tags_set' + ) + if ($SafeKeys -ccontains $Key) { return "$Key=$Value" } + return "$Key=[DROPPED]" + } + + function Protect-FalconDebugMessage([string] $Message) { + $Filtered = @() + foreach ($Token in ($Message -split '\s+')) { + if ([string]::IsNullOrEmpty($Token)) { continue } + $Split = $Token.IndexOf('=') + if ($Split -lt 1) { continue } + $Filtered += Protect-FalconDebugPair $Token.Substring(0, $Split) $Token.Substring($Split + 1) + } + return ($Filtered -join ' ') + } + + # Write-Host on purpose: keeps markers out of the pipeline and out of the log file. + function Write-FalconDebug { + param( + [Parameter(Mandatory = $true)][string] $Step, + [string] $Message, + [System.Collections.IDictionary] $Pairs + ) + if (-not (Test-FalconDebugEnabled)) { return } + $Parts = @() + if ($Message) { $Parts += Protect-FalconDebugMessage $Message } + # -Pairs is required for any value that can contain a space, such as an FQL + # filter holding a multi-word policy name. Splitting a joined string cannot + # carry those safely: a bare word would be glued onto the previous value. + if ($Pairs) { + foreach ($Key in $Pairs.Keys) { + $Parts += Protect-FalconDebugPair ([string]$Key) ([string]$Pairs[$Key]) + } + } + $Filtered = ($Parts | Where-Object { $_ }) -join ' ' + if ($Filtered) { + Write-Host "FALCON_DEBUG: $Step $Filtered" + } + else { + Write-Host "FALCON_DEBUG: $Step" + } + } + function Get-FalconCloud ([string] $xCsRegion) { $Output = switch ($xCsRegion) { 'autodiscover' { 'https://api.crowdstrike.com'; break } @@ -243,6 +323,7 @@ begin { $Headers = @{'Accept' = 'application/json'; 'Content-Type' = 'application/x-www-form-urlencoded'; 'charset' = 'utf-8' } $Headers.Add('User-Agent', $FullUserAgent) if ($FalconAccessToken) { + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "source=access_token cloud=${FalconCloud}" $Headers.Add('Authorization', "bearer $($FalconAccessToken)") } else { @@ -253,7 +334,11 @@ begin { # below. $RedirectResponse = $null try { + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=request cloud=${FalconCloud}" $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body -MaximumRedirection 0 + # Status marker before ConvertFrom-Json: on Windows PowerShell 5.1 a + # 308 is returned, not thrown, and parsing it would fail first. + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=response http_status=$([int]$response.StatusCode) cloud=${FalconCloud}" if ([int]$response.StatusCode -in @(301, 302, 303, 307, 308)) { $RedirectResponse = $response @@ -270,8 +355,10 @@ begin { } } catch { - # Handle redirects - Write-Verbose "Invoke-FalconAuth - CAUGHT EXCEPTION - `$_.Exception.Message`r`n$($_.Exception.Message)" + # Status only. Never log the exception, its message, or the response: + # they can carry the request body and the Authorization header. + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "http_status=$debugStatus error=oauth_request_failed" $response = $_.Exception.Response if (!$response) { @@ -309,6 +396,9 @@ begin { # Get-FalconCloud validates the region against its own allowlist, # not the Location header. $BaseUrl = Get-FalconCloud($region) + # Printed only after validation, so a hostile header cannot inject + # arbitrary text into the console. + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=region_retry region=$region" $BaseUrl, $Headers = Invoke-FalconAuth -WebRequestParams $WebRequestParams -BaseUrl $BaseUrl -Body $Body -FalconCloud $FalconCloud } } @@ -325,6 +415,24 @@ begin { } } + # Reads the AID for the debug marker only; registration is asynchronous, so + # a missing AID right after install is normal. + function Get-AID { + $reg_paths = 'HKLM:\SYSTEM\CrowdStrike\{9b03c1d9-3138-44ed-9fae-d9f4c034b88d}\{16e0423f-7058-48c9-a204-725362b67639}\Default', 'HKLM:\SYSTEM\CurrentControlSet\Services\CSAgent\Sim' + $aid = $null + foreach ($path in $reg_paths) { + try { + $agItemProperty = Get-ItemProperty -Path $path -Name AG -ErrorAction Stop + $aid = [System.BitConverter]::ToString( ($agItemProperty.AG)).ToLower() -replace '-', '' + break + } + catch { + continue + } + } + return $aid + } + $WinSystem = [Environment]::GetFolderPath('System') $WinTemp = $WinSystem -replace 'system32', 'Temp' if (!$LogPath) { @@ -349,7 +457,9 @@ begin { function Get-ResourceContent([hashtable] $WebRequestParams, [string] $url, [string] $logKey, [hashtable] $scope, [string] $errorMessage) { try { + Write-FalconDebug -Step 'Get-ResourceContent' -Message "step=request path=$(([Uri]$url).AbsolutePath)" $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'GET' -MaximumRedirection 0 + Write-FalconDebug -Step 'Get-ResourceContent' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content Write-VerboseLog -VerboseInput $content -PreMessage 'Get-ResourceContent - $content:' @@ -369,7 +479,8 @@ begin { } } catch { - Write-VerboseLog -VerboseInput $_.Exception.Message -PreMessage 'Get-ResourceContent - CAUGHT EXCEPTION - $_.Exception.Message:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Get-ResourceContent' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response if (!$response) { @@ -408,6 +519,7 @@ begin { function Invoke-FalconDownload ([hashtable] $WebRequestParams, [string] $url, [string] $Outfile) { try { $ProgressPreference = 'SilentlyContinue' + Write-FalconDebug -Step 'Invoke-FalconDownload' -Message "step=request path=$(([Uri]$url).AbsolutePath)" $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'GET' -OutFile $Outfile } catch { @@ -441,6 +553,22 @@ begin { } } process { + Write-FalconDebug -Step 'start' -Pairs ([ordered]@{ + version = "$ScriptVersion (PowerShell $($PSVersionTable.PSVersion) $PSEditionValue)" + cloud = $FalconCloud + client_id_set = if ($FalconClientId) { 'yes' } else { 'no' } + client_secret_set = if ($FalconClientSecret) { 'yes' } else { 'no' } + access_token_set = if ($FalconAccessToken) { 'yes' } else { 'no' } + member_cid_set = if ($MemberCid) { 'yes' } else { 'no' } + proxy_set = if ($ProxyHost) { 'yes' } else { 'no' } + policy_name_set = if ($SensorUpdatePolicyName -ne 'platform_default') { 'yes' } else { 'no' } + }) + Write-FalconDebug -Step 'environment' -Pairs ([ordered]@{ + os = 'windows' + os_version = [System.Environment]::OSVersion.Version.ToString() + os_arch = $env:PROCESSOR_ARCHITECTURE + run_as = if (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'admin' } else { 'user' } + }) # TLS check should be first since it's needed for all HTTPS communication if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') { try { @@ -551,6 +679,7 @@ process { $message = "Retrieving sensor policy details for '$($SensorUpdatePolicyName)'" Write-FalconLog 'GetPolicy' $message $filter = "platform_name:'Windows'+name.raw:'$($SensorUpdatePolicyName)'" + Write-FalconDebug -Step 'GetPolicy' -Pairs ([ordered]@{ step = 'query'; path = '/policy/combined/sensor-update/v2'; filter = $filter }) $url = "${BaseUrl}/policy/combined/sensor-update/v2?filter=$([System.Web.HttpUtility]::UrlEncode($filter)))" $policy_scope = @{ 'Sensor update policies' = @('Read') @@ -572,22 +701,33 @@ process { $message = "Retrieved sensor policy details: Policy ID: $policyId, Build: $build, Version: $version" Write-FalconLog 'GetPolicy' $message + Write-FalconDebug -Step 'GetPolicy' -Pairs ([ordered]@{ step = 'resolved'; policy_version = $version }) # Get installer details based on normalized policy version $message = "Retrieving installer details for sensor version: '$($version)'" Write-FalconLog 'GetInstaller' $message - $encodedFilter = [System.Web.HttpUtility]::UrlEncode("platform:'windows'+version:'$($version)'") + $installerFilter = "platform:'windows'+version:'$($version)'" + Write-FalconDebug -Step 'GetInstaller' -Pairs ([ordered]@{ step = 'query'; path = '/sensors/combined/installers/v3'; filter = $installerFilter; sort = 'none' }) + $encodedFilter = [System.Web.HttpUtility]::UrlEncode($installerFilter) $url = "${BaseUrl}/sensors/combined/installers/v3?filter=${encodedFilter}" $installer_scope = @{ 'Sensor Download' = @('Read') } $installerDetails = Get-ResourceContent -WebRequestParams $WebRequestParams -url $url -logKey 'GetInstaller' -scope $installer_scope -errorMessage "Unable to fetch installer details from the CrowdStrike Falcon API." + Write-FalconDebug -Step 'GetInstaller' -Pairs ([ordered]@{ step = 'matched'; count = @($installerDetails).Count }) if ( $installerDetails.sha256 -and $installerDetails.name ) { $cloudHash = $installerDetails.sha256 $cloudFile = $installerDetails.name $message = "Found installer: ($cloudFile) with sha256: '$cloudHash'" Write-FalconLog 'GetInstaller' $message + $shaString = [string]$cloudHash + Write-FalconDebug -Step 'GetInstaller' -Pairs ([ordered]@{ + step = 'selected' + index = 0 + file_type = "$($installerDetails.file_type)" + sha = $shaString.Substring(0, [Math]::Min(12, $shaString.Length)) + }) } else { $message = "Failed to retrieve installer details." @@ -605,6 +745,7 @@ process { $localHash = Get-InstallerHash -Path $localFile $message = "Successfull downloaded installer '$localFile' ($localHash)" Write-FalconLog 'DownloadFile' $message + Write-FalconDebug -Step 'DownloadFile' -Pairs ([ordered]@{ step = 'downloaded'; installer = $localFile; bytes = (Get-Item $localFile).Length }) } else { $message = "Failed to download installer." @@ -644,6 +785,13 @@ process { $InstallParams += " ProvWaitTime=$ProvWaitTime" # Begin installation + Write-FalconDebug -Step 'Installer' -Pairs ([ordered]@{ + step = 'configure' + cid_source = if ($FalconCid) { 'param' } else { 'api' } + provisioning_token_set = if ($ProvToken) { 'yes' } else { 'no' } + tags_count = if ($Tags) { $Tags.Count } else { 0 } + proxy_set = if ($ProxyHost) { 'yes' } else { 'no' } + }) Write-FalconLog 'Installer' 'Installing Falcon Sensor...' Write-FalconLog 'StartProcess' 'Starting installer; command-line parameters omitted from the log because they may contain sensitive values' try { @@ -698,6 +846,10 @@ process { } Write-FalconLog 'InstallerProcess' 'Falcon sensor installed successfully.' + # Authoritative install-time version; aid=none is normal here since + # registration completes asynchronously once the sensor reaches the cloud. + $InstalledAid = Get-AID + Write-FalconDebug -Step 'InstallerProcess' -Pairs ([ordered]@{ step = 'installed'; version = $version; aid = if ($InstalledAid) { $InstalledAid } else { 'none' } }) } end { Write-FalconLog 'EndScript' 'Script completed.' diff --git a/powershell/install/falcon_windows_uninstall.ps1 b/powershell/install/falcon_windows_uninstall.ps1 index 8b969b4..aac0faf 100755 --- a/powershell/install/falcon_windows_uninstall.ps1 +++ b/powershell/install/falcon_windows_uninstall.ps1 @@ -47,6 +47,13 @@ The proxy port for the sensor to use when communicating with CrowdStrike [defaul User agent string to append to the User-Agent header when making requests to the CrowdStrike API. .PARAMETER Verbose Enable verbose logging +.PARAMETER FalconDebug +Print redacted progress markers: detected OS and PowerShell version, the exact sensor +query filter, how many installers matched and which was chosen, the API route and HTTP +status for every call, and the sensor version installed plus the AID (that version is the one resolved from +the policy or query, not re-read from the binary). Values are dropped +unless the key is on a fixed allow-list, so secrets cannot appear. Also honors `$env:FALCON_DEBUG=1`. +Do not use `Set-PSDebug -Trace` or the common `-Debug` parameter for support; they print credentials. .EXAMPLE PS>.\falcon_windows_uninstall.ps1 -MaintenanceToken @@ -61,6 +68,10 @@ after uninstalling. [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DeleteUninstaller')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DeleteScript')] +# Read inside Test-FalconDebugEnabled, which the rule does not follow. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'FalconDebug')] +# Debug markers must stay out of the pipeline and out of the on-disk log. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param( [Parameter(Position = 1)] [string] $MaintenanceToken, @@ -112,7 +123,10 @@ param( [string] $FalconAccessToken, [Parameter(Position = 16)] - [string] $UserAgent + [string] $UserAgent, + + [Parameter(Position = 17)] + [switch] $FalconDebug ) begin { Set-PSDebug -Off @@ -141,6 +155,8 @@ begin { } else { $BaseUserAgent } + # PSEdition is absent on PowerShell 3/4; Desktop is the only edition they had. + $PSEditionValue = if ($PSVersionTable.PSEdition) { $PSVersionTable.PSEdition } else { 'Desktop' } function Write-FalconLog ([string] $Source, [string] $Message, [bool] $stdout = $true) { $Content = @(Get-Date -Format 'yyyy-MM-dd hh:MM:ss') @@ -178,6 +194,70 @@ begin { Write-FalconLog -Source 'VERBOSE' -Message $message -stdout $false } + function Test-FalconDebugEnabled { + if ($FalconDebug) { return $true } + if ($env:FALCON_DEBUG -match '^(1|true)\z') { return $true } + return $false + } + + # Allow-list, the single decision point for both marker paths. Only known-safe + # keys keep their value; everything else is dropped, so a future debug line + # cannot leak a secret by accident. + function Protect-FalconDebugPair([string] $Key, [string] $Value) { + $SafeKeys = @( + 'step', 'source', 'error', 'stage', + 'cloud', 'old_cloud', 'new_cloud', 'region', 'region_hint', 'sensor_cloud', + 'http_status', 'curl_exit', 'exit_code', 'path', 'filter', 'sort', + 'os', 'os_version', 'os_arch', 'os_family', 'kernel', 'pkg_manager', 'distro_id', 'run_as', + 'count', 'index', 'decrement', 'version', 'sensor_version', 'policy_version', 'file_type', 'sha', + 'installer', 'bytes', 'sha_verify', 'billing', 'backend', 'apd', 'aid', 'cid_source', + 'tags_count', 'grouping_tags_count', 'sensor_type', 'param', 'registry', 'repository', 'tag', + 'client_id_set', 'client_secret_set', 'access_token_set', 'member_cid_set', + 'provisioning_token_set', 'maintenance_token_set', 'proxy_set', 'policy_name_set', + 'tags_set', 'grouping_tags_set' + ) + if ($SafeKeys -ccontains $Key) { return "$Key=$Value" } + return "$Key=[DROPPED]" + } + + function Protect-FalconDebugMessage([string] $Message) { + $Filtered = @() + foreach ($Token in ($Message -split '\s+')) { + if ([string]::IsNullOrEmpty($Token)) { continue } + $Split = $Token.IndexOf('=') + if ($Split -lt 1) { continue } + $Filtered += Protect-FalconDebugPair $Token.Substring(0, $Split) $Token.Substring($Split + 1) + } + return ($Filtered -join ' ') + } + + # Write-Host on purpose: keeps markers out of the pipeline and out of the log file. + function Write-FalconDebug { + param( + [Parameter(Mandatory = $true)][string] $Step, + [string] $Message, + [System.Collections.IDictionary] $Pairs + ) + if (-not (Test-FalconDebugEnabled)) { return } + $Parts = @() + if ($Message) { $Parts += Protect-FalconDebugMessage $Message } + # -Pairs is required for any value that can contain a space, such as an FQL + # filter holding a multi-word policy name. Splitting a joined string cannot + # carry those safely: a bare word would be glued onto the previous value. + if ($Pairs) { + foreach ($Key in $Pairs.Keys) { + $Parts += Protect-FalconDebugPair ([string]$Key) ([string]$Pairs[$Key]) + } + } + $Filtered = ($Parts | Where-Object { $_ }) -join ' ' + if ($Filtered) { + Write-Host "FALCON_DEBUG: $Step $Filtered" + } + else { + Write-Host "FALCON_DEBUG: $Step" + } + } + function Get-FalconCloud ([string] $xCsRegion) { $Output = switch ($xCsRegion) { 'autodiscover' { 'https://api.crowdstrike.com'; break } @@ -229,6 +309,7 @@ begin { $Headers = @{'Accept' = 'application/json'; 'Content-Type' = 'application/x-www-form-urlencoded'; 'charset' = 'utf-8' } $Headers.Add('User-Agent', $FullUserAgent) if ($FalconAccessToken) { + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "source=access_token cloud=${FalconCloud}" $Headers.Add('Authorization', "bearer $($FalconAccessToken)") } else { @@ -239,7 +320,11 @@ begin { # below. $RedirectResponse = $null try { + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=request cloud=${FalconCloud}" $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body -MaximumRedirection 0 + # Status marker before ConvertFrom-Json: on Windows PowerShell 5.1 a + # 308 is returned, not thrown, and parsing it would fail first. + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=response http_status=$([int]$response.StatusCode) cloud=${FalconCloud}" if ([int]$response.StatusCode -in @(301, 302, 303, 307, 308)) { $RedirectResponse = $response @@ -256,8 +341,10 @@ begin { } } catch { - # Handle redirects - Write-Verbose "Invoke-FalconAuth - CAUGHT EXCEPTION - `$_.Exception.Message`r`n$($_.Exception.Message)" + # Status only. Never log the exception, its message, or the response: + # they can carry the request body and the Authorization header. + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "http_status=$debugStatus error=oauth_request_failed" $response = $_.Exception.Response if (!$response) { @@ -295,6 +382,9 @@ begin { # Get-FalconCloud validates the region against its own allowlist, # not the Location header. $BaseUrl = Get-FalconCloud($region) + # Printed only after validation, so a hostile header cannot inject + # arbitrary text into the console. + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=region_retry region=$region" $BaseUrl, $Headers = Invoke-FalconAuth -WebRequestParams $WebRequestParams -BaseUrl $BaseUrl -Body $Body -FalconCloud $FalconCloud } } @@ -376,7 +466,9 @@ begin { $url = "${BaseUrl}/devices/entities/devices-actions/v2?action_name=${action}" try { + Write-FalconDebug -Step 'Invoke-HostVisibility' -Message 'step=request path=/devices/entities/devices-actions/v2' $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Body $bodyJson -MaximumRedirection 0 + Write-FalconDebug -Step 'Invoke-HostVisibility' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-HostVisibility - $content:' @@ -392,7 +484,8 @@ begin { } } catch { - Write-VerboseLog -VerboseInput $_.Exception.Message -PreMessage 'Invoke-HostVisibility - CAUGHT EXCEPTION - $_.Exception.Message:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Invoke-HostVisibility' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response if (!$response) { @@ -424,6 +517,22 @@ begin { } } process { + Write-FalconDebug -Step 'start' -Pairs ([ordered]@{ + version = "$ScriptVersion (PowerShell $($PSVersionTable.PSVersion) $PSEditionValue)" + cloud = $FalconCloud + client_id_set = if ($FalconClientId) { 'yes' } else { 'no' } + client_secret_set = if ($FalconClientSecret) { 'yes' } else { 'no' } + access_token_set = if ($FalconAccessToken) { 'yes' } else { 'no' } + member_cid_set = if ($MemberCid) { 'yes' } else { 'no' } + maintenance_token_set = if ($MaintenanceToken) { 'yes' } else { 'no' } + proxy_set = if ($ProxyHost) { 'yes' } else { 'no' } + }) + Write-FalconDebug -Step 'environment' -Pairs ([ordered]@{ + os = 'windows' + os_version = [System.Environment]::OSVersion.Version.ToString() + os_arch = $env:PROCESSOR_ARCHITECTURE + run_as = if (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'admin' } else { 'user' } + }) if (!$GetAccessToken) { if (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) -eq $false) { @@ -549,6 +658,7 @@ process { $Message = "Found AID: $aid" } Write-FalconLog 'GetAID' $Message + Write-FalconDebug -Step 'GetAID' -Pairs ([ordered]@{ aid = if ($aid) { $aid } else { 'none' } }) } if ($RemoveHost) { @@ -560,6 +670,7 @@ process { if ($MaintenanceToken) { # Assume the maintenance token is a valid Token and skip API calls $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" + Write-FalconDebug -Step 'GetToken' -Pairs ([ordered]@{ source = 'param'; maintenance_token_set = 'yes' }) } else { if ($aid) { @@ -575,7 +686,9 @@ process { $url = "${BaseUrl}/policy/combined/reveal-uninstall-token/v1" try { + Write-FalconDebug -Step 'GetToken' -Message 'step=request path=/policy/combined/reveal-uninstall-token/v1' $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Body $bodyJson -MaximumRedirection 0 + Write-FalconDebug -Step 'GetToken' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content if ($content.errors) { @@ -588,10 +701,12 @@ process { $MaintenanceToken = $content.resources[0].uninstall_token Write-FalconLog 'GetToken' 'Retrieved maintenance token' $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" + Write-FalconDebug -Step 'GetToken' -Pairs ([ordered]@{ source = 'api'; maintenance_token_set = 'yes' }) } } catch { - Write-VerboseLog -VerboseInput $_.Exception.Message -PreMessage 'GetToken - CAUGHT EXCEPTION - $_.Exception.Message:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'GetToken' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response if (!$response) { @@ -640,6 +755,7 @@ process { $UninstallerProcess = Start-Process -FilePath "$UninstallerPath" -ArgumentList $UninstallParams -PassThru -Wait $UninstallerProcessId = $UninstallerProcess.Id Write-FalconLog 'StartProcess' "Started '$UninstallerPath' ($UninstallerProcessId)" + Write-FalconDebug -Step 'StartProcess' -Pairs ([ordered]@{ step = 'result'; exit_code = $UninstallerProcess.ExitCode }) if ($UninstallerProcess.ExitCode -ne 0) { Write-VerboseLog -VerboseInput $UninstallerProcess -PreMessage 'PROCESS EXIT CODE ERROR - $UninstallerProcess:' if ($UninstallerProcess.ExitCode -eq 106) { diff --git a/powershell/migrate/README.md b/powershell/migrate/README.md index ed3e07c..df2b092 100644 --- a/powershell/migrate/README.md +++ b/powershell/migrate/README.md @@ -102,6 +102,13 @@ Remove host from CrowdStrike Falcon Opt in/out of migrating tags. Tags passed to the Tags flag will still be added. .PARAMETER Verbose Enable verbose logging +.PARAMETER FalconDebug +Print redacted progress markers: detected OS and PowerShell version, the exact sensor +query filter, how many installers matched and which was chosen, the API route and HTTP +status for every call, and the sensor version installed plus the AID (that version is the one resolved from +the policy or query, not re-read from the binary). Values are dropped +unless the key is on a fixed allow-list, so secrets cannot appear. Also honors `$env:FALCON_DEBUG=1`. +Do not use `Set-PSDebug -Trace` or the common `-Debug` parameter for support; they print credentials. ``` ---------- @@ -174,43 +181,34 @@ Migrate from one CID to another within the same cloud: ## Troubleshooting -To assist in troubleshooting the migration script, you can try the following: - -- Use the `-Verbose` parameter to enable verbose logging. - - > Note: This will display additional logging in the console, as well as in the log file. - - Example: - - ```pwsh - .\falcon_windows_migrate.ps1 ` - -Verbose ` - -NewFalconClientId 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -NewFalconClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -OldFalconClientId 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -OldFalconClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -NewFalconCloud "us-2" ` - -OldFalconCloud "us-1" - ``` - -- For a more detailed approach, you can use `Set-PSDebug -Trace`. This cmdlet offers three trace levels (0-2): - - - 0 : Turn script block logging off. (Equivalent to -Off) - - 1 : Turn script block logging on. (Equivalent to -On) - - 2 : Turn script block logging on and generate a trace of all commands in a script block and the arguments they were used with. - > Similar to the output of `set -x` in bash. Very noisy but contains a lot of useful information. - - Example: - - ```pwsh - Set-PSDebug -Trace 2 - .\falcon_windows_migrate.ps1 ` - -NewFalconClientId 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -NewFalconClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -OldFalconClientId 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -OldFalconClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` - -NewFalconCloud "us-2" ` - -OldFalconCloud "us-1" - # To turn off tracing - Set-PSDebug -Trace 0 - ``` +Use the redacted debug mode. It prints step names, HTTP status and cloud/region. +Values are dropped unless the key is on a fixed allow-list, so neither the old nor +the new CID credentials can appear in the output you send to support. + +```pwsh +.\falcon_windows_migrate.ps1 ` + -FalconDebug ` + -NewFalconClientId 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` + -NewFalconClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` + -OldFalconClientId 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` + -OldFalconClientSecret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ` + -NewFalconCloud "us-2" ` + -OldFalconCloud "us-1" +``` + +`$env:FALCON_DEBUG = '1'` does the same thing, which is useful when the script runs +from a job where you cannot add a parameter. + +`-Verbose` still enables the script's own operational logging in the console and the +log file. It is not a replacement for `-FalconDebug`. + +> Note: debug markers go to the console via `Write-Host`, so they are not written to +> the script's log file. A `Start-Transcript` session does capture them, so stop any +> transcript first if you do not want the markers on disk. + +Do **not** use `Set-PSDebug -Trace` or the common `-Debug` parameter for support. +Tracing prints every statement with its arguments, including +`OldFalconClientSecret`, `NewFalconClientSecret`, access tokens, maintenance tokens, +`Authorization` headers and the OAuth request body. This script calls +`Set-PSDebug -Off` on entry, but that runs after PowerShell binds the parameters, so +a trace started beforehand can still expose the values you passed in. diff --git a/powershell/migrate/falcon_windows_migrate.ps1 b/powershell/migrate/falcon_windows_migrate.ps1 index 21e73c6..938c3e2 100644 --- a/powershell/migrate/falcon_windows_migrate.ps1 +++ b/powershell/migrate/falcon_windows_migrate.ps1 @@ -64,10 +64,21 @@ This parameter forces the sensor to skip those attempts and ignore any proxy con User agent string to append to the User-Agent header when making requests to the CrowdStrike API. .PARAMETER Verbose Enable verbose logging +.PARAMETER FalconDebug +Print redacted progress markers: detected OS and PowerShell version, the exact sensor +query filter, how many installers matched and which was chosen, the API route and HTTP +status for every call, and the sensor version installed plus the AID (that version is the one resolved from +the policy or query, not re-read from the binary). Values are dropped +unless the key is on a fixed allow-list, so secrets cannot appear. Also honors `$env:FALCON_DEBUG=1`. +Do not use `Set-PSDebug -Trace` or the common `-Debug` parameter for support; they print credentials. #> #Requires -Version 3.0 [CmdletBinding()] +# Read inside Test-FalconDebugEnabled, which the rule does not follow. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'FalconDebug')] +# Debug markers must stay out of the pipeline and out of the on-disk log. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param( [Parameter(Position = 1)] [ValidatePattern('\w{32}')] @@ -132,7 +143,9 @@ param( [Parameter(Position = 27)] [switch] $ProxyDisable, [Parameter(Position = 28)] - [string] $UserAgent + [string] $UserAgent, + [Parameter(Position = 29)] + [switch] $FalconDebug ) Set-PSDebug -Off @@ -259,6 +272,70 @@ function Write-VerboseLog ([psobject] $VerboseInput, [string] $PreMessage) { Write-FalconLog -Source 'VERBOSE' -Message $message -stdout $false } +function Test-FalconDebugEnabled { + if ($FalconDebug) { return $true } + if ($env:FALCON_DEBUG -match '^(1|true)\z') { return $true } + return $false +} + +# Allow-list, the single decision point for both marker paths. Only known-safe +# keys keep their value; everything else is dropped, so a future debug line +# cannot leak a secret by accident. +function Protect-FalconDebugPair([string] $Key, [string] $Value) { + $SafeKeys = @( + 'step', 'source', 'error', 'stage', + 'cloud', 'old_cloud', 'new_cloud', 'region', 'region_hint', 'sensor_cloud', + 'http_status', 'curl_exit', 'exit_code', 'path', 'filter', 'sort', + 'os', 'os_version', 'os_arch', 'os_family', 'kernel', 'pkg_manager', 'distro_id', 'run_as', + 'count', 'index', 'decrement', 'version', 'sensor_version', 'policy_version', 'file_type', 'sha', + 'installer', 'bytes', 'sha_verify', 'billing', 'backend', 'apd', 'aid', 'cid_source', + 'tags_count', 'grouping_tags_count', 'sensor_type', 'param', 'registry', 'repository', 'tag', + 'client_id_set', 'client_secret_set', 'access_token_set', 'member_cid_set', + 'provisioning_token_set', 'maintenance_token_set', 'proxy_set', 'policy_name_set', + 'tags_set', 'grouping_tags_set' + ) + if ($SafeKeys -ccontains $Key) { return "$Key=$Value" } + return "$Key=[DROPPED]" +} + +function Protect-FalconDebugMessage([string] $Message) { + $Filtered = @() + foreach ($Token in ($Message -split '\s+')) { + if ([string]::IsNullOrEmpty($Token)) { continue } + $Split = $Token.IndexOf('=') + if ($Split -lt 1) { continue } + $Filtered += Protect-FalconDebugPair $Token.Substring(0, $Split) $Token.Substring($Split + 1) + } + return ($Filtered -join ' ') +} + +# Write-Host on purpose: keeps markers out of the pipeline and out of the log file. +function Write-FalconDebug { + param( + [Parameter(Mandatory = $true)][string] $Step, + [string] $Message, + [System.Collections.IDictionary] $Pairs + ) + if (-not (Test-FalconDebugEnabled)) { return } + $Parts = @() + if ($Message) { $Parts += Protect-FalconDebugMessage $Message } + # -Pairs is required for any value that can contain a space, such as an FQL + # filter holding a multi-word policy name. Splitting a joined string cannot + # carry those safely: a bare word would be glued onto the previous value. + if ($Pairs) { + foreach ($Key in $Pairs.Keys) { + $Parts += Protect-FalconDebugPair ([string]$Key) ([string]$Pairs[$Key]) + } + } + $Filtered = ($Parts | Where-Object { $_ }) -join ' ' + if ($Filtered) { + Write-Host "FALCON_DEBUG: $Step $Filtered" + } + else { + Write-Host "FALCON_DEBUG: $Step" + } +} + # Uninstall Falcon Sensor function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $UninstallParams, [switch] $RemoveHost, [bool] $DeleteUninstaller, [string] $MaintenanceToken, [string] $UninstallTool) { @@ -308,6 +385,7 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst if ($MaintenanceToken) { # Assume the maintenance token is a valid Token and skip API calls $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" + Write-FalconDebug -Step 'GetToken' -Pairs ([ordered]@{ source = 'param'; maintenance_token_set = 'yes' }) } else { if ($oldAid) { @@ -324,7 +402,9 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst try { $url = "${oldBaseUrl}/policy/combined/reveal-uninstall-token/v1" + Write-FalconDebug -Step 'GetToken' -Message 'step=request path=/policy/combined/reveal-uninstall-token/v1' $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Headers $oldCloudHeaders -Body $bodyJson -MaximumRedirection 0 + Write-FalconDebug -Step 'GetToken' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content if ($content.errors) { @@ -337,10 +417,12 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst $MaintenanceToken = $content.resources[0].uninstall_token Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Retrieved maintenance token' $UninstallParams += " MAINTENANCE_TOKEN=$MaintenanceToken" + Write-FalconDebug -Step 'GetToken' -Pairs ([ordered]@{ source = 'api'; maintenance_token_set = 'yes' }) } } catch { - Write-VerboseLog -VerboseInput $_.Exception -PreMessage 'GetToken - CAUGHT EXCEPTION - $_.Exception:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'GetToken' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response if (!$response) { @@ -374,6 +456,7 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst $UninstallerProcess = Start-Process -FilePath "$UninstallerPath" -ArgumentList $UninstallParams -PassThru -Wait $UninstallerProcessId = $UninstallerProcess.Id Write-FalconLog -Source 'Invoke-FalconUninstall' -Message "Started '$UninstallerPath' ($UninstallerProcessId)" + Write-FalconDebug -Step 'Invoke-FalconUninstall' -Pairs ([ordered]@{ step = 'result'; aid = if ($oldAid) { $oldAid } else { 'none' }; exit_code = $UninstallerProcess.ExitCode }) if ($UninstallerProcess.ExitCode -ne 0) { Write-VerboseLog -VerboseInput $UninstallerProcess -PreMessage 'PROCESS EXIT CODE ERROR - $UninstallerProcess:' if ($UninstallerProcess.ExitCode -eq 106) { @@ -387,6 +470,7 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst if ($RemoveHost) { $Message = 'Uninstall failed, attempting to restore host visibility...' Write-FalconLog -Source 'Invoke-FalconUninstall' -Message $Message + Write-FalconDebug -Step 'Invoke-FalconUninstall' -Pairs ([ordered]@{ stage = 'rollback_host_visibility' }) Invoke-HostVisibility -WebRequestParams $WebRequestParams -Aid $oldAid -action 'show' -BaseUrl $oldBaseUrl -Headers $oldCloudHeaders } throw $Message @@ -432,7 +516,10 @@ function Invoke-FalconUninstall ([hashtable] $WebRequestParams, [string] $Uninst Write-FalconLog -Source 'Invoke-FalconUninstall' -Message 'Falcon Sensor successfully uninstalled.' } catch { - Write-VerboseLog -VerboseInput $_.Exception -PreMessage 'Invoke-FalconUninstall - CAUGHT EXCEPTION - $_.Exception:' + # Status only. Serialising the exception can put a response body, and + # with it the Authorization header, into the on-disk log. + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Invoke-FalconUninstall' -Message "http_status=$debugStatus error=uninstall_failed" $errorMessage = if ($_.Exception -and $_.Exception.Message) { $_.Exception.Message } else { @@ -486,6 +573,7 @@ function Invoke-FalconInstall ([hashtable] $WebRequestParams, [string] $InstallP $message = "Retrieving sensor policy details for '$($SensorUpdatePolicyName)'" Write-FalconLog -Source 'Invoke-FalconInstall' -Message $message $filter = "platform_name:'Windows'+name.raw:'$($SensorUpdatePolicyName)'" + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ step = 'query'; path = '/policy/combined/sensor-update/v2'; filter = $filter }) $url = "${newBaseUrl}/policy/combined/sensor-update/v2?filter=$([System.Web.HttpUtility]::UrlEncode($filter))" $policy_scope = @{ 'Sensor update policies' = @('Read') @@ -507,22 +595,33 @@ function Invoke-FalconInstall ([hashtable] $WebRequestParams, [string] $InstallP $message = "Retrieved sensor policy details: Policy ID: $policyId, Build: $build, Version: $version" Write-FalconLog -Source 'Invoke-FalconInstall' -Message $message + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ step = 'resolved'; policy_version = $version }) # Get installer details based on policy version $message = "Retrieving installer details for sensor version: '$($version)'" Write-FalconLog -Source 'Invoke-FalconInstall' -Message $message - $encodedFilter = [System.Web.HttpUtility]::UrlEncode("platform:'windows'+version:'$($version)'") + $installerFilter = "platform:'windows'+version:'$($version)'" + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ step = 'query'; path = '/sensors/combined/installers/v3'; filter = $installerFilter; sort = 'none' }) + $encodedFilter = [System.Web.HttpUtility]::UrlEncode($installerFilter) $url = "${newBaseUrl}/sensors/combined/installers/v3?filter=${encodedFilter}" $installer_scope = @{ 'Sensor Download' = @('Read') } $installerDetails = Get-ResourceContent -WebRequestParams $WebRequestParams -url $url -logKey 'GetInstaller' -scope $installer_scope -errorMessage "Unable to fetch installer details from the CrowdStrike Falcon API." -Headers $newCloudHeaders + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ step = 'matched'; count = @($installerDetails).Count }) if ( $installerDetails.sha256 -and $installerDetails.name ) { $cloudHash = $installerDetails.sha256 $cloudFile = $installerDetails.name $message = "Found installer: ($cloudFile) with sha256: '$cloudHash'" Write-FalconLog -Source 'Invoke-FalconInstall' -Message $message + $shaString = [string]$cloudHash + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ + step = 'selected' + index = 0 + file_type = "$($installerDetails.file_type)" + sha = $shaString.Substring(0, [Math]::Min(12, $shaString.Length)) + }) } else { $message = "Failed to retrieve installer details." @@ -540,6 +639,7 @@ function Invoke-FalconInstall ([hashtable] $WebRequestParams, [string] $InstallP $localHash = Get-InstallerHash -Path $localFile $message = "Successfull downloaded installer '$localFile' ($localHash)" Write-FalconLog -Source 'Invoke-FalconInstall' -Message $message + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ step = 'downloaded'; installer = $localFile; bytes = (Get-Item $localFile).Length }) } else { $message = "Failed to download installer." @@ -579,6 +679,13 @@ function Invoke-FalconInstall ([hashtable] $WebRequestParams, [string] $InstallP $InstallParams += " ProvWaitTime=$ProvWaitTime" # Begin installation + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ + step = 'configure' + cid_source = if ($NewFalconCid) { 'param' } else { 'api' } + provisioning_token_set = if ($ProvToken) { 'yes' } else { 'no' } + tags_count = if ($Tags) { @($Tags -split ',').Count } else { 0 } + proxy_set = if ($ProxyHost) { 'yes' } else { 'no' } + }) Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Installing Falcon Sensor..." Write-FalconLog -Source 'Invoke-FalconInstall' -Message "Starting installer '$LocalFile'; command-line parameters omitted from the log because they may contain sensitive values" @@ -622,6 +729,10 @@ function Invoke-FalconInstall ([hashtable] $WebRequestParams, [string] $InstallP $Message = 'Successfully finished install...' Write-FalconLog -Source 'Invoke-FalconInstall' -Message $Message + # Authoritative install-time version; aid=none is normal here since + # registration completes asynchronously once the sensor reaches the cloud. + $InstalledAid = Get-AID + Write-FalconDebug -Step 'Invoke-FalconInstall' -Pairs ([ordered]@{ step = 'installed'; version = $version; aid = if ($InstalledAid) { $InstalledAid } else { 'none' } }) } catch { $errorMessage = if ($_.Exception -and $_.Exception.Message) { @@ -683,7 +794,9 @@ function Format-403Error([string] $url, [hashtable] $scope) { function Get-ResourceContent([hashtable] $WebRequestParams, [string] $url, [string] $logKey, [hashtable] $scope, [string] $errorMessage, [hashtable] $Headers) { try { + Write-FalconDebug -Step 'Get-ResourceContent' -Message "step=request path=$(([Uri]$url).AbsolutePath)" $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'GET' -Headers $Headers -MaximumRedirection 0 + Write-FalconDebug -Step 'Get-ResourceContent' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content Write-VerboseLog -VerboseInput $content -PreMessage 'Get-ResourceContent - $content:' @@ -703,7 +816,8 @@ function Get-ResourceContent([hashtable] $WebRequestParams, [string] $url, [stri } } catch { - Write-VerboseLog -VerboseInput $_.Exception -PreMessage 'Get-ResourceContent - CAUGHT EXCEPTION - $_.Exception:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Get-ResourceContent' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response if (!$response) { @@ -771,7 +885,9 @@ function Invoke-HostVisibility ([hashtable] $WebRequestParams, [string] $Aid, [s $bodyJson = $Body | ConvertTo-Json try { $url = "${BaseUrl}/devices/entities/devices-actions/v2?action_name=${action}" + Write-FalconDebug -Step 'Invoke-HostVisibility' -Message 'step=request path=/devices/entities/devices-actions/v2' $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'POST' -Headers $Headers -Body $bodyJson -MaximumRedirection 0 + Write-FalconDebug -Step 'Invoke-HostVisibility' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content Write-VerboseLog -VerboseInput $content -PreMessage 'Invoke-HostVisibility - $content:' @@ -787,7 +903,8 @@ function Invoke-HostVisibility ([hashtable] $WebRequestParams, [string] $Aid, [s } } catch { - Write-VerboseLog -VerboseInput $_.Exception -PreMessage 'Invoke-HostVisibility - CAUGHT EXCEPTION - $_.Exception:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Invoke-HostVisibility' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response if (!$response) { @@ -836,6 +953,7 @@ function Get-InstallerHash ([string] $Path) { function Invoke-FalconDownload ([hashtable] $WebRequestParams, [string] $url, [string] $Outfile, [hashtable] $Headers) { try { $ProgressPreference = 'SilentlyContinue' + Write-FalconDebug -Step 'Invoke-FalconDownload' -Message "step=request path=$(([Uri]$url).AbsolutePath)" $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'GET' -Headers $Headers -OutFile $Outfile } catch { @@ -876,7 +994,9 @@ function Set-Tag ([hashtable] $WebRequestParams, [string] $Aid, [array] $Tags, [ 'tags' = $Tags } $body = ConvertTo-Json -InputObject $body + Write-FalconDebug -Step 'Set-Tag' -Message 'step=request path=/devices/entities/devices/tags/v1' $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'PATCH' -Headers $Headers -Body $body -MaximumRedirection 0 + Write-FalconDebug -Step 'Set-Tag' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content Write-VerboseLog -VerboseInput $content -PreMessage 'Set-Tag - $content:' @@ -898,7 +1018,8 @@ function Set-Tag ([hashtable] $WebRequestParams, [string] $Aid, [array] $Tags, [ return $tagsSet, $errorMessage } catch { - Write-VerboseLog -VerboseInput $_.Exception -PreMessage 'Set-Tag - CAUGHT EXCEPTION - $_.Exception:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Set-Tag' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response if (!$response) { @@ -926,7 +1047,9 @@ function Get-Tag ([hashtable] $WebRequestParams, [string] $Aid, [string] $BaseUr $url = "${BaseUrl}/devices/entities/devices/v2?ids=${aid}" Write-FalconLog -Source 'Get-Tag' -Message "Calling ${url}" + Write-FalconDebug -Step 'Get-Tag' -Message 'step=request path=/devices/entities/devices/v2' $response = Invoke-WebRequest @WebRequestParams -Uri $url -UseBasicParsing -Method 'GET' -Headers $Headers -MaximumRedirection 0 + Write-FalconDebug -Step 'Get-Tag' -Message "step=response http_status=$([int]$response.StatusCode)" $content = ConvertFrom-Json -InputObject $response.Content Write-VerboseLog -VerboseInput $content -PreMessage 'Get-Tag - $content:' @@ -944,7 +1067,8 @@ function Get-Tag ([hashtable] $WebRequestParams, [string] $Aid, [string] $BaseUr } } catch { - Write-VerboseLog -VerboseInput $_.Exception -PreMessage 'Get-Tag - CAUGHT EXCEPTION - $_.Exception:' + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Get-Tag' -Message "http_status=$debugStatus error=request_failed" $response = $_.Exception.Response Write-FalconLog -Source 'Get-Tag' -Message $_.Exception @@ -1045,7 +1169,11 @@ function Invoke-FalconAuth([hashtable] $WebRequestParams, [string] $BaseUrl, [ha # funnel into $RedirectResponse and are handled once below. $RedirectResponse = $null try { + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=request cloud=${FalconCloud}" $response = Invoke-WebRequest @WebRequestParams -Uri "$($BaseUrl)/oauth2/token" -UseBasicParsing -Method 'POST' -Headers $Headers -Body $Body -MaximumRedirection 0 + # Status marker before ConvertFrom-Json: on Windows PowerShell 5.1 a 308 + # is returned, not thrown, and parsing it would fail first. + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=response http_status=$([int]$response.StatusCode) cloud=${FalconCloud}" if ([int]$response.StatusCode -in @(301, 302, 303, 307, 308)) { $RedirectResponse = $response @@ -1062,8 +1190,10 @@ function Invoke-FalconAuth([hashtable] $WebRequestParams, [string] $BaseUrl, [ha } } catch { - # Handle redirects - Write-Verbose "Invoke-FalconAuth - CAUGHT EXCEPTION - `$_.Exception.Message`r`n$($_.Exception.Message)" + # Status only. Never log the exception, its message, or the response: + # they can carry the request body and the Authorization header. + $debugStatus = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 'none' } + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "http_status=$debugStatus error=oauth_request_failed" $response = $_.Exception.Response if (!$response) { @@ -1101,6 +1231,9 @@ function Invoke-FalconAuth([hashtable] $WebRequestParams, [string] $BaseUrl, [ha # Get-FalconCloud validates the region against its own allowlist, not # the Location header. $BaseUrl = Get-FalconCloud($region) + # Printed only after validation, so a hostile header cannot inject + # arbitrary text into the console. + Write-FalconDebug -Step 'Invoke-FalconAuth' -Message "step=region_retry region=$region" $BaseUrl, $Headers = Invoke-FalconAuth -WebRequestParams $WebRequestParams -BaseUrl $BaseUrl -Body $Body -FalconCloud $FalconCloud } @@ -1146,6 +1279,25 @@ $FullUserAgent = if ($UserAgent) { } else { $BaseUserAgent } +# PSEdition is absent on PowerShell 3/4; Desktop is the only edition they had. +$PSEditionValue = if ($PSVersionTable.PSEdition) { $PSVersionTable.PSEdition } else { 'Desktop' } +Write-FalconDebug -Step 'start' -Pairs ([ordered]@{ + version = "$ScriptVersion (PowerShell $($PSVersionTable.PSVersion) $PSEditionValue)" + old_cloud = $OldFalconCloud + new_cloud = $NewFalconCloud + client_id_set = if ($NewFalconClientId -and $OldFalconClientId) { 'yes' } else { 'no' } + client_secret_set = if ($NewFalconClientSecret -and $OldFalconClientSecret) { 'yes' } else { 'no' } + member_cid_set = if ($NewMemberCid -or $OldMemberCid) { 'yes' } else { 'no' } + proxy_set = if ($ProxyHost) { 'yes' } else { 'no' } + tags_set = if ($Tags) { 'yes' } else { 'no' } + grouping_tags_set = if ($FalconTags) { 'yes' } else { 'no' } + }) +Write-FalconDebug -Step 'environment' -Pairs ([ordered]@{ + os = 'windows' + os_version = [System.Environment]::OSVersion.Version.ToString() + os_arch = $env:PROCESSOR_ARCHITECTURE + run_as = if (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'admin' } else { 'user' } + }) # Hashtable for common Invoke-WebRequest parameters $WebRequestParams = @{} @@ -1179,10 +1331,12 @@ if ($proxy) { $sensorGroupingTags = @() $falconGroupingTags = @() $oldAid = Get-AID +Write-FalconDebug -Step 'GetOldAID' -Pairs ([ordered]@{ aid = if ($oldAid) { $oldAid } else { 'none' } }) $recoveryMode = (Test-Path $recoveryCsvPath) if ($recoveryMode) { Write-FalconLog -Source 'RecoveryMode' -Message 'Recovery mode detected. Attempting to recover from previous migration attempt.' + Write-FalconDebug -Step 'RecoveryMode' -Pairs ([ordered]@{ stage = 'recovery' }) $recoveryData = Read-RecoveryCsv -Path $recoveryCsvPath $sensorGroupingTags = $recoveryData.SensorGroupingTags $falconGroupingTags = $recoveryData.FalconGroupingTags @@ -1216,7 +1370,9 @@ $sensorGroupingTags += $sensorGroupingTagsDiff | Where-Object { $_ -ne "" } $falconGroupingTags += $falconGroupingTagsDiff | Where-Object { $_ -ne "" } Write-FalconLog -Source 'DisplaySensorTags' -Message "Sensor Grouping tags: $sensorGroupingTags" +Write-FalconDebug -Step 'DisplaySensorTags' -Pairs ([ordered]@{ grouping_tags_count = @($sensorGroupingTags).Count }) Write-FalconLog -Source 'DisplayFalconTags' -Message "Falcon Grouping tags: $falconGroupingTags" +Write-FalconDebug -Step 'DisplayFalconTags' -Pairs ([ordered]@{ tags_count = @($falconGroupingTags).Count }) Write-FalconLog -Source 'CreateRecoveryCSV' -Message 'Creating recovery csv to keep track of tags...' Write-RecoveryCsv -SensorGroupingTags $sensorGroupingTags -FalconGroupingTags $falconGroupingTags -OldAid $oldAid -Path $recoveryCsvPath @@ -1242,6 +1398,7 @@ if ($null -eq $newAid) { } else { Write-FalconLog -Source 'GetNewAID' -Message "Successfully retrieved new AID: $newAid" + Write-FalconDebug -Step 'GetNewAID' -Pairs ([ordered]@{ aid = $newAid }) } # Set falcon sensor tags