Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions ad_miner/scripts/analyse_cache.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import pickle
import os
import sys
from pathlib import Path

from ad_miner.sources.modules.safe_pickle import safe_load

# Constants
MODULES_DIRECTORY = Path(__file__).parent / 'sources/modules'


def request_a():
module_name = sys.argv[1]
return retrieveCacheEntry(full_module_path=MODULES_DIRECTORY / module_name)

# Security: Validate that module_name is a simple filename without path components
if os.path.sep in module_name or '/' in module_name or module_name.startswith('.'):
raise ValueError(f"Invalid module name: path components not allowed")

return retrieveCacheEntry(module_name=module_name)


def retrieveCacheEntry(module_name: str):
# Resolve the full path
full_path = (MODULES_DIRECTORY / module_name).resolve()

# Security: Verify the path stays within the allowed directory
modules_dir_resolved = MODULES_DIRECTORY.resolve()
if not str(full_path).startswith(str(modules_dir_resolved)):
raise ValueError("Path traversal detected: access denied")

def retrieveCacheEntry(full_module_path: Path):
with open(full_module_path, "rb") as f:
return pickle.load(f)
with open(full_path, "rb") as f:
return safe_load(f)


list_path = request_a()
Expand Down
30 changes: 15 additions & 15 deletions ad_miner/sources/js/graph.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// This object is used by vis.js to retrieve node icon
var icon_group_options = {};

