commit c0ad223ac579128c76a00af049fd7145938139cf
Author: phermeys
Date: Fri Nov 7 09:40:06 2025 -0800
init commit
diff --git a/defaults/main.yaml b/defaults/main.yaml
new file mode 100644
index 0000000..211ccbe
--- /dev/null
+++ b/defaults/main.yaml
@@ -0,0 +1,21 @@
+---
+
+service_user: "cosmos"
+
+gpo_yaml_share: "/media/gpo_yaml"
+gpo_yaml_path: "{{ gpo_yaml_share }}/gpo.yaml"
+
+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"
+
+dashboard_web_root: "{{ project_folder }}/dashboard"
+container_name: "GPO-Dashboard"
+container_service_name: "gpo_dash"
+container_http_port: "80"
+extra_volumes: ""
+
+...
\ No newline at end of file
diff --git a/files/dashboard/index.php b/files/dashboard/index.php
new file mode 100644
index 0000000..c8351a2
--- /dev/null
+++ b/files/dashboard/index.php
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+ GPO Query
+
+
+
+ Query GPOs by Group
+
+
+
+
+
+
+Error: {$data['error']}
";
+ } else {
+ echo "| AD Group | GPO | Local Group |
";
+ foreach ($data as $item) {
+ echo "| {$item['ad_group']} | {$item['gpo']} | {$item['local_group']} |
";
+ }
+ echo "
";
+ }
+
+ // Close cURL session
+ curl_close($ch);
+} else {
+ echo "No group specified.
";
+}
+?>
+
+
+
\ No newline at end of file
diff --git a/files/dashboard/styles.css b/files/dashboard/styles.css
new file mode 100644
index 0000000..e7c72ba
--- /dev/null
+++ b/files/dashboard/styles.css
@@ -0,0 +1,92 @@
+/* styles.css */
+
+body {
+ font-family: Arial, sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: #2c3e50; /* Dark background color */
+ color: #bdc3c7; /* Dimmer text color */
+}
+
+.hidden-info {
+ display: none;
+}
+
+.title-button {
+ background-color: #34495e;
+ border: none;
+ color: white;
+ padding: 15px 32px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 16px;
+ margin: 4px 2px;
+ cursor: pointer;
+}
+.container {
+ max-width: 950px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: #34495e; /* Darker background for container */
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Slightly darker shadow */
+ margin-top: 20px;
+}
+
+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 */
+}
+
+.group-columns {
+ display: flex;
+}
+
+.group-rows {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start; /* Left justification */
+ margin-top: 10px;
+}
+
+.group-column {
+ flex: 0 0 calc(33% - 10px); /* Adjust width of each column */
+}
+
+.column {
+ flex: 1;
+ padding: 0 10px; /* Adjust spacing between columns */
+}
+
+.subcolumn {
+ margin-left: 10px;
+}
+
+.grid {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin-top: 5px;
+}
+
+.meter {
+ width: calc(90% - 5px);
+ max-width: calc(45% - 5px);
+ margin-bottom: 5px;
+ border: 1px solid #7f8c8d; /* Light border color */
+ border-radius: 5px;
+ padding: 5px;
+ text-align: center;
+ background-color: #2c3e50; /* Dark background for meter */
+}
+
diff --git a/files/restricted-group-check.ps1 b/files/restricted-group-check.ps1
new file mode 100644
index 0000000..20ab9c9
--- /dev/null
+++ b/files/restricted-group-check.ps1
@@ -0,0 +1,112 @@
+ # ------------------------------------------------------------------
+# 0. Load modules (they are usually loaded by default, but be safe)
+# ------------------------------------------------------------------
+
+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"
+# Add-Content -Path $OutputFile -Value $line
+if (Test-Path $OutputFile) {
+ Remove-Item -Path $OutputFile -Force
+}
+
+# ------------------------------------------------------------------
+# 1. Grab every OU under $TargetOU (including $TargetOU itself)
+# ------------------------------------------------------------------
+
+Write-Output "Enumerating OUs under '$TargetOU' ..."
+Add-Content -Path $OutputFile -Value "---"
+Add-Content -Path $OutputFile -Value "- root_ou: $TargetOU"
+$ouObjects = Get-ADObject `
+ -Filter 'ObjectClass -eq "organizationalUnit"' `
+ -SearchBase $TargetOU `
+ -SearchScope Subtree `
+ -Properties gPLink
+
+# Also grab the target OU itself (in case it is linked directly)
+$targetOUObj = Get-ADObject `
+ -Identity $TargetOU `
+ -Properties gPLink
+if ($targetOUObj) {
+ $ouObjects += $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 = @{}
+
+foreach ($ou in $ouObjects) {
+ if ($ou.gPLink) {
+ foreach ($link in ($ou.gPLink -split ';')) {
+ $linkTrim = $link.Trim('[', ']')
+ if ($linkTrim -match '\{(?[0-9a-fA-F-]+)\}') {
+ $guid = $Matches.guid
+ if (-not $gpoLinks.ContainsKey($guid)) {
+ $gpoLinks[$guid] = @()
+ }
+ $gpoLinks[$guid] += $ou.DistinguishedName
+ }
+ }
+ }
+}
+
+if (-not $gpoLinks.Count) {
+ Write-Host "No GPOs linked under the searched OU tree." -ForegroundColor Yellow
+ exit
+}
+
+Write-Host "Found $($gpoLinks.Count) distinct GPO(s) linked in the OU tree."
+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."
+ continue
+ }
+ Write-Host "GPO: $($gpo.DisplayName)"
+ Add-Content -Path $OutputFile -Value " $($gpo.DisplayName):"
+
+ # Pull the 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') # default namespace
+ $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')
+
+ # Find every element
+ $restrictedGroups = $xml.SelectNodes('//q1:RestrictedGroups', $ns)
+
+ # Loop and build the string
+ foreach ($rg in $restrictedGroups) {
+ $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"
+ Add-Content -Path $OutputFile -Value " - ad_group: $plainAD_Groupname"
+ Add-Content -Path $OutputFile -Value " local_group: $localGroupName"
+ }
+ Add-Content -Path $OutputFile -Value ""
+}
+Add-Content -Path $OutputFile -Value "..."
diff --git a/tasks/api.yaml b/tasks/api.yaml
new file mode 100644
index 0000000..738e96f
--- /dev/null
+++ b/tasks/api.yaml
@@ -0,0 +1,72 @@
+---
+
+- name: API Service - file and folder handler
+ block:
+
+ - name: API Service - create python service folder
+ file:
+ path: "{{ api_service_folder }}"
+ state: directory
+ owner: "{{ service_user }}"
+ group: "{{ service_user }}"
+ mode: '0755'
+
+ - name: API Service - template app.py
+ template:
+ src: app.py
+ dest: "{{ api_service_folder }}/app.py"
+ owner: "{{ service_user }}"
+ group: "{{ service_user }}"
+ mode: 0755
+
+# Create python service venv
+- name: API Service - Python venv handler
+ block:
+
+ - name: API Service - create python venv requirement file
+ copy:
+ dest: "{{ api_service_folder }}/requirements.txt"
+ content: |
+ flask
+ pytz
+ requests
+ opencv-python
+ pyyaml
+ owner: "{{ service_user }}"
+ group: "{{ service_user }}"
+ mode: 0644
+
+ - name: API Service - build python venv
+ pip:
+ virtualenv: "{{ api_service_folder }}/venv"
+ requirements: "{{ api_service_folder }}/requirements.txt"
+ virtualenv_command: python3 -m venv
+ state: present
+
+- 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 - template {{ api_service_name }}.service"
+ vars:
+ service_name: "{{ api_service_name }}"
+ service_working_folder: "{{ api_service_folder }}"
+ service_exe: "{{ api_service_folder }}/venv/bin/python -u {{ api_service_folder }}/app.py"
+ template:
+ src: "service_template.service"
+ dest: "/etc/systemd/system/{{ api_service_name }}.service"
+ mode: 0644
+
+ - name: "API Service - {{ api_service_name }} - enable and start service"
+ systemd:
+ name: "{{ api_service_name }}.service"
+ state: started
+ enabled: yes
+ daemon_reload: yes
+
+...
\ No newline at end of file
diff --git a/tasks/dashboard.yaml b/tasks/dashboard.yaml
new file mode 100644
index 0000000..50fa7f3
--- /dev/null
+++ b/tasks/dashboard.yaml
@@ -0,0 +1,39 @@
+---
+###############################################
+# This part sets up drive history dashboard
+###############################################
+
+# Create docker Folder
+- name: drive history dashboard - create dashboard_web_root folder
+ file:
+ path: "{{ dashboard_web_root }}"
+ state: directory
+ mode: '0755'
+ owner: root
+ group: root
+
+- name: drive history dashboard - copy files for docker container
+ copy:
+ src: "dashboard/"
+ dest: "{{ dashboard_web_root }}/html"
+ mode: 0755
+ owner: root
+ group: root
+
+- name: docker container
+ block:
+
+ - name: service_control_website - template docker-compose.yaml
+ template:
+ src: docker-compose-php.yaml
+ dest: "{{ dashboard_web_root }}/docker-compose.yaml"
+ mode: 0644
+
+ - name: "service_control_website - Start container at {{ container_http_port }}"
+ shell: "docker-compose -f {{ dashboard_web_root }}/docker-compose.yaml up -d"
+ register: docker_output
+ - debug: |
+ msg="{{ docker_output.stdout_lines }}"
+ msg="{{ docker_output.stderr_lines }}"
+
+...
\ No newline at end of file
diff --git a/tasks/main.yaml b/tasks/main.yaml
new file mode 100644
index 0000000..36df758
--- /dev/null
+++ b/tasks/main.yaml
@@ -0,0 +1,23 @@
+---
+
+# Mount GPO Share path
+#- name: Mount GPO Share path
+# include_role:
+# name: "cifs_mount"
+# vars:
+# smb_username: "{{ ITMFG_USERNAME }}"
+# smb_password: "{{ ITMFG_PASSWORD }}"
+# server_path: "//172.23.1.23/gpo"
+# target_path: "{{ gpo_yaml_share }}"
+
+# create python gpo service
+#- name: GPO Site - python api service
+# include_tasks: api.yaml
+
+# create python gpo service
+- name: GPO Site - dashboard container
+ include_tasks: dashboard.yaml
+
+
+
+...
\ No newline at end of file
diff --git a/templates/app.py b/templates/app.py
new file mode 100644
index 0000000..20e178e
--- /dev/null
+++ b/templates/app.py
@@ -0,0 +1,65 @@
+import yaml
+from flask import Flask, request, jsonify
+
+app = Flask(__name__)
+
+# Load GPO data
+with open('{{ gpo_yaml_path }}', 'r') as file:
+ data = yaml.safe_load(file)
+
+# Debugging output to check the structure of loaded data
+# print("Loaded YAML data:", data)
+
+@app.route('/gpos', methods=['GET'])
+def get_gpos():
+ group_name = request.args.get('group_name')
+
+ if not group_name:
+ return jsonify({"error": "Group name is required"}), 400
+
+ gpos = []
+ for gpo, links in data['configured_gpos'].items():
+ for link in links:
+ if link['ad_group'] == group_name or link['local_group'] == group_name:
+ gpos.append(gpo)
+ break
+
+ return jsonify({"group_name": group_name, "GPOs": gpos})
+
+@app.route('/gpo', methods=['GET'])
+def get_groups():
+ group_name = request.args.get('group')
+ print("Group specified:", group_name)
+ if not group_name:
+ return jsonify({"error": "Group parameter is required"}), 400
+
+ gpos = []
+ for entry in data:
+ configured_gpos = entry.get('configured_gpos', {})
+ for gpo, details in configured_gpos.items():
+ if details is None:
+ continue
+ for detail in details:
+ if detail['ad_group'] == group_name:
+ gpos.append({
+ "gpo": gpo,
+ "local_group": detail['local_group'],
+ "ad_group": detail['ad_group']
+ })
+ return jsonify(gpos)
+
+# test route
+@app.route('/test', methods=['GET'])
+def test():
+ try:
+ root_ou = data[0]['root_ou']
+ except KeyError:
+ # Handle the case where 'root_ou' does not exist in the dictionary
+ return jsonify({"error": "KeyError: 'root_ou' not found in provided data"}), 400
+ 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})
+
+if __name__ == '__main__':
+ app.run(debug=True, host='{{ api_service_bind_ip }}', port={{ api_service_port }})
\ No newline at end of file
diff --git a/templates/docker-compose-php.yaml b/templates/docker-compose-php.yaml
new file mode 100644
index 0000000..076c1d8
--- /dev/null
+++ b/templates/docker-compose-php.yaml
@@ -0,0 +1,12 @@
+services:
+
+ {{ container_service_name }}:
+ container_name: {{ container_name }}
+ image: php:8.0-apache
+ ports:
+ - {{ container_http_port }}:80
+ volumes:
+ - ./html:/var/www/html/
+ {{ extra_volumes }}
+ network_mode: bridge
+ restart: always
diff --git a/templates/service_template.service b/templates/service_template.service
new file mode 100644
index 0000000..6f5ef22
--- /dev/null
+++ b/templates/service_template.service
@@ -0,0 +1,14 @@
+
+[Unit]
+Description={{ service_name }}
+After=network.target
+
+[Service]
+User=root
+Group=root
+WorkingDirectory={{ service_working_folder }}
+ExecStart={{ service_exe }}
+Restart=always
+
+[Install]
+WantedBy=multi-user.target