diff --git a/.github/workflows/bash_migrate.yml b/.github/workflows/bash_migrate.yml new file mode 100644 index 00000000..280e5059 --- /dev/null +++ b/.github/workflows/bash_migrate.yml @@ -0,0 +1,163 @@ +name: "CI: bash_migrate" +on: + schedule: + - cron: '0 4 * * *' + + push: + paths: + - 'molecule/bash_migrate/**' + - 'bash/migrate/**.sh' + - '.github/workflows/bash_migrate.yml' + + pull_request_target: + types: [ labeled ] + paths: + - 'molecule/bash_migrate/**' + - 'bash/migrate/**.sh' + - '.github/workflows/bash_migrate.yml' + +jobs: + molecule: + if: | + github.event_name == 'push' || + github.event_name == 'schedule' || + (github.event_name == 'pull_request_target' && + github.event.label.name == 'ok-to-test') + name: ${{ matrix.molecule.distro }}-${{ matrix.collection_role }} + runs-on: ubuntu-latest + env: + PY_COLORS: 1 + ANSIBLE_FORCE_COLOR: 1 + FALCON_CLIENT_ID: ${{ secrets.FALCON_CLIENT_ID }} + FALCON_CLIENT_SECRET: ${{ secrets.FALCON_CLIENT_SECRET }} + AWS_REGION: "us-west-1" + MOLECULE_VPC_SUBNET_ID: ${{ secrets.MOLECULE_VPC_SUBNET_ID }} + permissions: + contents: read + id-token: write + strategy: + fail-fast: false + matrix: + molecule: + - distro: ubuntu-20.04 + image_owner: '099720109477' + image_arch: x86_64 + image_name: ubuntu/images/hvm-ssd/ubuntu-focal-20.04* + instance_type: t2.micro + - distro: ubuntu-22.04 + image_owner: '099720109477' + image_arch: x86_64 + image_name: ubuntu/images/hvm-ssd/ubuntu-jammy-22.04* + instance_type: t2.micro + - distro: amazon-2023 + image_owner: '137112412989' + image_arch: x86_64 + image_name: al2023-ami-2023* + instance_type: t2.micro + - distro: amazon-2 + image_owner: '137112412989' + image_arch: x86_64 + image_name: amzn2-ami-hvm-2.0*gp2 + instance_type: t2.micro + - distro: sles-15-sp5 + image_owner: '013907871322' + image_arch: x86_64 + image_name: suse-sles-15-sp5-v????????-hvm* + instance_type: t2.micro + - distro: almalinux-8 + image_owner: '679593333241' + image_arch: x86_64 + image_name: AlmaLinux OS 8* + instance_type: t2.micro + - distro: rhel-9 + image_owner: '309956199498' + image_arch: x86_64 + image_name: RHEL-9.?.?_HVM-* + instance_type: t2.micro + - distro: rhel-9-arm + image_owner: '309956199498' + image_arch: arm64 + image_name: RHEL-9.?.?_HVM-* + instance_type: t4g.micro + - distro: debian-11 + image_owner: '136693071363' + image_arch: x86_64 + image_name: debian-11-amd64-* + instance_type: t2.micro + collection_role: + - bash_migrate + + steps: + - name: Check out code + uses: actions/checkout@v4 + if: github.event_name != 'pull_request_target' + + - name: Check out code + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.sha}} + if: github.event_name == 'pull_request_target' + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_OIDC_ROLE }} + role-session-name: github-actions-molecule-ansible + aws-region: ${{ env.AWS_REGION }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: '.github/workflows/bash_install.yml' + + - name: Install dependencies + run: | + sudo apt install apt-transport-https ca-certificates curl software-properties-common libssl-dev + python -m pip install --upgrade pip + pip install molecule "molecule-plugins[ec2]" ansible ansible-core==2.16.7 ansible-lint boto3 botocore + + - name: Run role tests + id: molecule-role-test + uses: nick-fields/retry@v3 + env: + MOLECULE_INSTANCE_NAME: ${{ matrix.molecule.distro }}-${{ matrix.collection_role }} + MOLECULE_IMAGE_OWNER: ${{ matrix.molecule.image_owner }} + MOLECULE_IMAGE_ARCH: ${{ matrix.molecule.image_arch }} + MOLECULE_IMAGE_NAME: '${{ matrix.molecule.image_name }}' + MOLECULE_INSTANCE_TYPE: ${{ matrix.molecule.instance_type }} + MOLECULE_REGION: ${{ env.AWS_REGION}} + with: + timeout_minutes: 15 + max_attempts: 3 + retry_on: error + command: >- + molecule --version && + ansible --version && + molecule --debug test --destroy never -s ${{ matrix.collection_role }} + continue-on-error: true + + - name: Ensure instances are destroyed + uses: nick-fields/retry@v3 + env: + MOLECULE_INSTANCE_NAME: ${{ matrix.molecule.distro }}-${{ matrix.collection_role }} + MOLECULE_IMAGE_OWNER: ${{ matrix.molecule.image_owner }} + MOLECULE_IMAGE_ARCH: ${{ matrix.molecule.image_arch }} + MOLECULE_IMAGE_NAME: '${{ matrix.molecule.image_name }}' + MOLECULE_INSTANCE_TYPE: ${{ matrix.molecule.instance_type }} + MOLECULE_REGION: ${{ env.AWS_REGION}} + with: + timeout_minutes: 10 + max_attempts: 3 + retry_on: error + command: >- + molecule --version && + ansible --version && + molecule --debug destroy -s ${{ matrix.collection_role }} + + - name: Assert molecule tests passed + uses: nick-fields/assert-action@v2 + with: + expected: success + actual: ${{ steps.molecule-role-test.outcome }} diff --git a/bash/install/falcon-linux-install.sh b/bash/install/falcon-linux-install.sh index d1680d08..daea439d 100755 --- a/bash/install/falcon-linux-install.sh +++ b/bash/install/falcon-linux-install.sh @@ -33,6 +33,7 @@ Other Options - FALCON_SENSOR_VERSION_DECREMENT (default: 0 [latest]) The number of versions prior to the latest release to install. + For example, 1 would install version N-1. - FALCON_PROVISIONING_TOKEN (default: unset) The provisioning token to use for installing the sensor. diff --git a/bash/migrate/README.md b/bash/migrate/README.md new file mode 100644 index 00000000..f77f0fa0 --- /dev/null +++ b/bash/migrate/README.md @@ -0,0 +1,264 @@ +# Falcon Linux Migration Script + +Bash script to migrate Falcon sensor from one CID to another through the Falcon APIs on a Linux endpoint. By default, this script will uninstall the sensor from the old CID, install it in the new CID, and migrate any existing sensor and Falcon grouping tags. The script also creates a recovery file to facilitate resuming the migration in case of failure. + +## Security Recommendations + +### Use cURL version 7.55.0 or newer + +We have identified a security concern related to cURL versions prior to 7.55, which required request headers to be set using the `-H` option, thus allowing potential secrets to be exposed via the command line. In newer versions of cURL, you can pass headers from stdin using the `@-` syntax, which addresses this security concern. Although our script offers compatibility with the older method by allowing you to set the environment variable `ALLOW_LEGACY_CURL=true`, we strongly urge you to upgrade cURL if your environment permits. + +To check your version of cURL, run the following command: `curl --version` + +## Table of Contents + +- [Falcon API Permissions](#falcon-api-permissions) +- [Configuration](#configuration) + - [Setting up Authentication](#setting-up-authentication) +- [Usage](#usage) + - [Examples](#examples) +- [Troubleshooting](#troubleshooting) + +## Falcon API Permissions + +API clients are granted one or more API scopes. Scopes allow access to specific CrowdStrike APIs and describe the actions that an API client can perform. + +Ensure the following API scopes are enabled for both the old and new CIDs: + +- **Sensor Download** [read] + > Required for downloading the Falcon Sensor installation package. + +- **Installation Tokens** [read] + > Required if your environment enforces installation tokens for Falcon Sensor installation. + +- **Sensor update policies** [read] + > Required when using the `FALCON_SENSOR_UPDATE_POLICY_NAME` environment variable to specify a sensor update policy. + +- **Sensor update policies** [write] + > Required if you want the script to automatically retrieve a maintenance token from the API. + > Not needed if you directly provide the maintenance token via the `FALCON_MAINTENANCE_TOKEN` environment variable. + > Maintenance tokens are required to uninstall sensors that have uninstall protection enabled. + +- **Hosts** [read] + > Required for retrieving sensor and Falcon grouping tags. + +- **Hosts** [write] + > Required for setting Falcon grouping tags and when using the `FALCON_REMOVE_HOST=true` environment variable. + > + > :warning: + > It is recommended to use Host Retention Policies in the Falcon console instead. + +## Configuration + +### Setting up Authentication + +You must provide API credentials for both the old and new CIDs: + +```bash +# Old CID credentials +export OLD_FALCON_CLIENT_ID="XXXXXXX" +export OLD_FALCON_CLIENT_SECRET="YYYYYYYYY" +export OLD_FALCON_CLOUD="us-1" # Optional, defaults to us-1 + +# New CID credentials +export NEW_FALCON_CLIENT_ID="ZZZZZZZ" +export NEW_FALCON_CLIENT_SECRET="WWWWWWW" +export NEW_FALCON_CLOUD="us-2" # Optional, defaults to us-1 +export NEW_FALCON_CID="AAAAAAAAAAA" # Optional, will be auto-detected if not provided +``` + +#### Auto-Discovery of Falcon Cloud Region + +> [!IMPORTANT] +> Auto-discovery is only available for [us-1, us-2, eu-1] regions. + +The scripts support auto-discovery of the Falcon cloud region. If the `[OLD|NEW]FALCON_CLOUD` environment variable is not set, the script will attempt to auto-discover it. If you want to set the cloud region manually, or if your region does not support auto-discovery, you can set the `[OLD|NEW]FALCON_CLOUD` environment variable: + +```bash +export [OLD|NEW]FALCON_CLOUD="us-gov-1" +``` + +## Usage + +```terminal +Usage: falcon-linux-migrate.sh [-h|--help] + +Migrates the Falcon sensor to another Falcon CID. +Version: 1.7.4 + +This script recognizes the following environmental variables: + +Old CID Authentication: + - OLD_FALCON_CLIENT_ID (default: unset) [Required] + Your CrowdStrike Falcon API client ID for the old CID. + + - OLD_FALCON_CLIENT_SECRET (default: unset) [Required] + Your CrowdStrike Falcon API client secret for the old CID. + + - OLD_FALCON_MEMBER_CID (default: unset) + Member CID, used only in multi-CID ("Falcon Flight Control") configurations and + with a parent management CID for the old CID. + + - OLD_FALCON_CLOUD (default: 'us-1') + The cloud region where your old CrowdStrike Falcon instance is hosted. + Accepted values are ['us-1', 'us-2', 'eu-1', 'us-gov-1']. + +New CID Authentication: + - NEW_FALCON_CLIENT_ID (default: unset) [Required] + Your CrowdStrike Falcon API client ID for the new CID. + + - NEW_FALCON_CLIENT_SECRET (default: unset) [Required] + Your CrowdStrike Falcon API client secret for the new CID. + + - NEW_FALCON_MEMBER_CID (default: unset) + Member CID, used only in multi-CID ("Falcon Flight Control") configurations and + with a parent management CID for the new CID. + + - NEW_FALCON_CLOUD (default: 'us-1') + The cloud region where your new CrowdStrike Falcon instance is hosted. + Accepted values are ['us-1', 'us-2', 'eu-1', 'us-gov-1']. + + - NEW_FALCON_CID (default: unset) + Your CrowdStrike Falcon customer ID (CID) for the new CID. + If not specified, will be detected automatically via API. + +Migration Options: + - MIGRATE_TAGS (default: true) + Migrate the host's existing tags to the target CID. + Accepted values are ['true', 'false']. + + - LOG_PATH (default: /tmp) + Location for the log and recovery files. + +Other Options + - FALCON_MAINTENANCE_TOKEN (default: unset) + Sensor uninstall maintenance token used to unlock sensor uninstallation. + If not provided the script will try to retrieve the token from the API. + + - FALCON_PROVISIONING_TOKEN (default: unset) + The provisioning token to use for installing the sensor. + If the provisioning token is unset, the script will attempt to retrieve it from + the API using your authentication credentials and token requirements. + + - FALCON_REMOVE_HOST (default: unset) + Determines whether the host should be removed from the Falcon console after uninstalling the sensor. + Requires API Authentication. + NOTE: It is recommended to use Host Retention Policies in the Falcon console instead. + Accepted values are ['true', 'false']. + + - FALCON_SENSOR_VERSION_DECREMENT (default: 0 [latest]) + The number of versions prior to the latest release to install. + For example, 1 would install version N-1. + + - FALCON_SENSOR_UPDATE_POLICY_NAME (default: unset) + The name of the sensor update policy to use for installing the sensor. + + - FALCON_TAGS (default: unset) + A comma-separated list of sensor grouping tags to apply to the host. + If MIGRATE_TAGS=true these tags will be appended to any existing sensor tags. + + - FALCON_GROUPING_TAGS (default: unset) + A comma-separated list of Falcon grouping tags to apply to the host. + If MIGRATE_TAGS=true these tags will be appended to any existing grouping tags. + + - FALCON_APD (default: unset) + Configures if the proxy should be enabled or disabled. + + - FALCON_APH (default: unset) + The proxy host for the sensor to use when communicating with CrowdStrike. + + - FALCON_APP (default: unset) + The proxy port for the sensor to use when communicating with CrowdStrike. + + - FALCON_BILLING (default: default) + To configure the sensor billing type. + Accepted values are [default|metered]. + + - FALCON_BACKEND (default: auto) + For sensor backend. + Accepted values are values: [auto|bpf|kernel]. + + - FALCON_TRACE (default: none) + To configure the trace level. + Accepted values are [none|err|warn|info|debug] + + - ALLOW_LEGACY_CURL (default: false) + To use the legacy version of curl; version < 7.55.0. + + - USER_AGENT (default: unset) + User agent string to append to the User-Agent header when making + requests to the CrowdStrike API. + +This script recognizes the following argument: + -h, --help + Print this help message and exit. +``` + +### Examples + +#### Migrate a sensor from US-1 to US-2 with tag migration + +```bash +export OLD_FALCON_CLIENT_ID="XXXXXXX" +export OLD_FALCON_CLIENT_SECRET="YYYYYYYYY" +export OLD_FALCON_CLOUD="us-1" +export NEW_FALCON_CLIENT_ID="ZZZZZZZ" +export NEW_FALCON_CLIENT_SECRET="WWWWWWW" +export NEW_FALCON_CLOUD="us-2" +curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.7.4/bash/migrate/falcon-linux-migrate.sh | sudo bash +``` + +#### Migrate a sensor to EU-1 with removal from old console + +```bash +export OLD_FALCON_CLIENT_ID="XXXXXXX" +export OLD_FALCON_CLIENT_SECRET="YYYYYYYYY" +export OLD_FALCON_CLOUD="us-1" +export NEW_FALCON_CLIENT_ID="ZZZZZZZ" +export NEW_FALCON_CLIENT_SECRET="WWWWWWW" +export NEW_FALCON_CLOUD="eu-1" +export FALCON_REMOVE_HOST="true" +curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.7.4/bash/migrate/falcon-linux-migrate.sh | sudo bash +``` + +#### Migrate a sensor with custom tags + +```bash +export OLD_FALCON_CLIENT_ID="XXXXXXX" +export OLD_FALCON_CLIENT_SECRET="YYYYYYYYY" +export NEW_FALCON_CLIENT_ID="ZZZZZZZ" +export NEW_FALCON_CLIENT_SECRET="WWWWWWW" +export FALCON_TAGS="department/it,location/hq" +export FALCON_GROUPING_TAGS="environment/production,criticality/high" +curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.7.4/bash/migrate/falcon-linux-migrate.sh | sudo bash +``` + +#### Migrate a sensor from one CID to another within the same cloud + +```bash +export OLD_FALCON_CLIENT_ID="XXXXXXX" +export OLD_FALCON_CLIENT_SECRET="YYYYYYYYY" +export OLD_FALCON_CLOUD="us-1" +export NEW_FALCON_CLIENT_ID="ZZZZZZZ" +export NEW_FALCON_CLIENT_SECRET="WWWWWWW" +export NEW_FALCON_CLOUD="us-1" +curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.7.4/bash/migrate/falcon-linux-migrate.sh | sudo bash +``` + +## Troubleshooting + +To troubleshoot migration issues, you can run the script with `bash -x` for detailed output: + +```bash +bash -x falcon-linux-migrate.sh +``` + +or + +```bash +curl -L https://raw.githubusercontent.com/crowdstrike/falcon-scripts/v1.7.4/bash/migrate/falcon-linux-migrate.sh | bash -x +``` + +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 new file mode 100755 index 00000000..35fe89c5 --- /dev/null +++ b/bash/migrate/falcon-linux-migrate.sh @@ -0,0 +1,1401 @@ +#!/bin/bash +# +# Bash script to migrate Falcon sensor to another falcon CID. +# + +VERSION="1.7.4" + +print_usage() { + cat <&2 + exit 1 +} + +# If -h or --help is passed, print the usage and exit +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + print_usage + exit 0 +fi + +# Ensure script is ran as root/sudo +if [ "$(id -u)" -ne 0 ]; then + die "This script must be ran as root or with sudo." +fi + +# Validate required environment variables +if [ -z "$OLD_FALCON_CLIENT_ID" ] || [ -z "$OLD_FALCON_CLIENT_SECRET" ] || [ -z "$NEW_FALCON_CLIENT_ID" ] || [ -z "$NEW_FALCON_CLIENT_SECRET" ]; then + die "Required environment variables are not set. Please ensure OLD_FALCON_CLIENT_ID, OLD_FALCON_CLIENT_SECRET, NEW_FALCON_CLIENT_ID, and NEW_FALCON_CLIENT_SECRET are all set." +fi + +# Use the system's temporary directory +log_path="${LOG_PATH:-/tmp}" +recovery_file="$log_path/falcon_migration_recovery.csv" +log_file="$log_path/falcon_migration_$(date +%Y%m%d_%H%M%S).log" +migrate_tags="${MIGRATE_TAGS:-true}" + +log() { + local level="$1" + local message="$2" + echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message" | tee -a "$log_file" +} + +install_sensor() { + echo -n 'Check if Falcon Sensor is running ... ' + cs_sensor_is_running + echo '[ Not present ]' + echo -n 'Falcon Sensor Install ... ' + cs_sensor_install + echo '[ Ok ]' + echo -n 'Falcon Sensor Register ... ' + cs_sensor_register + echo '[ Ok ]' + echo -n 'Falcon Sensor Restart ... ' + cs_sensor_restart + echo '[ Ok ]' + echo 'Falcon Sensor installed successfully.' +} + +uninstall_sensor() { + # Check if Falcon sensor is installed + cs_sensor_installed + + # Handle maintenance token + cs_maintenance_token="" + if [ -n "$FALCON_MAINTENANCE_TOKEN" ]; then + cs_maintenance_token="$FALCON_MAINTENANCE_TOKEN" + elif [ -n "$FALCON_CLIENT_ID" ] && [ -n "$FALCON_CLIENT_SECRET" ] && [ -n "$aid" ]; then + get_maintenance_token + echo "Retrieved maintenance token via API" + fi + + echo -n 'Removing Falcon Sensor ... ' + cs_sensor_remove + echo '[ Ok ]' + if [ "${FALCON_REMOVE_HOST}" = "true" ]; then + echo -n 'Removing host from console ... ' + cs_remove_host_from_console + echo '[ Ok ]' + fi + echo 'Falcon Sensor removed successfully.' +} + +# Shared functions +old_curl=$( + if ! command -v curl >/dev/null 2>&1; then + die "The 'curl' command is missing. Please install it before continuing. Aborting..." + fi + + version=$(curl --version | head -n 1 | awk '{ print $2 }') + minimum="7.55" + + # Check if the version is less than the minimum + if printf "%s\n" "$version" "$minimum" | sort -V -C; then + echo 0 + else + echo 1 + fi +) + +curl_command() { + # Dash does not support arrays, so we have to pass the args as separate arguments + set -- "$@" + + if [ "$old_curl" -eq 0 ]; then + curl -s -x "$proxy" -L -H "Authorization: Bearer ${cs_falcon_oauth_token}" "$@" + else + echo "Authorization: Bearer ${cs_falcon_oauth_token}" | curl -s -x "$proxy" -L -H @- "$@" + fi +} + +handle_curl_error() { + if [ "$1" = "28" ]; then + err_msg="Operation timed out (exit code 28)." + if [ -n "$proxy" ]; then + err_msg="$err_msg A proxy was used to communicate ($proxy). Please check your proxy settings." + fi + die "$err_msg" + fi + + if [ "$1" = "5" ]; then + err_msg="Couldn't resolve proxy (exit code 5). The address ($proxy) of the given proxy host could not be resolved. Please check your proxy settings." + die "$err_msg" + fi + + if [ "$1" = "7" ]; then + err_msg="Failed to connect to host (exit code 7). Host found, but unable to open connection with host." + if [ -n "$proxy" ]; then + err_msg="$err_msg A proxy was used to communicate ($proxy). Please check your proxy settings." + fi + die "$err_msg" + fi +} + +check_package_manager_lock() { + lock_file="/var/lib/rpm/.rpm.lock" + lock_type="RPM" + local timeout=300 interval=5 elapsed=0 + + if type dpkg >/dev/null 2>&1; then + lock_file="/var/lib/dpkg/lock" + lock_type="DPKG" + fi + + while lsof -w "$lock_file" >/dev/null 2>&1; do + if [ $elapsed -eq 0 ]; then + echo "" + echo "Package manager is locked. Waiting up to ${timeout} seconds for lock to be released..." + fi + + if [ $elapsed -ge $timeout ]; then + echo "Timed out waiting for ${lock_type} lock to be released after ${timeout} seconds." + echo "You may need to manually investigate processes locking ${lock_file}:" + lsof -w "$lock_file" || true + die "Installation aborted due to package manager lock timeout." + fi + + sleep $interval + elapsed=$((elapsed + interval)) + echo "Retrying again in ${interval} seconds..." + done +} + +cs_cloud() { + case "${cs_falcon_cloud}" in + us-1) echo "api.crowdstrike.com" ;; + us-2) echo "api.us-2.crowdstrike.com" ;; + eu-1) echo "api.eu-1.crowdstrike.com" ;; + us-gov-1) echo "api.laggar.gcw.crowdstrike.com" ;; + *) die "Unrecognized Falcon Cloud: ${cs_falcon_cloud}" ;; + esac +} + +# Shared auth functions +get_falcon_credentials() { + if [ -z "$FALCON_ACCESS_TOKEN" ]; then + cs_falcon_client_id=$( + if [ -n "$FALCON_CLIENT_ID" ]; then + echo "$FALCON_CLIENT_ID" + else + die "Missing FALCON_CLIENT_ID environment variable. Please provide your OAuth2 API Client ID for authentication with CrowdStrike Falcon platform. Establishing and retrieving OAuth2 API credentials can be performed at https://falcon.crowdstrike.com/support/api-clients-and-keys." + fi + ) + + cs_falcon_client_secret=$( + if [ -n "$FALCON_CLIENT_SECRET" ]; then + echo "$FALCON_CLIENT_SECRET" + else + die "Missing FALCON_CLIENT_SECRET environment variable. Please provide your OAuth2 API Client Secret for authentication with CrowdStrike Falcon platform. Establishing and retrieving OAuth2 API credentials can be performed at https://falcon.crowdstrike.com/support/api-clients-and-keys." + fi + ) + + # Adding suppport for member_cid + cs_falcon_member_cid=$( + if [ -n "$FALCON_MEMBER_CID" ]; then + echo "$FALCON_MEMBER_CID" + fi + ) + else + if [ -z "$FALCON_CLOUD" ]; then + die "If setting the FALCON_ACCESS_TOKEN manually, you must also specify the FALCON_CLOUD" + fi + fi +} + +get_user_agent() { + local user_agent="crowdstrike-falcon-scripts/$VERSION" + if [ -n "$USER_AGENT" ]; then + user_agent="${user_agent} ${USER_AGENT}" + fi + echo "$user_agent" +} + +get_oauth_token() { + # Get credentials first + get_falcon_credentials + + response_headers=$(mktemp) + + cs_falcon_oauth_token=$( + if [ -n "$FALCON_ACCESS_TOKEN" ]; then + token=$FALCON_ACCESS_TOKEN + else + # Build the auth request payload, adding member_cid if specified + auth_payload="client_id=$cs_falcon_client_id&client_secret=$cs_falcon_client_secret" + + if [ -n "$cs_falcon_member_cid" ]; then + auth_payload="${auth_payload}&member_cid=${cs_falcon_member_cid}" + fi + + token_result=$(echo "$auth_payload" | + curl -X POST -s -x "$proxy" -L "https://$(cs_cloud)/oauth2/token" \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \ + -H "User-Agent: $(get_user_agent)" \ + --dump-header "${response_headers}" \ + --data @-) + + handle_curl_error $? + + token=$(echo "$token_result" | json_value "access_token" | sed 's/ *$//g' | sed 's/^ *//g') + if [ -z "$token" ]; then + die "Unable to obtain CrowdStrike Falcon OAuth Token. Double check your credentials and/or ensure you set the correct cloud region." + fi + fi + echo "$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') + + if [ -z "${FALCON_CLOUD}" ]; then + if [ -z "${region_hint}" ]; then + die "Unable to obtain region hint from CrowdStrike Falcon OAuth API, Please provide FALCON_CLOUD environment variable as an override." + fi + cs_falcon_cloud="${region_hint}" + else + if [ "x${FALCON_CLOUD}" != "x${region_hint}" ]; then + echo "WARNING: FALCON_CLOUD='${FALCON_CLOUD}' environment variable specified while credentials only exists in '${region_hint}'" >&2 + fi + fi + fi + + rm "${response_headers}" +} + +# Uninstall functions + +# +get_aid() { + local aid + aid="$(/opt/CrowdStrike/falconctl -g --aid | awk -F '"' '{print $2}')" + echo "$aid" +} + +cs_sensor_installed() { + if ! test -f /opt/CrowdStrike/falconctl; then + log "WARNING" "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 + if [ "${FALCON_REMOVE_HOST}" = "true" ] || [ -n "$FALCON_CLIENT_ID" ] && [ -n "$FALCON_CLIENT_SECRET" ] && [ -z "$FALCON_MAINTENANCE_TOKEN" ]; then + aid=$(get_aid) + fi +} + +cs_sensor_remove() { + remove_package() { + pkg="$1" + + if type dnf >/dev/null 2>&1; then + dnf remove -q -y "$pkg" || rpm -e --nodeps "$pkg" + elif type yum >/dev/null 2>&1; then + yum remove -q -y "$pkg" || rpm -e --nodeps "$pkg" + elif type zypper >/dev/null 2>&1; then + zypper --quiet remove -y "$pkg" || rpm -e --nodeps "$pkg" + elif type apt >/dev/null 2>&1; then + DEBIAN_FRONTEND=noninteractive apt purge -y "$pkg" >/dev/null 2>&1 + else + rpm -e --nodeps "$pkg" + fi + } + + # 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 + die "Failed to apply maintenance token. Uninstallation may fail." + fi + fi + + # Check for package manager lock prior to uninstallation + check_package_manager_lock + + remove_package "falcon-sensor" +} + +cs_remove_host_from_console() { + if [ -z "$aid" ]; then + echo 'Unable to find AID. Skipping host removal from console.' + else + 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 $? + fi +} + +get_maintenance_token() { + if [ -z "$aid" ]; then + die "Unable to find AID. Cannot retrieve maintenance token." + fi + + echo "Retrieving maintenance token from the CrowdStrike Falcon API..." + + payload="{\"device_id\": \"$aid\", \"audit_message\": \"CrowdStrike Falcon Uninstall Bash Script\"}" + url="https://$(cs_cloud)/policy/combined/reveal-uninstall-token/v1" + + response=$(curl_command -X "POST" -H "Content-Type: application/json" -d "$payload" "$url") + + handle_curl_error $? + + 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 + die "Retrieved empty maintenance token from API." + fi + else + die "Failed to retrieve maintenance token. Response: $response" + fi +} + +# Install functions +cs_sensor_register() { + # Get the falcon cid + cs_falcon_cid="$(get_falcon_cid)" + # If cs_falcon_token is not set, try getting it from api + if [ -z "${cs_falcon_token}" ]; then + cs_falcon_token="$(get_provisioning_token)" + fi + # add the cid to the params + cs_falcon_args=--cid="${cs_falcon_cid}" + if [ -n "${cs_falcon_token}" ]; then + cs_token=--provisioning-token="${cs_falcon_token}" + cs_falcon_args="$cs_falcon_args $cs_token" + fi + # add tags to the params + if [ -n "${FALCON_TAGS}" ]; then + cs_falconctl_opt_tags=--tags="$FALCON_TAGS" + cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_tags" + fi + # add proxy enable/disable param + if [ -n "${cs_falcon_apd}" ]; then + cs_falconctl_opt_apd=--apd=$cs_falcon_apd + cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_apd" + fi + # add proxy host to the params + if [ -n "${FALCON_APH}" ]; then + cs_falconctl_opt_aph=--aph="${FALCON_APH}" + cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_aph" + fi + # add proxy port to the params + if [ -n "${FALCON_APP}" ]; then + cs_falconctl_opt_app=--app="${FALCON_APP}" + cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_app" + fi + # add the billing type to the params + if [ -n "${FALCON_BILLING}" ]; then + cs_falconctl_opt_billing=--billing="${cs_falcon_billing}" + cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_billing" + fi + # add the backend to the params + if [ -n "${cs_falcon_backend}" ]; then + cs_falconctl_opt_backend=--backend="${cs_falcon_backend}" + cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_backend" + fi + # add the trace level to the params + if [ -n "${cs_falcon_trace}" ]; then + cs_falconctl_opt_trace=--trace="${cs_falcon_trace}" + cs_falcon_args="$cs_falcon_args $cs_falconctl_opt_trace" + fi + # run the configuration command + # shellcheck disable=SC2086 + /opt/CrowdStrike/falconctl -s -f ${cs_falcon_args} >/dev/null +} + +cs_sensor_is_running() { + if pgrep -u root falcon-sensor >/dev/null 2>&1; then + echo "sensor is already running... exiting" + exit 0 + fi +} + +cs_sensor_restart() { + if type systemctl >/dev/null 2>&1; then + systemctl restart falcon-sensor + elif type service >/dev/null 2>&1; then + service falcon-sensor restart + else + die "Could not restart falcon sensor" + fi +} + +cs_sensor_install() { + local tempdir package_name + tempdir=$(mktemp -d) + + tempdir_cleanup() { rm -rf "$tempdir"; } + trap tempdir_cleanup EXIT + + get_oauth_token + package_name=$(cs_sensor_download "$tempdir") + os_install_package "$package_name" + + tempdir_cleanup +} + +cs_sensor_policy_version() { + local cs_policy_name="$1" sensor_update_policy sensor_update_versions + + sensor_update_policy=$( + curl_command -G "https://$(cs_cloud)/policy/combined/sensor-update/v2" \ + --data-urlencode "filter=platform_name:\"Linux\"+name.raw:\"$cs_policy_name\"" + ) + + handle_curl_error $? + + if echo "$sensor_update_policy" | grep "authorization failed"; then + die "Access denied: Please make sure that your Falcon API credentials allow access to sensor update policies (scope Sensor update policies [read])" + elif echo "$sensor_update_policy" | grep "invalid bearer token"; then + die "Invalid Access Token: $cs_falcon_oauth_token" + fi + + sensor_update_versions=$(echo "$sensor_update_policy" | json_value "sensor_version") + if [ -z "$sensor_update_versions" ]; then + die "Could not find a sensor update policy with name: $cs_policy_name" + fi + + oldIFS=$IFS + IFS=" " + # shellcheck disable=SC2086 + set -- $sensor_update_versions + if [ "$(echo "$sensor_update_versions" | wc -w)" -gt 1 ]; then + if [ "$cs_os_arch" = "aarch64" ]; then + echo "$2" + else + echo "$1" + fi + else + echo "$1" + fi + IFS=$oldIFS +} + +cs_sensor_download() { + local destination_dir="$1" existing_installers sha_list INDEX sha file_type installer + + if [ -n "$cs_sensor_policy_name" ]; then + cs_sensor_version=$(cs_sensor_policy_version "$cs_sensor_policy_name") + cs_api_version_filter="+version:\"$cs_sensor_version\"" + + if [ "$cs_falcon_sensor_version_dec" -gt 0 ]; then + echo "WARNING: Disabling FALCON_SENSOR_VERSION_DECREMENT because it conflicts with FALCON_SENSOR_UPDATE_POLICY_NAME" + cs_falcon_sensor_version_dec=0 + fi + fi + + existing_installers=$( + curl_command -G "https://$(cs_cloud)/sensors/combined/installers/v2?sort=version|desc" \ + --data-urlencode "filter=os:\"$cs_os_name\"+os_version:\"*$cs_os_version*\"$cs_api_version_filter$cs_os_arch_filter" + ) + + handle_curl_error $? + + if echo "$existing_installers" | grep "authorization failed"; then + die "Access denied: Please make sure that your Falcon API credentials allow sensor download (scope Sensor Download [read])" + elif echo "$existing_installers" | grep "invalid bearer token"; then + die "Invalid Access Token: $cs_falcon_oauth_token" + fi + + sha_list=$(echo "$existing_installers" | json_value "sha256") + 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 + + # Set the index accordingly (the json_value expects and index+1 value) + INDEX=$((cs_falcon_sensor_version_dec + 1)) + + sha=$(echo "$existing_installers" | json_value "sha256" "$INDEX" | + sed 's/ *$//g' | sed 's/^ *//g') + if [ -z "$sha" ]; then + die "Unable to identify a sensor installer matching: $cs_os_name, version: $cs_os_version, index: N-$cs_falcon_sensor_version_dec" + fi + file_type=$(echo "$existing_installers" | json_value "file_type" "$INDEX" | sed 's/ *$//g' | sed 's/^ *//g') + + installer="${destination_dir}/falcon-sensor.${file_type}" + + curl_command "https://$(cs_cloud)/sensors/entities/download-installer/v1?id=$sha" -o "${installer}" + + handle_curl_error $? + + echo "$installer" +} + +os_install_package() { + local pkg="$1" + # Check for package manager lock prior to uninstallation + check_package_manager_lock + + rpm_install_package() { + local pkg="$1" + + cs_falcon_gpg_import + + if type dnf >/dev/null 2>&1; then + dnf install -q -y "$pkg" || rpm -ivh --nodeps "$pkg" + elif type yum >/dev/null 2>&1; then + yum install -q -y "$pkg" || rpm -ivh --nodeps "$pkg" + elif type zypper >/dev/null 2>&1; then + zypper --quiet install -y "$pkg" || rpm -ivh --nodeps "$pkg" + else + rpm -ivh --nodeps "$pkg" + fi + } + # shellcheck disable=SC2221,SC2222 + case "${os_name}" in + Amazon | CentOS* | Oracle | RHEL | Rocky | AlmaLinux | SLES) + rpm_install_package "$pkg" + ;; + Debian) + DEBIAN_FRONTEND=noninteractive apt-get -qq install -y "$pkg" >/dev/null + ;; + Ubuntu) + # If this is ubuntu 14, we need to use dpkg instead + if [ "${cs_os_version}" -eq 14 ]; then + DEBIAN_FRONTEND=noninteractive dpkg -i "$pkg" >/dev/null 2>&1 || true + DEBIAN_FRONTEND=noninteractive apt-get -qq install -f -y >/dev/null + else + DEBIAN_FRONTEND=noninteractive apt-get -qq install -y "$pkg" >/dev/null + fi + ;; + *) + die "Unrecognized OS: ${os_name}" + ;; + esac +} + +cs_falcon_gpg_import() { + tempfile=$(mktemp) + cat >"$tempfile" </dev/null; then + # For now we just return. We can error out once more people get a chance to update their API keys + return + fi + + is_required=$(echo "$check_settings" | json_value "tokens_required" | xargs) + if [ "$is_required" = "true" ]; then + local token_query token_id token_result + # Get the token ID + token_query=$(curl_command "https://$(cs_cloud)/installation-tokens/queries/tokens/v1") + token_id=$(echo "$token_query" | tr -d '\n" ' | awk -F'[][]' '{print $2}' | cut -d',' -f1) + if [ -z "$token_id" ]; then + die "No installation token found in a required token environment." + fi + + # Get the token value from ID + token_result=$(curl_command "https://$(cs_cloud)/installation-tokens/entities/tokens/v1?ids=$token_id") + token_value=$(echo "$token_result" | json_value "value" | xargs) + if [ -z "$token_value" ]; then + die "Could not obtain installation token value." + fi + fi + + echo "$token_value" +} + +get_falcon_cid() { + if [ -n "$FALCON_CID" ]; then + echo "$FALCON_CID" + else + cs_target_cid=$(curl_command "https://$(cs_cloud)/sensors/queries/installers/ccid/v1") + + handle_curl_error $? + + if [ -z "$cs_target_cid" ]; then + die "Unable to obtain CrowdStrike Falcon CID. Response was $cs_target_cid" + fi + echo "$cs_target_cid" | tr -d '\n" ' | awk -F'[][]' '{print $2}' + fi +} + +# Recovery file and tags functions +# Create a recovery file to track tags and AID in case migration fails +create_recovery_file() { + local sensor_tags="$1" + local falcon_tags="$2" + local old_aid="$3" + local path="$4" + + # Create directory if it doesn't exist + mkdir -p "$(dirname "$path")" + + # Write data to recovery file in CSV format using semicolons as delimiters + echo "OldAid;SensorTags;FalconTags" >"$path" + echo "$old_aid;$sensor_tags;$falcon_tags" >>"$path" + + log "INFO" "Recovery file created at $path" +} + +# Read data from recovery file +read_recovery_file() { + local path="$1" + + if [ ! -f "$path" ]; then + log "WARNING" "Recovery file not found at $path" + return 1 + fi + + # Skip header line and read data using semicolon as delimiter + old_aid=$(tail -n 1 "$path" | cut -d ';' -f 1) + sensor_tags=$(tail -n 1 "$path" | cut -d ';' -f 2) + falcon_tags=$(tail -n 1 "$path" | cut -d ';' -f 3) + + log "INFO" "Recovery data loaded: AID=$old_aid" + return 0 +} + +# Get the host's tags from a specific Falcon instance +get_falcon_tags() { + local aid="$1" + + log "INFO" "Retrieving tags for host with AID: $aid" + + local response + response=$(curl_command "https://$(cs_cloud)/devices/entities/devices/v2?ids=$aid") + + handle_curl_error $? + + if echo "$response" | grep "authorization failed" >/dev/null; then + die "Access denied: Please make sure your Falcon API credentials allow access to host data (scope Host [read])" + elif echo "$response" | grep "invalid bearer token" >/dev/null; then + die "Invalid Access Token: $cs_falcon_oauth_token" + fi + + # Extract tags from response + local tags + tags=$(echo "$response" | grep 'GroupingTags/' | sed 's/[",]//g' | sed 's/^[[:space:]]*//') + + echo "$tags" +} + +# Split tags into sensor tags and falcon tags +split_tags() { + local tags="$1" + local sensor_tags="" + local falcon_tags="" + local tag="" + + # Create a temporary file with one tag per line + local tmpfile + tmpfile=$(mktemp) + echo "$tags" | tr ' ' '\n' >"$tmpfile" + + while read -r tag; do + case "$tag" in + SensorGroupingTags/*) + # Extract the tag part after SensorGroupingTags/ + local sensor_tag + sensor_tag=$(echo "$tag" | sed 's/^SensorGroupingTags\///') + if [ -n "$sensor_tags" ]; then + sensor_tags="$sensor_tags,$sensor_tag" + else + sensor_tags="$sensor_tag" + fi + ;; + FalconGroupingTags/*) + # Extract the tag part after FalconGroupingTags/ + local falcon_tag + falcon_tag=$(echo "$tag" | sed 's/^FalconGroupingTags\///') + if [ -n "$falcon_tags" ]; then + falcon_tags="$falcon_tags,$falcon_tag" + else + falcon_tags="$falcon_tag" + fi + ;; + esac + done <"$tmpfile" + + rm -f "$tmpfile" + + echo "$sensor_tags;$falcon_tags" +} + +# Set tags for a host in a Falcon instance +set_falcon_tags() { + local aid="$1" + local tags="$2" + + log "INFO" "Setting tags for host with AID: $aid" + + # Prepare the tags payload + local payload + payload=$( + cat <"$tmpfile" + + while read -r tag; do + # Skip empty tags + if [ -n "$tag" ]; then + if [ -n "$formatted_tags" ]; then + formatted_tags="$formatted_tags,\"$prefix/$tag\"" + else + formatted_tags="\"$prefix/$tag\"" + fi + fi + done <"$tmpfile" + + rm -f "$tmpfile" + echo "$formatted_tags" +} + +# Merge tags with new tags, removing duplicates +merge_tags() { + local existing_tags="$1" + local new_tags="$2" + local merged_tags="$existing_tags" + + # Create a temp file with one tag per line from new_tags + local tmpfile + tmpfile=$(mktemp) + echo "$new_tags" | tr ',' '\n' >"$tmpfile" + + while read -r tag; do + # Skip empty tags + if [ -n "$tag" ]; then + # Check if tag exists in merged_tags + if ! echo ",$merged_tags," | grep -q ",$tag,"; then + if [ -n "$merged_tags" ]; then + merged_tags="$merged_tags,$tag" + else + merged_tags="$tag" + fi + fi + fi + done <"$tmpfile" + + rm -f "$tmpfile" + echo "$merged_tags" +} + +# Function to wait for and set Falcon grouping tags after sensor installation +set_falcon_grouping_tags() { + local migrate_tags=$1 + local falcon_tags=$2 + + # Skip if we're not migrating tags AND there are no new tags to add + if [ "$migrate_tags" != "true" ] && [ -z "$FALCON_GROUPING_TAGS" ]; then + log "INFO" "Skipping Falcon grouping tags (migrate_tags=$migrate_tags, no new tags specified)" + return 0 + fi + + # Skip if we have no existing tags to migrate AND no new tags to add + if [ -z "$falcon_tags" ] && [ -z "$FALCON_GROUPING_TAGS" ]; then + log "INFO" "Skipping Falcon grouping tags (no existing or new tags to apply)" + return 0 + fi + + log "INFO" "Waiting for new sensor registration before setting Falcon grouping tags..." + + # Wait for the new AID to be available + local max_wait=60 + local wait_count=0 + local new_aid="" + + while [ $wait_count -lt $max_wait ]; do + new_aid=$(get_aid) + if [ -n "$new_aid" ]; then + break + fi + log "INFO" "Waiting for new AID to be available... ($wait_count/$max_wait)" + wait_count=$((wait_count + 1)) + sleep 5 + done + + if [ -z "$new_aid" ]; then + log "WARNING" "Could not obtain new AID after $max_wait attempts" + return 1 + fi + + log "INFO" "New AID obtained: $new_aid" + + # If we have new tags to add, merge them with existing tags + local tags_to_apply="$falcon_tags" + if [ -n "$FALCON_GROUPING_TAGS" ]; then + if [ -n "$tags_to_apply" ]; then + tags_to_apply=$(merge_tags "$tags_to_apply" "$FALCON_GROUPING_TAGS") + else + tags_to_apply="$FALCON_GROUPING_TAGS" + fi + log "INFO" "Final Falcon grouping tags to apply: $tags_to_apply" + fi + + # Format tags for API request + local formatted_tags + formatted_tags=$(format_tags_for_api "$tags_to_apply" "FalconGroupingTags") + + # Set falcon tags via API + log "INFO" "Setting Falcon grouping tags via API..." + local max_attempts=5 + local attempt=1 + local success=false + + while [ $attempt -le $max_attempts ] && [ "$success" = false ]; do + if set_falcon_tags "$new_aid" "$formatted_tags"; then + success=true + else + log "WARNING" "Failed to set Falcon tags, attempt $attempt of $max_attempts" + attempt=$((attempt + 1)) + sleep 5 + fi + done + + if [ "$success" = true ]; then + log "INFO" "Successfully set Falcon grouping tags" + return 0 + else + log "WARNING" "Failed to set Falcon grouping tags after $max_attempts attempts" + return 1 + fi +} + +# Set vars needed for shared auth functions +authenticate_to_falcon() { + local client_id=$1 + local client_secret=$2 + local cloud=$3 + local cid=$4 + local member_cid=$5 + + FALCON_CLIENT_ID=$client_id + FALCON_CLIENT_SECRET=$client_secret + FALCON_CID=$cid + cs_falcon_cloud="${cloud:-us-1}" + cs_falcon_member_cid=$member_cid + get_oauth_token +} + +#### start processing +set -e + +# shellcheck disable=SC2001 +proxy=$( + proxy="" + if [ -n "$FALCON_APH" ]; then + proxy="$(echo "$FALCON_APH" | sed "s|http.*://||")" + + if [ -n "$FALCON_APP" ]; then + proxy="$proxy:$FALCON_APP" + fi + fi + + if [ -n "$proxy" ]; then + # Remove redundant quotes + proxy="$(echo "$proxy" | sed "s/[\'\"]//g")" + proxy="http://$proxy" + fi + echo "$proxy" +) + +os_name=$( + # returns either: Amazon, Ubuntu, CentOS, RHEL, or SLES + # lsb_release is not always present + name=$(cat /etc/*release | grep ^NAME= | awk -F'=' '{ print $2 }' | sed "s/\"//g;s/Red Hat.*/RHEL/g;s/ Linux$//g;s/ GNU\/Linux$//g;s/Oracle.*/Oracle/g;s/Amazon.*/Amazon/g") + if [ -z "$name" ]; then + if lsb_release -s -i | grep -q ^RedHat; then + name="RHEL" + elif [ -f /usr/bin/lsb_release ]; then + name=$(/usr/bin/lsb_release -s -i) + fi + fi + if [ -z "$name" ]; then + die "Cannot recognise operating system" + fi + + echo "$name" +) + +os_version=$( + version=$(cat /etc/*release | grep VERSION_ID= | awk '{ print $1 }' | awk -F'=' '{ print $2 }' | sed "s/\"//g") + if [ -z "$version" ]; then + if type rpm >/dev/null 2>&1; then + # older systems may have *release files of different form + version=$(rpm -qf /etc/redhat-release --queryformat '%{VERSION}' | sed 's/\([[:digit:]]\+\).*/\1/g') + elif [ -f /etc/debian_version ]; then + version=$(cat /etc/debian_version) + elif [ -f /usr/bin/lsb_release ]; then + version=$(/usr/bin/lsb_release -r | /usr/bin/cut -f 2-) + fi + fi + if [ -z "$version" ]; then + cat /etc/*release >&2 + die "Could not determine distribution version" + fi + echo "$version" +) + +cs_os_name=$( + # returns OS name as recognised by CrowdStrike Falcon API + # shellcheck disable=SC2221,SC2222 + case "${os_name}" in + Amazon) + echo "Amazon Linux" + ;; + CentOS* | Oracle | RHEL | Rocky | AlmaLinux) + echo "*RHEL*" + ;; + Debian) + echo "Debian" + ;; + SLES) + echo "SLES" + ;; + Ubuntu) + echo "Ubuntu" + ;; + *) + die "Unrecognized OS: ${os_name}" + ;; + esac +) + +cs_os_arch=$( + uname -m +) + +cs_os_arch_filter=$( + case "${cs_os_arch}" in + x86_64) + echo "+architectures:\"x86_64\"" + ;; + aarch64) + echo "+architectures:\"arm64\"" + ;; + s390x) + echo "+architectures:\"s390x\"" + ;; + *) + die "Unrecognized OS architecture: ${cs_os_arch}" + ;; + esac +) + +cs_os_version=$( + version=$(echo "$os_version" | awk -F'.' '{print $1}') + # Check if we are using Amazon Linux 1 + if [ "${os_name}" = "Amazon" ]; then + if [ "$version" != "2" ] && [ "$version" -le 2018 ]; then + version="1" + fi + fi + echo "$version" +) + +cs_falcon_token=$( + if [ -n "$FALCON_PROVISIONING_TOKEN" ]; then + echo "$FALCON_PROVISIONING_TOKEN" + fi +) + +cs_sensor_policy_name=$( + if [ -n "$FALCON_SENSOR_UPDATE_POLICY_NAME" ]; then + echo "$FALCON_SENSOR_UPDATE_POLICY_NAME" + else + echo "" + fi +) + +cs_falcon_sensor_version_dec=$( + re='^[0-9]\+$' + if [ -n "$FALCON_SENSOR_VERSION_DECREMENT" ]; then + if ! expr "$FALCON_SENSOR_VERSION_DECREMENT" : "$re" >/dev/null 2>&1; then + die "The FALCON_SENSOR_VERSION_DECREMENT must be an integer greater than or equal to 0 or less than 5. FALCON_SENSOR_VERSION_DECREMENT: \"$FALCON_SENSOR_VERSION_DECREMENT\"" + elif [ "$FALCON_SENSOR_VERSION_DECREMENT" -lt 0 ] || [ "$FALCON_SENSOR_VERSION_DECREMENT" -gt 5 ]; then + die "The FALCON_SENSOR_VERSION_DECREMENT must be an integer greater than or equal to 0 or less than 5. FALCON_SENSOR_VERSION_DECREMENT: \"$FALCON_SENSOR_VERSION_DECREMENT\"" + else + echo "$FALCON_SENSOR_VERSION_DECREMENT" + fi + else + echo "0" + fi +) + +if [ -n "$FALCON_APD" ]; then + cs_falcon_apd=$( + case "${FALCON_APD}" in + true) + echo "true" + ;; + false) + echo "false" + ;; + *) + die "Unrecognized APD: ${FALCON_APD} value must be one of : [true|false]" + ;; + esac + ) +fi + +if [ -n "$FALCON_BILLING" ]; then + cs_falcon_billing=$( + case "${FALCON_BILLING}" in + default) + echo "default" + ;; + metered) + echo "metered" + ;; + *) + die "Unrecognized BILLING: ${FALCON_BILLING} value must be one of : [default|metered]" + ;; + esac + ) +fi + +if [ -n "$FALCON_BACKEND" ]; then + cs_falcon_backend=$( + case "${FALCON_BACKEND}" in + auto) + echo "auto" + ;; + bpf) + echo "bpf" + ;; + kernel) + echo "kernel" + ;; + *) + die "Unrecognized BACKEND: ${FALCON_BACKEND} value must be one of : [auto|bpf|kernel]" + ;; + esac + ) +fi + +if [ -n "$FALCON_TRACE" ]; then + cs_falcon_trace=$( + case "${FALCON_TRACE}" in + none) + echo "none" + ;; + err) + echo "err" + ;; + warn) + echo "warn" + ;; + info) + echo "info" + ;; + debug) + echo "debug" + ;; + *) + die "Unrecognized TRACE: ${FALCON_TRACE} value must be one of : [none|err|warn|info|debug]" + ;; + esac + ) +fi + +main() { + # Start of migration + touch "$log_file" + echo "Migration file created at: $log_file" + echo "Migration started at $(date)" >> "$log_file" + + # auth with old credentials + log "INFO" "Authenticating to old CID..." + 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 + local recovery_mode=false + if [ -f "$recovery_file" ]; then + recovery_mode=true + log "INFO" "Recovery file detected. Attempting to recover from previous migration attempt." + if read_recovery_file "$recovery_file"; then + log "INFO" "Loaded recovery data: old_aid=$old_aid" + else + log "WARNING" "Failed to load recovery data. Starting fresh migration." + recovery_mode=false + fi + fi + + # Get the AID if not in recovery mode + if [ "$recovery_mode" = false ]; then + # Get the AID and tags + old_aid=$(get_aid) + + if [ -z "$old_aid" ]; then + log "WARNING" "No AID found. The sensor may not be properly installed." + else + log "INFO" "Found AID: $old_aid" + + if [ "$migrate_tags" = "true" ]; then + log "INFO" "Retrieving existing tags from current installation..." + + # Get the tags + local tags + tags=$(get_falcon_tags "$old_aid") + + # Split tags into sensor and falcon grouping tags + local split_result + split_result=$(split_tags "$tags") + + # Parse split result (semicolon-separated) + sensor_tags=$(echo "$split_result" | cut -d ';' -f 1) + falcon_tags=$(echo "$split_result" | cut -d ';' -f 2) + + log "INFO" "Found Sensor grouping tags: $sensor_tags" + log "INFO" "Found Falcon grouping tags: $falcon_tags" + + # Create recovery file + create_recovery_file "$sensor_tags" "$falcon_tags" "$old_aid" "$recovery_file" + fi + fi + fi + + # Uninstall sensor + log "INFO" "Uninstalling old Falcon sensor..." + uninstall_sensor | tee -a "$log_file" + + # Install new sensor + # auth with new credentials + log "INFO" "Authenticating to new CID..." + 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..." + # Handle tags if migrating + if [ "$migrate_tags" = "true" ]; then + # Merge existing sensor tags with any new tags specified + if [ -n "$FALCON_TAGS" ]; then + sensor_tags=$(merge_tags "$sensor_tags" "$FALCON_TAGS") + log "INFO" "Sensor grouping tags to be migrated: $sensor_tags" + fi + + # Set the tags for the sensor install + FALCON_TAGS="$sensor_tags" + fi + + log "INFO" "Installing Falcon sensor to new CID..." + install_sensor | tee -a "$log_file" + + # 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" + fi + + # Remove recovery file when done + if [ -f "$recovery_file" ]; then + log "INFO" "Removing recovery file..." + rm -f "$recovery_file" + fi + + log "INFO" "Falcon sensor migration completed successfully" + log "INFO" "Log file: $log_file" +} + +main "$@" diff --git a/molecule/bash_migrate/converge.yml b/molecule/bash_migrate/converge.yml new file mode 100644 index 00000000..eff9c387 --- /dev/null +++ b/molecule/bash_migrate/converge.yml @@ -0,0 +1,25 @@ +--- +- name: Converge + hosts: all + gather_facts: false + become: true + environment: + OLD_FALCON_CLIENT_ID: "{{ lookup('env', 'FALCON_CLIENT_ID') }}" + OLD_FALCON_CLIENT_SECRET: "{{ lookup('env', 'FALCON_CLIENT_SECRET') }}" + NEW_FALCON_CLIENT_ID: "{{ lookup('env', 'FALCON_CLIENT_ID') }}" + NEW_FALCON_CLIENT_SECRET: "{{ lookup('env', 'FALCON_CLIENT_SECRET') }}" + ALLOW_LEGACY_CURL: "true" + FALCON_REMOVE_HOST: "true" + FALCON_TAGS: "migrate1,migrate2" + FALCON_GROUPING_TAGS: "fg-migrate1,fg-migrate2" + tasks: + # Execute shell command + - name: Migrate Falcon Sensor + ansible.builtin.script: + cmd: ../../bash/migrate/falcon-linux-migrate.sh + register: falcon_install + + # Print stdout + - name: Task STDOUT + ansible.builtin.debug: + msg: "{{ falcon_install.stdout_lines }}" diff --git a/molecule/bash_migrate/molecule.yml b/molecule/bash_migrate/molecule.yml new file mode 100644 index 00000000..748ce1da --- /dev/null +++ b/molecule/bash_migrate/molecule.yml @@ -0,0 +1,37 @@ +--- +dependency: + name: galaxy +driver: + name: ec2 +# The Default Platform is Ubunutu 20.04 LTS | T2.Micro | us-west-2 +platforms: + - name: "${MOLECULE_INSTANCE_NAME:-default-bash-migrate}" + image_owner: "${MOLECULE_IMAGE_OWNER:-099720109477}" + image_filters: + - architecture: "${MOLECULE_IMAGE_ARCH:-x86_64}" + - name: "${MOLECULE_IMAGE_NAME:-ubuntu/images/hvm-ssd/ubuntu-focal-20.04*}" + instance_type: "${MOLECULE_INSTANCE_TYPE:-t2.micro}" + region: "${MOLECULE_REGION:-us-west-2}" + vpc_subnet_id: "${MOLECULE_VPC_SUBNET_ID}" + security_group_restrict_cidr_ip: "${MOLECULE_SECURITY_GROUP_RESTRICT_CIDR_IP:-true}" + boot_wait_seconds: ${MOLECULE_BOOT_WAIT_SECONDS:-60} +provisioner: + name: ansible + config_options: + defaults: + stdout_callback: yaml + playbooks: + create: ../shared/playbooks/create.yml + destroy: ../shared/playbooks/destroy.yml +verifier: + name: ansible +scenario: + test_sequence: + - dependency + - syntax + - create + - prepare + - converge + - side_effect + - verify + - destroy diff --git a/molecule/bash_migrate/prepare.yml b/molecule/bash_migrate/prepare.yml new file mode 100644 index 00000000..8cc8468e --- /dev/null +++ b/molecule/bash_migrate/prepare.yml @@ -0,0 +1,28 @@ +--- +- name: Prepare (Default) + hosts: all + become: true + environment: + FALCON_CLIENT_ID: "{{ lookup('env', 'FALCON_CLIENT_ID') }}" + FALCON_CLIENT_SECRET: "{{ lookup('env', 'FALCON_CLIENT_SECRET') }}" + ALLOW_LEGACY_CURL: "true" + tasks: + # Ubuntu specific + - name: Install apt dependencies + ansible.builtin.apt: + name: + - gpg-agent + - curl + update_cache: true + when: ansible_pkg_mgr == 'apt' + + - name: Install dependencies + ansible.builtin.package: + name: + - sudo + state: present + + - name: Install Falcon Sensor + ansible.builtin.script: + cmd: ../../bash/install/falcon-linux-install.sh + register: falcon_install diff --git a/molecule/bash_migrate/verify.yml b/molecule/bash_migrate/verify.yml new file mode 100644 index 00000000..e5421daa --- /dev/null +++ b/molecule/bash_migrate/verify.yml @@ -0,0 +1,35 @@ +--- +- name: Verify (Default) + hosts: all + gather_facts: false + become: true + pre_tasks: + - name: Set default sensor name and service + ansible.builtin.set_fact: + installed_sensor: falcon-sensor + sensor_service: falcon-sensor.service + + - name: Get list of installed packages + ansible.builtin.package_facts: + manager: auto + + - name: Collect status of falcon-sensor service + ansible.builtin.service_facts: + + - name: Get CID + ansible.builtin.command: /opt/CrowdStrike/falconctl -g --cid + register: get_cid + changed_when: false + + tasks: + - name: Verify Falcon Sensor is installed + ansible.builtin.assert: + that: installed_sensor in ansible_facts.packages + + - name: Verify falcon-sensor service is active and running + ansible.builtin.assert: + that: ansible_facts.services[sensor_service].state == 'running' + + - name: Verify CID + ansible.builtin.assert: + that: "'cid=' in get_cid.stdout"