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).
+
+ 
+
+### 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()