diff --git a/apps/pusher_app/appserver/static/git_pusher.js b/apps/pusher_app/appserver/static/git_pusher.js
new file mode 100755
index 00000000..ddb5ff7f
--- /dev/null
+++ b/apps/pusher_app/appserver/static/git_pusher.js
@@ -0,0 +1,406 @@
+// ============================================
+// 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 = '
';
+ 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");
+}
+
+// Attendre que le DOM soit chargé
+function initScript() {
+ console.log("initScript called");
+
+ // Charger les applications
+ loadAvailableApps();
+
+ // 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");
+ }
+}
+
+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"]:not(#select-all)');
+ checkboxes.forEach(cb => cb.checked = checkbox.checked);
+}
+
+// ============================================
+// POUSSER LES DASHBOARDS VERS GIT
+// ============================================
+
+function pushDashboards() {
+ 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;
+
+ // 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() {
+ 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);
+}
\ No newline at end of file
diff --git a/apps/pusher_app/bin/README b/apps/pusher_app/bin/README
new file mode 100755
index 00000000..9a70db09
--- /dev/null
+++ b/apps/pusher_app/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/bin/git_pusher.pid b/apps/pusher_app/bin/git_pusher.pid
new file mode 100755
index 00000000..81d2a971
--- /dev/null
+++ b/apps/pusher_app/bin/git_pusher.pid
@@ -0,0 +1 @@
+919562
diff --git a/apps/pusher_app/bin/git_pusher.py b/apps/pusher_app/bin/git_pusher.py
new file mode 100755
index 00000000..58b72cad
--- /dev/null
+++ b/apps/pusher_app/bin/git_pusher.py
@@ -0,0 +1,319 @@
+#!/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
+ git_url_with_token = self.prepare_git_url(git_url, git_token)
+
+ # Cloner
+ 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}")
+ 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"""
+ if git_url.startswith('https://'):
+ parts = git_url.replace('https://', '').split('/')
+ return f"https://{token}@{'/'.join(parts)}"
+ 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/bin/start_git_pusher.sh b/apps/pusher_app/bin/start_git_pusher.sh
new file mode 100755
index 00000000..951e1a0e
--- /dev/null
+++ b/apps/pusher_app/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/certs/server.crt b/apps/pusher_app/certs/server.crt
new file mode 100755
index 00000000..11fa39a7
--- /dev/null
+++ b/apps/pusher_app/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/certs/server.key b/apps/pusher_app/certs/server.key
new file mode 100755
index 00000000..8b790585
--- /dev/null
+++ b/apps/pusher_app/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/default/app.conf b/apps/pusher_app/default/app.conf
new file mode 100755
index 00000000..e473dbe9
--- /dev/null
+++ b/apps/pusher_app/default/app.conf
@@ -0,0 +1,16 @@
+#
+# Splunk app configuration file
+#
+
+[install]
+is_configured = 0
+
+[ui]
+is_visible = true
+label = Pusher
+
+[launcher]
+author =
+description =
+version = 1.0.0
+
diff --git a/apps/pusher_app/default/data/ui/nav/default.xml b/apps/pusher_app/default/data/ui/nav/default.xml
new file mode 100755
index 00000000..8e98eadb
--- /dev/null
+++ b/apps/pusher_app/default/data/ui/nav/default.xml
@@ -0,0 +1,8 @@
+
diff --git a/apps/pusher_app/default/data/ui/views/README b/apps/pusher_app/default/data/ui/views/README
new file mode 100755
index 00000000..6cf74f0b
--- /dev/null
+++ b/apps/pusher_app/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/local/app.conf b/apps/pusher_app/local/app.conf
new file mode 100755
index 00000000..78666a91
--- /dev/null
+++ b/apps/pusher_app/local/app.conf
@@ -0,0 +1,3 @@
+[ui]
+
+[launcher]
diff --git a/apps/pusher_app/local/data/ui/views/git_pusher_-_push_applications_to_git.xml b/apps/pusher_app/local/data/ui/views/git_pusher_-_push_applications_to_git.xml
new file mode 100644
index 00000000..7cc88dfa
--- /dev/null
+++ b/apps/pusher_app/local/data/ui/views/git_pusher_-_push_applications_to_git.xml
@@ -0,0 +1,255 @@
+
+
+
+ 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/local/data/ui/views/git_pusher_-_push_dashboards_to_git.xml b/apps/pusher_app/local/data/ui/views/git_pusher_-_push_dashboards_to_git.xml
new file mode 100644
index 00000000..79d34cc9
--- /dev/null
+++ b/apps/pusher_app/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/metadata/default.meta b/apps/pusher_app/metadata/default.meta
new file mode 100755
index 00000000..b77b8cb9
--- /dev/null
+++ b/apps/pusher_app/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/metadata/local.meta b/apps/pusher_app/metadata/local.meta
new file mode 100644
index 00000000..2e9cddbb
--- /dev/null
+++ b/apps/pusher_app/metadata/local.meta
@@ -0,0 +1,19 @@
+[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 = 1769283309.402236000