Skip to content

Commit

Permalink
Merge pull request #1293 from nusantara-self/jamfprotect-preventlists
Browse files Browse the repository at this point in the history
Add JAMF Protect Prevent List responder
  • Loading branch information
nusantara-self authored Oct 29, 2024
2 parents 578b016 + 88f395e commit cdc0d0d
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 0 deletions.
277 changes: 277 additions & 0 deletions responders/JAMFProtect/JAMFProtect_IOC.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
#!/usr/bin/env python3

from cortexutils.responder import Responder
import re
from urllib.parse import urlparse
import requests
import json

class JAMFProtect_IOC(Responder):
def __init__(self):
Responder.__init__(self)
self.base_url = self.get_param("config.base_url")
self.client_id = self.get_param("config.client_id")
self.password = self.get_param("config.password")
self.service = self.get_param("config.service", None)

def identify_and_extract(self, input_string):
# regular expressions for different types
patterns = {
"sha256": re.compile(r"^[a-fA-F0-9]{64}$"),
"md5": re.compile(r"^[a-fA-F0-9]{32}$"),
"sha1": re.compile(r"^[a-fA-F0-9]{40}$"),
"ipv4": re.compile(r"^(\d{1,3}\.){3}\d{1,3}$"),
"ipv6": re.compile(r"^([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:)|(([0-9a-fA-F]{1,4}:){1,7}|:)(:([0-9a-fA-F]{1,4}|:)){1,7}$"),
"domain": re.compile(r"^(?!:\/\/)([a-zA-Z0-9-_]+\.)*([a-zA-Z0-9-_]{2,})(\.[a-zA-Z]{2,11})$")
}

# check if the input_string matches any of the patterns
for key, pattern in patterns.items():
if pattern.match(input_string):
return key, input_string

# check if the input_string is a URL and extract the domain
try:
parsed_url = urlparse(input_string)
if parsed_url.scheme and parsed_url.netloc:
domain = parsed_url.netloc
# handle URLs with "www."
if domain.startswith("www."):
domain = domain[4:]
return "domain", domain
except Exception as e:
self.error(f"Error parsing URL: {e}")

return None

def get_jamf_token(self, base_url: str, client_id: str, password: str) -> str:
"""
Function to obtain a token from the Jamf Protect API.
Parameters:
- base_url (str): The base URL of your Jamf Protect instance (e.g., "https://mycompany.protect.jamfcloud.com").
- client_id (str): The client ID for authentication.
- password (str): The password for authentication.
Returns:
- str: The access token if successful, raises an exception if it fails.
"""
token_url = f"{base_url}/token"
headers = {'content-type': 'application/json'}
data = {
"client_id": client_id,
"password": password
}

try:
response = requests.post(token_url, headers=headers, data=json.dumps(data))
response.raise_for_status()
access_token = response.json().get('access_token')
if access_token:
return access_token
else:
raise ValueError("Failed to retrieve access token.")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to obtain token: {e}")

def add_hash_to_prevention_list(self, base_url: str, token: str, list_name: str, description: str, hash_value: str, tags: list):
"""
Function to add a hash to a custom prevention list in Jamf Protect using GraphQL.
"""
graphql_url = f"{base_url}/graphql"
headers = {
"Authorization": f"{token}",
"Content-Type": "application/json"
}

# Construct the GraphQL mutation payload
payload = {
"operationName": "createPreventList",
"variables": {
"name": list_name,
"description": description,
"type": "FILEHASH",
"list": [hash_value],
"tags": tags
},
"query": """
mutation createPreventList($name: String!, $tags: [String]!, $type: PREVENT_LIST_TYPE!, $list: [String]!, $description: String) {
createPreventList(
input: {name: $name, tags: $tags, type: $type, list: $list, description: $description}
) {
...PreventListFields
__typename
}
}
fragment PreventListFields on PreventList {
id
name
type
count
list
created
description
__typename
}
"""
}
# Make the GraphQL request
response = requests.post(graphql_url, headers=headers, json=payload)
response.raise_for_status()

result = response.json()
if 'errors' in result:
return f"Failed to add hash to prevention list: {result['errors']}"
else:
return f"Hash {hash_value} successfully added to prevention list {list_name}."

def get_prevention_list_id(self, base_url: str, token: str, list_name: str) -> str:
"""
Function to get the ID of a prevention list by its name.
"""
graphql_url = f"{base_url}/graphql"
headers = {
"Authorization": f"{token}",
"Content-Type": "application/json"
}

payload = {
"operationName": "listPreventLists",
"variables": {
"nextToken": None,
"direction": "ASC",
"field": "created",
"filter": None
},
"query": """
query listPreventLists($nextToken: String, $direction: OrderDirection!, $field: PreventListOrderField!, $filter: PreventListFilterInput) {
listPreventLists(
input: {next: $nextToken, order: {direction: $direction, field: $field}, pageSize: 100, filter: $filter}
) {
items {
...PreventListFields
__typename
}
pageInfo {
next
total
__typename
}
__typename
}
}
fragment PreventListFields on PreventList {
id
name
type
count
list
created
description
__typename
}
"""
}


response = requests.post(graphql_url, headers=headers, json=payload)
response.raise_for_status()

# check if the response contains valid json data
try:
result = response.json()
except ValueError as e:
raise RuntimeError(f"Failed to decode JSON response: {e}")

prevention_lists = result['data']['listPreventLists']['items']

prevention_lists_ids = []
# Search for the list with the specified name
for prevention_list in prevention_lists:
if prevention_list['name'] == list_name:
prevention_lists_ids.append(prevention_list['id'])

