diff --git a/.github/labeler.yml b/.github/labeler.yml index 81b27792..91b59873 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,5 +1,8 @@ documentation: -- any: ['docs/*', '*.md'] +- docs/* +- '*.md' +- src/falconpy/*.md +- samples/*.md package: - src/*.py @@ -13,3 +16,8 @@ pipeline: unit testing: - any: ['tests/*', 'util/*'] + +code samples: +- samples/*.py +- samples/real_time_response/*.py +- samples/sample_uploads/*.py diff --git a/.github/wordlist.txt b/.github/wordlist.txt index df98fac6..c14c785d 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -19,6 +19,7 @@ autogenerated pytest ipython dev +config cov FalconDebug Uber diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index ab16033f..6ee7989a 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -23,6 +23,9 @@ jobs: python -m pip install --upgrade pip python -m pip install bandit pip install -r requirements.txt - - name: Analyze with bandit + - name: Analyze package with bandit run: | bandit -r src + - name: Analyze samples with bandit + run: | + bandit -r samples diff --git a/.github/workflows/label_request.yml b/.github/workflows/label_request.yml index 70cf621c..0d6f448a 100644 --- a/.github/workflows/label_request.yml +++ b/.github/workflows/label_request.yml @@ -3,7 +3,6 @@ on: pull_request: branches: - main - - 'ver_*' jobs: triage: diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 86e02834..f3b879ae 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -23,10 +23,16 @@ jobs: python -m pip install --upgrade pip python -m pip install flake8 pip install -r requirements.txt - - name: Lint with flake8 + - name: Lint package source with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 src/falconpy --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # Stop the build on all linting errors - 04.02.21 / jshcodes@CrowdStrike flake8 src/falconpy --count --max-complexity=15 --max-line-length=127 --statistics + - name: Lint samples with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 samples --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 samples --exit-zero --count --max-complexity=15 --max-line-length=127 --statistics diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 00000000..c58534d5 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,51 @@ +![CrowdStrike Falcon](https://raw.githubusercontent.com/CrowdStrike/falconpy/main/docs/asset/cs-logo.png) + +[![Twitter URL](https://img.shields.io/twitter/url?label=Follow%20%40CrowdStrike&style=social&url=https%3A%2F%2Ftwitter.com%2FCrowdStrike)](https://twitter.com/CrowdStrike) + +# FalconPy usage examples +These examples are provided as a quick start for your project. + ++ [Authentication for Examples](#authentication-for-these-examples) ++ [Samples by API service collection](#samples-by-api-service-collection) + - [Detections](#detections) + - [Event Streams](#event-streams) + - [Falcon Discover](#falcon-discover) + - [Hosts](#hosts) + - [Real Time Response](#real-time-response) + - [Sample Uploads](#sample-uploads) ++ [Suggestions](#suggestions) + +## Authentication for these Examples +In order to expedite sample delivery, we will be following a standard pattern for defining and providing credentials to the API. +This is not the only method of providing these values, and not recommended for production deployments as the config.json file is +**not encrypted**. + +In order to test these samples locally in your development environment, rename the file `config_sample.json` to `config.json` and then +update this file to reflect your current development API credentials. + +## Samples by API service collection +These samples are categorized by API service collection. The list below will grow as more samples are planned. + +### Detections +_Coming Soon_ + +### Event Streams +_Coming Soon_ + +### Falcon Discover +_Coming Soon_ + +### Hosts +_Coming Soon_ + +### Real Time Response ++ [Quarantine a host](real_time_response/quarantine_hosts.py) + +### Sample Uploads ++ [Upload, Retrieve and then Delete a file (Service Class)](sample_uploads/sample_uploads_service.py) ++ [Upload, Retrieve and then Delete a file (Uber Class)](sample_uploads/sample_uploads_uber.py) + +## Suggestions +Got a suggestion for an example you'd like to see? Let us know by posting a message to our [discussion board](https://github.com/CrowdStrike/falconpy/discussions). + +Have an example you've developed yourself that you'd like to share? **_Excellent!_** Please review our [contributing guidelines](/CONTRIBUTING.md) and then submit a pull request. \ No newline at end of file diff --git a/samples/config_sample.json b/samples/config_sample.json new file mode 100644 index 00000000..51d83b29 --- /dev/null +++ b/samples/config_sample.json @@ -0,0 +1,4 @@ +{ + "falcon_client_id": "API ID GOES HERE", + "falcon_client_secret": "API SECRET GOES HERE" +} diff --git a/samples/real_time_response/quarantine_hosts.py b/samples/real_time_response/quarantine_hosts.py new file mode 100644 index 00000000..f9e5a69a --- /dev/null +++ b/samples/real_time_response/quarantine_hosts.py @@ -0,0 +1,91 @@ +# ____ _ _____ _ ____ +# | _ \ ___ __ _| | |_ _(_)_ __ ___ ___ | _ \ ___ ___ _ __ ___ _ __ ___ ___ +# | |_) / _ \/ _` | | | | | | '_ ` _ \ / _ \ | |_) / _ \/ __| '_ \ / _ \| '_ \/ __|/ _ \ +# | _ < __/ (_| | | | | | | | | | | | __/ | _ < __/\__ \ |_) | (_) | | | \__ \ __/ +# |_| \_\___|\__,_|_| |_| |_|_| |_| |_|\___| |_| \_\___||___/ .__/ \___/|_| |_|___/\___| +# |_| +# +# This example demonstrates how to apply or lift containment on a host using its hostname. +# This solution makes use of Service Class legacy authentication. +# +import json +import argparse +# Import necessary FalconPy classes +from falconpy import oauth2 as FalconAuth +from falconpy import hosts as FalconHosts + +# Setup our argument parser +parser = argparse.ArgumentParser("Script that leverages Falcon API to (un)contain hosts") +parser.add_argument('-c', '--creds_file', dest='creds_file', help='Path to creds json file', required=True) +parser.add_argument('-H', '--hostname', dest='hostname', help='Hostname to quarantine', required=True) +parser.add_argument('-l', '--lift', dest='lift_containment', action="store_true", help='Lift containment', default=False) +# Parse our ingested arguments +args = parser.parse_args() +# Hostname of the machine to contain / release +hostname = args.hostname +# Default action is to quarantine +if args.lift_containment: + action = "lift_containment" +else: + action = "contain" +# Use the credentials file provided +creds_file = args.creds_file +# Load the contents of the creds file into the creds dictionary +with open(creds_file) as f: + creds = json.load(f) +# Create an instance of our OAuth2 authorization class using our ingested creds +authorization = FalconAuth.OAuth2(creds={ + "client_id": creds['falcon_client_id'], + "client_secret": creds['falcon_client_secret'] + }) +# Try to generate a token +try: + token = authorization.token()['body']['access_token'] +except Exception as e: + # Exit out on authentication errors + print("Failed to authenticate") + print(e) + exit(-1) +# If we have a token, proceed to the next step +if token: + # Create an instance of the Hosts class + falcon = FalconHosts.Hosts(access_token=token) + # Create our parameter payload, using our ingested hostname as a filter + PARAMS = { + 'offset': 0, + 'limit': 10, + 'filter': f"hostname:'{hostname}'" + } + # Query the Hosts API for hosts that match our filter pattern + response = falcon.QueryDevicesByFilter(parameters=PARAMS) + # Retrieve the list of IDs returned + contain_ids = response['body']['resources'] + # Output the result + print(json.dumps(response, indent=4)) + + if not contain_ids: + # No hosts were found, exit out + print(f"[-] Could not find hostname: {hostname} - Please verify proper case") + exit(-2) + + # Create our next payload based upon the action requested + PARAMS = { + 'action_name': action + } + # Our body payload will contain our list of IDs + BODY = { + 'ids': contain_ids + } + # Provide a status update to the terminal + if action == "contain": + print(f"\n[+] Containing: {hostname}\n") + else: + print(f"\n[+] Lifting Containment: {hostname}\n") + + # Perform the requested action + # TODO: Get rid of action_name="contain" once bug is resolved + # BUG: https://github.com/CrowdStrike/falconpy/issues/114 + response = falcon.PerformActionV2(parameters=PARAMS, body=BODY, + action_name="contain") + # Output the result + print(json.dumps(response, indent=4)) diff --git a/samples/sample_uploads/sample_uploads_service.py b/samples/sample_uploads/sample_uploads_service.py new file mode 100644 index 00000000..134d38fe --- /dev/null +++ b/samples/sample_uploads/sample_uploads_service.py @@ -0,0 +1,48 @@ +# ____ _ _ _ _ _ +# / ___| __ _ _ __ ___ _ __ | | ___ | | | |_ __ | | ___ __ _ __| |___ +# \___ \ / _` | '_ ` _ \| '_ \| |/ _ \ | | | | '_ \| |/ _ \ / _` |/ _` / __| +# ___) | (_| | | | | | | |_) | | __/ | |_| | |_) | | (_) | (_| | (_| \__ \ +# |____/ \__,_|_| |_| |_| .__/|_|\___| \___/| .__/|_|\___/ \__,_|\__,_|___/ +# |_| |_| +# +# ____ _ ____ _ +# / ___| ___ _ ____ _(_) ___ ___ / ___| | __ _ ___ ___ +# \___ \ / _ \ '__\ \ / / |/ __/ _ \ | | | |/ _` / __/ __| +# ___) | __/ | \ V /| | (_| __/ | |___| | (_| \__ \__ \ +# |____/ \___|_| \_/ |_|\___\___| \____|_|\__,_|___/___/ +# +# +# These examples show how to interact with the Sample Uploads API using the Service Class +# This example uses Credential authentication and supports token refresh / authentication free usage. +# +import json +# Import the Sample Uploads service class +from falconpy import sample_uploads as FalconUploads + +# #Grab our config parameters +with open('config.json', 'r') as file_config: + config = json.loads(file_config.read()) + +falcon = FalconUploads.Sample_Uploads(creds={ + "client_id": config["falcon_client_id"], + "client_secret": config["falcon_client_secret"] + } +) + +# Define our file +FILENAME = "testfile.jpg" +# Open the file for binary read, this will be our payload +PAYLOAD = open(FILENAME, 'rb').read() +# Upload the file using the Sample Uploads API, name this file "newfile.jpg" in the API +# Since we are using the Service Class, we do not need to specify the content type +response = falcon.UploadSampleV3(file_name="newfile.jpg", file_data=PAYLOAD) +# Grab the SHA256 unique identifier for the file we just uploaded +sha = response["body"]["resources"][0]["sha256"] +# Download a copy of this file, use the SHA256 ID to retrieve it +response = falcon.GetSampleV3(ids=sha) +# Save the result to a new file +open('serviceclass.jpg', 'wb').write(response) +# Delete the file from the API +response = falcon.DeleteSampleV3(ids=sha) +# Print the results of our delete command +print(json.dumps(response, indent=4)) diff --git a/samples/sample_uploads/sample_uploads_uber.py b/samples/sample_uploads/sample_uploads_uber.py new file mode 100644 index 00000000..794c7008 --- /dev/null +++ b/samples/sample_uploads/sample_uploads_uber.py @@ -0,0 +1,47 @@ +# ____ _ _ _ _ _ +# / ___| __ _ _ __ ___ _ __ | | ___ | | | |_ __ | | ___ __ _ __| |___ +# \___ \ / _` | '_ ` _ \| '_ \| |/ _ \ | | | | '_ \| |/ _ \ / _` |/ _` / __| +# ___) | (_| | | | | | | |_) | | __/ | |_| | |_) | | (_) | (_| | (_| \__ \ +# |____/ \__,_|_| |_| |_| .__/|_|\___| \___/| .__/|_|\___/ \__,_|\__,_|___/ +# |_| |_| +# +# +# _ _ _ ____ _ +# | | | | |__ ___ _ __ / ___| | __ _ ___ ___ +# | | | | '_ \ / _ \ '__| | | | |/ _` / __/ __| +# | |_| | |_) | __/ | | |___| | (_| \__ \__ \ +# \___/|_.__/ \___|_| \____|_|\__,_|___/___/ +# +# These examples show how to interact with the Sample Uploads API using the Uber class. +# +import json +# Import the Uber Class +from falconpy import api_complete as FalconSDK + +# Grab our config parameters +with open('../config.json', 'r') as file_config: + config = json.loads(file_config.read()) + +# Create an instance of the Uber class +falcon = FalconSDK.APIHarness(creds={ + "client_id": config["falcon_client_id"], + "client_secret": config["falcon_client_secret"] + } +) + +# Define our file +FILENAME = "testfile.jpg" +# Open the file for binary read, this will be our payload +PAYLOAD = open(FILENAME, 'rb').read() +# Upload the file using the Sample Uploads API, name this file "newfile.jpg" in the API +response = falcon.command('UploadSampleV3', file_name="newfile.jpg", data=PAYLOAD, content_type="application/octet-stream") +# Grab the SHA256 unique identifier for the file we just uploaded +sha = response["body"]["resources"][0]["sha256"] +# Download a copy of this file, use the SHA256 ID to retrieve it +response = falcon.command("GetSampleV3", ids=sha) +# Save the result to a new file +open('uberclass.jpg', 'wb').write(response) +# Delete the file from the API +response = falcon.command("DeleteSampleV3", ids=sha) +# Print the results of our delete command +print(json.dumps(response, indent=4))