diff --git a/msp/active_directory_tools.py b/msp/active_directory_tools.py
new file mode 100644
index 0000000..9f5f614
--- /dev/null
+++ b/msp/active_directory_tools.py
@@ -0,0 +1,198 @@
+import datetime
+import uuid
+import pandas as pd
+from ldap3 import Server, Connection, ALL, NTLM
+from dotenv import load_dotenv
+import os
+
+# Load environment variables
+load_dotenv()
+
+# Configuration from environment variables
+server_name = os.getenv('LDAP_SERVER')
+domain_name = os.getenv('LDAP_DOMAIN')
+username = os.getenv('LDAP_USERNAME') # Format: DOMAIN\username
+password = os.getenv('LDAP_PASSWORD')
+
+# Connect to the server
+server = Server(server_name, get_info=ALL)
+conn = Connection(server, user=username, password=password, authentication=NTLM)
+
+# Bind to the server
+if not conn.bind():
+ print('Error in binding to the server')
+ exit()
+
+# Get server info for automatic domain detection
+server_info = server.info
+
+# Determine search base - use domain root for recursive search
+search_base = None
+
+# Try environment variable first
+env_search_base = os.getenv('LDAP_SEARCH_BASE')
+if env_search_base:
+ search_base = env_search_base
+# Try to construct from domain name
+elif domain_name:
+ domain_parts = domain_name.split('.')
+ search_base = ','.join([f'DC={part}' for part in domain_parts])
+# Try to get from server naming contexts
+elif hasattr(server_info, 'naming_contexts') and server_info.naming_contexts:
+ for nc in server_info.naming_contexts:
+ nc_str = str(nc)
+ if nc_str.startswith('DC=') and 'CN=Configuration' not in nc_str and 'CN=Schema' not in nc_str:
+ search_base = nc_str
+ break
+
+# Fallback
+if not search_base:
+ search_base = 'DC=corp,DC=local'
+
+print(f"Durchsuche rekursiv: {search_base}")
+
+# Simple user filter (exclude computer accounts)
+search_filter = '(&(objectClass=user)(!(sAMAccountName=*$)))'
+
+attributes = [
+ 'sAMAccountName', 'lastLogon', 'lastLogonTimestamp', 'objectGUID', 'userAccountControl',
+ 'givenName', 'sn', 'cn', 'displayName', 'distinguishedName',
+ 'userPrincipalName', 'proxyAddresses', 'mail', 'lockoutTime'
+]
+
+# Perform recursive search
+print("Starte rekursive Suche...")
+try:
+ from ldap3 import SUBTREE
+ success = conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes)
+
+ if success and conn.entries:
+ print(f"Benutzer-Accounts gefunden: {len(conn.entries)}")
+
+ # Remove duplicates based on distinguishedName
+ seen_dns = set()
+ unique_entries = []
+ for entry in conn.entries:
+ dn = str(entry.distinguishedName)
+ if dn not in seen_dns:
+ seen_dns.add(dn)
+ unique_entries.append(entry)
+
+ print(f"Nach Duplikat-Entfernung: {len(unique_entries)} eindeutige Benutzer-Accounts")
+ else:
+ unique_entries = []
+ print("Keine Benutzer-Accounts gefunden")
+
+except Exception as e:
+ print(f"Fehler bei der Suche: {str(e)}")
+ unique_entries = []
+
+# Function to convert Windows File Time to human-readable format
+def convert_filetime(filetime):
+ if filetime and isinstance(filetime, int):
+ return (datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=filetime / 10)).replace(tzinfo=None)
+ elif filetime and isinstance(filetime, datetime.datetime):
+ return filetime.replace(tzinfo=None)
+ else:
+ return None
+
+# Function to check if the user is disabled
+def is_user_disabled(user_account_control):
+ return bool(user_account_control & 0x0002)
+
+# Function to check if the user is locked
+def is_user_locked(lockout_time):
+ return lockout_time and lockout_time != 0
+
+# Collecting results in a list
+results = []
+
+for entry in unique_entries:
+ username = entry.sAMAccountName.value
+ last_logon = entry.lastLogon.value
+ last_logon_timestamp = entry.lastLogonTimestamp.value
+ object_guid = entry.objectGUID.value
+ user_account_control = entry.userAccountControl.value
+ lockout_time = entry.lockoutTime.value
+
+ # Convert timestamps
+ last_logon_date = convert_filetime(last_logon)
+ last_logon_timestamp_date = convert_filetime(last_logon_timestamp)
+
+ # Determine the most recent logon date
+ if last_logon_date and last_logon_timestamp_date:
+ most_recent_logon = max(last_logon_date, last_logon_timestamp_date)
+ else:
+ most_recent_logon = last_logon_date or last_logon_timestamp_date or 'Never logged in'
+
+ guid_string = object_guid
+ disabled_status = "Disabled" if is_user_disabled(user_account_control) else "Enabled"
+ locked_status = "Locked" if is_user_locked(lockout_time) else "Unlocked"
+
+ given_name = entry.givenName.value if entry.givenName else 'N/A'
+ sn = entry.sn.value if entry.sn else 'N/A'
+ cn = entry.cn.value if entry.cn else 'N/A'
+ display_name = entry.displayName.value if entry.displayName else 'N/A'
+ distinguished_name = entry.distinguishedName.value if entry.distinguishedName else 'N/A'
+ user_principal_name = entry.userPrincipalName.value if entry.userPrincipalName else 'N/A'
+ proxy_addresses = ', '.join(entry.proxyAddresses.values) if entry.proxyAddresses else 'N/A'
+ mail = entry.mail.value if entry.mail else 'N/A'
+
+ # Skip computer accounts based on the presence of a dollar sign in the username
+ if not username.endswith('$'):
+ results.append({
+ 'User': username,
+ 'Given Name': given_name,
+ 'Surname': sn,
+ 'Common Name': cn,
+ 'Display Name': display_name,
+ 'Distinguished Name': distinguished_name,
+ 'User Principal Name': user_principal_name,
+ 'Proxy Addresses': proxy_addresses,
+ 'Mail': mail,
+ 'Most Recent Logon': most_recent_logon,
+ 'GUID': guid_string,
+ 'Status': disabled_status,
+ 'Lockout Status': locked_status
+ })
+
+# Unbind the connection
+conn.unbind()
+
+# Create a DataFrame and export to Excel
+df = pd.DataFrame(results)
+
+# Check if we have any results
+if len(results) > 0:
+ # Ensure all datetime columns are timezone-unaware
+ datetime_columns = ['Most Recent Logon']
+ for column in datetime_columns:
+ if column in df.columns:
+ df[column] = df[column].apply(lambda x: x.replace(tzinfo=None) if isinstance(x, datetime.datetime) else x)
+
+ df.to_excel('active_directory_users.xlsx', index=False)
+ print(f"Benutzer-Accounts erfolgreich exportiert. Gefundene Benutzer: {len(results)}")
+else:
+ print("\n=== DEBUGGING-INFORMATIONEN ===")
+ print("Keine Benutzer-Accounts gefunden.")
+ print(f"Search-Base: {search_base}")
+ print(f"Search-Filter: {search_filter}")
+ print(f"LDAP-Server: {server_name}")
+ print(f"Domain: {domain_name}")
+
+ if server_info and hasattr(server_info, 'naming_contexts'):
+ print(f"Server Naming Contexts: {list(server_info.naming_contexts)}")
+ if server_info and hasattr(server_info, 'schema_entry'):
+ print(f"Schema Entry: {server_info.schema_entry}")
+
+ print("\nBitte überprüfen Sie:")
+ print("1. LDAP-Verbindung und Anmeldedaten")
+ print("2. Search-Base Konfiguration")
+ print("3. Berechtigungen für die Benutzer-Suche")
+ print("4. Domain-Controller Erreichbarkeit")
+
+ # Create empty Excel file with headers for reference
+ headers = ['User', 'Given Name', 'Surname', 'Common Name', 'Display Name', 'Distinguished Name',
+ 'User Principal Name', 'Proxy Addresses', 'Mail', 'Most Recent Logon', 'GUID', 'Status', 'Lockout Status']
+ empty_df = pd.DataFrame(columns=headers)
+ empty_df.to_excel('active_directory_users.xlsx', index=False)
\ No newline at end of file
diff --git a/msp/msp/doctype/it_object/it_object.json b/msp/msp/doctype/it_object/it_object.json
index 588cc14..e6a20f1 100644
--- a/msp/msp/doctype/it_object/it_object.json
+++ b/msp/msp/doctype/it_object/it_object.json
@@ -29,8 +29,10 @@
"network_config_section",
"ip_adresses",
"rmm_data_section",
+ "hardware_attributes",
"rmm_specs",
"rmm_software",
+ "created_from_rmm",
"external_links_section",
"admin_interface_link",
"monitoring_link",
@@ -238,11 +240,23 @@
"fieldname": "visible_in_documentation",
"fieldtype": "Check",
"label": "visible in Documentation"
+ },
+ {
+ "default": "0",
+ "fieldname": "created_from_rmm",
+ "fieldtype": "Check",
+ "label": "Created From RMM"
+ },
+ {
+ "fieldname": "hardware_attributes",
+ "fieldtype": "Table",
+ "label": "Hardware Attributes",
+ "options": "IT Object Hardware Attribute"
}
],
"image_field": "image",
"links": [],
- "modified": "2025-03-10 14:55:30.929828",
+ "modified": "2025-07-31 16:07:20.612702",
"modified_by": "Administrator",
"module": "MSP",
"name": "IT Object",
diff --git a/msp/msp/doctype/msp_documentation/msp_documentation.js b/msp/msp/doctype/msp_documentation/msp_documentation.js
index 1d29415..c40c50f 100644
--- a/msp/msp/doctype/msp_documentation/msp_documentation.js
+++ b/msp/msp/doctype/msp_documentation/msp_documentation.js
@@ -82,6 +82,345 @@ frappe.ui.form.on('MSP Documentation', {
}
});
}, 'Workflow');
+
+ // Add button to fetch and store all RMM agent data as JSON
+ frm.add_custom_button('4. RMM Daten speichern', function(){
+ frappe.dom.freeze('RMM-Daten werden abgerufen und gespeichert...');
+ frappe.call({
+ method: 'msp.tactical-rmm.fetch_and_store_all_agent_data',
+ args: {
+ documentation_name: frm.doc.name
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+ if (r.exc) {
+ frappe.msgprint({
+ title: __('Fehler'),
+ indicator: 'red',
+ message: __('RMM-Daten konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.')
+ });
+ return;
+ }
+ if (r.message && r.message.success) {
+ frappe.show_alert({
+ message: r.message.message || __('RMM-Daten erfolgreich gespeichert'),
+ indicator: 'green'
+ });
+ frm.reload_doc();
+ }
+ }
+ });
+ }, 'Workflow');
+
+ // Add button to fetch and store Active Directory computer data as JSON
+ frm.add_custom_button('5. AD Computer-Daten speichern', function(){
+ frappe.dom.freeze('AD-Computer-Daten werden abgerufen und gespeichert...');
+ frappe.call({
+ method: 'msp.tactical-rmm.fetch_and_store_ad_computer_data',
+ args: {
+ documentation_name: frm.doc.name
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+ if (r.exc) {
+ frappe.msgprint({
+ title: __('Fehler'),
+ indicator: 'red',
+ message: __('AD-Computer-Daten konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.')
+ });
+ return;
+ }
+ if (r.message && r.message.success) {
+ frappe.show_alert({
+ message: r.message.message || __('AD-Computer-Daten erfolgreich gespeichert'),
+ indicator: 'green'
+ });
+ frm.reload_doc();
+ }
+ }
+ });
+ }, 'Workflow');
+
+ // Add button to fetch and store Active Directory user data as JSON
+ frm.add_custom_button('6. AD Benutzer-Daten speichern', function(){
+ frappe.dom.freeze('AD-Benutzer-Daten werden abgerufen und gespeichert...');
+ frappe.call({
+ method: 'msp.tactical-rmm.fetch_and_store_ad_user_data',
+ args: {
+ documentation_name: frm.doc.name
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+ if (r.exc) {
+ frappe.msgprint({
+ title: __('Fehler'),
+ indicator: 'red',
+ message: __('AD-Benutzer-Daten konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.')
+ });
+ return;
+ }
+ if (r.message && r.message.success) {
+ frappe.show_alert({
+ message: r.message.message || __('AD-Benutzer-Daten erfolgreich gespeichert'),
+ indicator: 'green'
+ });
+ frm.reload_doc();
+ }
+ }
+ });
+ }, 'Workflow');
+
+ // Add button to compare RMM and AD data
+ frm.add_custom_button('7. RMM ↔ AD Abgleich', function(){
+ frappe.dom.freeze('RMM- und AD-Daten werden abgeglichen...');
+ frappe.call({
+ method: 'msp.tactical-rmm.compare_rmm_and_ad_data',
+ args: {
+ documentation_name: frm.doc.name
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+ if (r.exc) {
+ frappe.msgprint({
+ title: __('Fehler'),
+ indicator: 'red',
+ message: __('Datenabgleich konnte nicht durchgeführt werden. Bitte versuchen Sie es erneut.')
+ });
+ return;
+ }
+ if (r.message && r.message.success) {
+ let stats = r.message.stats;
+ let details = `${stats.total_computers} Computer analysiert: ` +
+ `${stats.in_both} in beiden Systemen, ` +
+ `${stats.only_in_rmm} nur RMM, ` +
+ `${stats.only_in_ad} nur AD`;
+
+ frappe.show_alert({
+ message: __('Datenabgleich erfolgreich abgeschlossen. ') + details,
+ indicator: 'green'
+ });
+ frm.reload_doc();
+ }
+ }
+ });
+ }, 'Workflow');
+
+ // Add button for Windows 11 compatibility check
+ frm.add_custom_button('8. Windows 11 Check', function(){
+ frappe.dom.freeze('Windows 11 CPU-Kompatibilität wird geprüft...');
+ frappe.call({
+ method: 'msp.tactical-rmm.check_windows11_compatibility',
+ args: {
+ documentation_name: frm.doc.name
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+ if (r.exc) {
+ frappe.msgprint({
+ title: __('Fehler'),
+ indicator: 'red',
+ message: __('Windows 11 Kompatibilitätsprüfung konnte nicht durchgeführt werden. Bitte versuchen Sie es erneut.')
+ });
+ return;
+ }
+ if (r.message && r.message.success) {
+ let stats = r.message.stats;
+ let details = `${stats.total_non_win11} Systeme ohne Windows 11 analysiert: ` +
+ `${stats.compatible_cpus} kompatible CPUs, ` +
+ `${stats.incompatible_cpus} inkompatible CPUs, ` +
+ `${stats.unknown_cpus} unbekannte CPUs`;
+
+ frappe.show_alert({
+ message: __('Windows 11 Kompatibilitätsprüfung abgeschlossen. ') + details,
+ indicator: 'green'
+ });
+ frm.reload_doc();
+ }
+ }
+ });
+ }, 'Workflow');
+
+ // Add debug button for CPU compatibility
+ frm.add_custom_button('🔍 CPU Debug', function(){
+ frappe.prompt([
+ {
+ 'fieldname': 'test_cpu',
+ 'label': 'Test CPU (leer = automatisch)',
+ 'fieldtype': 'Data',
+ 'reqd': 0,
+ 'description': 'Z.B: Intel(R) Core(TM) i3-8100 CPU @ 3.60GHz, 4C/4T'
+ }
+ ], function(values) {
+ frappe.dom.freeze('CPU-Kompatibilität wird debuggt...');
+ frappe.call({
+ method: 'msp.tactical-rmm.debug_cpu_compatibility',
+ args: {
+ documentation_name: frm.doc.name,
+ test_cpu_string: values.test_cpu || null
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+ if (r.exc) {
+ frappe.msgprint({
+ title: __('Fehler'),
+ indicator: 'red',
+ message: __('CPU-Debug konnte nicht durchgeführt werden: ') + r.exc
+ });
+ return;
+ }
+ if (r.message && r.message.success) {
+ let debug_info = r.message.debug_info;
+ let result = r.message.result;
+ let test_cpu = r.message.test_cpu;
+
+ // Debug-Dialog erstellen
+ let debug_html = `
+
+
🔍 CPU-Kompatibilitäts Debug
+
Test-CPU: ${test_cpu}
+
System-CPU (uppercase): ${debug_info.system_cpu_upper || 'N/A'}
+
Vendor: ${debug_info.vendor || 'N/A'}
+
Suchset-Größe: ${debug_info.search_set_size || 0}
+
Ergebnis:
+ ${result.compatible ? '✅ KOMPATIBEL' : '❌ NICHT KOMPATIBEL'} (${result.status})
+
+ ${debug_info.match_found ? `
Gefundene CPU: ${debug_info.matching_cpu}
` : ''}
+
+
📁 Dateipfad-Informationen:
+
+
App-Pfad: ${debug_info.app_path || 'N/A'}
+ ${debug_info.files_info ? Object.keys(debug_info.files_info).map(vendor => {
+ const info = debug_info.files_info[vendor];
+ const statusColor = info.exists ? 'green' : 'red';
+ const statusIcon = info.exists ? '✅' : '❌';
+ return `
+
+
${vendor.toUpperCase()} CPUs: ${statusIcon}
+
+ Pfad: ${info.path}
+ Existiert: ${info.exists ? 'Ja' : 'Nein'}
+ ${info.exists ? `Dateigröße: ${info.size} Bytes` : ''}
+
+
+ `;
+ }).join('') : 'Keine Dateipfad-Informationen verfügbar'}
+ ${debug_info.loaded_counts ? `
+
+ Geladene CPUs:
+ AMD: ${debug_info.loaded_counts.amd},
+ Intel: ${debug_info.loaded_counts.intel}
+
+ ` : ''}
+ ${debug_info.error ? `
+
+ Fehler: ${debug_info.error}
+
+ ` : ''}
+
+
+
🔎 Vergleiche (erste ${debug_info.comparisons ? debug_info.comparisons.length : 0}):
+
+ `;
+
+ if (debug_info.comparisons) {
+ debug_info.comparisons.forEach((comp, i) => {
+ let color = comp.match_result ? 'green' : '#666';
+ let icon = comp.match_result ? '✅' : '❌';
+ debug_html += `
+
+
${icon} ${comp.match_type}: ${comp.supported_cpu}
+ ${comp.extracted_part ? `
Extrahiert: ${comp.extracted_part}
` : ''}
+
Details: ${comp.details}
+
+ `;
+ });
+ }
+
+ if (debug_info.truncated) {
+ debug_html += '
... weitere Vergleiche abgeschnitten ...
';
+ }
+
+ debug_html += `
+
+
+ `;
+
+ frappe.msgprint({
+ title: 'CPU-Kompatibilitäts Debug',
+ message: debug_html,
+ indicator: result.compatible ? 'green' : 'red'
+ });
+ }
+ }
+ });
+ }, 'CPU Debug Test', 'Testen');
+ }, 'Workflow');
+
+ // Add Excel Export button
+ frm.add_custom_button('📊 Excel Export', function(){
+ frappe.dom.freeze('Excel-Export wird erstellt...');
+ frappe.call({
+ method: 'msp.tactical-rmm.export_tables_to_excel',
+ args: {
+ documentation_name: frm.doc.name
+ },
+ callback: function(r) {
+ frappe.dom.unfreeze();
+ if (r.exc) {
+ frappe.msgprint({
+ title: __('Fehler'),
+ indicator: 'red',
+ message: __('Excel-Export konnte nicht erstellt werden: ') + r.exc
+ });
+ return;
+ }
+ if (r.message && r.message.success) {
+ let filename = r.message.filename;
+ let content = r.message.content;
+
+ // Excel-Datei herunterladen
+ try {
+ // Base64 zu Blob konvertieren
+ const byteCharacters = atob(content);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ const byteArray = new Uint8Array(byteNumbers);
+ const blob = new Blob([byteArray], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ });
+
+ // Download-Link erstellen
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ const timestamp = new Date().toLocaleString('de-DE');
+ a.style.display = 'none';
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ frappe.show_alert({
+ message: __('Excel-Export erfolgreich heruntergeladen: ') + filename,
+ indicator: 'green'
+ });
+
+ } catch (download_error) {
+ console.error('Download-Fehler:', download_error);
+ frappe.msgprint({
+ title: __('Download-Fehler'),
+ indicator: 'red',
+ message: __('Die Excel-Datei konnte nicht heruntergeladen werden. Bitte versuchen Sie es erneut.')
+ });
+ }
+
+ }
+ }
+ });
+ }, 'Export');
}
});
diff --git a/msp/msp/doctype/msp_documentation/msp_documentation.json b/msp/msp/doctype/msp_documentation/msp_documentation.json
index 9fea7c3..edc1b18 100644
--- a/msp/msp/doctype/msp_documentation/msp_documentation.json
+++ b/msp/msp/doctype/msp_documentation/msp_documentation.json
@@ -18,7 +18,19 @@
"server_list",
"workstation_list",
"backup",
- "aditional_data"
+ "aditional_data",
+ "data_acquisition_section",
+ "credentials_for_ldap_acquisistion",
+ "domain_controller_for_ldap_acquisition",
+ "column_break_ydaj",
+ "upn",
+ "ip_address",
+ "json_data_section",
+ "rmm_data_json",
+ "ad_computer_data_json",
+ "ad_user_data_json",
+ "output",
+ "windows_11_check_output"
],
"fields": [
{
@@ -88,14 +100,78 @@
"fieldname": "tactical_rmm_site_name",
"fieldtype": "Data",
"label": "Tactical RMM Site Name"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "json_data_section",
+ "fieldtype": "Section Break",
+ "label": "JSON Data"
+ },
+ {
+ "fieldname": "rmm_data_json",
+ "fieldtype": "Long Text",
+ "label": "RMM Data JSON"
+ },
+ {
+ "fieldname": "ad_computer_data_json",
+ "fieldtype": "Long Text",
+ "label": "AD Computer Data JSON"
+ },
+ {
+ "fieldname": "ad_user_data_json",
+ "fieldtype": "Long Text",
+ "label": "AD User Data JSON"
+ },
+ {
+ "fieldname": "data_acquisition_section",
+ "fieldtype": "Section Break",
+ "label": "Data Acquisition"
+ },
+ {
+ "fieldname": "credentials_for_ldap_acquisistion",
+ "fieldtype": "Link",
+ "label": "Credentials for LDAP Acquisistion",
+ "options": "IT User Account"
+ },
+ {
+ "fieldname": "domain_controller_for_ldap_acquisition",
+ "fieldtype": "Link",
+ "label": "Domain Controller for LDAP Acquisition",
+ "options": "IT Object"
+ },
+ {
+ "fieldname": "column_break_ydaj",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "upn",
+ "fieldtype": "Data",
+ "label": "UPN"
+ },
+ {
+ "fieldname": "ip_address",
+ "fieldtype": "Data",
+ "label": "IP Address"
+ },
+ {
+ "fieldname": "output",
+ "fieldtype": "Text Editor",
+ "label": "Output",
+ "read_only": 1
+ },
+ {
+ "fieldname": "windows_11_check_output",
+ "fieldtype": "HTML",
+ "label": "Windows 11 Check Output"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-03-11 08:06:34.303790",
+ "modified": "2025-07-31 18:45:31.838030",
"modified_by": "Administrator",
"module": "MSP",
"name": "MSP Documentation",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
diff --git a/msp/tactical-rmm.py b/msp/tactical-rmm.py
index 609d637..540abc9 100644
--- a/msp/tactical-rmm.py
+++ b/msp/tactical-rmm.py
@@ -5,6 +5,9 @@ import requests
import json
from pprint import pprint
import re
+import datetime
+import os
+from ldap3 import Server, Connection, ALL, NTLM, SUBTREE
from .tools import render_card_html, render_single_card
@frappe.whitelist()
@@ -250,6 +253,2508 @@ def render_model(model):
return model
+@frappe.whitelist()
+def fetch_and_store_all_agent_data(documentation_name):
+ """
+ Holt Agent-Daten aus dem RMM (gefiltert nach Mandant und optional Site)
+ und speichert sie als JSON im rmm_data_json Feld der angegebenen MSP Documentation.
+
+ Args:
+ documentation_name (str): Name der MSP Documentation, in der die Daten gespeichert werden sollen
+
+ Returns:
+ dict: Erfolgsmeldung mit Anzahl der gespeicherten Agents
+ """
+ try:
+ # MSP Documentation Dokument laden
+ documentation_doc = frappe.get_doc("MSP Documentation", documentation_name)
+
+ # Prüfen ob Tenant Caption vorhanden ist
+ if not documentation_doc.tactical_rmm_tenant_caption:
+ frappe.throw("Tenant Caption fehlt in der MSP Documentation")
+
+ client_name = documentation_doc.tactical_rmm_tenant_caption
+ site_name = documentation_doc.tactical_rmm_site_name
+
+ # Alle Agent-Daten vom RMM holen
+ all_agents = get_all_agents()
+
+ # Nach Mandant und optional nach Site filtern
+ filtered_agents = []
+ for agent in all_agents:
+ # Filter by client_name and optionally by site_name if provided
+ if agent["client_name"] == client_name and (not site_name or agent["site_name"] == site_name):
+ filtered_agents.append(agent)
+
+ # Gefilterte Daten als JSON serialisieren und im rmm_data_json Feld speichern
+ documentation_doc.rmm_data_json = json.dumps(filtered_agents, indent=4, default=str)
+
+ # Dokument speichern
+ documentation_doc.save()
+
+ # Erfolgsmeldung zusammenstellen
+ filter_info = f"Mandant '{client_name}'"
+ if site_name:
+ filter_info += f" und Site '{site_name}'"
+
+ # Erfolgsmeldung zurückgeben
+ return {
+ "success": True,
+ "message": f"RMM-Daten erfolgreich gespeichert. {len(filtered_agents)} Agents gefunden für {filter_info}.",
+ "agent_count": len(filtered_agents),
+ "documentation": documentation_name,
+ "filter": {
+ "client_name": client_name,
+ "site_name": site_name
+ }
+ }
+
+ except Exception as e:
+ frappe.log_error(f"Fehler beim Speichern der RMM-Daten: {str(e)}", "fetch_and_store_all_agent_data")
+ frappe.throw(f"Fehler beim Speichern der RMM-Daten: {str(e)}")
+
+
+@frappe.whitelist()
+def fetch_and_store_ad_computer_data(documentation_name):
+ """
+ Holt Computer-Daten aus dem Active Directory (gefiltert nach Domain)
+ und speichert sie als JSON im ad_computer_data_json Feld der angegebenen MSP Documentation.
+
+ Args:
+ documentation_name (str): Name der MSP Documentation, in der die Daten gespeichert werden sollen
+
+ Returns:
+ dict: Erfolgsmeldung mit Anzahl der gefundenen Computer
+ """
+ try:
+ # MSP Documentation Dokument laden
+ documentation_doc = frappe.get_doc("MSP Documentation", documentation_name)
+
+ # Prüfen ob LDAP-Credentials vorhanden sind
+ if not documentation_doc.credentials_for_ldap_acquisistion:
+ frappe.throw("LDAP-Credentials fehlen in der MSP Documentation")
+
+ # IT User Account mit LDAP-Credentials laden
+ ldap_credentials = frappe.get_doc("IT User Account", documentation_doc.credentials_for_ldap_acquisistion)
+
+ # Prüfen ob alle notwendigen Daten vorhanden sind
+ if not ldap_credentials.username:
+ frappe.throw("Benutzername fehlt in den LDAP-Credentials")
+ if not ldap_credentials.password:
+ frappe.throw("Passwort fehlt in den LDAP-Credentials")
+ if not ldap_credentials.domain:
+ frappe.throw("Domain fehlt in den LDAP-Credentials")
+
+ # LDAP-Verbindungsparameter extrahieren
+ domain = ldap_credentials.domain
+ username = f"{domain}\\{ldap_credentials.username}"
+ password = ldap_credentials.get_password()
+
+ # Server-Name ermitteln - entweder aus Domain Controller oder Domain
+ server_name = domain # Standard-Fallback
+
+ # Prüfen ob Domain Controller für LDAP-Acquisition angegeben ist
+ if documentation_doc.domain_controller_for_ldap_acquisition:
+ try:
+ # IT Object (Domain Controller) laden
+ domain_controller = frappe.get_doc("IT Object", documentation_doc.domain_controller_for_ldap_acquisition)
+
+ # Prüfen ob eine Haupt-IP-Adresse angegeben ist
+ if domain_controller.main_ip:
+ # IP Address Dokument laden
+ ip_address_doc = frappe.get_doc("IP Address", domain_controller.main_ip)
+
+ # IP-Adresse als Server-Name verwenden
+ if ip_address_doc.ip_address:
+ server_name = ip_address_doc.ip_address
+ print(f"INFO: Domain Controller IP-Adresse verwendet: {server_name}")
+ else:
+ print("WARNING: IP Address Dokument hat keine ip_address - verwende Domain als Fallback")
+ else:
+ print("WARNING: IT Object hat keine main_ip - verwende Domain als Fallback")
+ except Exception as dc_error:
+ frappe.log_error(f"Fehler beim Laden des Domain Controllers: {str(dc_error)} - verwende Domain als Fallback", "fetch_and_store_ad_computer_data")
+ else:
+ print("INFO: Kein Domain Controller angegeben - verwende Domain als Server-Name")
+
+ # Search Base aus Domain konstruieren
+ domain_parts = domain.split('.')
+ search_base = ','.join([f'DC={part}' for part in domain_parts])
+
+ # LDAP-Server verbinden
+ server = Server(server_name, get_info=ALL)
+ conn = Connection(server, user=username, password=password, authentication=NTLM)
+
+ # Verbindung herstellen
+ if not conn.bind():
+ frappe.throw(f"LDAP-Verbindung fehlgeschlagen. Prüfen Sie Server, Benutzername und Passwort.")
+
+ # Server-Info für automatische Domain-Erkennung
+ server_info = server.info
+
+ # Search Base validieren und ggf. aus Server-Info extrahieren
+ if hasattr(server_info, 'naming_contexts') and server_info.naming_contexts:
+ for nc in server_info.naming_contexts:
+ nc_str = str(nc)
+ if nc_str.startswith('DC=') and 'CN=Configuration' not in nc_str and 'CN=Schema' not in nc_str:
+ search_base = nc_str
+ break
+
+ # Computer-Filter und Attribute definieren
+ search_filter = '(objectClass=computer)'
+ attributes = [
+ 'sAMAccountName', 'lastLogon', 'lastLogonTimestamp', 'objectGUID', 'userAccountControl',
+ 'cn', 'displayName', 'distinguishedName', 'dNSHostName', 'operatingSystem',
+ 'operatingSystemVersion', 'operatingSystemServicePack', 'description',
+ 'lockoutTime', 'pwdLastSet', 'whenCreated', 'whenChanged'
+ ]
+
+ # LDAP-Suche durchführen
+ success = conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes)
+
+ if not success or not conn.entries:
+ conn.unbind()
+ frappe.throw(f"Keine Computer-Accounts gefunden. Search Base: {search_base}")
+
+ # Duplikate basierend auf distinguishedName entfernen
+ seen_dns = set()
+ unique_entries = []
+ for entry in conn.entries:
+ dn = str(entry.distinguishedName)
+ if dn not in seen_dns:
+ seen_dns.add(dn)
+ unique_entries.append(entry)
+
+ # Ergebnisse sammeln
+ results = []
+
+ for entry in unique_entries:
+ # Nur Computer-Accounts (die mit $ enden) berücksichtigen
+ computer_name = entry.sAMAccountName.value
+ if not computer_name or not computer_name.endswith('$'):
+ continue
+
+ # Zeitstempel konvertieren
+ last_logon = _convert_filetime(entry.lastLogon.value)
+ last_logon_timestamp = _convert_filetime(entry.lastLogonTimestamp.value)
+ pwd_last_set = _convert_filetime(entry.pwdLastSet.value)
+
+ # Neueste Anmeldung ermitteln
+ if last_logon and last_logon_timestamp:
+ most_recent_logon = max(last_logon, last_logon_timestamp)
+ else:
+ most_recent_logon = last_logon or last_logon_timestamp or 'Never logged in'
+
+ # Computer-Status ermitteln
+ user_account_control = entry.userAccountControl.value or 0
+ disabled_status = "Disabled" if (user_account_control & 0x0002) else "Enabled"
+ lockout_time = entry.lockoutTime.value or 0
+ locked_status = "Locked" if lockout_time and lockout_time != 0 else "Unlocked"
+
+ # Daten sammeln
+ computer_data = {
+ 'Computer Name': computer_name,
+ 'Common Name': entry.cn.value if entry.cn else 'N/A',
+ 'Display Name': entry.displayName.value if entry.displayName else 'N/A',
+ 'Distinguished Name': entry.distinguishedName.value if entry.distinguishedName else 'N/A',
+ 'DNS Host Name': entry.dNSHostName.value if entry.dNSHostName else 'N/A',
+ 'Operating System': entry.operatingSystem.value if entry.operatingSystem else 'N/A',
+ 'OS Version': entry.operatingSystemVersion.value if entry.operatingSystemVersion else 'N/A',
+ 'OS Service Pack': entry.operatingSystemServicePack.value if entry.operatingSystemServicePack else 'N/A',
+ 'Description': entry.description.value if entry.description else 'N/A',
+ 'Most Recent Logon': most_recent_logon.isoformat() if isinstance(most_recent_logon, datetime.datetime) else str(most_recent_logon),
+ 'Password Last Set': pwd_last_set.isoformat() if isinstance(pwd_last_set, datetime.datetime) else str(pwd_last_set) if pwd_last_set else 'N/A',
+ 'When Created': entry.whenCreated.value.isoformat() if entry.whenCreated and entry.whenCreated.value else 'N/A',
+ 'When Changed': entry.whenChanged.value.isoformat() if entry.whenChanged and entry.whenChanged.value else 'N/A',
+ 'GUID': str(entry.objectGUID.value) if entry.objectGUID.value else 'N/A',
+ 'Status': disabled_status,
+ 'Lockout Status': locked_status
+ }
+
+ results.append(computer_data)
+
+ # LDAP-Verbindung schließen
+ conn.unbind()
+
+ # Daten als JSON serialisieren und im ad_computer_data_json Feld speichern
+ documentation_doc.ad_computer_data_json = json.dumps(results, indent=4, default=str)
+
+ # Dokument speichern
+ documentation_doc.save()
+
+ # Erfolgsmeldung zurückgeben
+ server_info = f" über Server '{server_name}'" if server_name != domain else ""
+ return {
+ "success": True,
+ "message": f"AD-Computer-Daten erfolgreich gespeichert. {len(results)} Computer gefunden in Domain '{domain}'{server_info}.",
+ "computer_count": len(results),
+ "documentation": documentation_name,
+ "domain": domain,
+ "server": server_name,
+ "search_base": search_base
+ }
+
+ except Exception as e:
+ frappe.log_error(f"Fehler beim Speichern der AD-Computer-Daten: {str(e)}", "fetch_and_store_ad_computer_data")
+ frappe.throw(f"Fehler beim Speichern der AD-Computer-Daten: {str(e)}")
+
+
+@frappe.whitelist()
+def fetch_and_store_ad_user_data(documentation_name):
+ """
+ Holt Benutzer-Daten aus dem Active Directory (gefiltert nach Domain)
+ und speichert sie als JSON im ad_user_data_json Feld der angegebenen MSP Documentation.
+
+ Args:
+ documentation_name (str): Name der MSP Documentation, in der die Daten gespeichert werden sollen
+
+ Returns:
+ dict: Erfolgsmeldung mit Anzahl der gefundenen Benutzer
+ """
+ try:
+ # MSP Documentation Dokument laden
+ documentation_doc = frappe.get_doc("MSP Documentation", documentation_name)
+
+ # Prüfen ob LDAP-Credentials vorhanden sind
+ if not documentation_doc.credentials_for_ldap_acquisistion:
+ frappe.throw("LDAP-Credentials fehlen in der MSP Documentation")
+
+ # IT User Account mit LDAP-Credentials laden
+ ldap_credentials = frappe.get_doc("IT User Account", documentation_doc.credentials_for_ldap_acquisistion)
+
+ # Prüfen ob alle notwendigen Daten vorhanden sind
+ if not ldap_credentials.username:
+ frappe.throw("Benutzername fehlt in den LDAP-Credentials")
+ if not ldap_credentials.password:
+ frappe.throw("Passwort fehlt in den LDAP-Credentials")
+ if not ldap_credentials.domain:
+ frappe.throw("Domain fehlt in den LDAP-Credentials")
+
+ # LDAP-Verbindungsparameter extrahieren
+ domain = ldap_credentials.domain
+ username = f"{domain}\\{ldap_credentials.username}"
+ password = ldap_credentials.get_password()
+
+ # Server-Name ermitteln - entweder aus Domain Controller oder Domain
+ server_name = domain # Standard-Fallback
+
+ # Prüfen ob Domain Controller für LDAP-Acquisition angegeben ist
+ if documentation_doc.domain_controller_for_ldap_acquisition:
+ try:
+ # IT Object (Domain Controller) laden
+ domain_controller = frappe.get_doc("IT Object", documentation_doc.domain_controller_for_ldap_acquisition)
+
+ # Prüfen ob eine Haupt-IP-Adresse angegeben ist
+ if domain_controller.main_ip:
+ # IP Address Dokument laden
+ ip_address_doc = frappe.get_doc("IP Address", domain_controller.main_ip)
+
+ # IP-Adresse als Server-Name verwenden
+ if ip_address_doc.ip_address:
+ server_name = ip_address_doc.ip_address
+ print(f"INFO: Domain Controller IP-Adresse verwendet: {server_name}")
+ else:
+ print("WARNING: IP Address Dokument hat keine ip_address - verwende Domain als Fallback")
+ else:
+ print("WARNING: IT Object hat keine main_ip - verwende Domain als Fallback")
+ except Exception as dc_error:
+ frappe.log_error(f"Fehler beim Laden des Domain Controllers: {str(dc_error)} - verwende Domain als Fallback", "fetch_and_store_ad_user_data")
+ else:
+ print("INFO: Kein Domain Controller angegeben - verwende Domain als Server-Name")
+
+ # Search Base aus Domain konstruieren
+ domain_parts = domain.split('.')
+ search_base = ','.join([f'DC={part}' for part in domain_parts])
+
+ # LDAP-Server verbinden
+ server = Server(server_name, get_info=ALL)
+ conn = Connection(server, user=username, password=password, authentication=NTLM)
+
+ # Verbindung herstellen
+ if not conn.bind():
+ frappe.throw(f"LDAP-Verbindung fehlgeschlagen. Prüfen Sie Server, Benutzername und Passwort.")
+
+ # Server-Info für automatische Domain-Erkennung
+ server_info = server.info
+
+ # Search Base validieren und ggf. aus Server-Info extrahieren
+ if hasattr(server_info, 'naming_contexts') and server_info.naming_contexts:
+ for nc in server_info.naming_contexts:
+ nc_str = str(nc)
+ if nc_str.startswith('DC=') and 'CN=Configuration' not in nc_str and 'CN=Schema' not in nc_str:
+ search_base = nc_str
+ break
+
+ # Benutzer-Filter und Attribute definieren (Computer-Accounts ausschließen)
+ search_filter = '(&(objectClass=user)(!(sAMAccountName=*$)))'
+ attributes = [
+ 'sAMAccountName', 'lastLogon', 'lastLogonTimestamp', 'objectGUID', 'userAccountControl',
+ 'givenName', 'sn', 'cn', 'displayName', 'distinguishedName',
+ 'userPrincipalName', 'proxyAddresses', 'mail', 'lockoutTime'
+ ]
+
+ # LDAP-Suche durchführen
+ success = conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes)
+
+ if not success or not conn.entries:
+ conn.unbind()
+ frappe.throw(f"Keine Benutzer-Accounts gefunden. Search Base: {search_base}")
+
+ # Duplikate basierend auf distinguishedName entfernen
+ seen_dns = set()
+ unique_entries = []
+ for entry in conn.entries:
+ dn = str(entry.distinguishedName)
+ if dn not in seen_dns:
+ seen_dns.add(dn)
+ unique_entries.append(entry)
+
+ # Ergebnisse sammeln
+ results = []
+
+ for entry in unique_entries:
+ # Nur echte Benutzer-Accounts (die nicht mit $ enden)
+ username_sam = entry.sAMAccountName.value
+ if not username_sam or username_sam.endswith('$'):
+ continue
+
+ # Zeitstempel konvertieren
+ last_logon = _convert_filetime(entry.lastLogon.value)
+ last_logon_timestamp = _convert_filetime(entry.lastLogonTimestamp.value)
+
+ # Neueste Anmeldung ermitteln
+ if last_logon and last_logon_timestamp:
+ most_recent_logon = max(last_logon, last_logon_timestamp)
+ else:
+ most_recent_logon = last_logon or last_logon_timestamp or 'Never logged in'
+
+ # Benutzer-Status ermitteln
+ user_account_control = entry.userAccountControl.value or 0
+ disabled_status = "Disabled" if (user_account_control & 0x0002) else "Enabled"
+ lockout_time = entry.lockoutTime.value or 0
+ locked_status = "Locked" if lockout_time and lockout_time != 0 else "Unlocked"
+
+ # Daten sammeln
+ user_data = {
+ 'User': username_sam,
+ 'Given Name': entry.givenName.value if entry.givenName else 'N/A',
+ 'Surname': entry.sn.value if entry.sn else 'N/A',
+ 'Common Name': entry.cn.value if entry.cn else 'N/A',
+ 'Display Name': entry.displayName.value if entry.displayName else 'N/A',
+ 'Distinguished Name': entry.distinguishedName.value if entry.distinguishedName else 'N/A',
+ 'User Principal Name': entry.userPrincipalName.value if entry.userPrincipalName else 'N/A',
+ 'Proxy Addresses': ', '.join(entry.proxyAddresses.values) if entry.proxyAddresses else 'N/A',
+ 'Mail': entry.mail.value if entry.mail else 'N/A',
+ 'Most Recent Logon': most_recent_logon.isoformat() if isinstance(most_recent_logon, datetime.datetime) else str(most_recent_logon),
+ 'Last Logon': last_logon.isoformat() if isinstance(last_logon, datetime.datetime) else str(last_logon) if last_logon else 'N/A',
+ 'Last Logon Timestamp': last_logon_timestamp.isoformat() if isinstance(last_logon_timestamp, datetime.datetime) else str(last_logon_timestamp) if last_logon_timestamp else 'N/A',
+ 'GUID': str(entry.objectGUID.value) if entry.objectGUID.value else 'N/A',
+ 'Status': disabled_status,
+ 'Lockout Status': locked_status
+ }
+
+ results.append(user_data)
+
+ # LDAP-Verbindung schließen
+ conn.unbind()
+
+ # Daten als JSON serialisieren und im ad_user_data_json Feld speichern
+ documentation_doc.ad_user_data_json = json.dumps(results, indent=4, default=str)
+
+ # Dokument speichern
+ documentation_doc.save()
+
+ # Erfolgsmeldung zurückgeben
+ server_info = f" über Server '{server_name}'" if server_name != domain else ""
+ return {
+ "success": True,
+ "message": f"AD-Benutzer-Daten erfolgreich gespeichert. {len(results)} Benutzer gefunden in Domain '{domain}'{server_info}.",
+ "user_count": len(results),
+ "documentation": documentation_name,
+ "domain": domain,
+ "server": server_name,
+ "search_base": search_base
+ }
+
+ except Exception as e:
+ frappe.log_error(f"Fehler beim Speichern der AD-Benutzer-Daten: {str(e)}", "fetch_and_store_ad_user_data")
+ frappe.throw(f"Fehler beim Speichern der AD-Benutzer-Daten: {str(e)}")
+
+
+@frappe.whitelist()
+def compare_rmm_and_ad_data(documentation_name):
+ """
+ Vergleicht RMM- und AD-Computer-Daten und erstellt eine HTML-Tabelle mit Statistiken.
+
+ Args:
+ documentation_name (str): Name der MSP Documentation
+
+ Returns:
+ dict: Erfolgsmeldung mit Zusammenfassung
+ """
+ try:
+ # MSP Documentation Dokument laden
+ documentation_doc = frappe.get_doc("MSP Documentation", documentation_name)
+
+ # JSON-Daten laden und parsen
+ rmm_data = []
+ ad_data = []
+
+ if documentation_doc.rmm_data_json:
+ try:
+ rmm_data = json.loads(documentation_doc.rmm_data_json)
+ except json.JSONDecodeError:
+ frappe.throw("RMM Data JSON ist nicht gültig")
+ else:
+ frappe.throw("Keine RMM-Daten vorhanden. Bitte zuerst RMM-Daten speichern.")
+
+ if documentation_doc.ad_computer_data_json:
+ try:
+ ad_data = json.loads(documentation_doc.ad_computer_data_json)
+ except json.JSONDecodeError:
+ frappe.throw("AD Computer Data JSON ist nicht gültig")
+ else:
+ frappe.throw("Keine AD-Computer-Daten vorhanden. Bitte zuerst AD-Computer-Daten speichern.")
+
+ # Daten für Abgleich vorbereiten
+ rmm_by_hostname = {}
+ for rmm_item in rmm_data:
+ hostname = rmm_item.get('hostname', '').upper()
+ if hostname:
+ rmm_by_hostname[hostname] = rmm_item
+
+ # AD-Daten mit intelligentem Matching vorbereiten (ohne Duplikate)
+ ad_by_hostname = {}
+ ad_items_processed = set() # Verhindert Duplikate
+
+ # Erstelle ein Mapping aller möglichen Namen zu AD-Items
+ name_to_ad_item = {}
+ for ad_item in ad_data:
+ # Eindeutige ID für dieses AD-Item erstellen
+ ad_id = ad_item.get('GUID', str(id(ad_item)))
+
+ # Alle verfügbaren Namen sammeln
+ potential_names = []
+
+ # 1. Common Name (ohne $-Zeichen)
+ common_name = ad_item.get('Common Name', '').upper()
+ if common_name and common_name != 'N/A':
+ potential_names.append(common_name)
+
+ # 2. Computer Name ohne $ (sAMAccountName ohne $)
+ computer_name = ad_item.get('Computer Name', '').upper()
+ if computer_name and computer_name != 'N/A' and computer_name.endswith('$'):
+ computer_name_clean = computer_name[:-1] # Entferne $
+ potential_names.append(computer_name_clean)
+
+ # 3. DNS Host Name
+ dns_host_name = ad_item.get('DNS Host Name', '').upper()
+ if dns_host_name and dns_host_name != 'N/A':
+ potential_names.append(dns_host_name)
+
+ # 4. Display Name
+ display_name = ad_item.get('Display Name', '').upper()
+ if display_name and display_name != 'N/A':
+ potential_names.append(display_name)
+
+ # Alle Namen zu diesem AD-Item mappen
+ for name in potential_names:
+ if name:
+ name_to_ad_item[name] = (ad_item, ad_id)
+
+ # Jetzt das finale Mapping erstellen (jeder RMM-Hostname wird nur einmal gemappt)
+ for rmm_hostname in rmm_by_hostname.keys():
+ # Prüfe direkte Matches über alle Namen
+ if rmm_hostname in name_to_ad_item:
+ ad_item, ad_id = name_to_ad_item[rmm_hostname]
+ if ad_id not in ad_items_processed:
+ ad_by_hostname[rmm_hostname] = ad_item
+ ad_items_processed.add(ad_id)
+ else:
+ # Fuzzy-Matching für gekürzte Namen
+ best_match = _find_best_ad_match(rmm_hostname, ad_data)
+ if best_match:
+ best_match_id = best_match.get('GUID', str(id(best_match)))
+ if best_match_id not in ad_items_processed:
+ ad_by_hostname[rmm_hostname] = best_match
+ ad_items_processed.add(best_match_id)
+
+ # Sammle alle eindeutigen Hostnamen (RMM + ungematchte AD)
+ all_hostnames = set(rmm_by_hostname.keys())
+
+ # Füge AD-Items hinzu, die nicht gematcht wurden (nur in AD vorhanden)
+ for ad_item in ad_data:
+ ad_id = ad_item.get('GUID', str(id(ad_item)))
+ if ad_id not in ad_items_processed:
+ # Verwende den besten verfügbaren Namen für dieses AD-Item
+ best_name = _get_best_ad_name(ad_item)
+ if best_name and best_name not in all_hostnames:
+ ad_by_hostname[best_name] = ad_item
+ all_hostnames.add(best_name)
+
+ # Funktion zur Bestimmung des Computer-Typs für Sortierung
+ def get_computer_type_priority(hostname):
+ rmm_item = rmm_by_hostname.get(hostname)
+ if rmm_item:
+ monitoring_type = rmm_item.get('monitoring_type', 'workstation')
+ # Server haben Priorität 0, Workstations haben Priorität 1
+ return 0 if monitoring_type == 'server' else 1
+ else:
+ # Wenn nur in AD vorhanden, als Workstation behandeln
+ return 1
+
+ # Sortiere: erst nach Server/Workstation, dann alphabetisch
+ sorted_hostnames = sorted(all_hostnames, key=lambda x: (get_computer_type_priority(x), x))
+
+ # Tabellendaten erstellen
+ table_rows = []
+ stats = {
+ 'total_computers': len(all_hostnames),
+ 'in_both': 0,
+ 'only_in_rmm': 0,
+ 'only_in_ad': 0,
+ 'ad_disabled': 0
+ }
+
+ for hostname in sorted_hostnames:
+ rmm_item = rmm_by_hostname.get(hostname)
+ ad_item = ad_by_hostname.get(hostname)
+
+ # Daten extrahieren
+ os_info = "N/A"
+ cpu_info = "N/A"
+ last_ad_login = "N/A"
+ rmm_last_seen = "N/A"
+ last_user = "N/A"
+
+ if rmm_item:
+ os_info = rmm_item.get('operating_system', 'N/A')
+ cpu_model = rmm_item.get('cpu_model', [])
+ if isinstance(cpu_model, list) and cpu_model:
+ cpu_info = cpu_model[0]
+ elif isinstance(cpu_model, str):
+ cpu_info = cpu_model
+
+ # Letzter Benutzer aus RMM
+ logged_username = rmm_item.get('logged_username', 'N/A')
+ if logged_username and logged_username != 'N/A':
+ last_user = logged_username
+
+ # RMM zuletzt online formatieren
+ last_seen = rmm_item.get('last_seen', 'N/A')
+ if last_seen and last_seen != 'N/A':
+ try:
+ # ISO-Format zu deutschem Datum konvertieren
+ dt = datetime.datetime.fromisoformat(last_seen.replace('Z', '+00:00'))
+ rmm_last_seen = dt.strftime('%d.%m.%Y %H:%M')
+ except:
+ rmm_last_seen = last_seen
+ elif ad_item:
+ os_info = ad_item.get('Operating System', 'N/A')
+
+ if ad_item:
+ # AD letzter Login formatieren
+ most_recent_logon = ad_item.get('Most Recent Logon', 'N/A')
+ if most_recent_logon and most_recent_logon != 'N/A' and most_recent_logon != 'Never logged in':
+ try:
+ # ISO-Format zu deutschem Datum konvertieren
+ if isinstance(most_recent_logon, str) and 'T' in most_recent_logon:
+ dt = datetime.datetime.fromisoformat(most_recent_logon.replace('Z', '+00:00'))
+ last_ad_login = dt.strftime('%d.%m.%Y %H:%M')
+ else:
+ last_ad_login = str(most_recent_logon)
+ except:
+ last_ad_login = str(most_recent_logon)
+ else:
+ last_ad_login = most_recent_logon if most_recent_logon else "N/A"
+
+ # Status ermitteln
+ in_ad = bool(ad_item)
+ in_rmm = bool(rmm_item)
+ is_ad_disabled = ad_item and ad_item.get('Status') == 'Disabled'
+
+ # Statistiken aktualisieren
+ if in_ad and in_rmm:
+ stats['in_both'] += 1
+ elif in_rmm and not in_ad:
+ stats['only_in_rmm'] += 1
+ elif in_ad and not in_rmm:
+ stats['only_in_ad'] += 1
+
+ if is_ad_disabled:
+ stats['ad_disabled'] += 1
+
+ # Computer-Typ für Anzeige bestimmen
+ computer_type = "Workstation" # Standard
+ type_icon = "💻" # Standard-Icon für Workstation
+ if rmm_item:
+ monitoring_type = rmm_item.get('monitoring_type', 'workstation')
+ if monitoring_type == 'server':
+ computer_type = "Server"
+ type_icon = "🖥️" # Server-Icon
+
+ # Tabellen-Row erstellen
+ row = {
+ 'hostname': hostname,
+ 'computer_type': computer_type,
+ 'type_icon': type_icon,
+ 'os': os_info,
+ 'cpu': cpu_info,
+ 'in_ad': '✓' if in_ad else '✗',
+ 'in_rmm': '✓' if in_rmm else '✗',
+ 'last_user': last_user,
+ 'last_ad_login': last_ad_login,
+ 'rmm_last_seen': rmm_last_seen,
+ 'css_class': _get_row_css_class(in_ad, in_rmm, is_ad_disabled)
+ }
+ table_rows.append(row)
+
+ # HTML-Tabelle und Statistiken generieren
+ html_output = _generate_comparison_html(table_rows, stats)
+
+ # Ergebnis in output Feld speichern
+ documentation_doc.output = html_output
+ documentation_doc.save()
+
+ return {
+ "success": True,
+ "message": f"Datenabgleich erfolgreich. {stats['total_computers']} Computer analysiert.",
+ "stats": stats,
+ "documentation": documentation_name
+ }
+
+ except Exception as e:
+ frappe.log_error(f"Fehler beim Datenabgleich: {str(e)}", "compare_rmm_and_ad_data")
+ frappe.throw(f"Fehler beim Datenabgleich: {str(e)}")
+
+
+def _get_row_css_class(in_ad, in_rmm, is_ad_disabled):
+ """Bestimmt CSS-Klasse für Tabellenzeile basierend auf Status"""
+ if in_ad and in_rmm and not is_ad_disabled:
+ return "table-success" # Grün - in beiden vorhanden und aktiv
+ elif in_ad and in_rmm and is_ad_disabled:
+ return "table-warning" # Gelb - in beiden aber AD deaktiviert
+ elif in_rmm and not in_ad:
+ return "table-info" # Blau - nur in RMM
+ elif in_ad and not in_rmm:
+ return "table-danger" # Rot - nur in AD
+ else:
+ return ""
+
+
+def _generate_comparison_html(table_rows, stats):
+ """Generiert HTML-Tabelle und Statistiken für den Datenabgleich"""
+
+ # Statistiken HTML
+ stats_html = f"""
+
+
+
+
+
+
+
+
{stats['total_computers']}
+
Gesamt Computer
+
+
+
+
+
+
+
{stats['in_both']}
+
In beiden Systemen
+
+
+
+
+
+
+
{stats['only_in_rmm']}
+
Nur im RMM
+
+
+
+
+
+
+
{stats['only_in_ad']}
+
Nur im AD
+
+
+
+
+
+
+
+
+
{stats['ad_disabled']}
+
AD-Computer deaktiviert
+
+
+
+
+
+
+
{round((stats['in_both'] / stats['total_computers'] * 100) if stats['total_computers'] > 0 else 0, 1)}%
+
Übereinstimmung
+
+
+
+
+
+
+
{stats['only_in_rmm'] + stats['only_in_ad']}
+
Abweichungen
+
+
+
+
+
+
+ """
+
+ # Legende
+ legend_html = """
+
+
+
+
+
+ Zeilenfärbung:
+ Grün In beiden Systemen vorhanden und aktiv
+ Gelb In beiden Systemen, aber AD-Computer deaktiviert
+ Blau Nur im RMM vorhanden
+ Rot Nur im Active Directory vorhanden
+
+
+ Computer-Typen:
+ 🖥️ Server Server-Systeme
+ 💻 Workstation Arbeitsplatz-PCs
+ Sortierung: Server zuerst, dann Workstations
+
+
+ Status-Icons:
+ ✓ Vorhanden
+ ✗ Nicht vorhanden
+ Benutzerinformationen:
+ Letzter Benutzer: Aus RMM-System (logged_username)
+ Zeitstempel im deutschen Format (DD.MM.YYYY HH:MM)
+
+
+
+
+ """
+
+ # Tabelle HTML
+ table_html = """
+
+
+
+
+
+
+
+ | Hostname |
+ Typ |
+ Betriebssystem |
+ CPU |
+ In AD |
+ In RMM |
+ Letzter Benutzer |
+ Letzter AD Login |
+ RMM zuletzt online |
+
+
+
+ """
+
+ # Tabellenzeilen hinzufügen
+ for row in table_rows:
+ table_html += f"""
+
+ | {row['hostname']} |
+ {row['type_icon']} {row['computer_type']} |
+ {row['os']} |
+ {row['cpu']} |
+ {row['in_ad']} |
+ {row['in_rmm']} |
+ {row['last_user']} |
+ {row['last_ad_login']} |
+ {row['rmm_last_seen']} |
+
+ """
+
+ table_html += """
+
+
+
+
+
+ """
+
+ # Gesamtes HTML zusammenfügen
+ return stats_html + legend_html + table_html
+
+
+@frappe.whitelist()
+def debug_cpu_compatibility(documentation_name, test_cpu_string=None):
+ """Debuggt CPU-Kompatibilität für eine einzelne CPU"""
+ try:
+ if not test_cpu_string:
+ # Verwende die erste nicht-Windows-11 CPU aus RMM als Test
+ documentation_doc = frappe.get_doc("MSP Documentation", documentation_name)
+ if not documentation_doc.rmm_data_json:
+ frappe.throw("Keine RMM-Daten vorhanden. Bitte zuerst RMM-Daten abrufen.")
+
+ rmm_data = json.loads(documentation_doc.rmm_data_json)
+
+ # Finde erste Test-CPU
+ for agent in rmm_data:
+ os = agent.get('operating_system', '').lower()
+ if 'windows' in os and 'windows 11' not in os:
+ test_cpu_string = agent.get('cpu_model', ['N/A'])[0] if agent.get('cpu_model') else 'N/A'
+ break
+
+ if not test_cpu_string or test_cpu_string == 'N/A':
+ frappe.throw("Keine Test-CPU gefunden")
+
+ # Lade CPU-Listen mit Debug-Info
+ debug_info = {}
+ supported_cpus = _load_win11_cpu_lists(debug_info)
+
+ # CPU-Kompatibilität prüfen
+ result = _check_cpu_compatibility(test_cpu_string, supported_cpus, debug_info)
+
+ # Begrenze Vergleiche für bessere Lesbarkeit
+ if len(debug_info.get('comparisons', [])) > 20:
+ debug_info['comparisons'] = debug_info['comparisons'][:20]
+ debug_info['truncated'] = True
+
+ return {
+ 'success': True,
+ 'debug_info': debug_info,
+ 'result': result,
+ 'test_cpu': test_cpu_string
+ }
+
+ except Exception as e:
+ frappe.log_error(f"Fehler bei CPU-Debug: {str(e)}", "debug_cpu_compatibility")
+ frappe.throw(f"Fehler bei CPU-Debug: {str(e)}")
+
+
+@frappe.whitelist()
+def export_tables_to_excel(documentation_name):
+ """Exportiert RMM/AD Abgleich, Windows 11 Check und AD Benutzer-Daten als Excel"""
+ try:
+ from io import BytesIO
+ import base64
+
+ # Prüfe auf pandas, verwende alternativ openpyxl direkt
+ use_pandas = True
+ try:
+ import pandas as pd
+ except ImportError:
+ use_pandas = False
+ try:
+ from openpyxl import Workbook
+ except ImportError:
+ frappe.throw("Weder pandas noch openpyxl sind installiert. Bitte installieren Sie eine der Bibliotheken für Excel-Export.")
+
+ # MSP Documentation Dokument laden
+ documentation_doc = frappe.get_doc("MSP Documentation", documentation_name)
+
+ # RMM, AD Computer und AD Benutzer Daten laden
+ rmm_data = []
+ ad_data = []
+ ad_user_data = []
+
+ if documentation_doc.rmm_data_json:
+ try:
+ rmm_data = json.loads(documentation_doc.rmm_data_json)
+ except json.JSONDecodeError:
+ frappe.throw("RMM Data JSON ist nicht gültig")
+
+ if documentation_doc.ad_computer_data_json:
+ try:
+ ad_data = json.loads(documentation_doc.ad_computer_data_json)
+ except json.JSONDecodeError:
+ frappe.throw("AD Computer Data JSON ist nicht gültig")
+
+ if documentation_doc.ad_user_data_json:
+ try:
+ ad_user_data = json.loads(documentation_doc.ad_user_data_json)
+ except json.JSONDecodeError:
+ frappe.throw("AD User Data JSON ist nicht gültig")
+
+ if not rmm_data and not ad_data and not ad_user_data:
+ frappe.throw("Keine RMM-, AD Computer- oder AD Benutzer-Daten vorhanden")
+
+ # Excel-Datei erstellen
+ excel_buffer = BytesIO()
+
+ if use_pandas:
+ # Pandas-basierte Erstellung
+ with pd.ExcelWriter(excel_buffer, engine='openpyxl') as writer:
+
+ # 1. RMM ↔ AD Abgleich Tabelle
+ if rmm_data and ad_data:
+ comparison_data = _generate_comparison_excel_data(rmm_data, ad_data)
+ if comparison_data:
+ df_comparison = pd.DataFrame(comparison_data)
+ df_comparison.to_excel(writer, sheet_name='RMM AD Abgleich', index=False)
+ _format_pandas_worksheet(writer, 'RMM AD Abgleich', comparison_data)
+
+ # 2. Windows 11 Kompatibilitätstabelle
+ if rmm_data:
+ win11_data = _generate_win11_excel_data(rmm_data)
+ if win11_data:
+ df_win11 = pd.DataFrame(win11_data)
+ df_win11.to_excel(writer, sheet_name='Windows 11 Check', index=False)
+ _format_pandas_worksheet(writer, 'Windows 11 Check', win11_data)
+
+ # 3. RMM Rohdaten (optional)
+ if rmm_data:
+ rmm_export_data = _generate_rmm_excel_data(rmm_data)
+ if rmm_export_data:
+ df_rmm = pd.DataFrame(rmm_export_data)
+ df_rmm.to_excel(writer, sheet_name='RMM Rohdaten', index=False)
+ _format_pandas_worksheet(writer, 'RMM Rohdaten', rmm_export_data)
+
+ # 4. AD Rohdaten (optional)
+ if ad_data:
+ ad_export_data = _generate_ad_excel_data(ad_data)
+ if ad_export_data:
+ df_ad = pd.DataFrame(ad_export_data)
+ df_ad.to_excel(writer, sheet_name='AD Rohdaten', index=False)
+ _format_pandas_worksheet(writer, 'AD Rohdaten', ad_export_data)
+
+ # 5. AD Benutzer-Daten (optional)
+ if ad_user_data:
+ ad_user_export_data = _generate_ad_user_excel_data(ad_user_data)
+ if ad_user_export_data:
+ df_ad_users = pd.DataFrame(ad_user_export_data)
+ df_ad_users.to_excel(writer, sheet_name='AD Benutzer', index=False)
+ _format_pandas_worksheet(writer, 'AD Benutzer', ad_user_export_data)
+ else:
+ # Openpyxl-basierte Alternative
+ wb = Workbook()
+ # Entferne standard Worksheet
+ wb.remove(wb.active)
+
+ # 1. RMM ↔ AD Abgleich Tabelle
+ if rmm_data and ad_data:
+ comparison_data = _generate_comparison_excel_data(rmm_data, ad_data)
+ if comparison_data:
+ _add_worksheet_with_data(wb, 'RMM AD Abgleich', comparison_data)
+
+ # 2. Windows 11 Kompatibilitätstabelle
+ if rmm_data:
+ win11_data = _generate_win11_excel_data(rmm_data)
+ if win11_data:
+ _add_worksheet_with_data(wb, 'Windows 11 Check', win11_data)
+
+ # 3. RMM Rohdaten (optional)
+ if rmm_data:
+ rmm_export_data = _generate_rmm_excel_data(rmm_data)
+ if rmm_export_data:
+ _add_worksheet_with_data(wb, 'RMM Rohdaten', rmm_export_data)
+
+ # 4. AD Rohdaten (optional)
+ if ad_data:
+ ad_export_data = _generate_ad_excel_data(ad_data)
+ if ad_export_data:
+ _add_worksheet_with_data(wb, 'AD Rohdaten', ad_export_data)
+
+ # 5. AD Benutzer-Daten (optional)
+ if ad_user_data:
+ ad_user_export_data = _generate_ad_user_excel_data(ad_user_data)
+ if ad_user_export_data:
+ _add_worksheet_with_data(wb, 'AD Benutzer', ad_user_export_data)
+
+ # Workbook in Buffer speichern
+ wb.save(excel_buffer)
+
+ # Excel-Datei als Base64 kodieren
+ excel_buffer.seek(0)
+ excel_content = excel_buffer.getvalue()
+ excel_base64 = base64.b64encode(excel_content).decode('utf-8')
+
+ # Dateiname generieren
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f"MSP_Export_{documentation_name}_{timestamp}.xlsx"
+
+ return {
+ 'success': True,
+ 'filename': filename,
+ 'content': excel_base64,
+ 'message': f'Excel-Export erfolgreich erstellt: {filename}',
+ 'method': 'pandas' if use_pandas else 'openpyxl'
+ }
+
+ except Exception as e:
+ frappe.log_error(f"Fehler beim Excel-Export: {str(e)}", "export_tables_to_excel")
+ frappe.throw(f"Fehler beim Excel-Export: {str(e)}")
+
+
+@frappe.whitelist()
+def check_windows11_compatibility(documentation_name):
+ """
+ Prüft Windows 11 CPU-Kompatibilität für alle Systeme, die noch nicht Windows 11 haben.
+
+ Args:
+ documentation_name (str): Name der MSP Documentation
+
+ Returns:
+ dict: Erfolgsmeldung mit Zusammenfassung
+ """
+ try:
+ # MSP Documentation Dokument laden
+ documentation_doc = frappe.get_doc("MSP Documentation", documentation_name)
+
+ # RMM-Daten laden
+ rmm_data = []
+ if documentation_doc.rmm_data_json:
+ try:
+ rmm_data = json.loads(documentation_doc.rmm_data_json)
+ except json.JSONDecodeError:
+ frappe.throw("RMM Data JSON ist nicht gültig")
+ else:
+ frappe.throw("Keine RMM-Daten vorhanden. Bitte zuerst RMM-Daten speichern.")
+
+ # CPU-Listen laden
+ supported_cpus = _load_win11_cpu_lists()
+
+ # Systeme analysieren
+ analysis_results = []
+ stats = {
+ 'total_non_win11': 0,
+ 'compatible_cpus': 0,
+ 'incompatible_cpus': 0,
+ 'unknown_cpus': 0,
+ 'servers_excluded': 0
+ }
+
+ for rmm_item in rmm_data:
+ # Nur Client-Systeme (Workstations) berücksichtigen
+ monitoring_type = rmm_item.get('monitoring_type', 'workstation')
+ if monitoring_type == 'server':
+ stats['servers_excluded'] += 1
+ continue
+
+ hostname = rmm_item.get('hostname', 'Unknown')
+ os_info = rmm_item.get('operating_system', 'N/A')
+ cpu_model = rmm_item.get('cpu_model', [])
+
+ # CPU-String extrahieren
+ cpu_string = "N/A"
+ if isinstance(cpu_model, list) and cpu_model:
+ cpu_string = cpu_model[0]
+ elif isinstance(cpu_model, str):
+ cpu_string = cpu_model
+
+ # Prüfen ob System bereits Windows 11 hat
+ is_windows11 = 'Windows 11' in os_info
+
+ # Nur Systeme ohne Windows 11 analysieren
+ if not is_windows11:
+ stats['total_non_win11'] += 1
+
+ # CPU-Kompatibilität prüfen
+ compatibility = _check_cpu_compatibility(cpu_string, supported_cpus)
+
+ if compatibility['compatible']:
+ stats['compatible_cpus'] += 1
+ elif compatibility['status'] == 'unknown':
+ stats['unknown_cpus'] += 1
+ else:
+ stats['incompatible_cpus'] += 1
+
+ # Letzter Benutzer
+ last_user = rmm_item.get('logged_username', 'N/A')
+
+ # Letztes Online-Datum
+ last_seen = rmm_item.get('last_seen', 'N/A')
+ rmm_last_seen = "N/A"
+ if last_seen and last_seen != 'N/A':
+ try:
+ dt = datetime.datetime.fromisoformat(last_seen.replace('Z', '+00:00'))
+ rmm_last_seen = dt.strftime('%d.%m.%Y %H:%M')
+ except:
+ rmm_last_seen = last_seen
+
+ result = {
+ 'hostname': hostname,
+ 'os': os_info,
+ 'cpu': cpu_string,
+ 'cpu_vendor': compatibility['vendor'],
+ 'compatible': compatibility['compatible'],
+ 'compatibility_status': compatibility['status'],
+ 'last_user': last_user,
+ 'last_seen': rmm_last_seen,
+ 'css_class': _get_compatibility_css_class(compatibility['compatible'], compatibility['status'])
+ }
+
+ analysis_results.append(result)
+
+ # Nach Kompatibilität und dann alphabetisch sortieren
+ analysis_results.sort(key=lambda x: (
+ 0 if x['compatible'] else 1, # Kompatible zuerst
+ x['hostname'].upper()
+ ))
+
+ # HTML-Output generieren
+ html_output = _generate_win11_compatibility_html(analysis_results, stats)
+
+ # Ergebnis in output Feld anhängen (oder ersetzen wenn schon vorhanden)
+ existing_output = documentation_doc.output or ""
+ separator = "\n\n
\n\n"
+
+ # Entferne bereits vorhandenen Windows 11 Check (falls vorhanden)
+ if "Windows 11 CPU-Kompatibilitätsprüfung" in existing_output:
+ # Teile bei HR-Tags und filtere Windows 11 Abschnitt heraus
+ parts = existing_output.split("
max_length:
+ max_length = len(str(cell.value))
+ except:
+ pass
+ adjusted_width = min(max_length + 2, 50)
+ ws.column_dimensions[column_letter].width = adjusted_width
+ return
+
+ # Spezifische Breiten setzen
+ for col_num, header in enumerate(headers, 1):
+ column_letter = ws.cell(row=1, column=col_num).column_letter
+ width = width_config.get(header, 20) # Standard 20 wenn nicht definiert
+ ws.column_dimensions[column_letter].width = width
+
+ except Exception as e:
+ frappe.log_error(f"Fehler beim Setzen der Spaltenbreiten: {str(e)}", "_set_column_widths")
+
+
+def _apply_zebra_stripes(ws, data_rows):
+ """Fügt Zebrastreifen für bessere Lesbarkeit hinzu (nur bei Spalten ohne Farbformatierung)"""
+ try:
+ from openpyxl.styles import PatternFill
+
+ # Leichte graue Füllung für ungerade Zeilen (ab Zeile 3, da 1=Header, 2=erste Datenzeile)
+ zebra_fill = PatternFill(start_color="F8F9FA", end_color="F8F9FA", fill_type="solid")
+
+ for row_num in range(3, data_rows + 2, 2): # Jede zweite Zeile
+ for cell in ws[row_num]:
+ # Nur wenn Zelle noch keine spezielle Hintergrundfarbe hat
+ current_fill = cell.fill.start_color.index if cell.fill else '00000000'
+ if current_fill in ['00000000', 'FFFFFFFF']: # Transparente oder weiße Füllung
+ cell.fill = zebra_fill
+
+ except Exception:
+ pass # Ignoriere Formatierungsfehler
+
+
+def _format_pandas_worksheet(writer, sheet_name, data):
+ """Formatiert Pandas-generierte Worksheets"""
+ try:
+ from openpyxl.styles import PatternFill, Font, Alignment
+
+ # Worksheet aus Writer abrufen
+ ws = writer.sheets[sheet_name]
+ headers = list(data[0].keys()) if data else []
+
+ # Header-Style
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
+ header_font = Font(color="FFFFFF", bold=True)
+ center_alignment = Alignment(horizontal="center", vertical="center")
+
+ # Header formatieren
+ for col_num, header in enumerate(headers, 1):
+ cell = ws.cell(row=1, column=col_num)
+ cell.fill = header_fill
+ cell.font = header_font
+ cell.alignment = center_alignment
+
+ # Spezifische Spaltenbreiten setzen
+ _set_column_widths(ws, sheet_name, headers)
+
+ # Deutsche Datum/Zeit-Formatierung für relevante Spalten
+ datetime_columns = [
+ 'RMM zuletzt online', 'Letzter AD Login', 'Zuletzt online', 'Letzter Login',
+ 'Passwort gesetzt', 'Erstellt', 'Geändert', 'Neuester Login',
+ 'lastLogon (DC-spezifisch)', 'lastLogonTimestamp (repliziert)'
+ ]
+
+ for col_num, header in enumerate(headers, 1):
+ if header in datetime_columns:
+ # Deutsche Datum/Zeit-Formatierung: TT.MM.JJJJ HH:MM
+ for row_num in range(2, len(data) + 2):
+ cell = ws.cell(row=row_num, column=col_num)
+ if cell.value: # Nur formatieren wenn Wert vorhanden
+ cell.number_format = 'DD.MM.YYYY HH:MM'
+
+ # Erst Zebrastreifen anwenden
+ _apply_zebra_stripes(ws, len(data))
+
+ # Dann spezielle Formatierung (überschreibt Zebrastreifen)
+ for row_num, row_data in enumerate(data, 2):
+ for col_num, header in enumerate(headers, 1):
+ cell = ws.cell(row=row_num, column=col_num)
+
+ if sheet_name == 'RMM AD Abgleich':
+ _apply_comparison_formatting(cell, row_data, header)
+ elif sheet_name == 'Windows 11 Check':
+ _apply_win11_formatting(cell, row_data, header)
+
+ except Exception as e:
+ frappe.log_error(f"Fehler beim Formatieren von Pandas Worksheet {sheet_name}: {str(e)}", "_format_pandas_worksheet")
+
+
+def _check_cpu_compatibility(cpu_string, supported_cpus, debug_info=None):
+ """Prüft ob eine CPU Windows 11 kompatibel ist - ultra-einfache Suche"""
+ if not cpu_string or cpu_string == 'N/A':
+ return {'compatible': False, 'status': 'unknown', 'vendor': 'Unknown'}
+
+ cpu_upper = cpu_string.upper()
+
+ if debug_info is not None:
+ debug_info['system_cpu'] = cpu_string
+ debug_info['system_cpu_upper'] = cpu_upper
+ debug_info['comparisons'] = []
+
+ # Vendor erkennen
+ vendor = 'Unknown'
+ if 'AMD' in cpu_upper or 'RYZEN' in cpu_upper or 'ATHLON' in cpu_upper or 'EPYC' in cpu_upper:
+ vendor = 'AMD'
+ search_set = supported_cpus['amd']
+ elif 'INTEL' in cpu_upper or 'CORE' in cpu_upper or 'XEON' in cpu_upper or 'CELERON' in cpu_upper or 'PENTIUM' in cpu_upper:
+ vendor = 'Intel'
+ search_set = supported_cpus['intel']
+ else:
+ return {'compatible': False, 'status': 'unknown', 'vendor': vendor}
+
+ if debug_info is not None:
+ debug_info['vendor'] = vendor
+ debug_info['search_set_size'] = len(search_set)
+
+ # Ultra-einfache Suche: Prüfe für jede unterstützte CPU
+ match_found = False
+ for supported_cpu in search_set:
+ match_result = _simple_cpu_match(cpu_upper, supported_cpu.upper(), debug_info)
+ if match_result:
+ match_found = True
+ if debug_info is not None:
+ debug_info['match_found'] = True
+ debug_info['matching_cpu'] = supported_cpu
+ return {'compatible': True, 'status': 'compatible', 'vendor': vendor}
+
+ if debug_info is not None:
+ debug_info['match_found'] = False
+
+ return {'compatible': False, 'status': 'incompatible', 'vendor': vendor}
+
+
+def _clean_cpu_string(cpu_string):
+ """Bereinigt CPU-String für bessere Kompatibilitätsprüfung"""
+ # Entferne häufige Zusätze die nicht in den Listen stehen
+ cleaned = cpu_string
+
+ # Entferne Markenzeichen
+ cleaned = re.sub(r'\(R\)', '', cleaned)
+ cleaned = re.sub(r'\(TM\)', '', cleaned)
+ cleaned = re.sub(r'®', '', cleaned)
+ cleaned = re.sub(r'™', '', cleaned)
+
+ # Entferne Geschwindigkeitsangaben
+ cleaned = re.sub(r'\s*@\s*[\d\.]+\s*GHz', '', cleaned)
+ cleaned = re.sub(r'\s*\d+\.\d+\s*GHz', '', cleaned)
+
+ # Entferne Core/Thread Informationen
+ cleaned = re.sub(r'\s*,\s*\d+C/\d+T', '', cleaned)
+ cleaned = re.sub(r'\s*\d+C/\d+T', '', cleaned)
+
+ # Entferne GPU-Zusätze bei AMD
+ cleaned = re.sub(r'\s+with\s+Radeon\s+Graphics.*', '', cleaned, flags=re.IGNORECASE)
+ cleaned = re.sub(r'\s+with\s+.*Graphics.*', '', cleaned, flags=re.IGNORECASE)
+
+ # Entferne Zusätze wie "CPU", "Processor"
+ cleaned = re.sub(r'\s+(CPU|Processor)\s*', ' ', cleaned)
+
+ # Entferne mehrfache Leerzeichen und führende/nachfolgende Leerzeichen
+ cleaned = re.sub(r'\s+', ' ', cleaned).strip()
+
+ return cleaned
+
+
+def _simple_cpu_match(system_cpu_upper, supported_cpu_upper, debug_info=None):
+ """Ultra-einfacher CPU-Match ohne komplexe Logik"""
+
+ comparison = {
+ 'supported_cpu': supported_cpu_upper,
+ 'match_type': '',
+ 'extracted_part': '',
+ 'match_result': False,
+ 'details': ''
+ }
+
+ # Intel CPUs: Extrahiere iX-XXXX aus "CORE(TM) iX-XXXX"
+ if 'CORE(TM)' in supported_cpu_upper:
+ comparison['match_type'] = 'Intel Core'
+ # Finde iX-XXXX Pattern
+ parts = supported_cpu_upper.split()
+ for part in parts:
+ if part.startswith('I') and '-' in part and len(part) >= 6: # i3-8100 etc.
+ comparison['extracted_part'] = part
+ if part in system_cpu_upper:
+ comparison['match_result'] = True
+ comparison['details'] = f"'{part}' gefunden in System-CPU"
+ if debug_info is not None:
+ debug_info['comparisons'].append(comparison)
+ return True
+ else:
+ comparison['details'] = f"'{part}' NICHT gefunden in System-CPU"
+
+ if not comparison['extracted_part']:
+ comparison['details'] = "Kein iX-XXXX Pattern gefunden"
+
+ if debug_info is not None:
+ debug_info['comparisons'].append(comparison)
+ return False
+
+ # AMD Ryzen CPUs: Direkter Match
+ elif 'RYZEN' in supported_cpu_upper:
+ comparison['match_type'] = 'AMD Ryzen'
+
+ # Für "Ryzen 5 PRO 5650GE" suche nach "RYZEN 5 PRO 5650GE"
+ if supported_cpu_upper in system_cpu_upper:
+ comparison['match_result'] = True
+ comparison['details'] = f"Direkter Match: '{supported_cpu_upper}' gefunden"
+ if debug_info is not None:
+ debug_info['comparisons'].append(comparison)
+ return True
+
+ # Auch Fallback ohne "with Radeon Graphics" etc.
+ # Entferne common suffixes from system CPU for comparison
+ system_cleaned = system_cpu_upper
+ system_cleaned = system_cleaned.replace('WITH RADEON GRAPHICS', '')
+ system_cleaned = system_cleaned.replace('WITH RADEON VEGA MOBILE GFX', '')
+ system_cleaned = system_cleaned.replace('WITH RADEON', '')
+ system_cleaned = ' '.join(system_cleaned.split()) # normalize spaces
+
+ if supported_cpu_upper in system_cleaned:
+ comparison['match_result'] = True
+ comparison['details'] = f"Bereinigter Match: '{supported_cpu_upper}' in '{system_cleaned}'"
+ if debug_info is not None:
+ debug_info['comparisons'].append(comparison)
+ return True
+
+ comparison['details'] = f"Kein Match: '{supported_cpu_upper}' weder in Original noch in bereinigter Version"
+ if debug_info is not None:
+ debug_info['comparisons'].append(comparison)
+ return False
+
+ # Andere CPUs (Athlon, EPYC, Xeon, Celeron, Pentium): Direkter Match
+ else:
+ comparison['match_type'] = 'Andere CPU'
+ match_result = supported_cpu_upper in system_cpu_upper
+ comparison['match_result'] = match_result
+ if match_result:
+ comparison['details'] = f"Direkter Match: '{supported_cpu_upper}' gefunden"
+ else:
+ comparison['details'] = f"Kein Match: '{supported_cpu_upper}' nicht gefunden"
+
+ if debug_info is not None:
+ debug_info['comparisons'].append(comparison)
+ return match_result
+
+
+def _extract_simple_core_model(supported_cpu):
+ """Extrahiert einfaches Kernmodell für Substring-Suche - sehr vereinfacht"""
+ if not supported_cpu:
+ return None
+
+ cpu_upper = supported_cpu.upper().strip()
+
+ # Intel CPUs: "Core(TM) i3-8100" → "I3-8100"
+ if 'CORE(TM)' in cpu_upper:
+ # Finde i3-XXXX, i5-XXXX, i7-XXXX, i9-XXXX
+ parts = cpu_upper.split()
+ for part in parts:
+ if part.startswith('I') and '-' in part:
+ return part
+
+ # Intel andere: Xeon, Celeron, Pentium - verwende den ganzen String ohne Core(TM)
+ if cpu_upper.startswith('CORE(TM)'):
+ return cpu_upper.replace('CORE(TM)', '').strip()
+
+ # AMD CPUs: Suche nach "Ryzen X XXXX" oder "Ryzen X PRO XXXX"
+ if 'RYZEN' in cpu_upper:
+ # Finde Ryzen Pattern
+ if 'PRO' in cpu_upper:
+ # "Ryzen 5 PRO 5650GE" → "RYZEN 5 PRO 5650GE"
+ return cpu_upper
+ else:
+ # "Ryzen 7 5800H" → "RYZEN 7 5800H"
+ return cpu_upper
+
+ # Andere AMD: Athlon, EPYC
+ if 'ATHLON' in cpu_upper or 'EPYC' in cpu_upper:
+ return cpu_upper
+
+ # Fallback - ganzer String
+ return cpu_upper
+
+
+def _create_cpu_search_variants(cpu_string):
+ """Erstellt verschiedene Suchvarianten eines CPU-Strings"""
+ variants = []
+
+ # Original uppercase
+ variants.append(cpu_string.upper())
+
+ # Bereinigt (entfernt (R), (TM), Geschwindigkeit, etc.)
+ cleaned = _clean_cpu_string(cpu_string)
+ variants.append(cleaned.upper())
+
+ # Weitere Varianten mit verschiedenen Markenzeichen-Behandlungen
+ # Version mit (TM) aber ohne (R)
+ temp = cpu_string
+ temp = re.sub(r'\(R\)', '', temp)
+ temp = re.sub(r'®', '', temp)
+ temp = _clean_cpu_string_keep_tm(temp)
+ variants.append(temp.upper())
+
+ # Version ohne Intel/AMD Präfix für bessere Suche
+ no_prefix = re.sub(r'^(INTEL|AMD)\s+', '', cleaned.upper())
+ variants.append(no_prefix)
+
+ # Kernmodell extrahieren
+ core_model = _extract_cpu_core_model(cpu_string)
+ if core_model:
+ variants.append(core_model.upper())
+
+ # Entferne leere und doppelte Einträge
+ variants = list(set([v.strip() for v in variants if v and v.strip()]))
+
+ return variants
+
+
+def _clean_cpu_string_keep_tm(cpu_string):
+ """Bereinigt CPU-String aber behält (TM) bei"""
+ cleaned = cpu_string
+
+ # Entferne Geschwindigkeitsangaben
+ cleaned = re.sub(r'\s*@\s*[\d\.]+\s*GHz', '', cleaned)
+ cleaned = re.sub(r'\s*\d+\.\d+\s*GHz', '', cleaned)
+
+ # Entferne Core/Thread Informationen
+ cleaned = re.sub(r'\s*,\s*\d+C/\d+T', '', cleaned)
+ cleaned = re.sub(r'\s*\d+C/\d+T', '', cleaned)
+
+ # Entferne GPU-Zusätze bei AMD
+ cleaned = re.sub(r'\s+with\s+Radeon\s+Graphics.*', '', cleaned, flags=re.IGNORECASE)
+ cleaned = re.sub(r'\s+with\s+.*Graphics.*', '', cleaned, flags=re.IGNORECASE)
+
+ # Entferne Zusätze wie "CPU", "Processor"
+ cleaned = re.sub(r'\s+(CPU|Processor)\s*', ' ', cleaned)
+
+ # Entferne mehrfache Leerzeichen und führende/nachfolgende Leerzeichen
+ cleaned = re.sub(r'\s+', ' ', cleaned).strip()
+
+ return cleaned
+
+
+def _cpu_strings_match(search_variant, supported_cpu):
+ """Prüft ob zwei CPU-Strings übereinstimmen"""
+ if not search_variant or not supported_cpu:
+ return False
+
+ search_upper = search_variant.upper().strip()
+ supported_upper = supported_cpu.upper().strip()
+
+ # Exakte Übereinstimmung
+ if search_upper == supported_upper:
+ return True
+
+ # Teilstring-Suche in beide Richtungen
+ if supported_upper in search_upper or search_upper in supported_upper:
+ return True
+
+ # Spezielle Behandlung für Core-CPUs
+ # Entferne "INTEL" und "AMD" Präfixe für Vergleich
+ search_no_vendor = re.sub(r'^(INTEL|AMD)\s+', '', search_upper)
+ supported_no_vendor = re.sub(r'^(INTEL|AMD)\s+', '', supported_upper)
+
+ if search_no_vendor in supported_no_vendor or supported_no_vendor in search_no_vendor:
+ return True
+
+ # Kernmodell-Vergleich
+ search_core = _extract_cpu_core_model(search_variant)
+ supported_core = _extract_cpu_core_model(supported_cpu)
+
+ if search_core and supported_core:
+ # Normalisiere beide Kernmodelle für Vergleich
+ search_core_clean = re.sub(r'\(TM\)', '', search_core.upper()).strip()
+ supported_core_clean = re.sub(r'\(TM\)', '', supported_core.upper()).strip()
+
+ if search_core_clean == supported_core_clean:
+ return True
+
+ # Auch mit (TM) vergleichen
+ if search_core.upper() == supported_core.upper():
+ return True
+
+ return False
+
+
+def _normalize_cpu_string(cpu_string):
+ """Normalisiert CPU-String für erweiterte Kompatibilitätsprüfung"""
+ normalized = _clean_cpu_string(cpu_string)
+
+ # Zusätzliche Normalisierungen
+ # Entferne "Intel" und "AMD" Präfixe für bessere Suche
+ normalized = re.sub(r'^(INTEL|AMD)\s+', '', normalized.upper())
+
+ # Normalisiere Core-Bezeichnungen
+ normalized = re.sub(r'CORE\s*\(TM\)\s*', 'CORE(TM) ', normalized)
+
+ # Entferne doppelte Leerzeichen
+ normalized = re.sub(r'\s+', ' ', normalized).strip()
+
+ return normalized
+
+
+def _extract_cpu_core_model(cpu_string):
+ """Extrahiert das Kernmodell einer CPU für präzise Suche"""
+ if not cpu_string:
+ return None
+
+ cpu_upper = cpu_string.upper()
+
+ # Intel CPU-Modelle extrahieren
+ intel_patterns = [
+ r'I\d+-\d+[A-Z]*', # i3-8100, i7-9700K, etc.
+ r'XEON.*?[A-Z]-\d+[A-Z]*', # Xeon Gold 6134, etc.
+ r'CELERON.*?[A-Z]?\d+[A-Z]*', # Celeron G4900, etc.
+ r'PENTIUM.*?[A-Z]?\d+[A-Z]*' # Pentium Gold G5400, etc.
+ ]
+
+ for pattern in intel_patterns:
+ match = re.search(pattern, cpu_upper)
+ if match:
+ return match.group()
+
+ # AMD CPU-Modelle extrahieren
+ amd_patterns = [
+ r'RYZEN\s+\d+\s+\d+[A-Z]*', # Ryzen 7 5800H, etc.
+ r'RYZEN\s+\d+\s+PRO\s+\d+[A-Z]*', # Ryzen 7 PRO 5850U, etc.
+ r'ATHLON.*?\d+[A-Z]*', # Athlon Gold 3150G, etc.
+ r'EPYC\s+\d+[A-Z]*' # EPYC 7502, etc.
+ ]
+
+ for pattern in amd_patterns:
+ match = re.search(pattern, cpu_upper)
+ if match:
+ return match.group()
+
+ return None
+
+
+def _get_compatibility_css_class(compatible, status):
+ """Bestimmt CSS-Klasse für Kompatibilitätszeile"""
+ if compatible:
+ return "table-success" # Grün - kompatibel
+ elif status == 'unknown':
+ return "table-warning" # Gelb - unbekannt
+ else:
+ return "table-danger" # Rot - nicht kompatibel
+
+
+def _generate_win11_compatibility_html(results, stats):
+ """Generiert HTML für Windows 11 Kompatibilitätsprüfung"""
+
+ # Header
+ header_html = """
+
+
+
+ """
+
+ # Statistiken
+ stats_html = f"""
+
+
+
+
+
+
+
+
{stats['total_non_win11']}
+
Systeme ohne Win11
+
+
+
+
+
+
+
{stats['compatible_cpus']}
+
Kompatible CPUs
+
+
+
+
+
+
+
{stats['incompatible_cpus']}
+
Inkompatible CPUs
+
+
+
+
+
+
+
{stats['unknown_cpus']}
+
Unbekannte CPUs
+
+
+
+
+
+
+
+
+
{round((stats['compatible_cpus'] / stats['total_non_win11'] * 100) if stats['total_non_win11'] > 0 else 0, 1)}%
+
Upgrade-bereit
+
+
+
+
+
+
+
{stats['servers_excluded']}
+
Server ausgeschlossen
+
+
+
+
+
+
+ """
+
+ # Legende
+ legend_html = """
+
+
+
+
+
+ Kompatibilität:
+ Grün CPU ist Windows 11 kompatibel
+ Rot CPU ist NICHT Windows 11 kompatibel
+ Gelb CPU-Kompatibilität unbekannt
+
+
+ Hinweise:
+ • Nur Workstations werden geprüft
+ • Server sind ausgeschlossen
+ • Systeme mit Windows 11 werden nicht angezeigt
+
+
+ Empfehlung:
+ • Grüne Systeme können auf Windows 11 aktualisiert werden
+ • Rote Systeme benötigen Hardware-Upgrade
+ • Gelbe Systeme einzeln prüfen
+
+
+
+
+ """
+
+ # Tabelle
+ table_html = """
+
+
+
+
+
+
+
+ | Hostname |
+ Aktuelles OS |
+ CPU |
+ Hersteller |
+ Win11 Kompatibel |
+ Letzter Benutzer |
+ Zuletzt online |
+
+
+
+ """
+
+ # Tabellenzeilen
+ for result in results:
+ compatibility_badge = ""
+ if result['compatible']:
+ compatibility_badge = '✓ Kompatibel'
+ elif result['compatibility_status'] == 'unknown':
+ compatibility_badge = '? Unbekannt'
+ else:
+ compatibility_badge = '✗ Nicht kompatibel'
+
+ vendor_badge = f'{result["cpu_vendor"]}'
+
+ table_html += f"""
+
+ | {result['hostname']} |
+ {result['os']} |
+ {result['cpu']} |
+ {vendor_badge} |
+ {compatibility_badge} |
+ {result['last_user']} |
+ {result['last_seen']} |
+
+ """
+
+ table_html += """
+
+
+
+
+
+ """
+
+ # Zusammenfügen
+ return header_html + stats_html + legend_html + table_html
+
+
+def _get_best_ad_name(ad_item):
+ """
+ Bestimmt den besten verfügbaren Namen für ein AD-Item für die Anzeige.
+ Priorität: DNS Host Name > Common Name > Computer Name ohne $ > Display Name
+ """
+ # 1. DNS Host Name (meist vollständig)
+ dns_host_name = ad_item.get('DNS Host Name', '')
+ if dns_host_name and dns_host_name != 'N/A':
+ return dns_host_name.upper()
+
+ # 2. Common Name
+ common_name = ad_item.get('Common Name', '')
+ if common_name and common_name != 'N/A':
+ return common_name.upper()
+
+ # 3. Computer Name ohne $
+ computer_name = ad_item.get('Computer Name', '')
+ if computer_name and computer_name != 'N/A' and computer_name.endswith('$'):
+ return computer_name[:-1].upper()
+
+ # 4. Display Name
+ display_name = ad_item.get('Display Name', '')
+ if display_name and display_name != 'N/A':
+ return display_name.upper()
+
+ return None
+
+
+def _find_best_ad_match(rmm_hostname, ad_data):
+ """
+ Findet den besten AD-Match für einen RMM-Hostnamen mit Fuzzy-Matching.
+ Behandelt gekürzte Computer-Namen in Active Directory.
+ """
+ if not rmm_hostname or not ad_data:
+ return None
+
+ rmm_hostname_upper = rmm_hostname.upper()
+ best_match = None
+ best_score = 0
+
+ for ad_item in ad_data:
+ # Alle verfügbaren Namen aus AD-Item sammeln
+ ad_names = []
+
+ # Common Name
+ common_name = ad_item.get('Common Name', '')
+ if common_name and common_name != 'N/A':
+ ad_names.append(common_name.upper())
+
+ # Computer Name ohne $
+ computer_name = ad_item.get('Computer Name', '')
+ if computer_name and computer_name != 'N/A' and computer_name.endswith('$'):
+ ad_names.append(computer_name[:-1].upper())
+
+ # DNS Host Name
+ dns_host_name = ad_item.get('DNS Host Name', '')
+ if dns_host_name and dns_host_name != 'N/A':
+ ad_names.append(dns_host_name.upper())
+
+ # Display Name
+ display_name = ad_item.get('Display Name', '')
+ if display_name and display_name != 'N/A':
+ ad_names.append(display_name.upper())
+
+ # Prüfe jeden AD-Namen gegen RMM-Hostname
+ for ad_name in ad_names:
+ if not ad_name:
+ continue
+
+ # 1. Exakte Übereinstimmung (höchste Priorität)
+ if ad_name == rmm_hostname_upper:
+ return ad_item
+
+ # 2. RMM-Hostname beginnt mit AD-Name (gekürzte AD-Namen)
+ if rmm_hostname_upper.startswith(ad_name) and len(ad_name) >= 8:
+ score = len(ad_name) / len(rmm_hostname_upper) * 100
+ if score > best_score:
+ best_score = score
+ best_match = ad_item
+
+ # 3. AD-Name beginnt mit RMM-Hostname (seltener Fall)
+ elif ad_name.startswith(rmm_hostname_upper) and len(rmm_hostname_upper) >= 8:
+ score = len(rmm_hostname_upper) / len(ad_name) * 90 # Etwas niedrigere Priorität
+ if score > best_score:
+ best_score = score
+ best_match = ad_item
+
+ # 4. Teilweise Übereinstimmung (niedrigste Priorität)
+ elif len(ad_name) >= 6 and len(rmm_hostname_upper) >= 6:
+ # Gemeinsame Zeichen zählen
+ common_chars = sum(1 for a, b in zip(ad_name, rmm_hostname_upper) if a == b)
+ if common_chars >= min(6, min(len(ad_name), len(rmm_hostname_upper)) * 0.8):
+ score = (common_chars / max(len(ad_name), len(rmm_hostname_upper))) * 70
+ if score > best_score and score >= 50: # Mindest-Score für partielle Matches
+ best_score = score
+ best_match = ad_item
+
+ # Nur zurückgeben wenn Score hoch genug ist
+ if best_score >= 60:
+ return best_match
+
+ return None
+
+
+def _convert_filetime(filetime):
+ """Konvertiert Windows File Time zu Python datetime"""
+ if filetime and isinstance(filetime, int) and filetime != 0:
+ try:
+ return datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=filetime / 10)
+ except (ValueError, OverflowError):
+ return None
+ elif filetime and isinstance(filetime, datetime.datetime):
+ return filetime
+ else:
+ return None
+
"""
{'agent_id': 'mXXJYhUHwrMPcAAuvsmGMFhcVsjWVMQqHKaVCfBN',
diff --git a/msp/win11-amd-cpus.txt b/msp/win11-amd-cpus.txt
new file mode 100644
index 0000000..b58765b
--- /dev/null
+++ b/msp/win11-amd-cpus.txt
@@ -0,0 +1,305 @@
+2.6.1.0
+3015e
+3020e
+Athlon 3000G
+Athlon 300GE
+Athlon 300U
+Athlon 320GE
+Athlon 7120e
+Athlon 7120U
+Athlon 7220e
+Athlon 7220U
+Athlon Gold 3150C
+Athlon Gold 3150G
+Athlon Gold 3150GE
+Athlon Gold 3150U
+Athlon Silver 3050C
+Athlon Silver 3050e
+Athlon Silver 3050GE
+Athlon Silver 3050U
+Athlon Gold PRO 3125GE
+Athlon Gold PRO 3150G
+Athlon Gold PRO 3150GE
+Athlon Gold PRO 4150GE
+Athlon PRO 300GE
+Athlon PRO 300U
+Athlon PRO 3045B
+EPYC 7252
+EPYC 7262
+EPYC 7272
+EPYC 7282
+EPYC 7302
+EPYC 7313
+EPYC 7343
+EPYC 7352
+EPYC 7402
+EPYC 7413
+EPYC 7443
+EPYC 7452
+EPYC 7453
+EPYC 7502
+EPYC 7513
+EPYC 7532
+EPYC 7542
+EPYC 7543
+EPYC 7552
+EPYC 7642
+EPYC 7643
+EPYC 7662
+EPYC 7663
+EPYC 7702
+EPYC 7713
+EPYC 7742
+EPYC 7763
+EPYC 7232P
+EPYC 72F3
+EPYC 7302P
+EPYC 7313P
+EPYC 73F3
+EPYC 7402P
+EPYC 7443P
+EPYC 74F3
+EPYC 7502P
+EPYC 7543P
+EPYC 75F3
+EPYC 7702P
+EPYC 7713P
+EPYC 7F32
+EPYC 7F52
+EPYC 7F72
+EPYC 7H12
+Ryzen Embedded R2000 Series R2312
+Ryzen Embedded R2000 Series R2314
+Ryzen Embedded R2000 Series R2514
+Ryzen Embedded R2000 Series R2544
+Ryzen Z1
+Ryzen Z1 Extreme
+Ryzen 3 3100
+Ryzen 3 4100
+Ryzen 3 2300X
+Ryzen 3 3200G
+Ryzen 3 3200GE
+Ryzen 3 3200U
+Ryzen 3 3250C
+Ryzen 3 3250U
+Ryzen 3 3300U
+Ryzen 3 3350U
+Ryzen 3 4300G
+Ryzen 3 4300GE
+Ryzen 3 4300U
+Ryzen 3 5125C
+Ryzen 3 5300G
+Ryzen 3 5300GE
+Ryzen 3 5300U
+Ryzen 3 5400U
+Ryzen 3 5425C
+Ryzen 3 5425U
+Ryzen 3 7320e
+Ryzen 3 7320U
+Ryzen 3 7330U
+Ryzen 3 7335U
+Ryzen 3 7440U
+Ryzen 3 5380U
+Ryzen 3 PRO 3200G
+Ryzen 3 PRO 3200GE
+Ryzen 3 PRO 3300U
+Ryzen 3 PRO 4350G
+Ryzen 3 PRO 4350GE
+Ryzen 3 PRO 4450U
+Ryzen 3 PRO 5350G
+Ryzen 3 PRO 5350GE
+Ryzen 3 PRO 5450U
+Ryzen 3 PRO 5475U
+Ryzen 3 PRO 7330U
+Ryzen 3 PRO 4355G
+Ryzen 3 PRO 4355GE
+Ryzen 5 2600
+Ryzen 5 3600
+Ryzen 5 4500
+Ryzen 5 5500
+Ryzen 5 5600
+Ryzen 5 7600
+Ryzen 5 2500X
+Ryzen 5 2600E
+Ryzen 5 2600X
+Ryzen 5 3350G
+Ryzen 5 3350GE
+Ryzen 5 3400G
+Ryzen 5 3400GE
+Ryzen 5 3450U
+Ryzen 5 3500
+Ryzen 5 3500C
+Ryzen 5 3500U
+Ryzen 5 3500X
+Ryzen 5 3550H
+Ryzen 5 3580U
+Ryzen 5 3600X
+Ryzen 5 3600XT
+Ryzen 5 4500U
+Ryzen 5 4600G
+Ryzen 5 4600GE
+Ryzen 5 4600H
+Ryzen 5 4600HS
+Ryzen 5 4600U
+Ryzen 5 5300G
+Ryzen 5 5300GE
+Ryzen 5 5425U
+Ryzen 5 5500U
+Ryzen 5 5560U
+Ryzen 5 5600G
+Ryzen 5 5600GE
+Ryzen 5 5600H
+Ryzen 5 5600HS
+Ryzen 5 5600U
+Ryzen 5 5600X
+Ryzen 5 5625C
+Ryzen 5 5625U
+Ryzen 5 6600H
+Ryzen 5 6600HS
+Ryzen 5 6600U
+Ryzen 5 7520U
+Ryzen 5 7530U
+Ryzen 5 7535HS
+Ryzen 5 7535U
+Ryzen 5 7540U
+Ryzen 5 7600X
+Ryzen 5 7640HS
+Ryzen 5 7640S
+Ryzen 5 7640U
+Ryzen 5 7645HX
+Ryzen 5 7640H
+Ryzen 5 PRO 2600
+Ryzen 5 PRO 3600
+Ryzen 5 PRO 5645
+Ryzen 5 PRO 3350G
+Ryzen 5 PRO 3350GE
+Ryzen 5 PRO 3400G
+Ryzen 5 PRO 3400GE
+Ryzen 5 PRO 3500U
+Ryzen 5 PRO 4650G
+Ryzen 5 PRO 4650GE
+Ryzen 5 PRO 4650U
+Ryzen 5 PRO 5475U
+Ryzen 5 PRO 5650G
+Ryzen 5 PRO 5650GE
+Ryzen 5 PRO 5650HS
+Ryzen 5 PRO 5650HX
+Ryzen 5 PRO 5650U
+Ryzen 5 PRO 5675U
+Ryzen 5 PRO 5750G
+Ryzen 5 PRO 5750GE
+Ryzen 5 PRO 6650H
+Ryzen 5 PRO 6650HS
+Ryzen 5 PRO 6650U
+Ryzen 5 PRO 7530U
+Ryzen 5 PRO 7540U
+Ryzen 5 PRO 7640U
+Ryzen 5 PRO 4655G
+Ryzen 5 PRO 4655GE
+Ryzen 7 2700
+Ryzen 7 5800
+Ryzen 7 5800
+Ryzen 7 7700
+Ryzen 7 2700E
+Ryzen 7 2700X
+Ryzen 7 3700C
+Ryzen 7 3700U
+Ryzen 7 3700X
+Ryzen 7 3750H
+Ryzen 7 3780U
+Ryzen 7 3800X
+Ryzen 7 3800XT
+Ryzen 7 4700G
+Ryzen 7 4700GE
+Ryzen 7 4700U
+Ryzen 7 4800H
+Ryzen 7 4800HS
+Ryzen 7 4800U
+Ryzen 7 5700G
+Ryzen 7 5700GE
+Ryzen 7 5700U
+Ryzen 7 5700X
+Ryzen 7 5800H
+Ryzen 7 5800HS
+Ryzen 7 5800U
+Ryzen 7 5800X
+Ryzen 7 5800X3D
+Ryzen 7 5825C
+Ryzen 7 5825U
+Ryzen 7 6800H
+Ryzen 7 6800HS
+Ryzen 7 6800U
+Ryzen 7 6810U
+Ryzen 7 7700X
+Ryzen 7 7730U
+Ryzen 7 7735HS
+Ryzen 7 7735U
+Ryzen 7 7736U
+Ryzen 7 7745HX
+Ryzen 7 7800X3D
+Ryzen 7 7840H
+Ryzen 7 7840HS
+Ryzen 7 7840S
+Ryzen 7 7840U
+Ryzen 7 PRO 2700
+Ryzen 7 PRO 3700
+Ryzen 7 PRO 5845
+Ryzen 7 PRO 2700X
+Ryzen 7 PRO 3700U
+Ryzen 7 PRO 4750G
+Ryzen 7 PRO 4750GE
+Ryzen 7 PRO 4750U
+Ryzen 7 PRO 5850HS
+Ryzen 7 PRO 5850HX
+Ryzen 7 PRO 5850U
+Ryzen 7 PRO 5875U
+Ryzen 7 PRO 6850H
+Ryzen 7 PRO 6850HS
+Ryzen 7 PRO 6850U
+Ryzen 7 PRO 6860Z
+Ryzen 7 PRO 7730U
+Ryzen 7 PRO 7840U
+Ryzen 9 5900
+Ryzen 9 7900
+Ryzen 9 3900
+Ryzen 9 3900X
+Ryzen 9 3900XT
+Ryzen 9 3950X
+Ryzen 9 4900H
+Ryzen 9 4900HS
+Ryzen 9 5900HS
+Ryzen 9 5900HX
+Ryzen 9 5900X
+Ryzen 9 5950X
+Ryzen 9 5980HS
+Ryzen 9 5980HX
+Ryzen 9 6900HS
+Ryzen 9 6900HX
+Ryzen 9 6980HS
+Ryzen 9 6980HX
+Ryzen 9 7845HX
+Ryzen 9 7900X
+Ryzen 9 7900X3D
+Ryzen 9 7940H
+Ryzen 9 7940HS
+Ryzen 9 7945HX
+Ryzen 9 7950X
+Ryzen 9 7950X3D
+Ryzen 9 PRO 3900
+Ryzen 9 PRO 5945
+Ryzen 9 PRO 6950H
+Ryzen 9 PRO 6950HS
+Ryzen Embedded V2516
+Ryzen Embedded V2546
+Ryzen Embedded V2718
+Ryzen Embedded V2748
+Ryzen Threadripper PRO 3945WX
+Ryzen Threadripper PRO 3955WX
+Ryzen Threadripper PRO 3975WX
+Ryzen Threadripper PRO 3995WX
+Ryzen Threadripper PRO 5945WX
+Ryzen Threadripper PRO 5955WX
+Ryzen Threadripper PRO 5965WX
+Ryzen Threadripper PRO 5975WX
+Ryzen Threadripper PRO 5995WX
+EOF
\ No newline at end of file
diff --git a/msp/win11-intel-cpus.txt b/msp/win11-intel-cpus.txt
new file mode 100644
index 0000000..cc0a3d9
--- /dev/null
+++ b/msp/win11-intel-cpus.txt
@@ -0,0 +1,790 @@
+2.5.0.4
+Atom(R) x6200FE
+Atom(R) x6211E
+Atom(R) x6212RE
+Atom(R) x6413E
+Atom(R) x6414RE
+Atom(R) x6425E
+Atom(R) x6425RE
+Atom(R) x6427FE
+Celeron(R) 6305
+Celeron(R) 7300
+Celeron(R) 7305
+Celeron(R) 3867U
+Celeron(R) 4205U
+Celeron(R) 4305U
+Celeron(R) 4305UE
+Celeron(R) 5205U
+Celeron(R) 5305U
+Celeron(R) 6305E
+Celeron(R) 6600HE
+Celeron(R) 7305E
+Celeron(R) 7305L
+Celeron(R) G4900
+Celeron(R) G4900T
+Celeron(R) G4920
+Celeron(R) G4930
+Celeron(R) G4930E
+Celeron(R) G4930T
+Celeron(R) G4932E
+Celeron(R) G4950
+Celeron(R) G5900
+Celeron(R) G5900E
+Celeron(R) G5900T
+Celeron(R) G5900TE
+Celeron(R) G5905
+Celeron(R) G5905T
+Celeron(R) G5920
+Celeron(R) G5925
+Celeron(R) G6900
+Celeron(R) G6900E
+Celeron(R) G6900T
+Celeron(R) G6900TE
+Celeron(R) J4005
+Celeron(R) J4025
+Celeron(R) J4105
+Celeron(R) J4115
+Celeron(R) J4125
+Celeron(R) J6412
+Celeron(R) J6413
+Celeron(R) N4000
+Celeron(R) N4020
+Celeron(R) N4100
+Celeron(R) N4120
+Celeron(R) N4500
+Celeron(R) N4505
+Celeron(R) N5095
+Celeron(R) N5100
+Celeron(R) N5105
+Celeron(R) N6210
+Celeron(R) N6211
+Core(TM) i3-1000G1
+Core(TM) i3-1000G4
+Core(TM) i3-1005G1
+Core(TM) i3-10100
+Core(TM) i3-10100E
+Core(TM) i3-10100F
+Core(TM) i3-10100T
+Core(TM) i3-10100TE
+Core(TM) i3-10100Y
+Core(TM) i3-10105
+Core(TM) i3-10105F
+Core(TM) i3-10105T
+Core(TM) i3-10110U
+Core(TM) i3-10110Y
+Core(TM) i3-10300
+Core(TM) i3-10300T
+Core(TM) i3-10305
+Core(TM) i3-10305T
+Core(TM) i3-10320
+Core(TM) i3-10325
+Core(TM) i3-11100HE
+Core(TM) i3-1110G4
+Core(TM) i3-1115G4
+Core(TM) i3-1115G4E
+Core(TM) i3-1115GRE
+Core(TM) i3-1120G4
+Core(TM) i3-1125G4
+Core(TM) i3-12100
+Core(TM) i3-12100E
+Core(TM) i3-12100F
+Core(TM) i3-12100T
+Core(TM) i3-12100TE
+Core(TM) i3-1210U
+Core(TM) i3-1215U
+Core(TM) i3-1215UE
+Core(TM) i3-1215UL
+Core(TM) i3-1220P
+Core(TM) i3-1220PE
+Core(TM) i3-12300
+Core(TM) i3-12300HE
+Core(TM) i3-12300HL
+Core(TM) i3-12300T
+Core(TM) i3-1305U
+Core(TM) i3-13100
+Core(TM) i3-13100E
+Core(TM) i3-13100F
+Core(TM) i3-13100T
+Core(TM) i3-13100TE
+Core(TM) i3-1315U
+Core(TM) i3-1315UE
+Core(TM) i3-1320PE
+Core(TM) i3-13300HE
+Core(TM) i3-8100
+Core(TM) i3-8100B
+Core(TM) i3-8100H
+Core(TM) i3-8100T
+Core(TM) i3-8109U
+Core(TM) i3-8130U
+Core(TM) i3-8140U
+Core(TM) i3-8145U
+Core(TM) i3-8145UE
+Core(TM) i3-8300
+Core(TM) i3-8300T
+Core(TM) i3-8350K
+Core(TM) i3-9100
+Core(TM) i3-9100E
+Core(TM) i3-9100F
+Core(TM) i3-9100HL
+Core(TM) i3-9100T
+Core(TM) i3-9100TE
+Core(TM) i3-9300
+Core(TM) i3-9300T
+Core(TM) i3-9320
+Core(TM) i3-9350K
+Core(TM) i3-9350KF
+Core(TM) i3-L13G4
+Core(TM) i3-N300
+Core(TM) i3-N305
+Core(TM) i5-10200H
+Core(TM) i5-10210U
+Core(TM) i5-10210Y
+Core(TM) i5-10300H
+Core(TM) i5-1030G4
+Core(TM) i5-1030G7
+Core(TM) i5-10310U
+Core(TM) i5-10310Y
+Core(TM) i5-1035G1
+Core(TM) i5-1035G4
+Core(TM) i5-1035G7
+Core(TM) i5-1038NG7
+Core(TM) i5-10400
+Core(TM) i5-10400F
+Core(TM) i5-10400H
+Core(TM) i5-10400T
+Core(TM) i5-10500
+Core(TM) i5-10500E
+Core(TM) i5-10500H
+Core(TM) i5-10500T
+Core(TM) i5-10500TE
+Core(TM) i5-10505
+Core(TM) i5-10600
+Core(TM) i5-10600K
+Core(TM) i5-10600KF
+Core(TM) i5-10600T
+Core(TM) i5-11260H
+Core(TM) i5-11300H
+Core(TM) i5-1130G7
+Core(TM) i5-11320H
+Core(TM) i5-1135G7
+Core(TM) i5-1135G7
+Core(TM) i5-11400
+Core(TM) i5-11400F
+Core(TM) i5-11400H
+Core(TM) i5-11400T
+Core(TM) i5-1140G7
+Core(TM) i5-1145G7
+Core(TM) i5-1145G7E
+Core(TM) i5-1145GRE
+Core(TM) i5-11500
+Core(TM) i5-11500H
+Core(TM) i5-11500HE
+Core(TM) i5-11500T
+Core(TM) i5-1155G7
+Core(TM) i5-11600
+Core(TM) i5-11600K
+Core(TM) i5-11600KF
+Core(TM) i5-11600T
+Core(TM) i5-1230U
+Core(TM) i5-1235U
+Core(TM) i5-1235UL
+Core(TM) i5-12400
+Core(TM) i5-12400F
+Core(TM) i5-12400T
+Core(TM) i5-1240P
+Core(TM) i5-1240U
+Core(TM) i5-12450H
+Core(TM) i5-12450HX
+Core(TM) i5-1245U
+Core(TM) i5-1245UE
+Core(TM) i5-1245UL
+Core(TM) i5-12500
+Core(TM) i5-12500E
+Core(TM) i5-12500H
+Core(TM) i5-12500HL
+Core(TM) i5-12500T
+Core(TM) i5-12500TE
+Core(TM) i5-1250P
+Core(TM) i5-1250PE
+Core(TM) i5-12600
+Core(TM) i5-12600H
+Core(TM) i5-12600HE
+Core(TM) i5-12600HL
+Core(TM) i5-12600HX
+Core(TM) i5-12600K
+Core(TM) i5-12600KF
+Core(TM) i5-12600T
+Core(TM) i5-1334U
+Core(TM) i5-1335U
+Core(TM) i5-1335UE
+Core(TM) i5-13400
+Core(TM) i5-13400E
+Core(TM) i5-13400F
+Core(TM) i5-13400T
+Core(TM) i5-1340P
+Core(TM) i5-1340PE
+Core(TM) i5-13420H
+Core(TM) i5-13450HX
+Core(TM) i5-1345U
+Core(TM) i5-1345UE
+Core(TM) i5-13490F
+Core(TM) i5-13500
+Core(TM) i5-13500E
+Core(TM) i5-13500H
+Core(TM) i5-13500HX
+Core(TM) i5-13500T
+Core(TM) i5-13500TE
+Core(TM) i5-13505H
+Core(TM) i5-1350P
+Core(TM) i5-1350PE
+Core(TM) i5-13600
+Core(TM) i5-13600H
+Core(TM) i5-13600HE
+Core(TM) i5-13600HX
+Core(TM) i5-13600K
+Core(TM) i5-13600KF
+Core(TM) i5-13600T
+Core(TM) i5-8200Y
+Core(TM) i5-8210Y
+Core(TM) i5-8250U
+Core(TM) i5-8257U
+Core(TM) i5-8259U
+Core(TM) i5-8260U
+Core(TM) i5-8265U
+Core(TM) i5-8269U
+Core(TM) i5-8279U
+Core(TM) i5-8300H
+Core(TM) i5-8305G
+Core(TM) i5-8310Y
+Core(TM) i5-8350U
+Core(TM) i5-8365U
+Core(TM) i5-8365UE
+Core(TM) i5-8400
+Core(TM) i5-8400B
+Core(TM) i5-8400H
+Core(TM) i5-8400T
+Core(TM) i5-8500
+Core(TM) i5-8500B
+Core(TM) i5-8500T
+Core(TM) i5-8600
+Core(TM) i5-8600K
+Core(TM) i5-8600T
+Core(TM) i5-9300H
+Core(TM) i5-9300HF
+Core(TM) i5-9400
+Core(TM) i5-9400F
+Core(TM) i5-9400H
+Core(TM) i5-9400T
+Core(TM) i5-9500
+Core(TM) i5-9500E
+Core(TM) i5-9500F
+Core(TM) i5-9500T
+Core(TM) i5-9500TE
+Core(TM) i5-9600
+Core(TM) i5-9600K
+Core(TM) i5-9600KF
+Core(TM) i5-9600T
+Core(TM) i5-L16G7
+Core(TM) i7-10510U
+Core(TM) i7-10510Y
+Core(TM) i7-1060G7
+Core(TM) i7-10610U
+Core(TM) i7-1065G7
+Core(TM) i7-1068G7
+Core(TM) i7-1068NG7
+Core(TM) i7-10700
+Core(TM) i7-10700E
+Core(TM) i7-10700F
+Core(TM) i7-10700K
+Core(TM) i7-10700KF
+Core(TM) i7-10700T
+Core(TM) i7-10700TE
+Core(TM) i7-10710U
+Core(TM) i7-10750H
+Core(TM) i7-10810U
+Core(TM) i7-10850H
+Core(TM) i7-10870H
+Core(TM) i7-10875H
+Core(TM) i7-11370H
+Core(TM) i7-11375H
+Core(TM) i7-11390H
+Core(TM) i7-11600H
+Core(TM) i7-1160G7
+Core(TM) i7-1165G7
+Core(TM) i7-1165G7
+Core(TM) i7-11700
+Core(TM) i7-11700F
+Core(TM) i7-11700K
+Core(TM) i7-11700KF
+Core(TM) i7-11700T
+Core(TM) i7-11800H
+Core(TM) i7-1180G7
+Core(TM) i7-11850H
+Core(TM) i7-11850HE
+Core(TM) i7-1185G7
+Core(TM) i7-1185G7E
+Core(TM) i7-1185GRE
+Core(TM) i7-1195G7
+Core(TM) i7-1250U
+Core(TM) i7-1255U
+Core(TM) i7-1255UL
+Core(TM) i7-1260P
+Core(TM) i7-1260U
+Core(TM) i7-12650HX
+Core(TM) i7-1265U
+Core(TM) i7-1265UE
+Core(TM) i7-1265UL
+Core(TM) i7-12700
+Core(TM) i7-12700E
+Core(TM) i7-12700F
+Core(TM) i7-12700H
+Core(TM) i7-12700HL
+Core(TM) i7-12700K
+Core(TM) i7-12700KF
+Core(TM) i7-12700T
+Core(TM) i7-12700TE
+Core(TM) i7-1270P
+Core(TM) i7-1270PE
+Core(TM) i7-12800H
+Core(TM) i7-12800HE
+Core(TM) i7-12800HL
+Core(TM) i7-12800HX
+Core(TM) i7-1280P
+Core(TM) i7-12850HX
+Core(TM) i7-1355U
+Core(TM) i7-1360P
+Core(TM) i7-13620H
+Core(TM) i7-13650HX
+Core(TM) i7-1365U
+Core(TM) i7-1365UE
+Core(TM) i7-13700
+Core(TM) i7-13700E
+Core(TM) i7-13700F
+Core(TM) i7-13700H
+Core(TM) i7-13700HX
+Core(TM) i7-13700K
+Core(TM) i7-13700KF
+Core(TM) i7-13700T
+Core(TM) i7-13700TE
+Core(TM) i7-13705H
+Core(TM) i7-1370P
+Core(TM) i7-1370PE
+Core(TM) i7-13790F
+Core(TM) i7-13800H
+Core(TM) i7-13800HE
+Core(TM) i7-13850HX
+Core(TM) i7-7800X
+Core(TM) i7-7820HQ
+Core(TM) i7-7820X
+Core(TM) i7-8086K
+Core(TM) i7-8500Y
+Core(TM) i7-8550U
+Core(TM) i7-8557U
+Core(TM) i7-8559U
+Core(TM) i7-8565U
+Core(TM) i7-8569U
+Core(TM) i7-8650U
+Core(TM) i7-8665U
+Core(TM) i7-8665UE
+Core(TM) i7-8700
+Core(TM) i7-8700B
+Core(TM) i7-8700K
+Core(TM) i7-8700T
+Core(TM) i7-8705G
+Core(TM) i7-8706G
+Core(TM) i7-8709G
+Core(TM) i7-8750H
+Core(TM) i7-8809G
+Core(TM) i7-8850H
+Core(TM) i7-9700
+Core(TM) i7-9700E
+Core(TM) i7-9700F
+Core(TM) i7-9700K
+Core(TM) i7-9700KF
+Core(TM) i7-9700T
+Core(TM) i7-9700TE
+Core(TM) i7-9750H
+Core(TM) i7-9750HF
+Core(TM) i7-9800X
+Core(TM) i7-9850H
+Core(TM) i7-9850HE
+Core(TM) i7-9850HL
+Core(TM) i9-10850K
+Core(TM) i9-10885H
+Core(TM) i9-10900
+Core(TM) i9-10900E
+Core(TM) i9-10900F
+Core(TM) i9-10900K
+Core(TM) i9-10900KF
+Core(TM) i9-10900T
+Core(TM) i9-10900TE
+Core(TM) i9-10900X
+Core(TM) i9-10920X
+Core(TM) i9-10940X
+Core(TM) i9-10980HK
+Core(TM) i9-10980XE
+Core(TM) i9-11900
+Core(TM) i9-11900F
+Core(TM) i9-11900H
+Core(TM) i9-11900K
+Core(TM) i9-11900KF
+Core(TM) i9-11900T
+Core(TM) i9-11950H
+Core(TM) i9-11980HK
+Core(TM) i9-12900
+Core(TM) i9-12900E
+Core(TM) i9-12900F
+Core(TM) i9-12900H
+Core(TM) i9-12900HK
+Core(TM) i9-12900HX
+Core(TM) i9-12900K
+Core(TM) i9-12900KF
+Core(TM) i9-12900KS
+Core(TM) i9-12900T
+Core(TM) i9-12900TE
+Core(TM) i9-12950HX
+Core(TM) i9-13900
+Core(TM) i9-13900F
+Core(TM) i9-13900K
+Core(TM) i9-13900KF
+Core(TM) i9-13900T
+Core(TM) i9-7900X
+Core(TM) i9-7920X
+Core(TM) i9-7940X
+Core(TM) i9-7960X
+Core(TM) i9-7980XE
+Core(TM) i9-8950HK
+Core(TM) i9-9820X
+Core(TM) i9-9880H
+Core(TM) i9-9900
+Core(TM) i9-9900K
+Core(TM) i9-9900KF
+Core(TM) i9-9900KS
+Core(TM) i9-9900T
+Core(TM) i9-9900X
+Core(TM) i9-9920X
+Core(TM) i9-9940X
+Core(TM) i9-9960X
+Core(TM) i9-9980HK
+Core(TM) i9-9980XE
+Core(TM) m3-8100Y
+Pentium(R) Gold 4417U
+Pentium(R) Gold 4425Y
+Pentium(R) Gold 5405U
+Pentium(R) Gold 6405U
+Pentium(R) Gold 6500Y
+Pentium(R) Gold 6805
+Pentium(R) Gold 7505
+Pentium(R) Gold 8500
+Pentium(R) Gold 8505
+Pentium(R) Gold G5400
+Pentium(R) Gold G5400T
+Pentium(R) Gold G5420
+Pentium(R) Gold G5420T
+Pentium(R) Gold G5500
+Pentium(R) Gold G5500T
+Pentium(R) Gold G5600
+Pentium(R) Gold G5600E
+Pentium(R) Gold G5600T
+Pentium(R) Gold G5620
+Pentium(R) Gold G6400
+Pentium(R) Gold G6400E
+Pentium(R) Gold G6400T
+Pentium(R) Gold G6400TE
+Pentium(R) Gold G6405
+Pentium(R) Gold G6405T
+Pentium(R) Gold G6500
+Pentium(R) Gold G6500T
+Pentium(R) Gold G6505
+Pentium(R) Gold G6505T
+Pentium(R) Gold G6600
+Pentium(R) Gold G6605
+Pentium(R) Gold G7400
+Pentium(R) Gold G7400E
+Pentium(R) Gold G7400T
+Pentium(R) Gold G7400TE
+Pentium(R) J6426
+Pentium(R) N6415
+Pentium(R) Silver J5005
+Pentium(R) Silver J5040
+Pentium(R) Silver N5000
+Pentium(R) Silver N5030
+Pentium(R) Silver N6000
+Pentium(R) Silver N6005
+Processor N100
+Processor N200
+Xeon(R) Bronze 3104
+Xeon(R) Bronze 3106
+Xeon(R) Bronze 3204
+Xeon(R) Bronze 3206R
+Xeon(R) D-1702
+Xeon(R) D-1712TR
+Xeon(R) D-1713NT
+Xeon(R) D-1713NTE
+Xeon(R) D-1714
+Xeon(R) D-1715TER
+Xeon(R) D-1718T
+Xeon(R) D-1722NE
+Xeon(R) D-1726
+Xeon(R) D-1732TE
+Xeon(R) D-1733NT
+Xeon(R) D-1735TR
+Xeon(R) D-1736
+Xeon(R) D-1736NT
+Xeon(R) D-1739
+Xeon(R) D-1746TER
+Xeon(R) D-1747NTE
+Xeon(R) D-1748TE
+Xeon(R) D-1749NT
+Xeon(R) D-2712T
+Xeon(R) D-2733NT
+Xeon(R) D-2738
+Xeon(R) D-2752NTE
+Xeon(R) D-2752TER
+Xeon(R) D-2753NT
+Xeon(R) D-2766NT
+Xeon(R) D-2775TE
+Xeon(R) D-2776NT
+Xeon(R) D-2779
+Xeon(R) D-2786NTE
+Xeon(R) D-2795NT
+Xeon(R) D-2796NT
+Xeon(R) D-2796TE
+Xeon(R) D-2798NT
+Xeon(R) D-2799
+Xeon(R) Gold 5115
+Xeon(R) Gold 5118
+Xeon(R) Gold 5119T
+Xeon(R) Gold 5120
+Xeon(R) Gold 5120T
+Xeon(R) Gold 5122
+Xeon(R) Gold 5215
+Xeon(R) Gold 5215L
+Xeon(R) Gold 5215M
+Xeon(R) Gold 5217
+Xeon(R) Gold 5218
+Xeon(R) Gold 5218B
+Xeon(R) Gold 5218N
+Xeon(R) Gold 5218R
+Xeon(R) Gold 5218T
+Xeon(R) Gold 5220
+Xeon(R) Gold 5220R
+Xeon(R) Gold 5220S
+Xeon(R) Gold 5220T
+Xeon(R) Gold 5222
+Xeon(R) Gold 5315Y
+Xeon(R) Gold 5317
+Xeon(R) Gold 5318N
+Xeon(R) Gold 5318S
+Xeon(R) Gold 5318Y
+Xeon(R) Gold 5320
+Xeon(R) Gold 5320T
+Xeon(R) Gold 6126
+Xeon(R) Gold 6126F
+Xeon(R) Gold 6126T
+Xeon(R) Gold 6128
+Xeon(R) Gold 6130
+Xeon(R) Gold 6130F
+Xeon(R) Gold 6130T
+Xeon(R) Gold 6132
+Xeon(R) Gold 6134
+Xeon(R) Gold 6136
+Xeon(R) Gold 6138
+Xeon(R) Gold 6138F
+Xeon(R) Gold 6138P
+Xeon(R) Gold 6138T
+Xeon(R) Gold 6140
+Xeon(R) Gold 6142
+Xeon(R) Gold 6142F
+Xeon(R) Gold 6144
+Xeon(R) Gold 6146
+Xeon(R) Gold 6148
+Xeon(R) Gold 6148F
+Xeon(R) Gold 6150
+Xeon(R) Gold 6152
+Xeon(R) Gold 6154
+Xeon(R) Gold 6208U
+Xeon(R) Gold 6209U
+Xeon(R) Gold 6210U
+Xeon(R) Gold 6212U
+Xeon(R) Gold 6222V
+Xeon(R) Gold 6226
+Xeon(R) Gold 6226R
+Xeon(R) Gold 6230
+Xeon(R) Gold 6230N
+Xeon(R) Gold 6230R
+Xeon(R) Gold 6230T
+Xeon(R) Gold 6234
+Xeon(R) Gold 6238
+Xeon(R) Gold 6238L
+Xeon(R) Gold 6238M
+Xeon(R) Gold 6238R
+Xeon(R) Gold 6238T
+Xeon(R) Gold 6240
+Xeon(R) Gold 6240L
+Xeon(R) Gold 6240M
+Xeon(R) Gold 6240R
+Xeon(R) Gold 6240Y
+Xeon(R) Gold 6242
+Xeon(R) Gold 6242R
+Xeon(R) Gold 6244
+Xeon(R) Gold 6246
+Xeon(R) Gold 6246R
+Xeon(R) Gold 6248
+Xeon(R) Gold 6248R
+Xeon(R) Gold 6250
+Xeon(R) Gold 6250L
+Xeon(R) Gold 6252
+Xeon(R) Gold 6252N
+Xeon(R) Gold 6254
+Xeon(R) Gold 6256
+Xeon(R) Gold 6258R
+Xeon(R) Gold 6262V
+Xeon(R) Gold 6312U
+Xeon(R) Gold 6314U
+Xeon(R) Gold 6326
+Xeon(R) Gold 6330
+Xeon(R) Gold 6330N
+Xeon(R) Gold 6334
+Xeon(R) Gold 6336Y
+Xeon(R) Gold 6338
+Xeon(R) Gold 6338N
+Xeon(R) Gold 6338T
+Xeon(R) Gold 6342
+Xeon(R) Gold 6346
+Xeon(R) Gold 6348
+Xeon(R) Gold 6354
+Xeon(R) Platinum 8153
+Xeon(R) Platinum 8156
+Xeon(R) Platinum 8158
+Xeon(R) Platinum 8160
+Xeon(R) Platinum 8160F
+Xeon(R) Platinum 8160T
+Xeon(R) Platinum 8164
+Xeon(R) Platinum 8168
+Xeon(R) Platinum 8170
+Xeon(R) Platinum 8171M
+Xeon(R) Platinum 8176
+Xeon(R) Platinum 8176F
+Xeon(R) Platinum 8180
+Xeon(R) Platinum 8253
+Xeon(R) Platinum 8256
+Xeon(R) Platinum 8260
+Xeon(R) Platinum 8260L
+Xeon(R) Platinum 8260M
+Xeon(R) Platinum 8260Y
+Xeon(R) Platinum 8268
+Xeon(R) Platinum 8270
+Xeon(R) Platinum 8272CL
+Xeon(R) Platinum 8276
+Xeon(R) Platinum 8276L
+Xeon(R) Platinum 8276M
+Xeon(R) Platinum 8280
+Xeon(R) Platinum 8280L
+Xeon(R) Platinum 8280M
+Xeon(R) Platinum 8351N
+Xeon(R) Platinum 8352M
+Xeon(R) Platinum 8352S
+Xeon(R) Platinum 8352V
+Xeon(R) Platinum 8352Y
+Xeon(R) Platinum 8358
+Xeon(R) Platinum 8358P
+Xeon(R) Platinum 8360Y
+Xeon(R) Platinum 8362
+Xeon(R) Platinum 8368
+Xeon(R) Platinum 8368Q
+Xeon(R) Platinum 8380
+Xeon(R) Platinum 9221
+Xeon(R) Platinum 9222
+Xeon(R) Platinum 9242
+Xeon(R) Platinum 9282
+Xeon(R) Silver 4108
+Xeon(R) Silver 4109T
+Xeon(R) Silver 4110
+Xeon(R) Silver 4112
+Xeon(R) Silver 4114
+Xeon(R) Silver 4114T
+Xeon(R) Silver 4116
+Xeon(R) Silver 4116T
+Xeon(R) Silver 4208
+Xeon(R) Silver 4209T
+Xeon(R) Silver 4210
+Xeon(R) Silver 4210R
+Xeon(R) Silver 4210T
+Xeon(R) Silver 4214
+Xeon(R) Silver 4214R
+Xeon(R) Silver 4214Y
+Xeon(R) Silver 4215
+Xeon(R) Silver 4215R
+Xeon(R) Silver 4216
+Xeon(R) Silver 4309Y
+Xeon(R) Silver 4310
+Xeon(R) Silver 4310T
+Xeon(R) Silver 4314
+Xeon(R) Silver 4316
+Xeon(R) W-10855M
+Xeon(R) W-10885M
+Xeon(R) W-11055M
+Xeon(R) W-11155MLE
+Xeon(R) W-11155MRE
+Xeon(R) W-11555MLE
+Xeon(R) W-11555MRE
+Xeon(R) W-11855M
+Xeon(R) W-11855M
+Xeon(R) W-11865MLE
+Xeon(R) W-11865MRE
+Xeon(R) W-11955M
+Xeon(R) W-1250
+Xeon(R) W-1250E
+Xeon(R) W-1250P
+Xeon(R) W-1250TE
+Xeon(R) W-1270
+Xeon(R) W-1270E
+Xeon(R) W-1270P
+Xeon(R) W-1270TE
+Xeon(R) W-1290
+Xeon(R) W-1290E
+Xeon(R) W-1290P
+Xeon(R) W-1290T
+Xeon(R) W-1290TE
+Xeon(R) W-1350
+Xeon(R) W-1350P
+Xeon(R) W-1370
+Xeon(R) W-1370P
+Xeon(R) W-1390
+Xeon(R) W-1390P
+Xeon(R) W-1390T
+Xeon(R) W-2102
+Xeon(R) W-2104
+Xeon(R) W-2123
+Xeon(R) W-2125
+Xeon(R) W-2133
+Xeon(R) W-2135
+Xeon(R) W-2145
+Xeon(R) W-2155
+Xeon(R) W-2175
+Xeon(R) W-2195
+Xeon(R) W-2223
+Xeon(R) W-2225
+Xeon(R) W-2235
+Xeon(R) W-2245
+Xeon(R) W-2255
+Xeon(R) W-2265
+Xeon(R) W-2275
+Xeon(R) W-2295
+Xeon(R) W-3175X
+Xeon(R) W-3223
+Xeon(R) W-3225
+Xeon(R) W-3235
+Xeon(R) W-3245
+Xeon(R) W-3245M
+Xeon(R) W-3265
+Xeon(R) W-3265M
+Xeon(R) W-3275
+Xeon(R) W-3275M
+Xeon(R) W-3323
+Xeon(R) W-3335
+Xeon(R) W-3345
+Xeon(R) W-3365
+Xeon(R) W-3375
+EOF