Premimun app pusher

Pushed by: unknown_user
Timestamp: 2026-01-25T21:58:55.092327
masterdev
Splunk Git Pusher 3 months ago
parent cf68f55b9f
commit 50267b7340

@ -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 = '<div class="app-header">';
html += '<span>Select Applications</span>';
html += '<input type="checkbox" id="select-all">';
html += '</div>';
apps.forEach((app, index) => {
const checkboxId = 'app-' + index;
html += '<div class="app-item">';
html += '<input type="checkbox" id="' + checkboxId + '" value="' + app.id + '" data-app="true" data-name="' + app.name + '">';
html += '<label for="' + checkboxId + '"><strong>' + app.name + '</strong> <span class="app-badge">' + app.id + '</span>';
if (app.description) {
html += '<div style="font-size: 11px; color: #999; margin-top: 3px;">' + app.description + '</div>';
}
html += '</label>';
html += '</div>';
});
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 = '<div class="dashboard-empty">No apps found</div>';
}
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 = '<div class="select-all-group">';
html += '<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)">';
html += '<label for="select-all">Select All (' + apps.length + ' apps)</label>';
html += '</div>';
apps.forEach((app, index) => {
const checkboxId = 'app-' + index;
html += '<div class="dashboard-item">';
html += '<input type="checkbox" id="' + checkboxId + '" value="' + app.id + '" data-app="' + app.id + '" data-name="' + app.name + '">';
html += '<label for="' + checkboxId + '"><strong>' + app.name + '</strong> <span class="app-badge">' + app.id + '</span>';
if (app.description) {
html += '<div style="font-size: 11px; color: #666; margin-top: 3px;">' + app.description + '</div>';
}
html += '</label>';
html += '</div>';
});
container.innerHTML = html;
console.log('Successfully populated ' + apps.length + ' apps');
}
function showAppsEmpty() {
const container = document.getElementById('dashboard-list');
if (container) {
container.innerHTML = '<div class="dashboard-empty">No apps found</div>';
}
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;
}
}
}

@ -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 = `
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="font-size: 32px; margin: 0 0 10px 0; color: #333;">🔐 Git Pusher</h1>
<p style="color: #666; margin: 0; font-size: 14px;">Activation de licence requise</p>
</div>
<div style="background: #f5f7ff; padding: 15px; border-radius: 8px; margin-bottom: 25px; border-left: 4px solid #667eea;">
<p style="margin: 0; color: #667eea; font-weight: 500; font-size: 13px;">
📋 <strong>Hostname détecté:</strong> <span id="detected-hostname">Chargement...</span>
</p>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: 600; color: #333; margin-bottom: 8px;">
Entrez votre clé de licence
</label>
<textarea id="license-input" placeholder="Collez votre clé de licence ici..." style="
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
resize: vertical;
min-height: 100px;
box-sizing: border-box;
"></textarea>
<small style="color: #999; display: block; margin-top: 8px;">
Vous n'avez pas de licence? <a href="#" onclick="showGeneratorInfo(); return false;" style="color: #667eea; text-decoration: none;">Cliquez ici</a>
</small>
</div>
<div id="license-message" style="display: none; padding: 12px; border-radius: 8px; margin-bottom: 20px; font-size: 14px;"></div>
<div style="display: flex; gap: 10px;">
<button onclick="validateLicenseInput()" style="
flex: 1;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
">Activer la licence</button>
<button onclick="skipLicense()" style="
flex: 1;
padding: 12px;
background: #f5f7ff;
color: #667eea;
border: 2px solid #667eea;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
">Essai gratuit (7 jours)</button>
</div>
`;
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);
}

@ -0,0 +1 @@
This is where you put any scripts you want to add to this app.

@ -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)

@ -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))

@ -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

@ -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-----

@ -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-----

@ -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

@ -0,0 +1,8 @@
<nav search_view="search">
<view name="search" default='true' />
<view name="analytics_workspace" />
<view name="datasets" />
<view name="reports" />
<view name="alerts" />
<view name="dashboards" />
</nav>

@ -0,0 +1 @@
Add all the views that your app needs in this directory