if prevention_lists_ids == []:
raise ValueError(f"No prevention list found with name: {list_name}")

return prevention_lists_ids



def delete_prevention_list(self, base_url: str, token: str, prevent_list_ids: list):
"""
Function to delete a prevention list in Jamf Protect using GraphQL.
"""
graphql_url = f"{base_url}/graphql"
headers = {
"Authorization": f"{token}",
"Content-Type": "application/json"
}

failed_deletions = []

for prevent_list_id in prevent_list_ids:
# Construct the GraphQL mutation payload
payload = {
"operationName": "deletePreventList",
"variables": {
"id": prevent_list_id
},
"query": """
mutation deletePreventList($id: ID!) {
deletePreventList(id: $id) {
id
__typename
}
}
"""
}

# Make the GraphQL request
response = requests.post(graphql_url, headers=headers, json=payload)
response.raise_for_status()

result = response.json()
if 'errors' in result:
failed_deletions.append(prevent_list_id)

if failed_deletions:
return f"Failed to delete prevention list(s): {', '.join(failed_deletions)}"

return f"Prevention list with ID(s) {', '.join(prevent_list_ids)} successfully deleted."


def run(self):
result = ""
observable_value = self.get_param("data.data", None)
ioc_type, ioc_value = self.identify_and_extract(observable_value)
if ioc_type not in ["sha256", "sha1"]:
self.error("error -- Not a hash or a valid hash : sha1 or sha256")

case_title = self.get_param("data.case.title", None, "Can't get case title")
case_id = self.get_param("data.case.id", None, "Can't get case ID")
description = f"Pushed from TheHive - {case_title} - {case_id}"

if self.service == "addIOC":

token = self.get_jamf_token(self.base_url, self.client_id, self.password)

result = self.add_hash_to_prevention_list(self.base_url,token, description, description, ioc_value, ["TheHive", f"{case_id}"])
elif self.service == "removeIOC":
token = self.get_jamf_token(self.base_url, self.client_id, self.password)

prevention_list_ids = self.get_prevention_list_id(self.base_url, token, description)
result = self.delete_prevention_list(self.base_url, token, prevention_list_ids)

if 'error' in result:
self.error(result)

self.report({"message": result})




if __name__ == '__main__':
JAMFProtect_IOC().run()
50 changes: 50 additions & 0 deletions responders/JAMFProtect/JAMFProtect_addHashtoPreventList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "JAMFProtect_addHashtoPreventList",
"version": "1.0",
"author": "nusantara-self, StrangeBee",
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
"license": "AGPL-V3",
"description": "Add IOC to JAMF Protect - creates a custom prevent list for a hash",
"dataTypeList": [
"thehive:case_artifact"
],
"command": "JAMFProtect/JAMFProtect_IOC.py",
"baseConfig": "JAMFProtect",
"config": {
"service": "addIOC"
},
"configurationItems": [
{
"name": "base_url",
"description": "JAMF Protect base url",
"type": "string",
"multi": false,
"required": true,
"defaultValue": "https://mycompany.protect.jamfcloud.com"
},
{
"name": "client_id",
"description": "JAMF Protect client ID",
"type": "string",
"multi": false,
"required": true,
"defaultValue": ""
},
{
"name": "password",
"description": "JAMF Protect password",
"type": "string",
"multi": false,
"required": true,
"defaultValue": ""
}
],
"registration_required": true,
"subscription_required": true,
"free_subscription": false,
"service_homepage": "https://www.jamf.com/products/jamf-protect/",
"service_logo": {
"path": "assets/jamfprotect.png",
"caption": "JAMF Protect logo"
}
}
50 changes: 50 additions & 0 deletions responders/JAMFProtect/JAMFProtect_removeHashfromPreventList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "JAMFProtect_removeHashfromPreventList",
"version": "1.0",
"author": "nusantara-self, StrangeBee",
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
"license": "AGPL-V3",
"description": "Remove IOC on JAMF Protect - removes associated custom prevent list(s) containing the hash",
"dataTypeList": [
"thehive:case_artifact"
],
"command": "JAMFProtect/JAMFProtect_IOC.py",
"baseConfig": "JAMFProtect",
"config": {
"service": "removeIOC"
},
"configurationItems": [
{
"name": "base_url",
"description": "JAMF Protect base url",
"type": "string",
"multi": false,
"required": true,
"defaultValue": "https://mycompany.protect.jamfcloud.com"
},
{
"name": "client_id",
"description": "JAMF Protect client ID",
"type": "string",
"multi": false,
"required": true,
"defaultValue": ""
},
{
"name": "password",
"description": "JAMF Protect password",
"type": "string",
"multi": false,
"required": true,
"defaultValue": ""
}
],
"registration_required": true,
"subscription_required": true,
"free_subscription": false,
"service_homepage": "https://www.jamf.com/products/jamf-protect/",
"service_logo": {
"path": "assets/jamfprotect.png",
"caption": "JAMF Protect logo"
}
}
10 changes: 10 additions & 0 deletions responders/JAMFProtect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### JAMF Protect Prevent List

This responder manages [JAMF Protect prevent lists](https://docs.jamf.com/jamf-protect/administrator-guide/Prevent_Lists.html) by adding or removing hashes as needed.

#### Setup
- Navigate to **Administrative** > **Account**
- Create a role **PreventList-Write** with permissions **Prevent Lists: Read & Write**
- Create an API client and assign the above role
- Use these API credentials in your responders

Binary file added responders/JAMFProtect/assets/jamfprotect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions responders/JAMFProtect/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cortexutils
requests

0 comments on commit cdc0d0d

Please sign in to comment.