|
| 1 | +# ____ _ _ ____ _ ___ ______ |
| 2 | +# / ___| | ___ _ _ __| | / ___|___ _ __ _ __ ___ ___| |_ / \ \ / / ___| |
| 3 | +# | | | |/ _ \| | | |/ _` | | | / _ \| '_ \| '_ \ / _ \/ __| __| / _ \ \ /\ / /\___ \ |
| 4 | +# | |___| | (_) | |_| | (_| | | |__| (_) | | | | | | | __/ (__| |_ / ___ \ V V / ___) | |
| 5 | +# \____|_|\___/ \__,_|\__,_| \____\___/|_| |_|_| |_|\___|\___|\__| /_/ \_\_/\_/ |____/ |
| 6 | +# |
| 7 | +# This is a modified version of the script falcon_discover_accounts, a troubleshooting script |
| 8 | +# posted to our Cloud-AWS repository at https://github.com/CrowdStrike/Cloud-AWS. |
| 9 | +# |
| 10 | +# This solution demonstrates accepting user input to query the CrowdStrike Falcon Discover API |
| 11 | +# to register, update and delete AWS accounts. An additional check function loops through all |
| 12 | +# accounts registered, and returns configuration detail to assist with troubleshooting setup. |
| 13 | +# |
| 14 | +# This example leverages the Cloud Connect AWS Service Class and legacy authentication. |
| 15 | +# |
| 16 | +import argparse |
| 17 | +import json |
| 18 | +import sys |
| 19 | +# Falcon SDK - Cloud_Connect_AWS and OAuth2 API service classes |
| 20 | +from falconpy import cloud_connect_aws as FalconAWS |
| 21 | +from falconpy import oauth2 as FalconAuth |
| 22 | + |
| 23 | + |
| 24 | +# =============== FORMAT API PAYLOAD |
| 25 | +def format_api_payload(rate_limit_reqs=0, rate_limit_time=0): |
| 26 | + # Generates a properly formatted JSON payload for POST and PATCH requests |
| 27 | + data = { |
| 28 | + "resources": [ |
| 29 | + { |
| 30 | + "cloudtrail_bucket_owner_id": cloudtrail_bucket_owner_id, |
| 31 | + "cloudtrail_bucket_region": cloudtrail_bucket_region, |
| 32 | + "external_id": external_id, |
| 33 | + "iam_role_arn": iam_role_arn, |
| 34 | + "id": local_account, |
| 35 | + "rate_limit_reqs": rate_limit_reqs, |
| 36 | + "rate_limit_time": rate_limit_time |
| 37 | + } |
| 38 | + ] |
| 39 | + } |
| 40 | + return data |
| 41 | + |
| 42 | + |
| 43 | +# =============== ACCOUNT VALUE |
| 44 | +def account_value(id, val, accts): |
| 45 | + # Returns the specified value for a specific account id within account_list |
| 46 | + returned = False |
| 47 | + for item in accts: |
| 48 | + if item["id"] == id: |
| 49 | + returned = item[val] |
| 50 | + return returned |
| 51 | + |
| 52 | + |
| 53 | +# =============== CHECK ACCOUNTS |
| 54 | +def check_account(): |
| 55 | + |
| 56 | + # Retrieve the account list |
| 57 | + account_list = falcon_discover.QueryAWSAccounts(parameters={"limit": int(query_limit)})["body"]["resources"] |
| 58 | + # Log the results of the account query to a file if logging is enabled |
| 59 | + if log_enabled: |
| 60 | + with open('falcon-discover-accounts.json', 'w+') as f: |
| 61 | + json.dump(account_list, f) |
| 62 | + # Create a list of our account IDs out of account_list |
| 63 | + id_items = [] |
| 64 | + for z in account_list: |
| 65 | + id_items.append(z["id"]) |
| 66 | + q_max = 10 # VerifyAWSAccountAccess has a ID max count of 10 |
| 67 | + for index in range(0, len(id_items), q_max): |
| 68 | + sub_acct_list = id_items[index:index + q_max] |
| 69 | + temp_list = ",".join([a for a in sub_acct_list]) |
| 70 | + access_response = falcon_discover.VerifyAWSAccountAccess(ids=temp_list) |
| 71 | + if access_response['status_code'] == 200: |
| 72 | + # Loop through each ID we verified |
| 73 | + for result in access_response["body"]["resources"]: |
| 74 | + if result["successful"]: |
| 75 | + # This account is correctly configured |
| 76 | + print(f'Account {result["id"]} is ok!') |
| 77 | + else: |
| 78 | + # This account is incorrectly configured. We'll use our account_value function to |
| 79 | + # retrieve configuration values from the account list we've already ingested. |
| 80 | + account_values_to_check = { |
| 81 | + 'id': result["id"], |
| 82 | + 'iam_role_arn': account_value(result["id"], "iam_role_arn", account_list), |
| 83 | + 'external_id': account_value(result["id"], "external_id", account_list), |
| 84 | + 'cloudtrail_bucket_owner_id': account_value(result["id"], "cloudtrail_bucket_owner_id", account_list), |
| 85 | + 'cloudtrail_bucket_region': account_value(result["id"], "cloudtrail_bucket_region", account_list), |
| 86 | + } |
| 87 | + # Use the account_value function to retrieve the access_health branch, |
| 88 | + # which contains our api failure reason. |
| 89 | + try: |
| 90 | + print('Account {} has a problem: {}'.format(result["id"], |
| 91 | + account_value(result["id"], |
| 92 | + "access_health", |
| 93 | + account_list |
| 94 | + )["api"]["reason"] |
| 95 | + )) |
| 96 | + except Exception: |
| 97 | + # The above call will produce an error if we're running |
| 98 | + # check immediately after registering an account as |
| 99 | + # the access_health branch hasn't been populated yet. |
| 100 | + # Requery the API for the account_list when this happens. |
| 101 | + account_list = falcon_discover.QueryAWSAccounts( |
| 102 | + parameters={"limit": f"{str(query_limit)}"} |
| 103 | + )["body"]["resources"] |
| 104 | + print('Account {} has a problem: {}'.format(result["id"], |
| 105 | + account_value(result["id"], |
| 106 | + "access_health", |
| 107 | + account_list |
| 108 | + )["api"]["reason"] |
| 109 | + )) |
| 110 | + # Output the account details to the user to assist with troubleshooting the account |
| 111 | + print(f'Current settings {json.dumps(account_values_to_check, indent=4)}\n') |
| 112 | + else: |
| 113 | + try: |
| 114 | + # An error has occurred |
| 115 | + print("Got response error code {} message {}".format(access_response["status_code"], |
| 116 | + access_response["body"]["errors"][0]["message"] |
| 117 | + )) |
| 118 | + except Exception: |
| 119 | + # Handle any egregious errors that break our return error payload |
| 120 | + print("Got response error code {} message {}".format(access_response["status_code"], access_response["body"])) |
| 121 | + return |
| 122 | + |
| 123 | + |
| 124 | +# =============== REGISTER ACCOUNT |
| 125 | +def register_account(): |
| 126 | + # Call the API to update the requested account. |
| 127 | + register_response = falcon_discover.ProvisionAWSAccounts(parameters={}, body=format_api_payload()) |
| 128 | + if register_response["status_code"] == 201: |
| 129 | + print("Successfully registered account.") |
| 130 | + else: |
| 131 | + print("Registration failed with response: {} {}".format(register_response["status_code"], |
| 132 | + register_response["body"]["errors"][0]["message"] |
| 133 | + )) |
| 134 | + |
| 135 | + return |
| 136 | + |
| 137 | + |
| 138 | +# =============== UPDATE ACCOUNT |
| 139 | +def update_account(): |
| 140 | + # Call the API to update the requested account. |
| 141 | + update_response = falcon_discover.UpdateAWSAccounts(body=format_api_payload()) |
| 142 | + if update_response["status_code"] == 200: |
| 143 | + print("Successfully updated account.") |
| 144 | + else: |
| 145 | + print("Update failed with response: {} {}".format(update_response["status_code"], |
| 146 | + update_response["body"]["errors"][0]["message"] |
| 147 | + )) |
| 148 | + |
| 149 | + return |
| 150 | + |
| 151 | + |
| 152 | +# =============== DELETE ACCOUNT |
| 153 | +def delete_account(): |
| 154 | + # Call the API to delete the requested account, multiple IDs can be deleted by passing in a comma-delimited list |
| 155 | + delete_response = falcon_discover.DeleteAWSAccounts(ids=local_account) |
| 156 | + if delete_response["status_code"] == 200: |
| 157 | + print("Successfully deleted account.") |
| 158 | + else: |
| 159 | + print("Delete failed with response: {} {}".format(delete_response["status_code"], |
| 160 | + delete_response["body"]["errors"][0]["message"] |
| 161 | + )) |
| 162 | + |
| 163 | + return |
| 164 | + |
| 165 | + |
| 166 | +# =============== MAIN |
| 167 | + |
| 168 | +# Configure argument parsing |
| 169 | +parser = argparse.ArgumentParser(description="Get Params to send notification to CRWD topic") |
| 170 | +# Fully optional |
| 171 | +parser.add_argument('-q', '--query_limit', help='The query limit used for check account commands', required=False) |
| 172 | +parser.add_argument('-l', '--log_enabled', help='Save results to a file?', required=False, action="store_true") |
| 173 | +# Optionally required |
| 174 | +parser.add_argument('-r', '--cloudtrail_bucket_region', help='AWS Region where the S3 bucket is hosted', |
| 175 | + required=False) |
| 176 | +parser.add_argument('-o', '--cloudtrail_bucket_owner_id', help='Account where the S3 bucket is hosted', |
| 177 | + required=False) |
| 178 | +parser.add_argument('-a', '--local_account', help='This AWS Account', required=False) |
| 179 | +parser.add_argument('-e', '--external_id', help='External ID used to assume role in account', required=False) |
| 180 | +parser.add_argument('-i', '--iam_role_arn', |
| 181 | + help='IAM AWS IAM Role ARN that grants access to resources for Crowdstrike', required=False) |
| 182 | +# Always required |
| 183 | +parser.add_argument('-c', '--command', help='Troubleshooting action to perform', required=True) |
| 184 | +parser.add_argument("-f", "--falcon_client_id", help="Falcon Client ID", required=True) |
| 185 | +parser.add_argument("-s", "--falcon_client_secret", help="Falcon Client Secret", required=True) |
| 186 | +args = parser.parse_args() |
| 187 | + |
| 188 | +# =============== SET GLOBALS |
| 189 | +command = args.command |
| 190 | +# Only execute our defined commands |
| 191 | +if command.lower() in "check,update,register,delete": |
| 192 | + if command.lower() in "update,register": |
| 193 | + # All fields required for update and register |
| 194 | + if (args.cloudtrail_bucket_owner_id is None or |
| 195 | + args.cloudtrail_bucket_region is None or |
| 196 | + args.local_account is None or |
| 197 | + args.external_id is None or |
| 198 | + args.iam_role_arn is None): |
| 199 | + parser.error("The {} command requires the -r, -o, -a, -e, -i arguments to also be specified.".format(command)) |
| 200 | + else: |
| 201 | + cloudtrail_bucket_region = args.cloudtrail_bucket_region |
| 202 | + cloudtrail_bucket_owner_id = args.cloudtrail_bucket_owner_id |
| 203 | + local_account = args.local_account |
| 204 | + external_id = args.external_id |
| 205 | + iam_role_arn = args.iam_role_arn |
| 206 | + elif command.lower() in "delete": |
| 207 | + # Delete only requires the local account ID |
| 208 | + if args.local_account is None: |
| 209 | + parser.error("The {} command requires the -l argument to also be specified.".format(command)) |
| 210 | + else: |
| 211 | + local_account = args.local_account |
| 212 | +else: |
| 213 | + parser.error("The {} command is not recognized.".format(command)) |
| 214 | +# These globals exist for all requests |
| 215 | +falcon_client_id = args.falcon_client_id |
| 216 | +falcon_client_secret = args.falcon_client_secret |
| 217 | +log_enabled = args.log_enabled |
| 218 | +if args.query_limit is None: |
| 219 | + query_limit = 100 |
| 220 | +else: |
| 221 | + query_limit = args.query_limit |
| 222 | + |
| 223 | +# =============== MAIN ROUTINE |
| 224 | +# Authenticate using our provided falcon client_id and client_secret |
| 225 | +try: |
| 226 | + authorized = FalconAuth.OAuth2(creds={'client_id': falcon_client_id, 'client_secret': falcon_client_secret}) |
| 227 | +except Exception: |
| 228 | + # We can't communicate with the endpoint, return a false token |
| 229 | + authorized.token = lambda: False |
| 230 | +# Try to retrieve a token from our authentication, returning false on failure |
| 231 | +try: |
| 232 | + token = authorized.token()["body"]["access_token"] |
| 233 | +except Exception: |
| 234 | + token = False |
| 235 | + |
| 236 | +# Confirm the token was successfully retrieved |
| 237 | +if token: |
| 238 | + # Connect using our token and return an instance of the API gateway object |
| 239 | + falcon_discover = FalconAWS.Cloud_Connect_AWS(access_token=token) |
| 240 | + try: |
| 241 | + # Execute the requested command |
| 242 | + if command.lower() == "delete": |
| 243 | + delete_account() |
| 244 | + elif command.lower() == "register": |
| 245 | + register_account() |
| 246 | + elif command.lower() == "update": |
| 247 | + update_account() |
| 248 | + else: |
| 249 | + check_account() |
| 250 | + except Exception as e: |
| 251 | + # Handle any previously unhandled errors |
| 252 | + print("Command failed with error: {}.".format(str(e))) |
| 253 | + # Discard our token before we exit |
| 254 | + authorized.revoke(token) |
| 255 | +else: |
| 256 | + # Report that authentication failed and stop processing |
| 257 | + print("Failed to retrieve authentication token.") |
| 258 | + |
| 259 | +# Force clean exit |
| 260 | +sys.exit(0) |
0 commit comments