// HTML escape function to prevent XSS
function escapeHtmlGraph(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

var existing_attribute_strings = []

for (var i = 0; i < window.data_nodes.length; i++) {
Expand Down Expand Up @@ -1755,29 +1762,23 @@ function bindRightClick() {
<a class='list-group-item list-group-item-action disabled'>No Action</a>
</div>`;
} else {
// Sanitize lastSelectedNode by using JSON.stringify to prevent injection
var safeNodeId = JSON.stringify(lastSelectedNode);
if (
allNodescopy[lastSelectedNode].clusterType == 'complete' ||
allNodescopy[lastSelectedNode].clusterType == 'partial' ||
allNodescopy[lastSelectedNode].clusterType == 'forward'
) {
menu.innerHTML =
`<div class="list-group">
<a class='list-group-item list-group-item-action' onclick=openCluster(` +
lastSelectedNode +
`)>Open Cluster</a>
<a class='list-group-item list-group-item-action' onclick='openCluster(${safeNodeId})'>Open Cluster</a>
</div>`;
} else {
menu.innerHTML =
`<div class="list-group">
<a class='list-group-item list-group-item-action' onclick=closeClusterComplete(` +
lastSelectedNode +
`)>Cluster</a>
<a class='list-group-item list-group-item-action' onclick=closeClusterPartial(` +
lastSelectedNode +
`)>Cluster direct children only</a>
<a class='list-group-item list-group-item-action' onclick=closeClusterForward(` +
lastSelectedNode +
`)>Cluster direct forward children</a>
<a class='list-group-item list-group-item-action' onclick='closeClusterComplete(${safeNodeId})'>Cluster</a>
<a class='list-group-item list-group-item-action' onclick='closeClusterPartial(${safeNodeId})'>Cluster direct children only</a>
<a class='list-group-item list-group-item-action' onclick='closeClusterForward(${safeNodeId})'>Cluster direct forward children</a>
</div>`;
}
}
Expand Down Expand Up @@ -1809,13 +1810,12 @@ function bindRightClick() {
}

function printRecurseWarning(nodeLabel) {
var escapedLabel = escapeHtmlGraph(nodeLabel);
document.getElementById('hooker').innerHTML =
`<div class="alert alert-dismissible alert-warning">
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<h4 class="alert-heading">Warning!</h4>
<p class="mb-0">The configuration of the graph doesn't allow to cluster ` +
nodeLabel +
` </p>
<p class="mb-0">The configuration of the graph doesn't allow to cluster ${escapedLabel}</p>
</div>`;
}

Expand Down
23 changes: 19 additions & 4 deletions ad_miner/sources/js/search_bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
Controls the search bar on the main page.
*/

// HTML escape function to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

// Escape special regex characters
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Hide and display the search bar
function toggleSearch() {
const searchBarDiv = document.getElementById("search-bar-div");
Expand Down Expand Up @@ -47,11 +59,14 @@ function updateDropdown(filteredControls) {
dropdownItem.classList.add("dropdown-item");
dropdownItem.href = control.link;

// Highlight search input in result
// Highlight search input in result (escape HTML first to prevent XSS)
const searchTerm = searchBar.value.toLowerCase().trim();
var regex = new RegExp(searchTerm, 'gi');
var name = control.name.replace(regex, '<span class="search-highlight">$&</span>');
var title = control.title.replace(regex, '<span class="search-highlight">$&</span>');
var escapedSearchTerm = escapeRegex(searchTerm);
var regex = new RegExp(escapedSearchTerm, 'gi');
var escapedName = escapeHtml(control.name);
var escapedTitle = escapeHtml(control.title);
var name = escapedName.replace(regex, '<span class="search-highlight">$&</span>');
var title = escapedTitle.replace(regex, '<span class="search-highlight">$&</span>');

dropdownItem.innerHTML = `
<div class="container">
Expand Down
4 changes: 3 additions & 1 deletion ad_miner/sources/modules/cache_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import pickle
import csv

from ad_miner.sources.modules.safe_pickle import safe_load


class Cache:
def __init__(self, arguments):
Expand All @@ -25,7 +27,7 @@ def retrieveCacheEntry(self, filename):
full_name = self.cache_prefix + "_" + filename
if os.path.exists(full_name):
with open(full_name, "rb") as f:
return pickle.load(f)
return safe_load(f)
return False

def createCsvFileFromRequest(self, filename, data, object_type):
Expand Down
74 changes: 37 additions & 37 deletions ad_miner/sources/modules/common_analysis.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ad_miner.sources.modules import logger
from ad_miner.sources.modules.utils import CONFIG_MAP, days_format
from ad_miner.sources.modules.utils import CONFIG_MAP, days_format, escape_html
from ad_miner.sources.modules.page_class import Page
from ad_miner.sources.modules.graph_class import Graph
from ad_miner.sources.modules.path_neo4j import Path
Expand Down Expand Up @@ -517,16 +517,16 @@ def genNumberOfDCPage(requests_results, arguments):

for d in computers_nb_domain_controllers:
temp_data = {}
temp_data["domain"] = '<i class="bi bi-globe2"></i> ' + d["domain"]
temp_data["domain"] = '<i class="bi bi-globe2"></i> ' + escape_html(d["domain"])
if d["ghost"]:
temp_data["name"] = (
'<svg height="15px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="#ff595e" d="M40.1 467.1l-11.2 9c-3.2 2.5-7.1 3.9-11.1 3.9C8 480 0 472 0 462.2V192C0 86 86 0 192 0S384 86 384 192V462.2c0 9.8-8 17.8-17.8 17.8c-4 0-7.9-1.4-11.1-3.9l-11.2-9c-13.4-10.7-32.8-9-44.1 3.9L269.3 506c-3.3 3.8-8.2 6-13.3 6s-9.9-2.2-13.3-6l-26.6-30.5c-12.7-14.6-35.4-14.6-48.2 0L141.3 506c-3.3 3.8-8.2 6-13.3 6s-9.9-2.2-13.3-6L84.2 471c-11.3-12.9-30.7-14.6-44.1-3.9zM160 192a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm96 32a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg> '
+ d["name"]
+ escape_html(d["name"])
)
else:
temp_data["name"] = '<i class="bi bi-server"></i> ' + d["name"]
temp_data["name"] = '<i class="bi bi-server"></i> ' + escape_html(d["name"])
if "WINDOWS" in d["os"].upper():
temp_data["os"] = '<i class="bi bi-windows"></i> ' + d["os"]
temp_data["os"] = '<i class="bi bi-windows"></i> ' + escape_html(d["os"])
temp_data["last logon"] = days_format(d["lastLogon"])
data.append(temp_data)
grid.setData(data)
Expand Down Expand Up @@ -561,15 +561,15 @@ def genUsersListPage(requests_results, arguments):
data = []
for user in users:
tmp_dict = {}
tmp_dict["domain"] = '<i class="bi bi-globe2"></i> ' + user["domain"]
tmp_dict["domain"] = '<i class="bi bi-globe2"></i> ' + escape_html(user["domain"])
# Add admin icon
if user["name"] in admin_list:
tmp_dict["name"] = (
'<i class="bi bi-gem" title="This user is domain admin"></i> '
+ user["name"]
+ escape_html(user["name"])
)
else:
tmp_dict["name"] = '<i class="bi bi-person-fill"></i> ' + user["name"]
tmp_dict["name"] = '<i class="bi bi-person-fill"></i> ' + escape_html(user["name"])
# Add calendar icon
logon = -1
if user.get("logon"):
Expand Down Expand Up @@ -601,12 +601,12 @@ def genAllGroupsPage(requests_results, arguments):
grid.setheaders(["domain", "name"])
group_extract = [
{
"domain": '<i class="bi bi-globe2"></i> ' + groups[k]["domain"],
"domain": '<i class="bi bi-globe2"></i> ' + escape_html(groups[k]["domain"]),
"name": (
'<i class="bi bi-gem" title="This group is domain admin"></i> '
+ groups[k]["name"]
+ escape_html(groups[k]["name"])
if groups[k].get("da")
else '<i class="bi bi-people-fill"></i> ' + groups[k]["name"]
else '<i class="bi bi-people-fill"></i> ' + escape_html(groups[k]["name"])
),
}
for k in range(len(groups))
Expand Down Expand Up @@ -641,13 +641,13 @@ def generateComputersListPage(requests_results, arguments):
if computer["ghost"]:
name = (
'<svg height="15px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M40.1 467.1l-11.2 9c-3.2 2.5-7.1 3.9-11.1 3.9C8 480 0 472 0 462.2V192C0 86 86 0 192 0S384 86 384 192V462.2c0 9.8-8 17.8-17.8 17.8c-4 0-7.9-1.4-11.1-3.9l-11.2-9c-13.4-10.7-32.8-9-44.1 3.9L269.3 506c-3.3 3.8-8.2 6-13.3 6s-9.9-2.2-13.3-6l-26.6-30.5c-12.7-14.6-35.4-14.6-48.2 0L141.3 506c-3.3 3.8-8.2 6-13.3 6s-9.9-2.2-13.3-6L84.2 471c-11.3-12.9-30.7-14.6-44.1-3.9zM160 192a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm96 32a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg> '
+ computer["name"]
+ escape_html(computer["name"])
)
else:
name = '<i class="bi bi-pc-display"></i> ' + computer["name"]
name = '<i class="bi bi-pc-display"></i> ' + escape_html(computer["name"])
# OS
if computer["os"]:
os = computer["os"]
os = escape_html(computer["os"])
if "windows" in computer["os"].lower():
os = '<i class="bi bi-windows"></i> ' + os
elif "mac" in computer["os"].lower():
Expand All @@ -657,7 +657,7 @@ def generateComputersListPage(requests_results, arguments):
else:
os = "Unknown"
formated_computer = {
"domain": '<i class="bi bi-globe2"></i> ' + computer["domain"],
"domain": '<i class="bi bi-globe2"></i> ' + escape_html(computer["domain"]),
"name": name,
"operating system": os,
}
Expand Down Expand Up @@ -689,8 +689,8 @@ def generateADCSListPage(requests_results, arguments):
grid = Grid("ADCS servers")
grid.setheaders(["domain", "name"])
for adcs in computers_adcs:
adcs["domain"] = '<i class="bi bi-globe2"></i> ' + adcs["domain"]
adcs["name"] = '<i class="bi bi-server"></i> ' + adcs["name"]
adcs["domain"] = '<i class="bi bi-globe2"></i> ' + escape_html(adcs["domain"])
adcs["name"] = '<i class="bi bi-server"></i> ' + escape_html(adcs["name"])
grid.setData(computers_adcs)
page.addComponent(grid)
page.render()
Expand Down Expand Up @@ -734,8 +734,8 @@ def genAzureTenants(requests_results, arguments):
data.append(
{
"Tenant ID": '<i class="bi bi-file-earmark-person"></i> '
+ tenant["ID"],
"Tenant Name": '<i class="bi bi-globe2"></i> ' + tenant["Name"],
+ escape_html(tenant["ID"]),
"Tenant Name": '<i class="bi bi-globe2"></i> ' + escape_html(tenant["Name"]),
}
)

Expand Down Expand Up @@ -774,15 +774,15 @@ def genAzureUsers(requests_results, arguments):
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
data.append(
{
"Tenant Name": '<i class="bi bi-globe2"></i> ' + tenant_name,
"Name": '<i class="bi bi-person-fill"></i> ' + user["Name"],
"Tenant Name": '<i class="bi bi-globe2"></i> ' + escape_html(tenant_name),
"Name": '<i class="bi bi-person-fill"></i> ' + escape_html(user["Name"]),
"Synced on premise": (
'<i class="bi bi-check-square"></i>'
if user["onpremisesynced"] == True
else '<i class="bi bi-square"></i>'
),
"On premise SID": (
user["SID"]
escape_html(user["SID"])
if user["onpremisesynced"] == True and user["SID"] != None
else "-"
),
Expand Down Expand Up @@ -824,8 +824,8 @@ def genAzureAdmin(requests_results, arguments):
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
data.append(
{
"Tenant Name": '<i class="bi bi-globe2"></i> ' + tenant_name,
"Name": '<i class="bi bi-gem"></i> ' + admin["Name"],
"Tenant Name": '<i class="bi bi-globe2"></i> ' + escape_html(tenant_name),
"Name": '<i class="bi bi-gem"></i> ' + escape_html(admin["Name"]),
}
)

Expand Down Expand Up @@ -864,9 +864,9 @@ def genAzureGroups(requests_results, arguments):
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
data.append(
{
"Tenant Name": '<i class="bi bi-globe2"></i> ' + tenant_name,
"Name": '<i class="bi bi-people-fill"></i> ' + group["Name"],
"Description": group["Description"],
"Tenant Name": '<i class="bi bi-globe2"></i> ' + escape_html(tenant_name),
"Name": '<i class="bi bi-people-fill"></i> ' + escape_html(group["Name"]),
"Description": escape_html(group["Description"]),
}
)

Expand Down Expand Up @@ -905,13 +905,13 @@ def genAzureVM(requests_results, arguments):
for dict in azure_vm:
tenant_id = dict.get("Tenant ID")
tenant_name = tenant_id_name.get(tenant_id, tenant_id)
tmp_data = {"Tenant Name": tenant_name}
tmp_data = {"Tenant Name": escape_html(tenant_name)}

tmp_data["Name"] = dict["Name"]
tmp_data["Name"] = escape_html(dict["Name"])

# os
if dict.get("os"):
os = dict["os"]
os = escape_html(dict["os"])
if "windows" in dict["os"].lower():
os = '<i class="bi bi-windows"></i> ' + os
elif "mac" in dict["os"].lower():
Expand Down Expand Up @@ -958,13 +958,13 @@ def genAzureDevices(requests_results, arguments):
tenant_id = dict.get("Tenant ID")
tenant_name = tenant_id_name.get(tenant_id, tenant_id)

tmp_data = {"Tenant Name": tenant_name}
tmp_data = {"Tenant Name": escape_html(tenant_name)}

tmp_data["Name"] = dict["Name"]
tmp_data["Name"] = escape_html(dict["Name"])

# os
if dict.get("os"):
os = dict["os"]
os = escape_html(dict["os"])
if "windows" in dict["os"].lower():
os = '<i class="bi bi-windows"></i> ' + os
elif (
Expand Down Expand Up @@ -1023,15 +1023,15 @@ def genAzureApps(requests_results, arguments):
if tenant_id == "F8CDEF31-A31E-4B4A-93E4-5F571E91255A":
data_microsft.append(
{
"Tenant ID": '<i class="bi bi-globe2"></i> ' + tenant_name,
"Name": '<i class="bi bi-window-sidebar"></i> ' + app["Name"],
"Tenant ID": '<i class="bi bi-globe2"></i> ' + escape_html(tenant_name),
"Name": '<i class="bi bi-window-sidebar"></i> ' + escape_html(app["Name"]),
}
)
else:
data.append(
{
"Tenant ID": '<i class="bi bi-globe2"></i> ' + tenant_name,
"Name": '<i class="bi bi-window-sidebar"></i> ' + app["Name"],
"Tenant ID": '<i class="bi bi-globe2"></i> ' + escape_html(tenant_name),
"Name": '<i class="bi bi-window-sidebar"></i> ' + escape_html(app["Name"]),
}
)
data += data_microsft
Expand Down
6 changes: 3 additions & 3 deletions ad_miner/sources/modules/controls/anomaly_acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ad_miner.sources.modules.grid_class import Grid
from ad_miner.sources.modules import generic_formating, generic_computing

from ad_miner.sources.modules.utils import grid_data_stringify
from ad_miner.sources.modules.utils import grid_data_stringify, escape_html
from ad_miner.sources.modules.common_analysis import (
get_dico_admin_of_computer_id,
createGraphPage,
Expand Down Expand Up @@ -178,7 +178,7 @@ def run(self):
else:
icon = "bi-person-fill"
tmp_dict["targets"] = (
'<i class="bi bi-person-fill"></i> ' + name
'<i class="bi bi-person-fill"></i> ' + escape_html(name)
)
if name in self.dico_is_user_admin_on_computer:
count = len(self.users_admin_computer_list[name])
Expand Down Expand Up @@ -281,7 +281,7 @@ def run(self):
self.requests_results,
)

tmp_dict["targets"] = f'<i class="bi {icon}"></i> {name}'
tmp_dict["targets"] = f'<i class="bi {icon}"></i> ' + escape_html(name)
formated_data_details.append(tmp_dict)

page = Page(
Expand Down
Loading