diff --git a/files/check-hostname-restricted-groups.ps1 b/files/check-hostname-restricted-groups.ps1 new file mode 100644 index 0000000..b0cb83b --- /dev/null +++ b/files/check-hostname-restricted-groups.ps1 @@ -0,0 +1,137 @@ +# ------------------------------------------------------------------ +# 1. Set up environment +# ------------------------------------------------------------------ +$ComputerName = "FRRF1LLC1103011" +$printDebug = $false + +Import-Module GroupPolicy -ErrorAction Stop +Import-Module ActiveDirectory -ErrorAction Stop + +# ------------------------------------------------------------------ +# 2. Resolve the computer’s DN and build the OU chain (leaf → root) +# ------------------------------------------------------------------ +try { + $comp = Get-ADComputer -Identity $ComputerName -Properties DistinguishedName +} +catch { + Write-Error "Cannot find computer '$ComputerName' in AD: $_" + exit 1 +} + +$ouDN = $comp.DistinguishedName -replace '^CN=.*?,', '' +$ouChain = @() +$currentDN = $ouDN + +while ($currentDN -match '^OU=') { + $ouChain += $currentDN + $currentDN = $currentDN.Substring($currentDN.IndexOf(',') + 1) +} +$ouChain += $currentDN # root + +if ($printDebug) { + Write-Host "OU chain (leaf → root):" -ForegroundColor Cyan + $ouChain | ForEach-Object { Write-Host $_ } +} + +# ------------------------------------------------------------------ +# 3. Gather GPO links for each OU in the chain +# ------------------------------------------------------------------ +$gpoLinks = @{} # Hashtable: GPO_GUID => array of OU DNs + +foreach ($ouDN in $ouChain) { + try { + $ouObj = Get-ADObject -Identity $ouDN -Properties gPLink -ErrorAction Stop + } + catch { + Write-Warning "Could not read OU '$ouDN': $_" + continue + } + + if ($ouObj.gPLink) { + foreach ($link in ($ouObj.gPLink -split ';')) { + $clean = $link.Trim(' ', '[', ']') + if ($clean -match '\{(?[0-9a-fA-F-]+)\}') { + $guid = $Matches.guid + if (-not $gpoLinks.ContainsKey($guid)) { $gpoLinks[$guid] = @() } + $gpoLinks[$guid] += $ouDN + } + } + } +} + +if (-not $gpoLinks.Count) { + Write-Host "No GPOs linked to any OU in the hierarchy." -ForegroundColor Yellow + exit +} + +Write-Host "Found $($gpoLinks.Count) distinct GPO(s) linked in this OU tree." -ForegroundColor Green + +# ------------------------------------------------------------------ +# 4. Collect all restricted‑group entries into an array +# ------------------------------------------------------------------ +$results = @() # Will hold objects: @{ADGroup=; LocalGroup=; GPO=} + +$uniqueGpoGuids = $gpoLinks.Keys | Sort-Object -Unique +$ou_counter = 0 +$totalItems = $uniqueGpoGuids.Count +$startTime = Get-Date + +foreach ($gpoGuid in $uniqueGpoGuids) { + $ou_counter++ + + try { $gpo = Get-GPO -Guid $gpoGuid -ErrorAction Stop } + catch { Write-Warning "Unable to retrieve GPO $gpoGuid – skipping."; continue } + + $percentComplete = [int]( ($ou_counter / $gpoLinks.Count) * 100 ) + if ($ou_counter -gt 0) { + $elapsed = (Get-Date) - $startTime + $averagePerItem = $elapsed.TotalSeconds / $ou_counter + $remainingItems = $totalItems - $ou_counter + $secondsRemaining = [int]($averagePerItem * $remainingItems) + } else { $secondsRemaining = -1 } + + Write-Progress -Activity "Processing GPOs" ` + -Status $gpo.DisplayName ` + -PercentComplete $percentComplete ` + -SecondsRemaining $secondsRemaining ` + -CurrentOperation "GPO $ou_counter of $totalItems" + + # Grab XML report + try { $xmlString = Get-GPOReport -Guid $gpoGuid -ReportType Xml -ErrorAction Stop } + catch { Write-Warning "Unable to generate XML report for $($gpo.DisplayName)."; continue } + + $xml = [xml]$xmlString + $ns = New-Object System.Xml.XmlNamespaceManager ($xml.NameTable) + $ns.AddNamespace('m', 'http://www.microsoft.com/GroupPolicy/Settings') + $ns.AddNamespace('q1', 'http://www.microsoft.com/GroupPolicy/Settings/Security') + $ns.AddNamespace('t', 'http://www.microsoft.com/GroupPolicy/Types') + $ns.AddNamespace('xsi','http://www.w3.org/2001/XMLSchema-instance') + + $restrictedGroups = $xml.SelectNodes('//q1:RestrictedGroups', $ns) + + foreach ($rg in $restrictedGroups) { + $adGroupName = ($rg.SelectSingleNode('q1:GroupName/t:Name', $ns)).InnerText + $localGroupName = ($rg.SelectSingleNode('q1:Memberof/t:Name', $ns)).InnerText + + $results += [pscustomobject]@{ + ADGroup = $adGroupName + LocalGroup = $localGroupName + GPO = $gpo.DisplayName + } + } +} + +$elapsed = (Get-Date) - $startTime +$elapsedSeconds = [math]::Round($elapsed.TotalSeconds, 2) +Write-Host "`nFinished processing $totalItems GPO(s) after $elapsedSeconds seconds." -ForegroundColor Green + +# ------------------------------------------------------------------ +# 5. Print two separate lists +# ------------------------------------------------------------------ +Write-Host "`n=== AD groups that are members of BUILTIN\Administrators ===" -ForegroundColor Yellow +$results | Where-Object {$_.LocalGroup -eq 'BUILTIN\Administrators'} | + Select-Object -ExpandProperty ADGroup | Sort-Object | Get-Unique + +Write-Host "`n=== AD groups that are members of vnc_admin ===" -ForegroundColor Yellow +$results | Where-Object {$_.LocalGroup -eq 'vnc_admin'} | + Select-Object -ExpandProperty ADGroup | Sort-Object | Get-Unique diff --git a/files/dashboard/index.php b/files/dashboard/index.php index ad6f56a..602fec1 100644 --- a/files/dashboard/index.php +++ b/files/dashboard/index.php @@ -29,6 +29,28 @@ function getLinkedOUs(string $gpo): array|false //return implode("\n", $decoded); } +# helper to reload OUs +if (isset($_GET['reload'])) { + // Call the Flask endpoint that reloads the YAML index + $reloadUrl = "http://172.17.0.1:5000/reload_index"; + + $ch = curl_init($reloadUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 10‑second timeout + $rawResponse = curl_exec($ch); + + if ($rawResponse === false) { + error_log('cURL error [' . curl_errno($ch) . ']: ' . curl_error($ch)); + } else { + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($httpCode !== 200) { + error_log("API returned HTTP {$httpCode} for reload_index"); + } + // Optionally inspect $rawResponse if you want to show a message + } + curl_close($ch); +} + $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); @@ -180,6 +202,10 @@ else { + +
+ +

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

