Skip to content
Merged
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
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = IntuneCD
version = 2.4.1
version = 2.3.0
author = Tobias Almén
author_email = almenscorner@outlook.com
description = Tool to backup and update configurations in Intune
Expand Down
59 changes: 59 additions & 0 deletions src/IntuneCD/backup/Intune/Applications.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import os

from ...intunecdlib.BaseBackupModule import BaseBackupModule


Expand Down Expand Up @@ -29,6 +31,55 @@ def __init__(self, *args, **kwargs):
self.assignment_extra_url = self.assignment_extra_url or "/assignments"
self.config_audit_data = True

def _save_script_win32(self, item: dict, rule_type, script_data_path) -> None:
# If there is a detectionScriptContent, get the name of the script and write the content to a file
if self.prefix:
match = self.check_prefix_match(item["displayName"], self.prefix)
if not match:
return
if item.get(f"{rule_type}Rules"):
for rule in item[f"{rule_type}Rules"]:
if rule.get("scriptContent"):
if self.append_id:
script_name = (
f"{item['displayName']}_{rule_type}Script__{item['id']}.ps1"
)
else:
script_name = f"{item['displayName']}_{rule_type}Script.ps1"
if not os.path.exists(script_data_path):
os.makedirs(script_data_path)
decoded = self.decode_base64(rule["scriptContent"])
f = open(
f"{script_data_path}{script_name}",
"w",
encoding="utf-8",
)
f.write(decoded)

def _save_script_mac(self, item: dict, script_type, script_data_path) -> None:
# If there is a detectionScriptContent, get the name of the script and write the content to a file
if self.prefix:
match = self.check_prefix_match(item["displayName"], self.prefix)
if not match:
return
if item.get(f"{script_type}InstallScript"):
if item[f"{script_type}InstallScript"].get("scriptContent"):
if self.append_id:
script_name = f"{item['displayName']}_{script_type}InstallScript__{item['id']}.sh"
else:
script_name = f"{item['displayName']}_{script_type}InstallScript.sh"
if not os.path.exists(script_data_path):
os.makedirs(script_data_path)
decoded = self.decode_base64(
item[f"{script_type}InstallScript"]["scriptContent"]
)
f = open(
f"{script_data_path}{script_name}",
"w",
encoding="utf-8",
)
f.write(decoded)

def main(self) -> dict[str, any]:
"""The main method to backup the Applications

Expand Down Expand Up @@ -118,13 +169,21 @@ def generate_app_name(app, app_type, suffix=""):
else "_" + str(app["displayVersion"]).replace(".", "_")
)
app_name = generate_app_name(app, "Win32", suffix)
script_path = f"{self.path}{platform}/Script Data/"
self._save_script_win32(app, "detection", script_path)
self._save_script_win32(app, "requirement", script_path)
elif app_type == "#microsoft.graph.windowsMobileMSI":
app_name = generate_app_name(
app, "WinMSI", "_" + str(app["productVersion"]).replace(".", "_")
)
else:
app_name = generate_app_name(app, app_type.split(".")[2])

if app_type == "#microsoft.graph.macOSPkgApp":
script_path = f"{self.path}{platform}/Script Data/"
self._save_script_mac(app, "pre", script_path)
self._save_script_mac(app, "post", script_path)

self.preset_filename = app_name

self.path = f"{self.path}{platform}/"
Expand Down
16 changes: 16 additions & 0 deletions src/IntuneCD/backup_intune.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def backup_intune(
args,
max_workers,
platforms,
enrich_documentation=False,
):
"""
Imports all the backup functions dynamically and runs them in parallel.
Expand Down Expand Up @@ -66,6 +67,21 @@ def backup_intune(
"platforms": platforms,
}

# Enrich data if the --enrich-documentation flag is set
if enrich_documentation:
from .intunecdlib.BaseGraphModule import BaseGraphModule
import os
import json

graph = BaseGraphModule()
graph.token = token
settings = graph.make_graph_request("https://graph.microsoft.com/beta/deviceManagement/configurationSettings")
with open(os.path.join(path, "configurationSettings.json"), "w", encoding="utf-8") as f:
json.dump(settings, f, indent=2)
categories = graph.make_graph_request("https://graph.microsoft.com/beta/deviceManagement/configurationCategories")
with open(os.path.join(path, "configurationCategories.json"), "w", encoding="utf-8") as f:
json.dump(categories, f, indent=2)

