From 364da0159b51628548c02c54ab412a1ab5813356 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 17 Feb 2026 10:28:52 -0800 Subject: [PATCH] add gpo counter variable to powershell script for visbility --- defaults/main.yaml | 2 +- files/dashboard/index.php | 83 +++++++++++++++++++++----------- files/dashboard/styles.css | 5 -- files/restricted-group-check.ps1 | 23 ++++++--- templates/app.py | 64 +++++++++++++++++++++--- 5 files changed, 130 insertions(+), 47 deletions(-) diff --git a/defaults/main.yaml b/defaults/main.yaml index 211ccbe..4d302c0 100644 --- a/defaults/main.yaml +++ b/defaults/main.yaml @@ -10,7 +10,7 @@ project_folder: "/opt/cosmos/gpo_site" api_service_name: "gpo_api" api_service_folder: "{{ project_folder }}/api" api_service_port: "5000" -api_service_bind_ip: "0.0.0.0" +api_service_bind_ip: "172.17.0.1" dashboard_web_root: "{{ project_folder }}/dashboard" container_name: "GPO-Dashboard" diff --git a/files/dashboard/index.php b/files/dashboard/index.php index 25b13e6..c6e007f 100644 --- a/files/dashboard/index.php +++ b/files/dashboard/index.php @@ -29,8 +29,11 @@ function getLinkedOUs(string $gpo): array|false //return implode("\n", $decoded); } -?> +$yaml_timestamp = json_decode(file_get_contents('http://172.17.0.1:5000/timestamp'), true); +$gpo_links = json_decode(file_get_contents('http://172.17.0.1:5000/gpo_links'), true); +$index_duration = json_decode(file_get_contents('http://172.17.0.1:5000/index_duration'), true); +?> @@ -42,9 +45,13 @@ function getLinkedOUs(string $gpo): array|false

Query GPOs by Group

- This tool allows you to query Group Policy Objects in Active Directory by Restricted Group.
+ This tool is intended to help determine what OU to use for a specific group.
+ Users often request systems with a certain security group assigned.
+ Since AD has no way to look this up, this makes it challenging to match groups to OUs.

+ This site has all restricted groups indexed by GPO and OU.
Search is case insensitive and supports partial match.
- Once you have the GPO name, you can locate this in the GPO management snap-in and find what OUs it links to.

+ The OUs liked to the GPOs will show up as a hover tooltip on the GPO name.
+ Clicking on the GPO name will list all linked OUs.