@ -0,0 +1,493 @@
<?xml version="1.0" encoding="UTF-8"?>
<dashboard version="1.1" script="license_validation.js, git_pusher.js">
<label>🚀 Git Pusher - Deploy Applications</label>
<description>Modern interface to push Splunk applications to Git repository</description>
<!-- BADGE DE LICENCE -->
<row>
<panel>
<html>
<div id="license-badge-container" style="position: absolute; top: 20px; right: 20px; z-index: 1000;"></div>
</html>
</panel>
</row>
<search id="dsearch">
<query>| rest /services/apps/local | search disabled=0 | fields name, label, description | sort label</query>
<earliest>-4h@h</earliest>
<latest>now</latest>
</search>
<row>
<panel>
<html>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 20px;
text-align: center;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
margin-bottom: 30px;
border-radius: 0 0 20px 20px;
}
.header h1 {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.header p {
font-size: 14px;
opacity: 0.9;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
@media (max-width: 1024px) {
.grid {
grid-template-columns: 1fr;
}
}
.card {
background: white;
border-radius: 16px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 15px 50px rgba(102, 126, 234, 0.2);
transform: translateY(-5px);
}
.card-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.card-title::before {
content: '';
display: inline-block;
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
font-family: 'Segoe UI', sans-serif;
}
.form-group input[type="text"]:focus,
.form-group input[type="password"]:focus,
.form-group textarea:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
outline: none;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
padding: 12px;
background: #f5f7ff;
border-radius: 8px;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
font-weight: 500;
color: #555;
font-size: 13px;
text-transform: none;
}
.apps-list {
border: 2px solid #e0e0e0;
border-radius: 12px;
max-height: 400px;
overflow-y: auto;
background: #fafbfc;
}
.apps-list::-webkit-scrollbar {
width: 8px;
}
.apps-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.apps-list::-webkit-scrollbar-thumb {
background: #667eea;
border-radius: 10px;
}
.app-header {
padding: 15px 16px;
border-bottom: 2px solid #e0e0e0;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
font-weight: 600;
color: #667eea;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 10;
}
.app-count {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
}
.app-item {
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
transition: all 0.2s ease;
}
.app-item:hover {
background: #f0f4ff;
}
.app-item input[type="checkbox"] {
margin-right: 12px;
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.app-item label {
margin: 0;
cursor: pointer;
font-weight: 500;
color: #333;
display: inline-flex;
align-items: center;
gap: 10px;
width: calc(100% - 40px);
}
.app-badge {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 30px;
}
.btn {
padding: 12px 28px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
flex: 1;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.btn-primary:hover:not(:disabled) {
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
transform: translateY(-2px);
}
.btn-secondary {
background: #f5f7ff;
color: #667eea;
border: 2px solid #667eea;
}
.btn-secondary:hover:not(:disabled) {
background: #667eea;
color: white;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
display: none;
text-align: center;
padding: 30px;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-radius: 12px;
margin: 20px 0;
}
.loading.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.spinner {
border: 3px solid #e0e0e0;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #667eea;
font-weight: 600;
font-size: 14px;
}
.message {
padding: 16px 20px;
border-radius: 12px;
margin: 15px 0;
display: none;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.active {
display: block;
}
.success-message {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
border-left: 4px solid white;
}
.error-message {
background: linear-gradient(135deg, #f44336 0%, #da190b 100%);
color: white;
border-left: 4px solid white;
}
.info-banner {
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border: 2px solid #667eea;
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 20px;
color: #667eea;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #e0e0e0, transparent);
margin: 30px 0;
}
.footer {
text-align: center;
color: #999;
font-size: 12px;
margin-top: 40px;
padding-bottom: 20px;
}
</style>
<div class="header">
<h1>🚀 Git Pusher</h1>
<p>Deploy your Splunk applications to Git with confidence</p>
</div>
<div class="container">
<div class="info-banner">
✨ Configure your Git settings below and select the applications you want to deploy to your repository
</div>
<div class="grid">
<!-- Configuration Panel -->
<div class="card">
<div class="card-title">⚙️ Configuration</div>
<div class="form-group">
<label for="git-url">Git Repository URL</label>
<input type="text" id="git-url" placeholder="https://github.com/username/repo.git" />
</div>
<div class="form-group">
<label for="git-branch">Target Branch</label>
<input type="text" id="git-branch" placeholder="main" value="main" />
</div>
<div class="form-group">
<label for="git-token">Git Token / Password</label>
<input type="password" id="git-token" placeholder="Enter your Git token or password" />
</div>
<div class="checkbox-group">
<input type="checkbox" id="save-credentials" />
<label for="save-credentials">Save credentials locally</label>
</div>
<div class="form-group">
<label for="commit-message">Commit Message</label>
<textarea id="commit-message" placeholder="Describe your changes..."></textarea>
</div>
<div class="button-group">
<button class="btn btn-primary" id="push-btn" onclick="pushDashboards()">
✈️ Deploy to Git
</button>
<button class="btn btn-secondary" onclick="resetForm(true)">
🔄 Reset
</button>
</div>
</div>
<!-- Applications Panel -->
<div class="card">
<div class="card-title">📦 Applications</div>
<div class="apps-list" id="dashboard-list">
<div style="padding: 30px; text-align: center; color: #999;">
<div class="spinner"></div>
<p>Loading applications...</p>
</div>
</div>
</div>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<div class="loading-text">Deploying applications to Git... Please wait</div>
</div>
<div class="message success-message" id="success-msg">
<span id="success-text">Applications successfully deployed to Git!</span>
</div>
<div class="message error-message" id="error-msg">
<span id="error-text">Error occurred while deploying applications</span>
</div>
<div class="footer">
Made with ❤️ for Splunk • Git Pusher v2.0
</div>
</div>
</html>
</panel>
</row>
</dashboard>

@ -0,0 +1,256 @@
<?xml version="1.0" encoding="UTF-8"?>
<dashboard version="1.2" script="git_pusher.js" theme="light">
<label>Git Pusher - Push Applications to Git</label>
<description>Push Splunk applications to Git repository</description>
<!-- Recherche cachée (non utilisée, mais gardée pour compatibilité) -->
<search id="dsearch">
<query>| rest /services/apps/local | search disabled=0 | fields name, label, description | sort label</query>
<earliest>-4h@h</earliest>
<latest>now</latest>
</search>
<row>
<panel>
<title>Configuration &amp; Application Selection</title>
<html>
<style>
.git-container {
padding: 20px;
background-color: #f7f8fa;
border-radius: 4px;
margin: 10px 0;
}
.success-message {
padding: 10px;
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
border-radius: 4px;
margin: 10px 0;
display: none;
}
.error-message {
padding: 10px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
margin: 10px 0;
display: none;
}
.info-message {
padding: 10px;
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
border-radius: 4px;
margin: 10px 0;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.button-group {
margin-top: 20px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
display: none;
margin: 20px 0;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.dashboard-list {
border: 1px solid #ddd;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
padding: 10px;
}
.dashboard-item {
padding: 8px;
border-bottom: 1px solid #eee;
}
.dashboard-item:last-child {
border-bottom: none;
}
.dashboard-item input[type="checkbox"] {
margin-right: 10px;
}
.dashboard-item label {
margin: 0;
font-weight: normal;
cursor: pointer;
}
.dashboard-loading {
text-align: center;
padding: 20px;
color: #666;
}
.dashboard-empty {
text-align: center;
padding: 20px;
color: #999;
font-style: italic;
}
.select-all-group {
padding: 10px;
border-bottom: 2px solid #ddd;
background-color: #f9f9f9;
}
.select-all-group input[type="checkbox"] {
margin-right: 10px;
}
.select-all-group label {
margin: 0;
font-weight: bold;
cursor: pointer;
}
.app-badge {
display: inline-block;
background-color: #e7f3ff;
color: #0066cc;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
margin-left: 8px;
}
</style>
<div class="git-container">
<div class="info-message">
Configure your Git settings and select the applications you want to push to your repository.
</div>
<div class="form-group">
<label for="git-url">Git Repository URL:</label>
<input type="text" id="git-url" placeholder="https://github.com/username/repo.git"/>
</div>
<div class="form-group">
<label for="git-branch">Target Branch:</label>
<input type="text" id="git-branch" placeholder="main" value="main"/>
</div>
<div class="form-group">
<label for="git-token">Git Token/Password:</label>
<input type="password" id="git-token" placeholder="Enter your Git token or password"/>
</div>
<div class="form-group" style="margin-bottom: 10px;">
<input type="checkbox" id="save-credentials" />
<label for="save-credentials" style="display: inline; font-weight: normal; margin: 0;">Save credentials locally</label>
</div>
<div class="form-group">
<label>Available Applications:</label>
<div class="dashboard-list" id="dashboard-list">
<div class="dashboard-loading">
<div class="spinner"/>
<span>Loading applications...</span>
</div>
</div>
<small style="color: #666; margin-top: 5px; display: block;">Select one or more applications to push</small>
</div>
<div class="form-group">
<label for="commit-message">Commit Message:</label>
<textarea id="commit-message" placeholder="Describe your changes... e.g., 'Update applications with new dashboards'"/>
</div>
<div class="button-group">
<button class="btn btn-primary" id="push-btn" onclick="pushDashboards()">
Push to Git
</button>
<button class="btn btn-secondary" onclick="resetForm(true)">
Reset
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"/>
<span id="loading-text">Pushing applications to Git...</span>
</div>
<div class="success-message" id="success-msg">
<span id="success-text">Applications successfully pushed to Git!</span>
</div>
<div class="error-message" id="error-msg">
<span id="error-text">Error occurred while pushing applications</span>
</div>
</div>
</html>
</panel>
</row>
<row>
<panel>
<title>Push History</title>
<table>
<search>
<query>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</query>
<earliest>-30d@d</earliest>
<latest>now</latest>
</search>
<option name="drilldown">none</option>
<format type="color" field="Status">
<colorPalette type="map">{"success": "#28a745", "error": "#dc3545", "pending": "#ffc107"}</colorPalette>
</format>
</table>
</panel>
</row>
</dashboard>

@ -0,0 +1,254 @@
<dashboard version="1.1" script="git_pusher.js">
<label>Git Pusher - Push Dashboards to Git</label>
<description>Push Splunk dashboards to Git repository</description>
<!-- Recherche cachée pour charger les dashboards -->
<search id="dsearch">
<query>| 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"</query>
<earliest>-4h@h</earliest>
<latest>now</latest>
</search>
<row>
<panel>
<title>Configuration &amp; Dashboard Selection</title>
<html>
<style>
.git-container {
padding: 20px;
background-color: #f7f8fa;
border-radius: 4px;
margin: 10px 0;
}
.success-message {
padding: 10px;
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
border-radius: 4px;
margin: 10px 0;
display: none;
}
.error-message {
padding: 10px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
margin: 10px 0;
display: none;
}
.info-message {
padding: 10px;
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
border-radius: 4px;
margin: 10px 0;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.button-group {
margin-top: 20px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
display: none;
margin: 20px 0;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.dashboard-list {
border: 1px solid #ddd;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
padding: 10px;
}
.dashboard-item {
padding: 8px;
border-bottom: 1px solid #eee;
}
.dashboard-item:last-child {
border-bottom: none;
}
.dashboard-item input[type="checkbox"] {
margin-right: 10px;
}
.dashboard-item label {
margin: 0;
font-weight: normal;
cursor: pointer;
}
.dashboard-loading {
text-align: center;
padding: 20px;
color: #666;
}
.dashboard-empty {
text-align: center;
padding: 20px;
color: #999;
font-style: italic;
}
.select-all-group {
padding: 10px;
border-bottom: 2px solid #ddd;
background-color: #f9f9f9;
}
.select-all-group input[type="checkbox"] {
margin-right: 10px;
}
.select-all-group label {
margin: 0;
font-weight: bold;
cursor: pointer;
}
.app-badge {
display: inline-block;
background-color: #e7f3ff;
color: #0066cc;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
margin-left: 8px;
}
</style>
<div class="git-container">
<div class="info-message">
Configure your Git settings and select the dashboards you want to push to your repository.
</div>
<div class="form-group">
<label for="git-url">Git Repository URL:</label>
<input type="text" id="git-url" placeholder="https://github.com/username/repo.git" />
</div>
<div class="form-group">
<label for="git-branch">Target Branch:</label>
<input type="text" id="git-branch" placeholder="main" value="main" />
</div>
<div class="form-group">
<label for="git-token">Git Token/Password:</label>
<input type="password" id="git-token" placeholder="Enter your Git token or password" />
</div>
<div class="form-group">
<label>Available Dashboards:</label>
<div class="dashboard-list" id="dashboard-list">
<div class="dashboard-loading">
<div class="spinner"></div>
<span>Loading dashboards...</span>
</div>
</div>
<small style="color: #666; margin-top: 5px; display: block;">Select one or more dashboards to push</small>
</div>
<div class="form-group">
<label for="commit-message">Commit Message:</label>
<textarea id="commit-message" placeholder="Describe your changes... e.g., 'Update sales dashboard with new metrics'"></textarea>
</div>
<div class="button-group">
<button class="btn btn-primary" id="push-btn" onclick="pushDashboards()">
Push to Git
</button>
<button class="btn btn-secondary" onclick="resetForm()">
Reset
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<span id="loading-text">Pushing dashboards to Git...</span>
</div>
<div class="success-message" id="success-msg">
<span id="success-text">Dashboards successfully pushed to Git!</span>
</div>
<div class="error-message" id="error-msg">
<span id="error-text">Error occurred while pushing dashboards</span>
</div>
</div>
</html>
</panel>
</row>
<row>
<panel>
<title>Push History</title>
<table>
<search>
<query>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</query>
<earliest>-30d@d</earliest>
<latest>now</latest>
</search>
<option name="drilldown">none</option>
<format type="color" field="Status">
<colorPalette type="map">{"success": "#28a745", "error": "#dc3545", "pending": "#ffc107"}</colorPalette>
</format>
</table>
</panel>
</row>
</dashboard>

@ -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

@ -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
Loading…
Cancel
Save