diff --git a/tasks/api.yaml b/tasks/api.yaml index 738e96f..c4f8879 100644 --- a/tasks/api.yaml +++ b/tasks/api.yaml @@ -11,6 +11,13 @@ group: "{{ service_user }}" mode: '0755' + + - name: "API Service - {{ api_service_name }} - stop service" + systemd: + name: "{{ api_service_name }}.service" + state: stopped + ignore_errors: yes + - name: API Service - template app.py template: src: app.py @@ -46,11 +53,11 @@ - name: API Service - api service handler block: - - name: "API Service - {{ api_service_name }} - stop service if running" - ignore_errors: yes - systemd: - name: "{{ api_service_name }}.service" - state: stopped +# - name: "API Service - {{ api_service_name }} - stop service if running" +# ignore_errors: yes +# systemd: +# name: "{{ api_service_name }}.service" +# state: stopped - name: "API Service - template {{ api_service_name }}.service" vars: diff --git a/templates/app.py b/templates/app.py index d7a4ef6..d3ba27d 100644 --- a/templates/app.py +++ b/templates/app.py @@ -1,13 +1,27 @@ import yaml import re from datetime import datetime -from flask import Flask, request, jsonify +from flask import Flask, request, jsonify, abort +import threading app = Flask(__name__) +### adding reload route +# Global cache and lock +data = None +_data_lock = threading.Lock() + +def load_yaml(): + """Load the YAML file and return its contents.""" + with open('{{ gpo_yaml_path }}', 'r', encoding='utf-8') as f: + return yaml.safe_load(f) # Load GPO data -with open('{{ gpo_yaml_path }}', 'r') as file: - data = yaml.safe_load(file) +with _data_lock: + data = load_yaml() + +# O.G. method +#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() @@ -126,6 +140,30 @@ def linked_ous(): return jsonify(linked) +# reload index route +@app.route('/reload_index', methods=['GET']) +def reload_index(): + try: + new_data = load_yaml() + except Exception as exc: + # Log the exception if you have a logger; here we just abort + abort(500, description=f'Failed to load YAML: {exc}') + + # Thread‑safe write to the global cache + with _data_lock: + global data + data = new_data + + # Return the re‑loaded data (or some confirmation) + return jsonify({ + 'status': 'reloaded', + 'gpo_links': data[0]['gpo_links'] + }) + +# with open('{{ gpo_yaml_path }}', 'r') as file: +# data = yaml.safe_load(file) +# return jsonify({'status': 'reloaded', 'data': data}) + # test route @app.route('/test', methods=['GET']) def test(): @@ -137,7 +175,7 @@ def test(): except TypeError: # Handle cases where data might be of a different type than expected return jsonify({"error": "TypeError: Data is not a dictionary"}), 400 - return jsonify({"result": "test", "OU_Root": root_ou}) + return jsonify({"result": "test", "OU_Root": root_ou, "gpo_links": data[0]['gpo_links']}) if __name__ == '__main__': app.run(debug=False, host='{{ api_service_bind_ip }}', port={{ api_service_port }}) \ No newline at end of file