@@ -56,8 +63,9 @@ if (isset($_GET['group'])) {
- +

+ Return Home
'; $group = urlencode($_GET['group']); $url = "http://172.17.0.1:5000/gpo?group=$group"; @@ -107,31 +115,43 @@ if (isset($_GET['group'])) { } ksort($gpoMap, SORT_STRING | SORT_FLAG_CASE); if($numResults > 0){ - echo 'Total results: '.$numResults.'
+ echo ' +Total results: '.$numResults.'
+Data indexed at '.$yaml_timestamp['timestamp'].' after '.$index_duration['duration'].' minutes
+Total Linked GPOs: '.$gpo_links['gpo_links'].'

+ + + + + + '; - echo '
+ GPO + + Administrators + + vnc_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'])); $linkedOUs = implode("\n", getLinkedOUs($gpoName)); - echo ' -'; - echo " + echo " + + + + + "; - echo " -"; - echo " -"; - echo ' -'; } echo '
GPOAdministratorsvnc_admin
-
+ + + {$adminGroups} + + {$vncGroups} +
{$adminGroups}{$vncGroups}
'; @@ -140,17 +160,20 @@ if (isset($_GET['group'])) { echo 'No results found for query '.$_GET['group'].'.

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

'; + echo 'OUs linked to GPO '.$_GET['gpo'].':

Return Home'; } + else { echo '

@@ -158,10 +181,14 @@ else {

+ Data indexed at '.$yaml_timestamp['timestamp'].' after '.$index_duration['duration'].' minutes
+ Total Linked GPOs: '.$gpo_links['gpo_links'].'

+ No group specified.

'; - echo "

No group specified.

"; + } -echo "
"; +echo " +"; //echo "

Source code can be found here.

"; ?> diff --git a/files/dashboard/styles.css b/files/dashboard/styles.css index 028901e..5b89e3a 100644 --- a/files/dashboard/styles.css +++ b/files/dashboard/styles.css @@ -67,11 +67,6 @@ h1, h2, h3, h4 { color: #bdc3c7; /* Dimmer text color */ } -ul { - list-style-type: none; - padding: 0; -} - li { margin-bottom: 10px; color: #bdc3c7; /* Dimmer text color */ diff --git a/files/restricted-group-check.ps1 b/files/restricted-group-check.ps1 index 44e03d7..eaebede 100644 --- a/files/restricted-group-check.ps1 +++ b/files/restricted-group-check.ps1 @@ -18,6 +18,8 @@ if (Test-Path $OutputFile) { Write-Output "Enumerating OUs under '$TargetOU' ..." Add-Content -Path $OutputFile -Value "---" Add-Content -Path $OutputFile -Value "- root_ou: $TargetOU" +$Start_Date = (Get-Date).ToString('MM/dd/yyyy hh:mm:ss tt') +Add-Content -Path $OutputFile -Value " init_timestamp: $Start_Date" $ouObjects = Get-ADObject ` -Filter 'ObjectClass -eq "organizationalUnit"' ` -SearchBase $TargetOU ` @@ -69,7 +71,10 @@ Add-Content -Path $OutputFile -Value " gpo_links: $($gpoLinks.Count)" Add-Content -Path $OutputFile -Value " configured_gpos:" +$ou_counter = 0 foreach ($gpoGuid in $gpoLinks.Keys) { + $ou_counter++ + try { $gpo = Get-GPO -Guid $gpoGuid -ErrorAction Stop } @@ -77,18 +82,19 @@ foreach ($gpoGuid in $gpoLinks.Keys) { Write-Warning "Unable to retrieve GPO $gpoGuid – skipping." continue } - Write-Host "GPO: $($gpo.DisplayName)" + Write-Host "GPO $ou_counter of $($gpoLinks.Count): $($gpo.DisplayName)" Add-Content -Path $OutputFile -Value " $($gpo.DisplayName):" + # ok, i need to start generating a variable with all links in it + # -------- Linked OUs ---------- - $linkedOUs = $gpoLinks[$gpoGuid] - Write-Host "Linked OUs:" - Add-Content -Path $OutputFile -Value " Linked_OUs:" + $linkedOUs = $gpoLinks[$gpoGuid] # <-- the list we built earlier + 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 @@ -97,8 +103,9 @@ 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') @@ -113,10 +120,12 @@ foreach ($gpoGuid in $gpoLinks.Keys) { $adGroupName = ($rg.SelectSingleNode('q1:GroupName/t:Name', $ns)).InnerText $localGroupName = ($rg.SelectSingleNode('q1:Memberof/t:Name', $ns)).InnerText $plainAD_Groupname = ($adGroupName -split '\\')[-1] - Write-Output "AD Group: $adGroupName - Local Group: $localGroupName" + #Write-Output "AD Group: $adGroupName - Local Group: $localGroupName" Add-Content -Path $OutputFile -Value " - ad_group: $plainAD_Groupname" Add-Content -Path $OutputFile -Value " local_group: $localGroupName" } Add-Content -Path $OutputFile -Value "" } +$Current_Date = (Get-Date).ToString('MM/dd/yyyy hh:mm:ss tt') +Add-Content -Path $OutputFile -Value " timestamp: $Current_Date" Add-Content -Path $OutputFile -Value "..." diff --git a/templates/app.py b/templates/app.py index 9f214ed..46b3ec4 100644 --- a/templates/app.py +++ b/templates/app.py @@ -1,5 +1,6 @@ import yaml import re +from datetime import datetime from flask import Flask, request, jsonify app = Flask(__name__) @@ -8,6 +9,23 @@ app = Flask(__name__) with open('{{ gpo_yaml_path }}', 'r') as file: data = yaml.safe_load(file) +def strip_dn(dn: str, base_suffix: str = ",OU=Manufacturing,OU=Tesla Systems,DC=teslamotors,DC=com") -> str: + suffix = base_suffix.strip() + dn_clean = dn.strip() + lowered_dn = dn_clean.lower() + lowered_suffix = suffix.lower() + idx = lowered_dn.rfind(lowered_suffix) + if idx != -1: + dn_clean = dn_clean[:idx].rstrip(',') + + # Pull all OU=… values (leaf → root order) + ou_values: List[str] = re.findall(r'OU=([^,]+)', dn_clean, flags=re.IGNORECASE) + + if not ou_values: + return '' + ou_values = [v.strip() for v in reversed(ou_values)] + return '\\'.join(ou_values) + @app.route('/gpo', methods=['GET']) def get_groups(): group_name = request.args.get('group') @@ -37,6 +55,33 @@ def get_groups(): }) return jsonify(gpos) +@app.route("/gpo_links", methods=["GET"]) +def gpo_links(): + try: + gpo_link_count = data[0]['gpo_links'] + except: + gpo_link_count = "whoops" + return jsonify({"gpo_links": gpo_link_count}) + +@app.route("/timestamp", methods=["GET"]) +def timestamp(): + try: + yaml_timestamp = data[0]['timestamp'] + except: + yaml_timestamp = "whoops" + return jsonify({"timestamp": yaml_timestamp}) + +@app.route("/index_duration", methods=["GET"]) +def index_duration(): + fmt="%m/%d/%Y %I:%M:%S %p" + try: + start_timestamp = data[0]['init_timestamp'] + end_timestamp = data[0]['timestamp'] + duration = (datetime.strptime(end_timestamp, fmt) - datetime.strptime(start_timestamp, fmt)).total_seconds() / 60 + return jsonify({"duration": duration, "start_timestamp": start_timestamp, "end_timestamp": end_timestamp}) + except: + return jsonify({'duration': "whoops"}) + @app.route("/linked_ous", methods=["GET"]) def linked_ous(): gpo_name = request.args.get("gpo") @@ -55,21 +100,28 @@ def linked_ous(): if gpo_entry is None: continue + def _add_ous(ous): + if not isinstance(ous, list): + return + for dn in ous: + if isinstance(dn, str): + # 1. strip out OU/ DC components + # 2. keep the *leaf* and its *parent* (e.g. NA\SJC18) + stripped = strip_dn(dn) + if stripped: + linked.append(stripped) + # 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) + _add_ous(gpo_entry["Linked_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) + _add_ous(sub["Linked_OUs"]) return jsonify(linked)