diff --git a/apps/pusher_app_prem/appserver/static/git_pusher.js b/apps/pusher_app_prem/appserver/static/git_pusher.js
new file mode 100644
index 00000000..b65c4ae9
--- /dev/null
+++ b/apps/pusher_app_prem/appserver/static/git_pusher.js
@@ -0,0 +1,565 @@
+// ============================================
+// CHARGER LES DASHBOARDS DYNAMIQUEMENT
+// ============================================
+
+// Charger les applications
+function loadAvailableApps() {
+ console.log("loadAvailableApps called");
+
+ const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
+
+ fetch(apiUrl)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('HTTP ' + response.status);
+ }
+ return response.json();
+ })
+ .then(data => {
+ console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
+
+ if (data.entry && data.entry.length > 0) {
+ const apps = data.entry
+ .filter(item => {
+ // Filtrer les apps système
+ const isHidden = item.content && item.content.is_visible === 0;
+ const appName = item.name;
+ // Exclure les apps système
+ const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
+ return !isHidden && !systemApps.includes(appName);
+ })
+ .map(item => ({
+ id: item.name,
+ name: item.content.label || item.name,
+ description: item.content.description || ''
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+ console.log("Filtered apps: " + apps.length);
+ populateAppsList(apps);
+ } else {
+ console.warn("No apps found");
+ showAppsEmpty();
+ }
+ })
+ .catch(error => {
+ console.error("API Error:", error);
+ showAppsEmpty();
+ });
+}
+
+function populateAppsList(apps) {
+ console.log("populateAppsList called with", apps.length, "apps");
+
+ const container = document.getElementById('dashboard-list');
+
+ if (!container) {
+ console.error("dashboard-list container not found");
+ return;
+ }
+
+ if (!apps || apps.length === 0) {
+ showAppsEmpty();
+ return;
+ }
+
+ let html = '
';
+
+ apps.forEach((app, index) => {
+ const checkboxId = 'app-' + index;
+ html += '';
+ html += '
';
+ html += '
';
+ html += '
';
+ });
+
+ container.innerHTML = html;
+ console.log('Successfully populated ' + apps.length + ' apps');
+
+ // Ajouter les event listeners
+ addCheckboxListeners();
+}
+
+function addCheckboxListeners() {
+ console.log("addCheckboxListeners called");
+
+ const selectAllCheckbox = document.getElementById('select-all');
+ if (selectAllCheckbox) {
+ selectAllCheckbox.addEventListener('change', function() {
+ toggleSelectAll(this);
+ });
+ }
+
+ const appCheckboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
+ appCheckboxes.forEach(checkbox => {
+ checkbox.addEventListener('change', function() {
+ console.log("App checkbox changed");
+ });
+ });
+}
+
+function showAppsEmpty() {
+ const container = document.getElementById('dashboard-list');
+ if (container) {
+ container.innerHTML = 'No apps found
';
+ }
+ console.log("Displayed empty state");
+}
+
+// Attendre que le DOM soit chargé
+function initScript() {
+ console.log("initScript called");
+
+ // INITIALISER LA LICENCE EN PREMIER
+ initializeLicense();
+
+ // Charger les applications
+ loadAvailableApps();
+
+ // Charger les credentials sauvegardés
+ loadSavedCredentials();
+
+ // Attacher les event listeners au bouton
+ const pushBtn = document.getElementById('push-btn');
+ if (pushBtn) {
+ console.log("Push button found, attaching listener");
+ pushBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ console.log("Push button clicked!");
+ pushDashboards();
+ });
+ } else {
+ console.warn("Push button not found");
+ }
+
+ // Attacher l'event listener pour sauvegarder les credentials
+ const saveCheckbox = document.getElementById('save-credentials');
+ if (saveCheckbox) {
+ saveCheckbox.addEventListener('change', function() {
+ if (this.checked) {
+ saveCredentials();
+ } else {
+ clearSavedCredentials();
+ }
+ });
+ }
+}
+
+function loadSavedCredentials() {
+ console.log("Loading saved credentials...");
+
+ try {
+ const savedUrl = getCookie('git_pusher_url');
+ const savedToken = getCookie('git_pusher_token');
+ const savedBranch = getCookie('git_pusher_branch');
+
+ if (savedUrl) {
+ document.getElementById('git-url').value = decodeURIComponent(savedUrl);
+ console.log("Loaded saved URL from cookie");
+ }
+
+ if (savedToken) {
+ document.getElementById('git-token').value = decodeURIComponent(savedToken);
+ console.log("Loaded saved token from cookie");
+ }
+
+ if (savedBranch) {
+ document.getElementById('git-branch').value = decodeURIComponent(savedBranch);
+ }
+
+ if (savedUrl && savedToken) {
+ document.getElementById('save-credentials').checked = true;
+ }
+ } catch (e) {
+ console.warn("Could not load saved credentials:", e);
+ }
+}
+
+function saveCredentials() {
+ console.log("Saving credentials...");
+
+ try {
+ const gitUrl = document.getElementById('git-url').value;
+ const gitToken = document.getElementById('git-token').value;
+ const gitBranch = document.getElementById('git-branch').value;
+
+ if (gitUrl && gitToken) {
+ setCookie('git_pusher_url', gitUrl, 30);
+ setCookie('git_pusher_token', gitToken, 30);
+ setCookie('git_pusher_branch', gitBranch, 30);
+ console.log("Credentials saved to cookies");
+ showSuccess("Credentials saved locally");
+ } else {
+ showError("Please fill in URL and Token before saving");
+ }
+ } catch (e) {
+ console.error("Error saving credentials:", e);
+ showError("Could not save credentials");
+ }
+}
+
+function clearSavedCredentials() {
+ console.log("Clearing saved credentials...");
+
+ try {
+ deleteCookie('git_pusher_url');
+ deleteCookie('git_pusher_token');
+ deleteCookie('git_pusher_branch');
+ console.log("Credentials cleared from cookies");
+ showSuccess("Credentials cleared");
+ } catch (e) {
+ console.error("Error clearing credentials:", e);
+ }
+}
+
+// Fonctions utilitaires pour les cookies
+function setCookie(name, value, days) {
+ const d = new Date();
+ d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
+ const expires = "expires=" + d.toUTCString();
+ document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
+ console.log("Cookie set: " + name);
+}
+
+function getCookie(name) {
+ const nameEQ = name + "=";
+ const ca = document.cookie.split(';');
+ for (let i = 0; i < ca.length; i++) {
+ let c = ca[i].trim();
+ if (c.indexOf(nameEQ) === 0) {
+ return c.substring(nameEQ.length);
+ }
+ }
+ return "";
+}
+
+function deleteCookie(name) {
+ setCookie(name, "", -1);
+ console.log("Cookie deleted: " + name);
+}
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ console.log("DOM Ready - Initializing script...");
+ setTimeout(function() {
+ initScript();
+ }, 1000);
+ });
+} else {
+ console.log("DOM already ready - Initializing script...");
+ setTimeout(function() {
+ initScript();
+ }, 1000);
+}
+
+function getFormKeyValue() {
+ try {
+ // Chercher dans les meta tags du DOM
+ const metaTag = document.querySelector('meta[name="splunk_form_key"]');
+ if (metaTag) {
+ return metaTag.getAttribute('content');
+ }
+
+ // Chercher dans les cookies
+ const cookies = document.cookie.split(';');
+ for (let cookie of cookies) {
+ const [name, value] = cookie.trim().split('=');
+ if (name === 'splunk_form_key') {
+ return decodeURIComponent(value);
+ }
+ }
+
+ console.warn("Could not find form key, proceeding without it");
+ return '';
+ } catch (e) {
+ console.error("Error getting form key:", e);
+ return '';
+ }
+}
+
+function loadAvailableApps() {
+ console.log("loadAvailableApps called");
+
+ const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
+
+ fetch(apiUrl)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('HTTP ' + response.status);
+ }
+ return response.json();
+ })
+ .then(data => {
+ console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
+
+ if (data.entry && data.entry.length > 0) {
+ const apps = data.entry
+ .filter(item => {
+ // Filtrer les apps système
+ const isHidden = item.content && item.content.is_visible === 0;
+ const appName = item.name;
+ // Exclure les apps système
+ const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
+ return !isHidden && !systemApps.includes(appName);
+ })
+ .map(item => ({
+ id: item.name,
+ name: item.content.label || item.name,
+ description: item.content.description || ''
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+ console.log("Filtered apps: " + apps.length);
+ populateAppsList(apps);
+ } else {
+ console.warn("No apps found");
+ showAppsEmpty();
+ }
+ })
+ .catch(error => {
+ console.error("API Error:", error);
+ showAppsEmpty();
+ });
+}
+
+function populateAppsList(apps) {
+ console.log("populateAppsList called with", apps.length, "apps");
+
+ const container = document.getElementById('dashboard-list');
+
+ if (!container) {
+ console.error("dashboard-list container not found");
+ return;
+ }
+
+ if (!apps || apps.length === 0) {
+ showAppsEmpty();
+ return;
+ }
+
+ let html = '';
+ html += '';
+ html += '';
+ html += '
';
+
+ apps.forEach((app, index) => {
+ const checkboxId = 'app-' + index;
+ html += '';
+ html += '
';
+ html += '
';
+ html += '
';
+ });
+
+ container.innerHTML = html;
+ console.log('Successfully populated ' + apps.length + ' apps');
+}
+
+function showAppsEmpty() {
+ const container = document.getElementById('dashboard-list');
+ if (container) {
+ container.innerHTML = 'No apps found
';
+ }
+ console.log("Displayed empty state");
+}
+
+function toggleSelectAll(checkbox) {
+ const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
+ checkboxes.forEach(cb => cb.checked = checkbox.checked);
+}
+
+// ============================================
+// POUSSER LES DASHBOARDS VERS GIT
+// ============================================
+
+function pushDashboards() {
+
+ // Vérifier la licence
+ if (!checkLicenseBeforePush()) {
+ return;
+ }
+ console.log("pushDashboards called");
+
+ const gitUrl = document.getElementById('git-url').value;
+ const gitBranch = document.getElementById('git-branch').value;
+ const gitToken = document.getElementById('git-token').value;
+ const commitMessage = document.getElementById('commit-message').value;
+
+ console.log("Git URL:", gitUrl);
+ console.log("Git Branch:", gitBranch);
+ console.log("Commit Message:", commitMessage);
+
+ const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"]:not(#select-all):checked');
+ const selectedApps = Array.from(checkboxes).map(cb => ({
+ id: cb.value,
+ name: cb.getAttribute('data-name')
+ }));
+
+ console.log("Selected apps:", selectedApps);
+
+ // Validation
+ if (!gitUrl.trim()) {
+ console.warn("Validation failed: No Git URL");
+ showError('Please enter a Git repository URL');
+ return;
+ }
+
+ if (!gitToken.trim()) {
+ console.warn("Validation failed: No Git token");
+ showError('Please enter your Git token or password');
+ return;
+ }
+
+ if (!commitMessage.trim()) {
+ console.warn("Validation failed: No commit message");
+ showError('Please enter a commit message');
+ return;
+ }
+
+ if (selectedApps.length === 0) {
+ console.warn("Validation failed: No apps selected");
+ showError('Please select at least one application');
+ return;
+ }
+
+ console.log("Validation passed, showing loading state...");
+
+ // Afficher le loading
+ document.getElementById('loading').style.display = 'block';
+ document.getElementById('success-msg').style.display = 'none';
+ document.getElementById('error-msg').style.display = 'none';
+ document.getElementById('push-btn').disabled = true;
+
+ // Sauvegarder les credentials si la case est cochée
+ if (document.getElementById('save-credentials').checked) {
+ try {
+ setCookie('git_pusher_url', gitUrl, 30);
+ setCookie('git_pusher_token', gitToken, 30);
+ setCookie('git_pusher_branch', gitBranch, 30);
+ console.log("Credentials auto-saved to cookies");
+ } catch (e) {
+ console.warn("Could not auto-save credentials:", e);
+ }
+ }
+
+ // Préparer les données - passer les apps au lieu des dashboards
+ const payload = {
+ git_url: gitUrl,
+ git_branch: gitBranch,
+ git_token: gitToken,
+ apps: selectedApps,
+ commit_message: commitMessage,
+ timestamp: new Date().toISOString(),
+ user: getCurrentUser()
+ };
+
+ console.log("Payload prepared:", payload);
+
+ // Appeler le script Python via serveur
+ callPushScript(payload);
+}
+
+function callPushScript(payload) {
+ console.log("callPushScript called");
+ console.log("Payload:", payload);
+
+ // Construire l'URL vers le serveur Python sur le port 9999 en HTTP
+ const hostname = window.location.hostname;
+ const baseUrl = `http://${hostname}:9999`;
+
+ const url = new URL('/push', baseUrl);
+
+ // Ajouter les paramètres en query string
+ url.searchParams.append('git_url', payload.git_url);
+ url.searchParams.append('git_branch', payload.git_branch);
+ url.searchParams.append('git_token', payload.git_token);
+ url.searchParams.append('commit_message', payload.commit_message);
+
+ // Encoder correctement les apps en JSON
+ const appsJson = JSON.stringify(payload.apps);
+ console.log("Apps JSON:", appsJson);
+ url.searchParams.append('apps', appsJson);
+
+ url.searchParams.append('user', payload.user);
+
+ console.log("Calling:", url.toString());
+
+ fetch(url.toString(), {
+ method: 'POST',
+ mode: 'no-cors'
+ })
+ .then(response => {
+ // Avec no-cors, on ne peut pas lire response.json()
+ // Donc on suppose que si la requête arrive au serveur, c'est bon
+ console.log("Request sent successfully");
+ document.getElementById('loading').style.display = 'none';
+ document.getElementById('push-btn').disabled = false;
+ showSuccess('Push request sent! Check server logs for details.');
+ resetForm();
+ return;
+ })
+ .catch(error => {
+ console.error('Fetch error:', error);
+ document.getElementById('loading').style.display = 'none';
+ document.getElementById('push-btn').disabled = false;
+ showError('Network error: ' + error.message);
+ });
+}
+
+function getCurrentUser() {
+ try {
+ // Essayer plusieurs méthodes
+ if (Splunk && Splunk.util && typeof Splunk.util.getCurrentUser === 'function') {
+ return Splunk.util.getCurrentUser();
+ }
+
+ // Fallback: chercher dans le DOM ou retourner 'unknown'
+ return 'unknown_user';
+ } catch (e) {
+ console.warn("Could not get current user:", e);
+ return 'unknown_user';
+ }
+}
+
+function showSuccess(message) {
+ const successMsg = document.getElementById('success-msg');
+ document.getElementById('success-text').textContent = message;
+ successMsg.style.display = 'block';
+ setTimeout(() => {
+ successMsg.style.display = 'none';
+ }, 5000);
+}
+
+function showError(message) {
+ const errorMsg = document.getElementById('error-msg');
+ document.getElementById('error-text').textContent = 'X ' + message;
+ errorMsg.style.display = 'block';
+}
+
+function resetForm(showConfirm = false) {
+ document.getElementById('git-url').value = '';
+ document.getElementById('git-branch').value = 'main';
+ document.getElementById('git-token').value = '';
+ document.getElementById('commit-message').value = '';
+ document.querySelectorAll('#dashboard-list input[type="checkbox"]').forEach(cb => cb.checked = false);
+
+ // Demander si l'utilisateur veut aussi effacer les credentials sauvegardés
+ // SEULEMENT si showConfirm = true (c'est-à-dire si l'utilisateur a cliqué sur Reset)
+ if (showConfirm && document.getElementById('save-credentials').checked) {
+ const confirmClear = confirm('Do you want to clear saved credentials?');
+ if (confirmClear) {
+ clearSavedCredentials();
+ document.getElementById('save-credentials').checked = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/pusher_app_prem/appserver/static/license_validation.js b/apps/pusher_app_prem/appserver/static/license_validation.js
new file mode 100644
index 00000000..b668a4a8
--- /dev/null
+++ b/apps/pusher_app_prem/appserver/static/license_validation.js
@@ -0,0 +1,368 @@
+// ============================================
+// SYSTÈME DE VALIDATION DE LICENCE
+// ============================================
+
+const LICENSE_STORAGE_KEY = 'git_pusher_license';
+
+function initializeLicense() {
+ console.log("Initializing license system...");
+
+ // Vérifier si une licence est déjà stockée
+ const storedLicense = getCookie(LICENSE_STORAGE_KEY);
+
+ if (storedLicense) {
+ // Valider la licence stockée
+ validateStoredLicense(storedLicense);
+ // Afficher les infos de licence
+ displayLicenseInfo(storedLicense);
+ } else {
+ // Afficher la page de licence
+ showLicenseModal();
+ }
+}
+
+function displayLicenseInfo(license) {
+ console.log("Displaying license info...");
+
+ // Chercher le container du badge
+ const container = document.getElementById('license-badge-container');
+ if (!container) {
+ console.error("license-badge-container not found");
+ return;
+ }
+
+ // Créer le badge
+ const badge = document.createElement('div');
+ badge.id = 'license-badge';
+ badge.style.cssText = `
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 12px 20px;
+ border-radius: 8px;
+ font-size: 12px;
+ font-weight: 600;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ text-align: center;
+ min-width: 200px;
+ `;
+
+ let badgeText = '✓ Licence Activée';
+
+ // Si c'est une licence d'essai
+ if (license.startsWith('TRIAL-')) {
+ const daysRemaining = getTrialDaysRemaining(license);
+
+ if (daysRemaining <= 0) {
+ badgeText = '⏱️ Essai expiré';
+ badge.style.background = 'linear-gradient(135deg, #f44336 0%, #da190b 100%)';
+ } else if (daysRemaining <= 2) {
+ badgeText = `⚠️ ${daysRemaining} jour${daysRemaining > 1 ? 's' : ''} restant${daysRemaining > 1 ? 's' : ''}`;
+ badge.style.background = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';
+ } else {
+ badgeText = `⏱️ Essai: ${daysRemaining} jours`;
+ }
+ }
+
+ badge.textContent = badgeText;
+ badge.onclick = function() {
+ alert('Licence: ' + license.substring(0, 50) + '...\n\nClique sur le logo pour gérer ta licence.');
+ };
+
+ container.appendChild(badge);
+
+ // Ajouter un hover effect
+ badge.addEventListener('mouseenter', function() {
+ this.style.transform = 'translateY(-3px)';
+ this.style.boxShadow = '0 6px 25px rgba(102, 126, 234, 0.5)';
+ });
+
+ badge.addEventListener('mouseleave', function() {
+ this.style.transform = 'translateY(0)';
+ this.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.3)';
+ });
+}
+
+function getTrialDaysRemaining(trialLicense) {
+ // Extraire le timestamp du license (format: TRIAL-timestamp)
+ const parts = trialLicense.split('-');
+ if (parts.length !== 2) return 0;
+
+ const timestamp = parseInt(parts[1]);
+ if (isNaN(timestamp)) return 0;
+
+ // Créer la date de création
+ const createdDate = new Date(timestamp);
+
+ // Ajouter 7 jours
+ const expirationDate = new Date(createdDate.getTime() + (7 * 24 * 60 * 60 * 1000));
+
+ // Calculer les jours restants
+ const now = new Date();
+ const daysRemaining = Math.ceil((expirationDate - now) / (1000 * 60 * 60 * 24));
+
+ console.log("Trial created:", createdDate);
+ console.log("Trial expires:", expirationDate);
+ console.log("Days remaining:", daysRemaining);
+
+ return Math.max(0, daysRemaining);
+}
+
+function showLicenseModal() {
+ console.log("Showing license modal");
+
+ // Créer le modal HTML
+ const modal = document.createElement('div');
+ modal.id = 'license-modal';
+ modal.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ `;
+
+ const content = document.createElement('div');
+ content.style.cssText = `
+ background: white;
+ border-radius: 16px;
+ padding: 40px;
+ max-width: 500px;
+ width: 90%;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ `;
+
+ content.innerHTML = `
+
+
🔐 Git Pusher
+
Activation de licence requise
+
+
+
+
+ 📋 Hostname détecté: Chargement...
+
+
+
+
+
+
+
+ Vous n'avez pas de licence? Cliquez ici
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.appendChild(content);
+ document.body.appendChild(modal);
+
+ // Afficher le hostname
+ getHostname().then(hostname => {
+ document.getElementById('detected-hostname').textContent = hostname;
+ });
+}
+
+function showGeneratorInfo() {
+ alert(`Pour générer une clé de licence, exécutez sur le serveur Splunk:
+
+python /opt/splunk/etc/apps/pusher_app/bin/license_generator.py
+
+Cela générera une clé basée sur votre hostname.`);
+}
+
+function getHostname() {
+ return new Promise((resolve) => {
+ fetch('/en-US/splunkd/__raw/services/server/info?output_mode=json')
+ .then(r => r.json())
+ .then(d => {
+ const hostname = d.entry?.[0]?.content?.host || 'unknown';
+ resolve(hostname);
+ })
+ .catch(() => resolve('unknown'));
+ });
+}
+
+function validateLicenseInput() {
+ const licenseInput = document.getElementById('license-input').value.trim();
+
+ if (!licenseInput) {
+ showLicenseMessage('Veuillez entrer une clé de licence', 'error');
+ return;
+ }
+
+ // Afficher le message de chargement
+ showLicenseMessage('Validation en cours...', 'info');
+
+ // Simuler la validation (en production, faire un appel à un serveur)
+ // Pour l'instant, on accepte juste n'importe quelle licence
+ if (licenseInput.length > 20) {
+ // Stocker la licence
+ setCookie(LICENSE_STORAGE_KEY, licenseInput, 365);
+
+ showLicenseMessage('✓ Licence activée avec succès!', 'success');
+
+ setTimeout(() => {
+ closeLicenseModal();
+ // Afficher les infos de licence
+ displayLicenseInfo(licenseInput);
+ }, 1500);
+ } else {
+ showLicenseMessage('Format de licence invalide', 'error');
+ }
+}
+
+function skipLicense() {
+ // Créer une licence d'essai avec timestamp (format: TRIAL-timestamp)
+ const trialLicense = 'TRIAL-' + Date.now();
+ setCookie(LICENSE_STORAGE_KEY, trialLicense, 7);
+
+ const messageEl = document.getElementById('license-message');
+ messageEl.style.display = 'block';
+ messageEl.style.background = '#fff3cd';
+ messageEl.style.color = '#856404';
+ messageEl.style.border = '1px solid #ffeaa7';
+ messageEl.textContent = '⏱️ Mode essai activé pour 7 jours';
+
+ setTimeout(() => {
+ closeLicenseModal();
+ // Afficher les infos de licence
+ displayLicenseInfo(trialLicense);
+ }, 1500);
+}
+
+function showLicenseMessage(message, type) {
+ const messageEl = document.getElementById('license-message');
+ messageEl.style.display = 'block';
+ messageEl.textContent = message;
+
+ if (type === 'success') {
+ messageEl.style.background = '#d4edda';
+ messageEl.style.color = '#155724';
+ messageEl.style.border = '1px solid #c3e6cb';
+ } else if (type === 'error') {
+ messageEl.style.background = '#f8d7da';
+ messageEl.style.color = '#721c24';
+ messageEl.style.border = '1px solid #f5c6cb';
+ } else if (type === 'info') {
+ messageEl.style.background = '#d1ecf1';
+ messageEl.style.color = '#0c5460';
+ messageEl.style.border = '1px solid #bee5eb';
+ }
+}
+
+function validateStoredLicense(license) {
+ console.log("Validating stored license...");
+
+ // Pour l'instant, accepter simplement la licence stockée
+ // En production, faire une validation serveur
+ if (license && license.length > 5) {
+ console.log("License is valid");
+ return true;
+ }
+
+ // Si invalide, afficher le modal à nouveau
+ showLicenseModal();
+ return false;
+}
+
+function closeLicenseModal() {
+ const modal = document.getElementById('license-modal');
+ if (modal) {
+ modal.remove();
+ }
+}
+
+function checkLicenseBeforePush() {
+ const license = getCookie(LICENSE_STORAGE_KEY);
+
+ if (!license) {
+ alert('Veuillez d\'abord activer une licence');
+ showLicenseModal();
+ return false;
+ }
+
+ // Vérifier si c'est une licence d'essai expirée
+ if (license.startsWith('TRIAL-')) {
+ // À implémenter : vérifier la date
+ }
+
+ return true;
+}
+
+// ============================================
+// FONCTIONS UTILITAIRES DE COOKIE
+// ============================================
+
+function setCookie(name, value, days) {
+ const d = new Date();
+ d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
+ const expires = "expires=" + d.toUTCString();
+ document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
+ console.log("Cookie set: " + name);
+}
+
+function getCookie(name) {
+ const nameEQ = name + "=";
+ const ca = document.cookie.split(';');
+ for (let i = 0; i < ca.length; i++) {
+ let c = ca[i].trim();
+ if (c.indexOf(nameEQ) === 0) {
+ return decodeURIComponent(c.substring(nameEQ.length));
+ }
+ }
+ return "";
+}
+
+function deleteCookie(name) {
+ setCookie(name, "", -1);
+ console.log("Cookie deleted: " + name);
+}
\ No newline at end of file
diff --git a/apps/pusher_app_prem/bin/README b/apps/pusher_app_prem/bin/README
new file mode 100644
index 00000000..9a70db09
--- /dev/null
+++ b/apps/pusher_app_prem/bin/README
@@ -0,0 +1 @@
+This is where you put any scripts you want to add to this app.
diff --git a/apps/pusher_app_prem/bin/git_pusher.pid b/apps/pusher_app_prem/bin/git_pusher.pid
new file mode 100644
index 00000000..81d2a971
--- /dev/null
+++ b/apps/pusher_app_prem/bin/git_pusher.pid
@@ -0,0 +1 @@
+919562
diff --git a/apps/pusher_app_prem/bin/git_pusher.py b/apps/pusher_app_prem/bin/git_pusher.py
new file mode 100644
index 00000000..6f0c0ceb
--- /dev/null
+++ b/apps/pusher_app_prem/bin/git_pusher.py
@@ -0,0 +1,340 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+import json
+import logging
+import tempfile
+import shutil
+import subprocess
+from datetime import datetime
+from http.server import HTTPServer, BaseHTTPRequestHandler
+from urllib.parse import parse_qs, urlparse
+
+# Configuration du logging
+log_dir = '/opt/splunk/var/log/splunk'
+os.makedirs(log_dir, exist_ok=True)
+
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.FileHandler(os.path.join(log_dir, 'git_pusher.log')),
+ logging.StreamHandler()
+ ]
+)
+logger = logging.getLogger('git_pusher')
+
+
+class GitPusherRequestHandler(BaseHTTPRequestHandler):
+ """Handler pour les requêtes HTTP"""
+
+ def do_OPTIONS(self):
+ """Traiter les requêtes OPTIONS (CORS preflight)"""
+ self.send_response(200)
+ self.send_header('Access-Control-Allow-Origin', '*')
+ self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
+ self.end_headers()
+
+ def do_POST(self):
+ """Traiter les requêtes POST"""
+ # Envoyer les headers CORS EN PREMIER
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.send_header('Access-Control-Allow-Origin', '*')
+ self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
+ self.end_headers()
+
+ try:
+ logger.info(f"POST request to {self.path}")
+
+ # Parser l'URL et les paramètres
+ parsed_url = urlparse(self.path)
+ query_params = parse_qs(parsed_url.query)
+
+ logger.info(f"Query params keys: {list(query_params.keys())}")
+
+ # Extraire les paramètres
+ git_url = query_params.get('git_url', [''])[0]
+ git_branch = query_params.get('git_branch', ['main'])[0]
+ git_token = query_params.get('git_token', [''])[0]
+ commit_message = query_params.get('commit_message', [''])[0]
+
+ # Accepter soit 'apps' soit 'dashboards'
+ apps_json = query_params.get('apps', query_params.get('dashboards', ['[]']))[0]
+ user = query_params.get('user', ['unknown'])[0]
+
+ logger.info(f"Parameters received: git_url={git_url}, branch={git_branch}, user={user}")
+ logger.info(f"Raw apps_json: '{apps_json}'")
+
+ # Parser les apps
+ try:
+ # parse_qs décode déjà, mais au cas où
+ if isinstance(apps_json, str):
+ apps = json.loads(apps_json)
+ else:
+ apps = apps_json
+ except (json.JSONDecodeError, TypeError) as e:
+ logger.error(f"JSON parse error: {e} - trying to parse: {apps_json}")
+ apps = []
+
+ logger.info(f"Parsed apps: {len(apps)} items - {apps}")
+
+ # Valider
+ if not git_url or not git_token or not commit_message or not apps:
+ logger.warning(f"Validation failed: git_url={bool(git_url)}, git_token={bool(git_token)}, commit_message={bool(commit_message)}, apps={len(apps)}")
+ response = {
+ 'status': 'error',
+ 'message': 'Missing required parameters'
+ }
+ self.wfile.write(json.dumps(response).encode())
+ return
+
+ # Créer un répertoire temporaire
+ temp_dir = tempfile.mkdtemp(prefix='splunk_git_')
+ logger.info(f"Created temp directory: {temp_dir}")
+
+ try:
+ # Préparer l'URL Git avec le token
+ git_url_with_token = self.prepare_git_url(git_url, git_token)
+
+ logger.info(f"Git URL prepared (token inserted)")
+ logger.debug(f"Git URL with token: {git_url_with_token}")
+ logger.info("Cloning repository...")
+ self.clone_repository(temp_dir, git_url_with_token, git_branch)
+
+ # Récupérer TOUTES les applications (dossiers complets)
+ logger.info("Fetching applications from Splunk...")
+ dashboard_contents = self.fetch_apps_directories(apps)
+
+ # Créer le dossier apps
+ apps_dir = os.path.join(temp_dir, 'apps')
+ os.makedirs(apps_dir, exist_ok=True)
+
+ # Copier les applications
+ logger.info("Copying applications to repository...")
+ for app_data in dashboard_contents:
+ app_name = app_data['name']
+ app_path = app_data['path']
+ dest_path = os.path.join(apps_dir, app_name)
+
+ if os.path.exists(app_path):
+ logger.info(f"Copying app {app_name} from {app_path}")
+ # Supprimer le dossier s'il existe déjà
+ if os.path.exists(dest_path):
+ logger.info(f"Removing existing app directory: {dest_path}")
+ shutil.rmtree(dest_path)
+ # Copier le dossier
+ shutil.copytree(app_path, dest_path)
+ logger.info(f"Copied app: {app_name}")
+ else:
+ logger.warning(f"App path not found: {app_path}")
+
+ # Configurer git
+ logger.info("Configuring git...")
+ subprocess.run(['git', 'config', 'user.email', 'splunk@splunk.local'],
+ cwd=temp_dir, capture_output=True)
+ subprocess.run(['git', 'config', 'user.name', 'Splunk Git Pusher'],
+ cwd=temp_dir, capture_output=True)
+
+ # Commit et push
+ logger.info("Adding files...")
+ subprocess.run(['git', 'add', '-A'], cwd=temp_dir, capture_output=True)
+
+ full_message = f"{commit_message}\n\nPushed by: {user}\nTimestamp: {datetime.now().isoformat()}"
+ logger.info("Committing...")
+ result = subprocess.run(['git', 'commit', '-m', full_message],
+ cwd=temp_dir, capture_output=True, text=True)
+
+ if result.returncode != 0:
+ logger.warning(f"Commit may have failed or had no changes: {result.stderr}")
+
+ logger.info("Pushing...")
+ result = subprocess.run(['git', 'push', 'origin', git_branch],
+ cwd=temp_dir, capture_output=True, text=True, timeout=60)
+
+ if result.returncode != 0:
+ raise Exception(f"Push failed: {result.stderr}")
+
+ logger.info("Push successful!")
+ response = {
+ 'status': 'success',
+ 'message': f'Successfully pushed {len(dashboard_contents)} dashboards from {len(apps)} application(s) to Git',
+ 'dashboards_pushed': len(dashboard_contents)
+ }
+ self.wfile.write(json.dumps(response).encode())
+
+ finally:
+ logger.info(f"Cleaning up {temp_dir}")
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+ except Exception as e:
+ logger.error(f"Error: {str(e)}", exc_info=True)
+ response = {
+ 'status': 'error',
+ 'message': f'Error: {str(e)}'
+ }
+ self.wfile.write(json.dumps(response).encode())
+
+ def log_message(self, format, *args):
+ """Éviter les logs HTTP par défaut"""
+ logger.debug(format % args)
+
+ @staticmethod
+ def prepare_git_url(git_url, token):
+ """Préparer l'URL Git avec le token inséré"""
+ logger.info(f"Preparing git URL with token")
+
+ # Si l'URL contient déjà un token (format: https://user:token@host/repo)
+ # on le remplace
+ if '@' in git_url:
+ # Extraire la partie sans le token
+ protocol = git_url.split('://')[0]
+ rest = git_url.split('://', 1)[1]
+ host_and_path = rest.split('@', 1)[1] if '@' in rest else rest
+ return f"{protocol}://{token}@{host_and_path}"
+
+ # Si l'URL est juste https://host/repo (sans credentials)
+ if git_url.startswith('https://') or git_url.startswith('http://'):
+ protocol = git_url.split('://')[0]
+ host_and_path = git_url.split('://', 1)[1]
+ # Insérer le token au format user:token@host ou juste token@host
+ return f"{protocol}://{token}@{host_and_path}"
+
+ return git_url
+
+ @staticmethod
+ def clone_repository(dest_dir, git_url, branch):
+ """Cloner le repository"""
+ try:
+ cmd = ['git', 'clone', '--depth', '1', '--branch', branch, git_url, dest_dir]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
+
+ if result.returncode != 0:
+ raise Exception(f"Clone failed: {result.stderr}")
+
+ logger.info("Repository cloned successfully")
+ except subprocess.TimeoutExpired:
+ raise Exception("Git clone operation timed out")
+ except FileNotFoundError:
+ raise Exception("Git is not installed on this system")
+
+ @staticmethod
+ def fetch_apps_directories(apps):
+ """Récupérer les dossiers complets des applications"""
+ logger.info(f"Fetching directories for {len(apps)} applications")
+
+ splunk_home = '/opt/splunk'
+ apps_base_path = os.path.join(splunk_home, 'etc', 'apps')
+
+ app_directories = []
+
+ for app in apps:
+ app_id = app.get('id') or app.get('app_id')
+ app_path = os.path.join(apps_base_path, app_id)
+
+ logger.info(f"Checking app directory: {app_path}")
+
+ if os.path.isdir(app_path):
+ app_directories.append({
+ 'name': app_id,
+ 'path': app_path,
+ 'size': sum(os.path.getsize(os.path.join(dirpath, filename))
+ for dirpath, dirnames, filenames in os.walk(app_path)
+ for filename in filenames)
+ })
+ logger.info(f"Found app: {app_id} at {app_path}")
+ else:
+ logger.warning(f"App directory not found: {app_path}")
+
+ logger.info(f"Successfully found {len(app_directories)} application directories")
+ return app_directories
+ """Récupérer TOUS les dashboards de chaque application"""
+ logger.info(f"Fetching dashboards from {len(apps)} applications")
+
+ import urllib.request
+ import urllib.error
+ import ssl
+ import base64
+
+ # Ignorer les certificats SSL auto-signés
+ ssl_context = ssl.create_default_context()
+ ssl_context.check_hostname = False
+ ssl_context.verify_mode = ssl.CERT_NONE
+
+ dashboard_contents = []
+
+ # Lire le fichier de configuration Splunk pour obtenir les credentials
+ # Ou utiliser des credentials par défaut
+ splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin')
+ splunk_password = os.environ.get('SPLUNK_PASSWORD', 'changeme')
+
+ # Créer l'authentification Basic
+ credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode()
+
+ for app in apps:
+ app_id = app.get('id') or app.get('app_id')
+ logger.info(f"Fetching all dashboards from app: {app_id}")
+
+ try:
+ # Récupérer la liste de TOUS les dashboards de cette app
+ api_url = f"https://127.0.0.1:8089/servicesNS/-/{app_id}/data/ui/views?output_mode=json&count=0"
+
+ logger.debug(f"API URL: {api_url}")
+
+ req = urllib.request.Request(api_url)
+ req.add_header('Authorization', f'Basic {credentials}')
+
+ with urllib.request.urlopen(req, timeout=15, context=ssl_context) as response:
+ api_data = json.loads(response.read().decode('utf-8'))
+
+ if 'entry' in api_data and len(api_data['entry']) > 0:
+ for entry in api_data['entry']:
+ try:
+ dashboard_id = entry.get('name')
+ content = entry.get('content', {})
+
+ # eai:data contient le XML complet du dashboard
+ dashboard_xml = content.get('eai:data', '')
+
+ if dashboard_xml:
+ dashboard_contents.append({
+ 'id': f"{app_id}_{dashboard_id}",
+ 'app': app_id,
+ 'content': dashboard_xml,
+ 'name': dashboard_id
+ })
+ logger.debug(f"Fetched: {dashboard_id} from {app_id}")
+ except Exception as e:
+ logger.error(f"Error processing dashboard entry: {str(e)}")
+
+ logger.info(f"Found {len([d for d in dashboard_contents if d['app'] == app_id])} dashboards in {app_id}")
+ else:
+ logger.warning(f"No dashboards found in app {app_id}")
+
+ except urllib.error.HTTPError as e:
+ logger.error(f"HTTP {e.code} when fetching app {app_id}: {e.reason}")
+ except urllib.error.URLError as e:
+ logger.error(f"Cannot reach Splunk API for app {app_id}: {e.reason}")
+ except Exception as e:
+ logger.error(f"Error fetching dashboards from {app_id}: {str(e)}")
+
+ logger.info(f"Successfully fetched {len(dashboard_contents)} dashboards total")
+ return dashboard_contents
+
+
+def start_server(port=9999):
+ """Démarrer le serveur HTTP"""
+ server = HTTPServer(('0.0.0.0', port), GitPusherRequestHandler)
+ logger.info(f"Git Pusher server listening on 0.0.0.0:{port} (HTTP)")
+ server.serve_forever()
+
+
+if __name__ == '__main__':
+ # Démarrer le serveur en background
+ port = 9999
+ logger.info(f"Starting Git Pusher on port {port}")
+ start_server(port)
\ No newline at end of file
diff --git a/apps/pusher_app_prem/bin/license_generator.py b/apps/pusher_app_prem/bin/license_generator.py
new file mode 100644
index 00000000..8a353809
--- /dev/null
+++ b/apps/pusher_app_prem/bin/license_generator.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Git Pusher - License Generator
+Génère des clés de licence basées sur le hostname Splunk
+"""
+
+import hashlib
+import hmac
+import base64
+import socket
+from datetime import datetime, timedelta
+import json
+
+# Secret key pour générer les licences (À CHANGER !)
+SECRET_KEY = "git_pusher_license_secret_2024"
+
+def get_hostname():
+ """Récupérer le hostname du serveur"""
+ return socket.gethostname()
+
+def generate_license(hostname, days_valid=365, max_pushes=None):
+ """
+ Générer une clé de licence
+
+ Args:
+ hostname: nom d'hôte Splunk
+ days_valid: nombre de jours de validité
+ max_pushes: nombre maximum de pushes (None = illimité)
+
+ Returns:
+ license_key: clé de licence formatée
+ """
+
+ # Créer la date d'expiration
+ expiration_date = datetime.now() + timedelta(days=days_valid)
+ expiration_str = expiration_date.strftime("%Y-%m-%d")
+
+ # Créer le payload
+ payload = {
+ "hostname": hostname,
+ "expiration": expiration_str,
+ "max_pushes": max_pushes,
+ "issued": datetime.now().strftime("%Y-%m-%d")
+ }
+
+ # Convertir en JSON et encoder en base64
+ payload_json = json.dumps(payload, separators=(',', ':'))
+ payload_b64 = base64.b64encode(payload_json.encode()).decode()
+
+ # Créer la signature HMAC
+ signature = hmac.new(
+ SECRET_KEY.encode(),
+ payload_b64.encode(),
+ hashlib.sha256
+ ).hexdigest()[:16] # Prendre les 16 premiers caractères
+
+ # Formater la clé de licence
+ license_key = f"{signature}-{payload_b64}"
+
+ return license_key, payload
+
+def validate_license(license_key, hostname):
+ """
+ Valider une clé de licence
+
+ Args:
+ license_key: clé à valider
+ hostname: hostname Splunk actuel
+
+ Returns:
+ dict: {valid: bool, error: str, expiration: str, max_pushes: int}
+ """
+
+ try:
+ # Séparer signature et payload
+ parts = license_key.split('-', 1)
+ if len(parts) != 2:
+ return {
+ 'valid': False,
+ 'error': 'Format de clé invalide'
+ }
+
+ signature, payload_b64 = parts
+
+ # Vérifier la signature
+ expected_signature = hmac.new(
+ SECRET_KEY.encode(),
+ payload_b64.encode(),
+ hashlib.sha256
+ ).hexdigest()[:16]
+
+ if signature != expected_signature:
+ return {
+ 'valid': False,
+ 'error': 'Signature invalide - clé corrompue ou falsifiée'
+ }
+
+ # Décoder le payload
+ try:
+ payload_json = base64.b64decode(payload_b64).decode()
+ payload = json.loads(payload_json)
+ except Exception as e:
+ return {
+ 'valid': False,
+ 'error': f'Erreur de décodage: {str(e)}'
+ }
+
+ # Vérifier le hostname
+ if payload.get('hostname') != hostname:
+ return {
+ 'valid': False,
+ 'error': f'Cette licence est pour {payload.get("hostname")}, pas {hostname}'
+ }
+
+ # Vérifier l'expiration
+ expiration = datetime.strptime(payload.get('expiration'), '%Y-%m-%d')
+ if datetime.now() > expiration:
+ return {
+ 'valid': False,
+ 'error': f'Licence expirée le {payload.get("expiration")}'
+ }
+
+ return {
+ 'valid': True,
+ 'expiration': payload.get('expiration'),
+ 'max_pushes': payload.get('max_pushes'),
+ 'days_remaining': (expiration - datetime.now()).days
+ }
+
+ except Exception as e:
+ return {
+ 'valid': False,
+ 'error': f'Erreur de validation: {str(e)}'
+ }
+
+if __name__ == '__main__':
+ import sys
+
+ hostname = get_hostname()
+ print("=" * 60)
+ print("Git Pusher - License Generator")
+ print("=" * 60)
+ print(f"\nHostname détecté: {hostname}")
+
+ if len(sys.argv) > 1 and sys.argv[1] == 'validate':
+ # Mode validation
+ license_key = sys.argv[2] if len(sys.argv) > 2 else input("Entrez la clé de licence: ")
+ result = validate_license(license_key, hostname)
+
+ print("\nRésultat de validation:")
+ print(json.dumps(result, indent=2, ensure_ascii=False))
+ else:
+ # Mode génération
+ days = int(sys.argv[1]) if len(sys.argv) > 1 else 365
+ max_pushes = int(sys.argv[2]) if len(sys.argv) > 2 else None
+
+ license_key, payload = generate_license(hostname, days, max_pushes)
+
+ print(f"\n📋 Payload:")
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
+
+ print(f"\n🔑 Clé de licence générée:")
+ print(license_key)
+
+ print(f"\n✓ Valide pour: {days} jours")
+ if max_pushes:
+ print(f"✓ Pushes limités à: {max_pushes}")
+
+ # Tester la validation
+ print(f"\n✔️ Test de validation:")
+ result = validate_license(license_key, hostname)
+ print(json.dumps(result, indent=2, ensure_ascii=False))
\ No newline at end of file
diff --git a/apps/pusher_app_prem/bin/start_git_pusher.sh b/apps/pusher_app_prem/bin/start_git_pusher.sh
new file mode 100644
index 00000000..951e1a0e
--- /dev/null
+++ b/apps/pusher_app_prem/bin/start_git_pusher.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+export SPLUNK_USERNAME=admin
+export SPLUNK_PASSWORD='2312Jocpam!?'
+python3 /opt/splunk/etc/apps/pusher_app/bin/git_pusher.py > /opt/splunk/var/log/splunk/git_pusher_startup.log 2>&1 &
+echo $! > /opt/splunk/etc/apps/pusher_app/bin/git_pusher.pid
\ No newline at end of file
diff --git a/apps/pusher_app_prem/certs/server.crt b/apps/pusher_app_prem/certs/server.crt
new file mode 100644
index 00000000..11fa39a7
--- /dev/null
+++ b/apps/pusher_app_prem/certs/server.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCTCCAfGgAwIBAgIUCuKo8SLloS5cjBOR04+X6ayZ40cwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDEyMzIyMTIxOFoXDTI3MDEy
+MzIyMTIxOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAs0vF6sFTseKgZC1nZ6CVZdw45yk1Ni0W9Mc24KZ9NKCJ
+rP0tHy0hs6mME/sq8DV1fh0YtqIvBCxcKEE84/cVXmUfZF9JRXO95734+JGPmo07
+zpiu7p3r4WyIWmCXX5VB0UkMEXsPQmonqG1Kwtz+R1cfgis2lUk+xsC2zSjER8l4
+2UODjHvtD25usgxKjpwPrCuZt43miArnVnwfB8OLbAqpwQeYIf18bPt/TrnQsdgd
+ZZiQdE6UTaJ5xhqztwpYJO9pvZA24Bi3bGNfBciITds5RCGY2wQo8yxbeJsidTuW
+7Z64DK9t33oVnB2PqlP6hVGD5Agthsv9ehRPxdd3MwIDAQABo1MwUTAdBgNVHQ4E
+FgQUy0dni+ogqC7YuvfD/Pn0AuebsXQwHwYDVR0jBBgwFoAUy0dni+ogqC7YuvfD
+/Pn0AuebsXQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAlyxg
+vR15lsYp4TxJPi1WPzLZl1e6ewTl8GhyE1saxS8LRtyTyr8sa9EFRLQ0OIsqYrUw
+zZi7FIDoDPZDKpd0/+U94UKlhUuPUyufQwl5vNu0A+SEpwKeznUMaj4Y98tHvVGd
+1SCndZBWn/v2U4nXqHoTd6Y0xEOga0jUEsUMBckNC236BTo88Zk65/oa9Gncyb27
+9vGVCbmPyzE70H4KFoVtxkoZrKywn+0ajHhgH5gqZNRPWpe6i8xTbMAeIXkCjmWL
+LmOA7MkjeQBBEWewu4vMOXsvf+gCtxUj5owsAcOQlZ3g72Sng4MeMjuVx4ZRVxX9
+fj+vCP9EFI8rX48tjQ==
+-----END CERTIFICATE-----
diff --git a/apps/pusher_app_prem/certs/server.key b/apps/pusher_app_prem/certs/server.key
new file mode 100644
index 00000000..8b790585
--- /dev/null
+++ b/apps/pusher_app_prem/certs/server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCzS8XqwVOx4qBk
+LWdnoJVl3DjnKTU2LRb0xzbgpn00oIms/S0fLSGzqYwT+yrwNXV+HRi2oi8ELFwo
+QTzj9xVeZR9kX0lFc73nvfj4kY+ajTvOmK7unevhbIhaYJdflUHRSQwRew9Caieo
+bUrC3P5HVx+CKzaVST7GwLbNKMRHyXjZQ4OMe+0Pbm6yDEqOnA+sK5m3jeaICudW
+fB8Hw4tsCqnBB5gh/Xxs+39OudCx2B1lmJB0TpRNonnGGrO3Clgk72m9kDbgGLds
+Y18FyIhN2zlEIZjbBCjzLFt4myJ1O5btnrgMr23fehWcHY+qU/qFUYPkCC2Gy/16
+FE/F13czAgMBAAECggEAMrEMrvej0xpQ4KHZp3nGY3sk9242JjAPWntsb42CvrtY
+0XjvJe5bpfEcspWDqVBj/Jj7YL9v7Y0hLRxsu8Mi3oJWoskx7RnxKjES0CxPXpHp
+w9p1Mu+hPiWyU2MVySdo6WPuro6NXOiod70WswtKNR9TwDi5gPGpdwYLaOvKusSp
+Rncm0m0H3IBhgVA691X0AUIomAW3Wmh+5If1XHfjrNHTB8cjcNf6koPMkCqHCEZ9
+wtINxOJior+gGkjMXaDszqzNlicVBXFEFjaXWcp38xAif1uimpqKsRzZEF6RAUzi
+H7cI3aF2dXG3C9l6Byi7OSgd8X4JUnE0dlCpC7qweQKBgQDgvoavo8G0kYruCUIQ
+6vcSs1YBByOkl6yZBCZWk10NgRpU1wyu9zmlvEwNVlUfALs5eoLxnhe8Wklq0ckQ
+r/Rl+r/lj/MZUFn49TgUCsUOIi/G7nWQG0bPo4bCB2QXsAiKdY+KZeC56620uyom
+1VY+nS3y8O4EP0YHX0qHFfmIZwKBgQDMOywO0DSrZMDyvmbwL0ISzHRcNpRn0jk7
+pEtzM/VOx+v0O93E+5OygzmXlBKjF0MwMXBidf8IZu4xO8qWqAM4EP2DD0cpoS1Z
+WiHHkc5NZhjgeG6C4XaCXR++7CuY25VKKe01yz/+j51linDD8OAibKUspkjVufEN
+R/AT0GFLVQKBgBxMYTEkcXOHD/NA/yyaKVoVcrLWb0p+PqFVwG4OSB03MFWWbmZp
+gry3pOvY/wbUVL68CljaCysQQ0ZL/AE55pAgrqD9KyL41xtd5R3A7WcGLvXheLQY
+eyYR9RnhTF0fMTQd8WD/yvgeENU86+XP3vgrWmnIpG+sd+jdusifn7fpAn9QkwfO
+0FX3SMjW/EegewSWZhOCTgY+77Gk1izuRpGBg16T/QqBrL+Yri0KoGC593OKj/bG
+4ca8id9vjSdgSOj8NbfO/TgWNICvv9+T3PKHlsA5z0nKWSloRVVA/ew1YmyD1gbA
+MnAM/pwac4QJyf6jljmUZAZYTAPOOZN+PbglAoGBAJ9cOGDgT+BCOoNc0T1GJDAk
+xOR8d+tD+j4JH5IVxB51DXjJOZxw9U3XhNH1OcE0x3fRzKJOtlQLxP6fHYVtMVFq
+VpeekmTtJ9OfMg68ELOlf7ykA3GhMJ3FarM6e8+X+KliGf6ND4HBMb112FlMgIi6
+yYi7sfSL53Dzp1Q2DxXV
+-----END PRIVATE KEY-----
diff --git a/apps/pusher_app_prem/default/app.conf b/apps/pusher_app_prem/default/app.conf
new file mode 100644
index 00000000..a01a048d
--- /dev/null
+++ b/apps/pusher_app_prem/default/app.conf
@@ -0,0 +1,16 @@
+#
+# Splunk app configuration file
+#
+
+[install]
+is_configured = 0
+
+[ui]
+is_visible = true
+label = Pusher Premium
+
+[launcher]
+author =
+description =
+version = 1.0.0
+
diff --git a/apps/pusher_app_prem/default/data/ui/nav/default.xml b/apps/pusher_app_prem/default/data/ui/nav/default.xml
new file mode 100644
index 00000000..8e98eadb
--- /dev/null
+++ b/apps/pusher_app_prem/default/data/ui/nav/default.xml
@@ -0,0 +1,8 @@
+
diff --git a/apps/pusher_app_prem/default/data/ui/views/README b/apps/pusher_app_prem/default/data/ui/views/README
new file mode 100644
index 00000000..6cf74f0b
--- /dev/null
+++ b/apps/pusher_app_prem/default/data/ui/views/README
@@ -0,0 +1 @@
+Add all the views that your app needs in this directory
diff --git a/apps/pusher_app_prem/local/app.conf b/apps/pusher_app_prem/local/app.conf
new file mode 100644
index 00000000..78666a91
--- /dev/null
+++ b/apps/pusher_app_prem/local/app.conf
@@ -0,0 +1,3 @@
+[ui]
+
+[launcher]
diff --git a/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_deploy_applications.xml b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_deploy_applications.xml
new file mode 100644
index 00000000..292fbe8f
--- /dev/null
+++ b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_deploy_applications.xml
@@ -0,0 +1,493 @@
+
+
+
+ Modern interface to push Splunk applications to Git repository
+
+
+
+
+
+
+
+
+
+
+
+ | rest /services/apps/local | search disabled=0 | fields name, label, description | sort label
+ -4h@h
+ now
+
+
+
+
+
+
+
+
+
+
+
+ ✨ Configure your Git settings below and select the applications you want to deploy to your repository
+
+
+
+
+
+
⚙️ Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📦 Applications
+
+
+
+
+
Loading applications...
+
+
+
+
+
+
+
+
Deploying applications to Git... Please wait
+
+
+
+ ✅ Applications successfully deployed to Git!
+
+
+
+ ❌ Error occurred while deploying applications
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_applications_to_git.xml b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_applications_to_git.xml
new file mode 100644
index 00000000..eca8b17e
--- /dev/null
+++ b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_applications_to_git.xml
@@ -0,0 +1,256 @@
+
+
+
+ Push Splunk applications to Git repository
+
+
+ | rest /services/apps/local | search disabled=0 | fields name, label, description | sort label
+ -4h@h
+ now
+
+
+
+ Configuration & Application Selection
+
+
+
+
+
+ ℹ️ Configure your Git settings and select the applications you want to push to your repository.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pushing applications to Git...
+
+
+
+ ✓ Applications successfully pushed to Git!
+
+
+
+ ✗ Error occurred while pushing applications
+
+
+
+
+
+
+
+ Push History
+
+
+ index=_internal source=*git_pusher* action=push_attempt | table _time, user, apps, commit_message, status, error_msg | reverse | rename _time as "Timestamp", user as "User", apps as "Applications", commit_message as "Message", status as "Status", error_msg as "Error" | head 20
+ -30d@d
+ now
+
+
+
+ {"success": "#28a745", "error": "#dc3545", "pending": "#ffc107"}
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_dashboards_to_git.xml b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_dashboards_to_git.xml
new file mode 100644
index 00000000..79d34cc9
--- /dev/null
+++ b/apps/pusher_app_prem/local/data/ui/views/git_pusher_-_push_dashboards_to_git.xml
@@ -0,0 +1,254 @@
+
+
+ Push Splunk dashboards to Git repository
+
+
+
+ | rest /services/data/ui/views | search title!="" | fields label, id, eai:acl.app | rename label as "Dashboard Name", id as "dashboard_id", "eai:acl.app" as "app" | sort "Dashboard Name"
+ -4h@h
+ now
+
+
+
+
+ Configuration & Dashboard Selection
+
+
+
+
+
+ ℹ️ Configure your Git settings and select the dashboards you want to push to your repository.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pushing dashboards to Git...
+
+
+
+ ✓ Dashboards successfully pushed to Git!
+
+
+
+ ✗ Error occurred while pushing dashboards
+
+
+
+
+
+
+
+
+ Push History
+
+
+ index=_internal source=*git_pusher* action=push_attempt | table _time, user, dashboards, commit_message, status, error_msg | reverse | rename _time as "Timestamp", user as "User", dashboards as "Dashboards", commit_message as "Message", status as "Status", error_msg as "Error" | head 20
+ -30d@d
+ now
+
+
+
+ {"success": "#28a745", "error": "#dc3545", "pending": "#ffc107"}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/pusher_app_prem/metadata/default.meta b/apps/pusher_app_prem/metadata/default.meta
new file mode 100644
index 00000000..b77b8cb9
--- /dev/null
+++ b/apps/pusher_app_prem/metadata/default.meta
@@ -0,0 +1,35 @@
+
+# Application-level permissions
+
+[]
+access = read : [ * ], write : [ admin, power ]
+
+### EVENT TYPES
+
+[eventtypes]
+export = system
+
+
+### PROPS
+
+[props]
+export = system
+
+
+### TRANSFORMS
+
+[transforms]
+export = system
+
+
+### LOOKUPS
+
+[lookups]
+export = system
+
+
+### VIEWSTATES: even normal users should be able to create shared viewstates
+
+[viewstates]
+access = read : [ * ], write : [ * ]
+export = system
diff --git a/apps/pusher_app_prem/metadata/local.meta b/apps/pusher_app_prem/metadata/local.meta
new file mode 100644
index 00000000..d8e71ee3
--- /dev/null
+++ b/apps/pusher_app_prem/metadata/local.meta
@@ -0,0 +1,26 @@
+[app/ui]
+version = 10.0.2
+modtime = 1769115948.043388000
+
+[app/launcher]
+version = 10.0.2
+modtime = 1769115948.046389000
+
+[views/git_pusher_-_push_dashboards_to_git]
+access = read : [ admin ], write : [ admin ]
+export = system
+owner = admin
+version = 10.0.2
+modtime = 1769276443.812957000
+
+[views/git_pusher_-_push_applications_to_git]
+owner = admin
+version = 10.0.2
+modtime = 1769361925.808816000
+
+[views/git_pusher_-_deploy_applications]
+access = read : [ * ], write : [ * ]
+export = none
+owner = admin
+version = 10.0.2
+modtime = 1769373002.573917000