diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..9fe725b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,32 @@ +## scripts require: + + > pip3 install trilium-py + +## config.json + - get trilium token in trilium options->ETAPI and "create new token" + - create a new note with a title "Machines" + - click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file + - set the folder's owned attributes to: + +``` #label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 ``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text``` + + - get this page's ID and put in "trilium_machines_template_id" + +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text ``` +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + + - to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..9fe725b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,32 @@ +## scripts require: + + > pip3 install trilium-py + +## config.json + - get trilium token in trilium options->ETAPI and "create new token" + - create a new note with a title "Machines" + - click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file + - set the folder's owned attributes to: + +``` #label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 ``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text``` + + - get this page's ID and put in "trilium_machines_template_id" + +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text ``` +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + + - to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..9fe725b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,32 @@ +## scripts require: + + > pip3 install trilium-py + +## config.json + - get trilium token in trilium options->ETAPI and "create new token" + - create a new note with a title "Machines" + - click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file + - set the folder's owned attributes to: + +``` #label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 ``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text``` + + - get this page's ID and put in "trilium_machines_template_id" + +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text ``` +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + + - to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium_challenges.py b/htb2trilium_challenges.py new file mode 100644 index 0000000..0c08b74 --- /dev/null +++ b/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +from collections import defaultdict +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_challenges_folder = config['trilium_challenges_folder'] +trilium_challenges_template_id = config['trilium_challenges_template_id'] + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release_date'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) + + +print("[+] gathering challenges info") +categories = defaultdict(list) +challenges = client.get_all_challenges() +challenges.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed challenges + +print(f"[i] Retrieved {len(challenges)} challenges") + +# Group challenges by their categories +for challenge in challenges: + categories[challenge['category_name']].append(challenge) + if challenge['is_owned']: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped challenges with the number of completed and total challenges in each category +for category, grouped_challenges in categories.items(): + total = len(grouped_challenges) + completed = sum(1 for challenge in grouped_challenges if challenge['is_owned']) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_challenges_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].lower() == category.lower()+" - "+str(completed)+" / "+str(total): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for challenge in grouped_challenges: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty']) + if attribute['name'] == "Released": + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date) + if attribute['name'] == "Solved": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value=" ") + if attribute['name'] == "cssClass": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: # doesnt already exist, create page + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + new_note = ea.create_note( + parentNoteId=catId, + type="text", + title=challenge['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + if challenge['is_owned']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ") + + print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges))) \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..9fe725b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,32 @@ +## scripts require: + + > pip3 install trilium-py + +## config.json + - get trilium token in trilium options->ETAPI and "create new token" + - create a new note with a title "Machines" + - click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file + - set the folder's owned attributes to: + +``` #label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 ``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text``` + + - get this page's ID and put in "trilium_machines_template_id" + +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text ``` +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + + - to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium_challenges.py b/htb2trilium_challenges.py new file mode 100644 index 0000000..0c08b74 --- /dev/null +++ b/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +from collections import defaultdict +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_challenges_folder = config['trilium_challenges_folder'] +trilium_challenges_template_id = config['trilium_challenges_template_id'] + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release_date'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) + + +print("[+] gathering challenges info") +categories = defaultdict(list) +challenges = client.get_all_challenges() +challenges.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed challenges + +print(f"[i] Retrieved {len(challenges)} challenges") + +# Group challenges by their categories +for challenge in challenges: + categories[challenge['category_name']].append(challenge) + if challenge['is_owned']: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped challenges with the number of completed and total challenges in each category +for category, grouped_challenges in categories.items(): + total = len(grouped_challenges) + completed = sum(1 for challenge in grouped_challenges if challenge['is_owned']) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_challenges_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].lower() == category.lower()+" - "+str(completed)+" / "+str(total): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for challenge in grouped_challenges: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty']) + if attribute['name'] == "Released": + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date) + if attribute['name'] == "Solved": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value=" ") + if attribute['name'] == "cssClass": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: # doesnt already exist, create page + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + new_note = ea.create_note( + parentNoteId=catId, + type="text", + title=challenge['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + if challenge['is_owned']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ") + + print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges))) \ No newline at end of file diff --git a/htb2trilium_machines.py b/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) +print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..9fe725b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,32 @@ +## scripts require: + + > pip3 install trilium-py + +## config.json + - get trilium token in trilium options->ETAPI and "create new token" + - create a new note with a title "Machines" + - click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file + - set the folder's owned attributes to: + +``` #label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 ``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text``` + + - get this page's ID and put in "trilium_machines_template_id" + +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text ``` +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + + - to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium_challenges.py b/htb2trilium_challenges.py new file mode 100644 index 0000000..0c08b74 --- /dev/null +++ b/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +from collections import defaultdict +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_challenges_folder = config['trilium_challenges_folder'] +trilium_challenges_template_id = config['trilium_challenges_template_id'] + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release_date'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) + + +print("[+] gathering challenges info") +categories = defaultdict(list) +challenges = client.get_all_challenges() +challenges.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed challenges + +print(f"[i] Retrieved {len(challenges)} challenges") + +# Group challenges by their categories +for challenge in challenges: + categories[challenge['category_name']].append(challenge) + if challenge['is_owned']: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped challenges with the number of completed and total challenges in each category +for category, grouped_challenges in categories.items(): + total = len(grouped_challenges) + completed = sum(1 for challenge in grouped_challenges if challenge['is_owned']) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_challenges_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].lower() == category.lower()+" - "+str(completed)+" / "+str(total): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for challenge in grouped_challenges: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty']) + if attribute['name'] == "Released": + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date) + if attribute['name'] == "Solved": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value=" ") + if attribute['name'] == "cssClass": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: # doesnt already exist, create page + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + new_note = ea.create_note( + parentNoteId=catId, + type="text", + title=challenge['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + if challenge['is_owned']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ") + + print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges))) \ No newline at end of file diff --git a/htb2trilium_machines.py b/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) +print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium_sherlocks.py b/htb2trilium_sherlocks.py new file mode 100644 index 0000000..e5d9b98 --- /dev/null +++ b/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +from collections import defaultdict +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_template_id'] + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release_date'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) + + +print("[+] gathering sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].lower() == category.lower()+" - "+str(completed)+" / "+str(total): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date) + + if attribute['name'] == "Solved": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + new_note = ea.create_note( + parentNoteId=catId, + type="text", + title=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..9fe725b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,32 @@ +## scripts require: + + > pip3 install trilium-py + +## config.json + - get trilium token in trilium options->ETAPI and "create new token" + - create a new note with a title "Machines" + - click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file + - set the folder's owned attributes to: + +``` #label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 ``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text``` + + - get this page's ID and put in "trilium_machines_template_id" + +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: +```#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text ``` +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + + - to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium_challenges.py b/htb2trilium_challenges.py new file mode 100644 index 0000000..0c08b74 --- /dev/null +++ b/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +from collections import defaultdict +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_challenges_folder = config['trilium_challenges_folder'] +trilium_challenges_template_id = config['trilium_challenges_template_id'] + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release_date'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) + + +print("[+] gathering challenges info") +categories = defaultdict(list) +challenges = client.get_all_challenges() +challenges.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed challenges + +print(f"[i] Retrieved {len(challenges)} challenges") + +# Group challenges by their categories +for challenge in challenges: + categories[challenge['category_name']].append(challenge) + if challenge['is_owned']: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped challenges with the number of completed and total challenges in each category +for category, grouped_challenges in categories.items(): + total = len(grouped_challenges) + completed = sum(1 for challenge in grouped_challenges if challenge['is_owned']) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_challenges_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].lower() == category.lower()+" - "+str(completed)+" / "+str(total): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for challenge in grouped_challenges: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty']) + if attribute['name'] == "Released": + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date) + if attribute['name'] == "Solved": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value=" ") + if attribute['name'] == "cssClass": + if challenge['is_owned']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: # doesnt already exist, create page + release_str = challenge['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + new_note = ea.create_note( + parentNoteId=catId, + type="text", + title=challenge['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + if challenge['is_owned']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ") + + print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges))) \ No newline at end of file diff --git a/htb2trilium_machines.py b/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) +print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium_sherlocks.py b/htb2trilium_sherlocks.py new file mode 100644 index 0000000..e5d9b98 --- /dev/null +++ b/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +from collections import defaultdict +from bs4 import BeautifulSoup +from datetime import datetime, timezone +from htb_client import HTBClient +from trilium_py.client import ETAPI + +# Get the absolute path of the script's directory +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the full path to the config.json file +config_path = os.path.join(script_dir, 'config.json') + +# Load configuration from the JSON file +with open(config_path, 'r') as f: + config = json.load(f) + +# Accessing config values +htb_code = config['htb_code'] +trilium_server_url = config['trilium_server_url'] +trilium_token = config['trilium_token'] +trilium_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_template_id'] + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release_date'] + dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + + # Set the timezone to UTC + dt = dt.replace(tzinfo=timezone.utc) + + # Get the timestamp + timestamp = dt.timestamp() + return timestamp + +print("[+] connecting to HTB") +client = HTBClient(password=htb_code) +print("[+] connecting to trilium") +ea = ETAPI(trilium_server_url, trilium_token) +print("[i] version: ", ea.app_info()['appVersion']) + +print("[i] HTB User:", client.user['id'], "-", client.user['name']) + + +print("[+] gathering sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].lower() == category.lower()+" - "+str(completed)+" / "+str(total): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date) + + if attribute['name'] == "Solved": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['release_date'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + new_note = ea.create_note( + parentNoteId=catId, + type="text", + title=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) \ No newline at end of file diff --git a/htb_client.py b/htb_client.py new file mode 100644 index 0000000..a35929a --- /dev/null +++ b/htb_client.py @@ -0,0 +1,214 @@ +import requests +import warnings + +warnings.filterwarnings("ignore") + +class HTBClient: + def __init__(self, password): + self.password = password + self.base_url = 'https://labs.hackthebox.com/api/v4' + self.proxies = { + #"http": "http://127.0.0.1:8080", # Burp proxy for HTTP traffic + #"https": "http://127.0.0.1:8080" # Burp proxy for HTTPS traffic + } + # Fetch user info + self.user = self.get_user_info() + + # Fetch user stats and store them in self.user + user_owns, root_owns, respects = self.get_user_stats(self.user['id']) + self.user['user_owns'] = user_owns + self.user['root_owns'] = root_owns + self.user['respects'] = respects + + def get_user_info(self): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/info', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}") + + # Return the user info as a dictionary + data = response.json().get('info') + return {'id': data['id'], 'name': data['name'], 'email': data['email']} + + def get_user_stats(self, user_id): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/profile/basic/{user_id}', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}") + + # Extract user statistics from the response + data = response.json().get('profile') + user_owns = data['user_owns'] + root_owns = data['system_owns'] + respects = data['respects'] + return user_owns, root_owns, respects + + def get_active_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_retired_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_all_machines(self): + # Combine active and retired machines, ensuring no duplicates + active_machines = self.get_active_machines() + retired_machines = self.get_retired_machines() + + all_machines = active_machines + retired_machines + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + unique_machines = [] + + for machine in all_machines: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + unique_machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + return unique_machines + + def get_all_challenges(self): + challenges = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/challenges?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}") + + data = response.json() + for challenge in data['data']: + if challenge['id'] not in seen_ids and challenge['name'] not in seen_names: + challenges.append(challenge) + seen_ids.add(challenge['id']) + seen_names.add(challenge['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return challenges + + def get_all_sherlocks(self): + sherlocks = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/sherlocks?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}") + + data = response.json() + for sherlock in data['data']: + if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names: + sherlocks.append(sherlock) + seen_ids.add(sherlock['id']) + seen_names.add(sherlock['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return sherlocks \ No newline at end of file