# List of backup modules to dynamically import and execute
backup_modules = [
(
Expand Down
109 changes: 85 additions & 24 deletions src/IntuneCD/document_intune.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
from concurrent.futures import ThreadPoolExecutor, as_completed
import os
import json

from .intunecdlib.documentation_functions import (
document_configs,
document_management_intents,
document_settings_catalog,
)
from .decorators import time_command

Expand All @@ -18,6 +21,7 @@ def document_intune(
decode,
split_per_config,
max_workers,
enrich_documentation=False,
):
"""
This function is used to document Intune configuration using threading.
Expand All @@ -30,6 +34,7 @@ def document_intune(
:param decode: Decode base64 values
:param split_per_config: Whether to split each config into its own Markdown file
:param max_workers: Maximum number of concurrent threads
:param enrich_documentation: Whether to enrich Settings Catalog documentation with additional details
"""

# Ensure the output directory exists
Expand Down Expand Up @@ -77,22 +82,65 @@ def document_intune(
# sort doc_tasks alphabetically
doc_tasks = sorted(doc_tasks, key=lambda x: x[1])

settings_lookup = None
categories_lookup = None

if enrich_documentation:
settings_lookup = {}
categories_lookup = {}
settings_json = None
categories_json = None

settings_path = os.path.join(configpath, "configurationSettings.json")
categories_path = os.path.join(configpath, "configurationCategories.json")
if os.path.exists(settings_path):
with open(settings_path, "r", encoding="utf-8") as f:
settings_json = json.load(f)
if os.path.exists(categories_path):
with open(categories_path, "r", encoding="utf-8") as f:
categories_json = json.load(f)

# Build lookup dictionaries for enrichment
if settings_json:
for s in settings_json.get("value", []):
settings_lookup[s.get("id")] = s
if categories_json:
for c in categories_json.get("value", []):
categories_lookup[c.get("id")] = c

# If either one of the lookups are empty, disable enrichment
if not settings_json or not categories_json:
enrich_documentation = False

if split or split_per_config:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(
document_configs,
f"{configpath}/{task[0]}",
outpath,
task[1],
maxlength,
split,
cleanup,
decode,
split_per_config,
): task[1]
for task in doc_tasks
}
futures = {}
for task in doc_tasks:
# Submit Settings Catalog with enrichment if enabled
if task[1] == "Settings Catalog" and enrich_documentation:
futures[executor.submit(
document_settings_catalog,
f"{configpath}/{task[0]}",
outpath,
task[1],
maxlength,
split,
split_per_config,
settings_lookup,
categories_lookup,
)] = task[1]
else:
futures[executor.submit(
document_configs,
f"{configpath}/{task[0]}",
outpath,
task[1],
maxlength,
split,
cleanup,
decode,
split_per_config,
)] = task[1]

for future in as_completed(futures):
task_name = futures[future]
Expand All @@ -104,16 +152,29 @@ def document_intune(
else:
# Run sequentially if split options are disabled
for task in doc_tasks:
document_configs(
f"{configpath}/{task[0]}",
outpath,
task[1],
maxlength,
split,
cleanup,
decode,
split_per_config,
)
# Submit Settings Catalog with enrichment if enabled
if task[1] == "Settings Catalog" and enrich_documentation:
document_settings_catalog(
f"{configpath}/{task[0]}",
outpath,
task[1],
maxlength,
split,
split_per_config,
settings_lookup,
categories_lookup,
)
else:
document_configs(
f"{configpath}/{task[0]}",
outpath,
task[1],
maxlength,
split,
cleanup,
decode,
split_per_config,
)

# **Run Management Intents Sequentially**
document_management_intents(
Expand Down
14 changes: 8 additions & 6 deletions src/IntuneCD/intunecdlib/IntuneCDBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ def remove_keys(self, data: dict):
"deviceHealthScriptType",
}

if "VPPusedLicenseCount" in self.exclude:
keys.add("usedLicenseCount")
if "GPlaySyncTime" in self.exclude:
keys.add("lastAppSyncDateTime")
if "CompliancePartnerHeartbeat" in self.exclude:
keys.add("lastHeartbeatDateTime")
EXCLUDE_KEY_MAP = {
"VPPusedLicenseCount": "usedLicenseCount",
"GPlaySyncTime": "lastAppSyncDateTime",
"CompliancePartnerHeartbeat": "lastHeartbeatDateTime",
"VPPeleaseDateTime": "releaseDateTime",
}

keys.update(v for k, v in EXCLUDE_KEY_MAP.items() if k in self.exclude)

for k in keys:
data.pop(k, None)
Expand Down
Loading
Loading