From 5327f865b959ae1346785a076d0d1a63427ae6fc Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 19 Dec 2025 19:08:32 -0800 Subject: [PATCH] add linked GPOs functionality --- files/dashboard/index.php | 111 +++++++++++++++++++++++-------- files/restricted-group-check.ps1 | 22 ++++-- templates/app.py | 36 ++++++++++ 3 files changed, 135 insertions(+), 34 deletions(-) diff --git a/files/dashboard/index.php b/files/dashboard/index.php index 8d137eb..25b13e6 100644 --- a/files/dashboard/index.php +++ b/files/dashboard/index.php @@ -1,7 +1,38 @@ + + + - - - @@ -16,24 +47,20 @@ Once you have the GPO name, you can locate this in the GPO management snap-in and find what OUs it links to.

- -
- - - -

- -

- - - -

- - + + + +

+'; $group = urlencode($_GET['group']); - $url = "http://10.34.115.140:5000/gpo?group=$group"; + $url = "http://172.17.0.1:5000/gpo?group=$group"; // query API $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -80,22 +107,34 @@ if (isset($_GET['group'])) { } ksort($gpoMap, SORT_STRING | SORT_FLAG_CASE); if($numResults > 0){ - echo 'Total results: '.$numResults.'
'; - echo ''; - echo ''; + echo 'Total results: '.$numResults.'
+'; + echo '
GPOAdministratorsvnc_admin
+'; + echo ' +'; foreach ($gpoMap as $gpoName => $cols) { // Remove duplicates and build comma‑separated lists $adminGroups = implode('
', array_unique($cols['administrators'])); $vncGroups = implode('
', array_unique($cols['vnc_admin'])); - - echo ''; - echo ""; - echo ""; - echo ""; - echo ''; + $linkedOUs = implode("\n", getLinkedOUs($gpoName)); + echo ' +'; + echo " +"; + echo " +"; + echo " +"; + echo ' +'; } - echo '
GPOAdministratorsvnc_admin
{$gpoName}{$adminGroups}{$vncGroups}
+ {$adminGroups}{$vncGroups}
'; + echo ' +'; } else{ echo 'No results found for query '.$_GET['group'].'.

'; @@ -103,7 +142,23 @@ if (isset($_GET['group'])) { } } + +// if you click on a GPO name, print all OUs +else if (isset($_GET['gpo'])) { + echo 'OUs linked to GPO '.$_GET['gpo'].':

'; + $LinkedOUs = getLinkedOUs($_GET['gpo']); + echo implode("
", $LinkedOUs); + echo '
Return Home'; + +} else { + echo ' +

+ + + +

+'; echo "

No group specified.

"; } echo "
"; diff --git a/files/restricted-group-check.ps1 b/files/restricted-group-check.ps1 index a200355..44e03d7 100644 --- a/files/restricted-group-check.ps1 +++ b/files/restricted-group-check.ps1 @@ -5,7 +5,7 @@ Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop $TargetOU = "OU=NA,OU=Manufacturing,OU=Tesla Systems,DC=teslamotors,DC=com" -$OutputFile = "C:\Users\matanderson\gpo.yaml" +$OutputFile = "C:\Users\matanderson\gpo\gpo-update.yaml" # Add-Content -Path $OutputFile -Value $line if (Test-Path $OutputFile) { Remove-Item -Path $OutputFile -Force @@ -34,10 +34,9 @@ if ($targetOUObj) { # ------------------------------------------------------------------ # 2. Build a hashtable: GPO_GUID => list of OU DNs it is linked to -# This is entirely opaque voodoo, i have no idea what's going on # ------------------------------------------------------------------ -$gpoLinks = @{} +$gpoLinks = @{} foreach ($ou in $ouObjects) { if ($ou.gPLink) { @@ -59,6 +58,8 @@ if (-not $gpoLinks.Count) { exit } + + Write-Host "Found $($gpoLinks.Count) distinct GPO(s) linked in the OU tree." Add-Content -Path $OutputFile -Value " gpo_links: $($gpoLinks.Count)" @@ -66,18 +67,28 @@ Add-Content -Path $OutputFile -Value " gpo_links: $($gpoLinks.Count)" # 3. For each GPO, get an XML report and pull RestrictedGroup nodes # ---------------------------------------------------- + Add-Content -Path $OutputFile -Value " configured_gpos:" foreach ($gpoGuid in $gpoLinks.Keys) { try { $gpo = Get-GPO -Guid $gpoGuid -ErrorAction Stop } catch { - Write-Warning "Unable to retrieve GPO $gpoGuid - skipping." + Write-Warning "Unable to retrieve GPO $gpoGuid – skipping." continue } Write-Host "GPO: $($gpo.DisplayName)" Add-Content -Path $OutputFile -Value " $($gpo.DisplayName):" + # -------- Linked OUs ---------- + $linkedOUs = $gpoLinks[$gpoGuid] + Write-Host "Linked OUs:" + Add-Content -Path $OutputFile -Value " Linked_OUs:" + foreach ($ouDn in $linkedOUs) { + Write-Host "$ouDn" + Add-Content -Path $OutputFile -Value " - $ouDn" + } + # Pull the XML report try { $xmlString = Get-GPOReport -Guid $gpoGuid -ReportType Xml -ErrorAction Stop @@ -86,9 +97,8 @@ foreach ($gpoGuid in $gpoLinks.Keys) { Write-Warning "Unable to generate XML report for $($gpo.DisplayName)." continue } - $xml = [xml]$xmlString - + # process the XML data $ns = New-Object System.Xml.XmlNamespaceManager ($xml.NameTable) $ns.AddNamespace('m', 'http://www.microsoft.com/GroupPolicy/Settings') # default namespace $ns.AddNamespace('q1', 'http://www.microsoft.com/GroupPolicy/Settings/Security') diff --git a/templates/app.py b/templates/app.py index f9355b4..9f214ed 100644 --- a/templates/app.py +++ b/templates/app.py @@ -37,6 +37,42 @@ def get_groups(): }) return jsonify(gpos) +@app.route("/linked_ous", methods=["GET"]) +def linked_ous(): + gpo_name = request.args.get("gpo") + if not gpo_name: + return jsonify({"error": "Missing 'gpo' query parameter"}), 400 + linked = [] + # The YAML is a list of dictionaries – iterate over them + + for top in data or []: + cfg = top.get("configured_gpos", {}) + if not isinstance(cfg, dict): + continue + + # Grab the value for the requested GPO – it can be a dict *or* a list + gpo_entry = cfg.get(gpo_name) + if gpo_entry is None: + continue + + # Case 1: direct dict → look for Linked_OUs key + if isinstance(gpo_entry, dict): + if "Linked_OUs" in gpo_entry: + ous = gpo_entry["Linked_OUs"] + if isinstance(ous, list): + linked.extend(ous) + continue + + # Case 2: list of dicts → find the dict that has the Linked_OUs key + if isinstance(gpo_entry, list): + for sub in gpo_entry: + if isinstance(sub, dict) and "Linked_OUs" in sub: + ous = sub["Linked_OUs"] + if isinstance(ous, list): + linked.extend(ous) + + return jsonify(linked) + # test route @app.route('/test', methods=['GET']) def test():