diff --git a/Global Variables/Global Variables.mobileconfig b/Global Variables/Global Variables.mobileconfig index 6d08104..b24349f 100644 --- a/Global Variables/Global Variables.mobileconfig +++ b/Global Variables/Global Variables.mobileconfig @@ -31,6 +31,8 @@ $PROFILE_UUID EMAIL $EMAIL + USERNAME + $USERNAME FULL_NAME $FULL_NAME EMAIL_PREFIX @@ -64,8 +66,8 @@ PayloadType Configuration PayloadUUID - 265684E2-8DCC-464B-8C6A-606E89C630DA - PayloadVersion + 265684E2-8DC + PayloadVersion 1 diff --git a/Global Variables/GlobalVariablesExample.sh b/Global Variables/GlobalVariablesExample.sh index 9510c10..5fe1c3b 100755 --- a/Global Variables/GlobalVariablesExample.sh +++ b/Global Variables/GlobalVariablesExample.sh @@ -42,6 +42,7 @@ FULL_NAME=$(/usr/libexec/PlistBuddy -c 'print :FULL_NAME' /Library/Managed\ Preferences/io.kandji.globalvariables.plist) EMAIL=$(/usr/libexec/PlistBuddy -c 'print :EMAIL' /Library/Managed\ Preferences/io.kandji.globalvariables.plist) EMAIL_PREFIX=$(/usr/libexec/PlistBuddy -c 'print :EMAIL_PREFIX' /Library/Managed\ Preferences/io.kandji.globalvariables.plist) +USERNAME=$(/usr/libexec/PlistBuddy -c 'print :USERNAME' /Library/Managed\ Preferences/io.kandji.globalvariables.plist) DEPARTMENT=$(/usr/libexec/PlistBuddy -c 'print :DEPARTMENT' /Library/Managed\ Preferences/io.kandji.globalvariables.plist) JOB_TITLE=$(/usr/libexec/PlistBuddy -c 'print :JOB_TITLE' /Library/Managed\ Preferences/io.kandji.globalvariables.plist) DEVICE_NAME=$(/usr/libexec/PlistBuddy -c 'print :DEVICE_NAME' /Library/Managed\ Preferences/io.kandji.globalvariables.plist) @@ -64,6 +65,8 @@ Email: $EMAIL Email Prefix: $EMAIL_PREFIX +Username: $USERNAME + Department: $DEPARTMENT Job Title: $JOB_TITLE diff --git a/api-tools/README.md b/api-tools/README.md index 993d2a3..993bc00 100644 --- a/api-tools/README.md +++ b/api-tools/README.md @@ -17,6 +17,7 @@ Name | Description `device-parameters` | These scripts leverage the Kandji API to interact with and generate reports from the device parameters endpoint. `device-secrets` | List and generate reports from the device secrets API. Filevault PRK, Activation lock bypass code, and unlock PIN. `device-status` | Generate device reports from the device status API. +`threats` | These scripts leverage the Kandji API to generate reports about detected threats. `update-device-record` | update-device-record leverages the Kandji API along with a CSV input file to update one or more device inventory records. At present, a device the blueprint, asset tag, and assigned user can be updated using this script. The full API documentation can be found at [https://api.kandji.io](https://api.kandji.io/). diff --git a/api-tools/threats/README.md b/api-tools/threats/README.md new file mode 100644 index 0000000..711ef36 --- /dev/null +++ b/api-tools/threats/README.md @@ -0,0 +1,115 @@ +# Threats + +### About + +This `python3` script utilizes the Kandji API to fetch and generate detailed reports regarding threats detected across devices in a Kandji tenant. It enables filtering based on threat classification, status, device ID, and date range to provide targeted insights into the security posture of managed devices. + +For more information on the Threats endpoint, see the [**Kandji API Docs**](https://api-docs.kandji.io/#d041043a-ea47-47d5-b6f1-234ef422494d) + +### Kandji API + +- The necessary API permissions for executing this script are outlined below. For more information on API access, refer to the Kandji [Knowledge Base](https://support.kandji.io). + + ![API Permissions Required](images/api_permissions_required.png) + +### Dependencies + +- The script requires Python 3. You can install Python 3 directly as an [Auto App](https://updates.kandji.io/auto-app-python-3-214020), from [python.org](https://www.python.org/downloads/), or via [Homebrew](https://brew.sh) on macOS. + +- Required Python modules can be installed using the command below. This assumes the presence of a `requirements.txt` file listing `requests` and any other required libraries. + + ```sh + python3 -m pip install -r requirements.txt + ``` + + If installing manually: + + ```sh + python3 -m pip install requests pathlib + ``` + +### Script Modification + +1. Open the script in a text editor such as BBEdit or VSCode. +2. Update the `SUBDOMAIN` variable to match your Kandji subdomain, the Kandji tenant `REGION`, and update `TOKEN` information with your Bearer token. + + - The `BASE_URL`, `REGION`, and `TOKEN` can be found by logging into Kandji then navigate to `Settings > Access > API Token`. From there, you can copy the information out of the API URL and generate API tokens. + - For US-based tenants the `REGION` can either be `us` or left as an empty string (`""`) + + _NOTE_: The API token is only visible at the point of creation so be sure to copy it to a safe location. + + ```python + ######################################################################################## + ######################### UPDATE VARIABLES BELOW ####################################### + ######################################################################################## + + SUBDOMAIN = "accuhive" # bravewaffles, example, company_name + + # us("") and eu - this can be found in the Kandji settings on the Access tab + REGION = "" + + # Kandji Bearer Token + TOKEN = "" + ``` + +3. Save and close the script. + +### Running the Script + +1. Place the script in a suitable directory, for example, your Desktop. +2. Open a Terminal and navigate to the directory containing the script. + + ```sh + cd ~/Desktop + ``` + +3. To view available command options, execute: + + ```sh + python3 threats.py --help + ``` + + You'll see options related to threat classification, date range, device ID, and status filters. + + ```sh + usage: threats.py [-h] [--classification CLASSIFICATION] [--date-range DATE_RANGE] [--device-id DEVICE_ID] [--status STATUS] [--version] + + Fetch and report Kandji threat details. + + options: + -h, --help show this help message and exit + --classification CLASSIFICATION + Filter by threat classification (malware, pup). + --date-range DATE_RANGE + Filter by number of days (e.g., 7, 30, 90). + --device-id DEVICE_ID + Filter by specific device ID. + --status STATUS Filter by threat status (quarantined, not_quarantined, released). + --version Show script version. + ``` + +### Examples + +- Generate a report for malware threats detected within the last 30 days: + + ```sh + python3 threats_report.py --classification malware --date-range 30 + ``` + +- Generate a report for all threats that have been quarantined: + + ```sh + python3 threats_report.py --status quarantined + ``` + +- Generate a comprehensive report without filters, detailing all identified threats: + + ```sh + python3 threats_report.py + ``` + +A CSV report named `threats_report_.csv` will be generated in the directory from which the script is run, offering insights into the threats detected by Kandji within the specified parameters. + +### Note + +This script was written by [@TheBoatyMcBoatFace](https://github.com/TheBoatyMcBoatFace) on behalf of [@CivicActions](https://github.com/CivicActions) in March of 2024. We used portions of the various scripts in the [Kandji Support](https://github.com/kandji-inc/support) repo to accomplish this. diff --git a/api-tools/threats/images/api_permissions_required.png b/api-tools/threats/images/api_permissions_required.png new file mode 100644 index 0000000..7c2aa84 Binary files /dev/null and b/api-tools/threats/images/api_permissions_required.png differ diff --git a/api-tools/threats/threats.py b/api-tools/threats/threats.py new file mode 100644 index 0000000..98107f8 --- /dev/null +++ b/api-tools/threats/threats.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 + +"""Generate reports from the Kandji Threats endpoint""" + +################################################################################################ +# Created by Bentley Hensel | CivicActions | CivicActions.com +################################################################################################ +# Created - 2024-03-21 +# Last modified - 2024-03-22 +################################################################################################ +# License Information +################################################################################################ +# +# Copyright 2024 Kandji, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this +# software and associated documentation files (the "Software"), to deal in the Software +# without restriction, including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +# to whom the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +################################################################################################ + +__version__ = "0.1.0" + + +# Built in imports +import sys +import csv +import pathlib +from datetime import datetime +import argparse + +# 3rd party imports + +# Try to import the module. If the module cannot be imported let the user know so that they can +# install it. +try: + import requests +except ImportError as error: + sys.exit( + "Looks like you need to install the requests module. Open a Terminal and run python3 -m " + "pip install requests." + ) + +from requests.adapters import HTTPAdapter + +######################################################################################## +######################### UPDATE VARIABLES BELOW ####################################### +######################################################################################## + +SUBDOMAIN = "accuhive" # bravewaffles, example, company_name + +# us("") and eu - this can be found in the Kandji settings on the Access tab +REGION = "" + +# Kandji Bearer Token +TOKEN = "" + +######################################################################################## +######################### DO NOT MODIFY BELOW THIS LINE ################################ +######################################################################################## + +# Kandji API base URL +if REGION in ["", "us"]: + BASE_URL = f"https://{SUBDOMAIN}.api.kandji.io/api" + +elif REGION in ["eu"]: + BASE_URL = f"https://{SUBDOMAIN}.api.{REGION}.kandji.io/api" + +else: + sys.exit(f'\nUnsupported region "{REGION}". Please update and try again\n') + + +HEADERS = { + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/json", + "Content-Type": "application/json;charset=utf-8", + "Cache-Control": "no-cache", +} + +# Current working directory +HERE = pathlib.Path("__file__").parent.absolute() + + +def var_validation(): + """Validate variables.""" + if SUBDOMAIN in ["", "accuhive"]: + print( + f'\nThe subdomain "{SUBDOMAIN}" in {BASE_URL} needs to be updated to ' + "your Kandji tenant subdomain..." + ) + print("Please see the example in the README for this repo.\n") + sys.exit() + + if TOKEN in ["api_key", ""]: + print(f'\nThe TOKEN should not be "{TOKEN}"...') + print("Please update this to your API Token.\n") + sys.exit() + +def http_errors(resp, resp_code, err_msg): + """Handle HTTP errors.""" + # 400 + if resp_code == requests.codes["bad_request"]: + print(f"\n\t{err_msg}") + print(f"\tResponse msg: {resp.text}\n") + # 401 + elif resp_code == requests.codes["unauthorized"]: + print("Make sure that you have the required permissions to access this data.") + print( + "Depending on the API platform this could mean that access has just been " + "blocked." + ) + sys.exit(f"\t{err_msg}") + # 403 + elif resp_code == requests.codes["forbidden"]: + print("The api key may be invalid or missing.") + sys.exit(f"\t{err_msg}") + # 404 + elif resp_code == requests.codes["not_found"]: + print("\nWe cannot find the one that you are looking for...") + print("Move along...") + print(f"\tError: {err_msg}") + print(f"\tResponse msg: {resp}") + print( + "\tPossible reason: If this is a device it could be because the device is " + "not longer\n" + "\t\t\t enrolled in Kandji. This would prevent the MDM command from being\n" + "\t\t\t sent successfully.\n" + ) + # 429 + elif resp_code == requests.codes["too_many_requests"]: + print("You have reached the rate limit ...") + print("Try again later ...") + sys.exit(f"\t{err_msg}") + # 500 + elif resp_code == requests.codes["internal_server_error"]: + print("The service is having a problem...") + sys.exit(err_msg) + # 503 + elif resp_code == requests.codes["service_unavailable"]: + print("Unable to reach the service. Try again later...") + else: + print("Something really bad must have happened...") + print(err_msg) + sys.exit() + + +def kandji_api(method, endpoint, params=None, payload=None): + """Make an API request and return data. + + method - an HTTP Method (GET, POST, PATCH, DELETE). + endpoint - the API URL endpoint to target. + params - optional parameters can be passed as a dict. + payload - optional payload is passed as a dict and used with PATCH and POST + methods. + Returns a JSON data object. + """ + attom_adapter = HTTPAdapter(max_retries=3) + session = requests.Session() + session.mount(BASE_URL, attom_adapter) + + try: + response = session.request( + method, + BASE_URL + endpoint, + data=payload, + headers=HEADERS, + params=params, + timeout=30, + ) + + # If a successful status code is returned (200 and 300 range) + if response: + try: + data = response.json() + except Exception: + data = response.text + + # if the request is successful exceptions will not be raised + response.raise_for_status() + + except requests.exceptions.RequestException as err: + http_errors(resp=response, resp_code=response.status_code, err_msg=err) + data = {"error": f"{response.status_code}", "api resp": f"{err}"} + + return data + + +def program_arguments(): + """Parse and return command-line arguments.""" + parser = argparse.ArgumentParser( + description="Fetch and report Kandji threat details." + ) + + parser.add_argument("--classification", help="Filter by threat classification (malware, pup).", type=str) + parser.add_argument("--date-range", help="Filter by number of days (e.g., 7, 30, 90).", type=int) + parser.add_argument("--device-id", help="Filter by specific device ID.", type=str) + parser.add_argument("--status", help="Filter by threat status (quarantined, not_quarantined, released).", type=str) + + parser.version = __version__ + parser.add_argument("--version", action="version", help="Show script version.") + + return parser.parse_args() + + +def main(): + """Main function to generate the threat report.""" + args = program_arguments() + params = { + "classification": args.classification, + "date_range": args.date_range, + "device_id": args.device_id, + "status": args.status, + } + + # Remove None values from params + params = {k: v for k, v in params.items() if v is not None} + + print("Fetching threat details...") + threats = kandji_api("GET", "/v1/threat-details", params=params) + results = threats.get("results", []) + + if not results: + print("No threats found with the given parameters.") + return + + report_name = f"threats_report_{TODAY}.csv" + fieldnames = results[0].keys() + + + print("Generating device report...") + + with open(report_name, "w", newline="") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + for threat in results: + writer.writerow(threat) + + + print(f"Kandji report at: {HERE.resolve()}/{report_name}\n") + +if __name__ == "__main__": + main()