masterdev
admingit 2 months ago
parent 0c58db798a
commit a0e9d3e2a0

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored

@ -8,6 +8,7 @@
*.txt *.txt
*.ini *.ini
*.xsl *.xsl
.DS_Store
passwd passwd
!apps/ !apps/
/anonymizer/ /anonymizer/

BIN
apps/.DS_Store vendored

Binary file not shown.

@ -0,0 +1,766 @@
# 🚀 Git Pusher for Splunk
**Version 2.1** | Application Splunk pour déployer vos applications vers Git et le Search Head Cluster
---
## 📋 Table des matières
- [Présentation](#-présentation)
- [Fonctionnalités](#-fonctionnalités)
- [Architecture](#-architecture)
- [Installation - Serveur Source](#-installation---serveur-source)
- [Installation - SH Deployer Agent](#-installation---sh-deployer-agent)
- [Configuration](#-configuration)
- [Configuration HTTPS](#-configuration-https)
- [Système de Licence](#-système-de-licence)
- [Utilisation](#-utilisation)
- [Sécurité](#-sécurité)
- [Personnalisation du Dashboard](#-personnalisation-du-dashboard)
- [Dépannage](#-dépannage)
- [API Reference](#-api-reference)
- [Changelog](#-changelog)
- [Support](#-support)
---
## 🎯 Présentation
**Git Pusher** est une application Splunk Enterprise qui permet de :
1. **Versionner** vos applications Splunk dans un repository Git
2. **Déployer automatiquement** vers un Search Head Cluster via le SH Deployer
Le workflow complet en un clic :
```
Splunk Source → Push Git → Pull SH Deployer → Apply Bundle → Search Head Cluster
```
Idéal pour :
- 📦 Sauvegarder vos configurations Splunk
- 🔄 Versionner vos dashboards et applications
- 👥 Collaborer en équipe sur les développements Splunk
- 🚀 Mettre en place un workflow CI/CD pour Splunk
- 🎯 Déployer automatiquement vers votre Search Head Cluster
---
## ✨ Fonctionnalités
| Fonctionnalité | Description |
|----------------|-------------|
| **Push vers Git** | Déployez une ou plusieurs applications Splunk vers votre repository Git |
| **Déploiement SH Cluster** | Déploiement automatique vers le Search Head Cluster après le push Git |
| **Interface moderne** | Dashboard intuitif avec sélection visuelle des applications |
| **Multi-repository** | Support de GitHub, GitLab, Gitea, Bitbucket et tout serveur Git |
| **Support HTTPS** | Communication sécurisée avec certificats SSL |
| **Système de licence** | Gestion par fichier `.lic` sécurisé |
| **Credentials sécurisés** | Chiffrement des mots de passe Splunk |
| **Logs détaillés** | Traçabilité complète des opérations |
---
## 🏗 Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ SERVEUR SOURCE │
│ ┌─────────────┐ ┌─────────────────────────────────────┐ │
│ │ Splunk │────▶│ Git Pusher Server (:9999) │ │
│ │ Dashboard │ │ 1. Push apps vers Git │ │
│ │ (HTTPS) │ │ 2. Appelle le SH Deployer Agent │ │
│ └─────────────┘ └──────────────────┬──────────────────┘ │
└─────────────────────────────────────────┼───────────────────────┘
│ HTTPS (:9998)
┌─────────────────────────────────────────────────────────────────┐
│ SH DEPLOYER │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Deployer Agent (:9998) │ │
│ │ 1. Git pull dans /opt/splunk/etc/shcluster/apps/ │ │
│ │ 2. Exécute: splunk apply shcluster-bundle │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SEARCH HEAD CLUSTER │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ SH1 │ │ SH2 │ │ SH3 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### Structure des fichiers
```
SERVEUR SOURCE - pusher_app_prem/
├── bin/
│ ├── git_pusher.py # Serveur HTTP/HTTPS principal (port 9999)
│ ├── license_validator.py # Validation des licences
│ ├── license_generator.py # Génération des licences (vendeur)
│ ├── credentials_manager.py # Gestion sécurisée des credentials
│ └── start_git_pusher.sh # Script de démarrage
├── appserver/static/
│ ├── git_pusher.js # Logique JavaScript principale
│ └── license_validation.js # Interface de gestion des licences
├── default/data/ui/views/
│ └── git_pusher_-_deploy_applications.xml # Dashboard principal
├── local/
│ ├── license.lic # Fichier de licence (après activation)
│ ├── .credentials # Credentials chiffrés
│ ├── .key # Clé de chiffrement
│ └── certs/ # Certificats SSL
│ ├── server.crt
│ └── server.key
└── README.md
SH DEPLOYER - deployer_agent/
├── bin/
│ ├── deployer_agent.py # Agent de déploiement (port 9998)
│ └── start_deployer_agent.sh # Script de démarrage
└── local/
└── certs/ # Certificats SSL
├── server.crt
└── server.key
```
---
## 📥 Installation - Serveur Source
### Prérequis
- Splunk Enterprise 8.x ou supérieur
- Python 3.7+
- Git installé sur le serveur (`yum install git` ou `apt install git`)
- OpenSSL (pour la génération des certificats HTTPS)
- Accès réseau vers votre repository Git
### Étapes d'installation
#### 1. Extraire l'application
```bash
# Copier l'application dans Splunk
cp -r pusher_app_prem /opt/splunk/etc/apps/
# Définir les permissions
chown -R splunk:splunk /opt/splunk/etc/apps/pusher_app_prem
chmod +x /opt/splunk/etc/apps/pusher_app_prem/bin/*.sh
```
#### 2. Configurer les credentials Splunk
```bash
cd /opt/splunk/etc/apps/pusher_app_prem/bin/
./start_git_pusher.sh credentials setup
```
```
==================================================
Git Pusher - Credentials Setup
==================================================
Splunk Username [admin]: admin
Splunk Password: ********
Confirm Password: ********
✓ Credentials saved securely!
```
#### 3. Configurer HTTPS (si Splunk est en HTTPS)
```bash
# Créer le dossier des certificats
mkdir -p /opt/splunk/etc/apps/pusher_app_prem/local/certs
# Générer un certificat auto-signé (valide 365 jours)
openssl req -x509 -newkey rsa:4096 \
-keyout /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key \
-out /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt \
-days 365 -nodes -subj "/CN=git-pusher"
# Définir les permissions
chmod 600 /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key
chown -R splunk:splunk /opt/splunk/etc/apps/pusher_app_prem/local/certs/
```
#### 4. Démarrer le serveur Git Pusher
```bash
./start_git_pusher.sh start
```
#### 5. Ouvrir le firewall (si nécessaire)
```bash
# FirewallD
sudo firewall-cmd --add-port=9999/tcp --permanent
sudo firewall-cmd --reload
# UFW
sudo ufw allow 9999/tcp
```
#### 6. Redémarrer Splunk
```bash
/opt/splunk/bin/splunk restart
```
#### 7. Accepter le certificat dans le navigateur
1. Ouvrir : `https://VOTRE_IP_SPLUNK:9999/health`
2. Accepter le certificat auto-signé
3. Rafraîchir le dashboard (Ctrl+Shift+R)
---
## 📥 Installation - SH Deployer Agent
### Sur le serveur SH Deployer
#### 1. Créer l'application
```bash
# Créer les dossiers
mkdir -p /opt/splunk/etc/apps/deployer_agent/bin
mkdir -p /opt/splunk/etc/apps/deployer_agent/local/certs
# Copier les fichiers
cp deployer_agent.py /opt/splunk/etc/apps/deployer_agent/bin/
cp start_deployer_agent.sh /opt/splunk/etc/apps/deployer_agent/bin/
# Rendre exécutable
chmod +x /opt/splunk/etc/apps/deployer_agent/bin/start_deployer_agent.sh
```
#### 2. Configurer le token d'authentification
```bash
nano /opt/splunk/etc/apps/deployer_agent/bin/deployer_agent.py
```
Modifier la ligne `AUTH_TOKEN` :
```python
AUTH_TOKEN = "votre_token_secret_personnalise"
```
#### 3. Configurer le Search Head Captain
Dans le même fichier, trouver la fonction `apply_shcluster_bundle` et modifier l'IP du captain :
```python
target_uri = "https://IP_DU_CAPTAIN:8089"
```
#### 4. Générer les certificats SSL
```bash
cd /opt/splunk/etc/apps/deployer_agent/bin/
./start_deployer_agent.sh gencerts
```
#### 5. Définir les permissions
```bash
chown -R splunk:splunk /opt/splunk/etc/apps/deployer_agent
```
#### 6. Ouvrir le firewall
```bash
sudo firewall-cmd --add-port=9998/tcp --permanent
sudo firewall-cmd --reload
```
#### 7. Démarrer l'agent
```bash
./start_deployer_agent.sh start
```
#### 8. Vérifier le statut
```bash
./start_deployer_agent.sh status
```
#### 9. Accepter le certificat
Ouvrir dans un navigateur : `https://IP_SH_DEPLOYER:9998/health`
Accepter le certificat auto-signé.
---
## ⚙️ Configuration
### Commandes Git Pusher (Serveur Source)
```bash
cd /opt/splunk/etc/apps/pusher_app_prem/bin/
# Démarrer le serveur
./start_git_pusher.sh start
# Arrêter le serveur
./start_git_pusher.sh stop
# Redémarrer le serveur
./start_git_pusher.sh restart
# Voir le statut
./start_git_pusher.sh status
# Voir les logs
./start_git_pusher.sh logs
./start_git_pusher.sh logs -f # Mode follow
# Gestion des credentials
./start_git_pusher.sh credentials setup
./start_git_pusher.sh credentials status
./start_git_pusher.sh credentials delete
```
### Commandes Deployer Agent (SH Deployer)
```bash
cd /opt/splunk/etc/apps/deployer_agent/bin/
# Démarrer l'agent
./start_deployer_agent.sh start
# Arrêter l'agent
./start_deployer_agent.sh stop
# Redémarrer l'agent
./start_deployer_agent.sh restart
# Voir le statut
./start_deployer_agent.sh status
# Voir les logs
./start_deployer_agent.sh logs
./start_deployer_agent.sh logs -f
# Générer les certificats SSL
./start_deployer_agent.sh gencerts
# Tester la connexion
./start_deployer_agent.sh test
```
### Configuration du SH Deployer dans l'interface
1. Ouvrir le dashboard Git Pusher
2. Cliquer sur **⚙️ Configure** dans la section "Deploy to Search Head Cluster"
3. Remplir :
- **Host** : IP du SH Deployer (ex: 10.10.40.14)
- **Port** : 9998
- **Token** : Le même token que dans `deployer_agent.py`
4. Cliquer sur **Save & Test**
---
## 🔒 Configuration HTTPS
### Git Pusher Server (Port 9999)
```bash
mkdir -p /opt/splunk/etc/apps/pusher_app_prem/local/certs
openssl req -x509 -newkey rsa:4096 \
-keyout /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key \
-out /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt \
-days 365 -nodes -subj "/CN=git-pusher"
chmod 600 /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key
chown -R splunk:splunk /opt/splunk/etc/apps/pusher_app_prem/local/certs/
```
### Deployer Agent (Port 9998)
```bash
mkdir -p /opt/splunk/etc/apps/deployer_agent/local/certs
openssl req -x509 -newkey rsa:4096 \
-keyout /opt/splunk/etc/apps/deployer_agent/local/certs/server.key \
-out /opt/splunk/etc/apps/deployer_agent/local/certs/server.crt \
-days 365 -nodes -subj "/CN=deployer-agent"
chmod 600 /opt/splunk/etc/apps/deployer_agent/local/certs/server.key
chown -R splunk:splunk /opt/splunk/etc/apps/deployer_agent/local/certs/
```
### Vérifier HTTPS
```bash
# Git Pusher
curl -k https://localhost:9999/health
# Deployer Agent
curl -k https://localhost:9998/health
```
---
## 🔐 Système de Licence
Git Pusher utilise un système de licence par fichier `.lic` pour activer les fonctionnalités.
### Types de licences
| Type | Durée | Apps max | Pushes/jour | Fonctionnalités |
|------|-------|----------|-------------|-----------------|
| **Trial** | 14 jours | 3 | 5 | Push basique |
| **Starter** | 1 an | 10 | 50 | + Push programmé |
| **Professional** | 1 an | Illimité | Illimité | + Multi-repo, Support prioritaire |
| **Enterprise** | 1 an | Illimité | Illimité | + SH Cluster deployment, API |
### Activer une licence
1. Ouvrir le dashboard Git Pusher
2. Glisser-déposer le fichier `.lic`
3. Cliquer sur "Activer"
---
## 📖 Utilisation
### Workflow complet : Push Git + Déploiement SH Cluster
1. **Ouvrir le dashboard** Git Pusher
2. **Sélectionner les applications** à déployer
3. **Configurer Git** :
- Repository URL : `https://github.com/user/repo.git`
- Branch : `main`
- Token : Personal Access Token
4. **Activer le déploiement SH Cluster** :
- Cocher "Enable automatic deployment"
- (Optionnel) Remplir les credentials Splunk si différents de l'admin par défaut
5. **Cliquer sur "Deploy to Git"**
Le processus automatique :
```
✅ Push vers Git
✅ Pull sur le SH Deployer
✅ Apply shcluster-bundle
✅ Mise à jour du Search Head Cluster
```
### Obtenir un token Git
#### GitHub
1. Settings → Developer settings → Personal access tokens
2. Generate new token
3. Cocher : `repo` (Full control)
#### GitLab
1. Preferences → Access Tokens
2. Create personal access token
3. Scopes : `write_repository`
#### Gitea
1. Settings → Applications → Generate New Token
2. Permissions : `repository: write`
---
## 🔒 Sécurité
### Credentials Splunk
- **Chiffrés** avec une clé dérivée de l'ID machine
- **Stockés** dans un fichier avec permissions `600`
- **Jamais visibles** en clair
### Communication HTTPS
- SSL/TLS entre tous les composants
- Certificats auto-signés supportés
- Validation du token d'authentification
### Token Deployer Agent
- Authentification par token entre Git Pusher et Deployer Agent
- Token configurable dans les deux composants
- Doit être identique des deux côtés
### Bonnes pratiques
1. ✅ Utiliser HTTPS pour tous les composants
2. ✅ Changer le token par défaut du Deployer Agent
3. ✅ Utiliser des tokens Git avec permissions minimales
4. ✅ Restreindre l'accès réseau au port 9998/9999
5. ✅ Renouveler les certificats régulièrement
---
## 🎨 Personnalisation du Dashboard
### Masquer les boutons d'édition
```bash
nano /opt/splunk/etc/apps/pusher_app_prem/default/data/ui/views/git_pusher_-_deploy_applications.xml
```
Modifier la première ligne :
```xml
<dashboard version="1.1" script="license_validation.js, git_pusher.js" hideEdit="true" hideExport="true">
```
### Options disponibles
| Attribut | Description |
|----------|-------------|
| `hideEdit="true"` | Masque le bouton "Modifier" |
| `hideExport="true"` | Masque le bouton "Exporter" |
| `hideTitle="true"` | Masque le titre du dashboard |
| `hideSplunkBar="true"` | Masque la barre Splunk en haut |
| `hideAppBar="true"` | Masque la barre de l'application |
| `hideFooter="true"` | Masque le footer |
### Appliquer les changements
```bash
rm -rf /opt/splunk/var/run/splunk/appserver/*
/opt/splunk/bin/splunk restart
```
---
## 🔧 Dépannage
### Les boutons ne fonctionnent pas
Le fichier JavaScript doit exposer les fonctions globalement. Vérifier que le bloc suivant est présent à la fin de `git_pusher.js` :
```javascript
// Exposer les fonctions globalement
window.showDeployerConfigModal = showDeployerConfigModal;
window.closeDeployerConfigModal = closeDeployerConfigModal;
window.saveDeployerConfigFromModal = saveDeployerConfigFromModal;
window.toggleDeployerAuth = toggleDeployerAuth;
// Attacher les événements aux boutons
(function attachButtonEvents() {
function tryAttach() {
var pushBtn = document.getElementById('push-btn');
if (pushBtn) {
pushBtn.addEventListener('click', function(e) {
e.preventDefault();
pushDashboards();
});
}
var buttons = document.querySelectorAll('button.btn');
buttons.forEach(function(btn) {
if (btn.textContent.includes('Reset')) {
btn.addEventListener('click', function(e) {
e.preventDefault();
resetForm(true);
});
}
});
var configBtn = document.querySelector('.deployer-config-btn');
if (configBtn) {
configBtn.addEventListener('click', function(e) {
e.preventDefault();
showDeployerConfigModal();
});
}
if (!pushBtn) setTimeout(tryAttach, 500);
}
if (document.readyState === 'complete') {
setTimeout(tryAttach, 1000);
} else {
window.addEventListener('load', function() {
setTimeout(tryAttach, 1000);
});
}
})();
```
Puis vider le cache :
```bash
rm -rf /opt/splunk/var/run/splunk/appserver/*
/opt/splunk/bin/splunk restart
```
Et dans le navigateur : **Ctrl+Shift+R**
### Erreur 401 Unauthorized (Deployer)
Le token ne correspond pas entre les deux composants.
1. Vérifier le token sur le SH Deployer :
```bash
grep "AUTH_TOKEN" /opt/splunk/etc/apps/deployer_agent/bin/deployer_agent.py
```
2. Configurer le même token dans l'interface (⚙️ Configure)
3. Tester :
```bash
curl -k -H "X-Auth-Token: VOTRE_TOKEN" https://IP_DEPLOYER:9998/status
```
### Erreur "tcsetattr: Inappropriate ioctl for device"
La commande `splunk apply shcluster-bundle` attend une entrée interactive.
Solution : Modifier la fonction `apply_shcluster_bundle` dans `deployer_agent.py` pour utiliser `echo 'y' |` :
```python
shell_cmd = f"echo 'y' | {SPLUNK_BIN} apply shcluster-bundle -target {target_uri} -auth {auth_user}:'{escaped_pass}' -preserve-lookups true"
```
### Erreur CORS
Vider le cache Splunk et navigateur :
```bash
rm -rf /opt/splunk/var/run/splunk/appserver/*
/opt/splunk/bin/splunk restart
```
Puis **Ctrl+Shift+R** dans le navigateur.
### Le SH Deployer ne répond pas
```bash
# Vérifier le statut
./start_deployer_agent.sh status
# Voir les logs
tail -50 /opt/splunk/var/log/splunk/deployer_agent.log
# Redémarrer
./start_deployer_agent.sh restart
```
### Le bundle ne s'applique pas
1. Vérifier que le pull a fonctionné :
```bash
ls -la /opt/splunk/etc/shcluster/apps/
```
2. Vérifier les logs :
```bash
grep -i "bundle\|error" /opt/splunk/var/log/splunk/deployer_agent.log | tail -30
```
3. Tester manuellement :
```bash
echo 'y' | /opt/splunk/bin/splunk apply shcluster-bundle -target https://CAPTAIN_IP:8089 -auth admin:'password' -preserve-lookups true
```
### Vider le cache Splunk
```bash
rm -rf /opt/splunk/var/run/splunk/appserver/*
/opt/splunk/bin/splunk restart
```
---
## 📡 API Reference
### Git Pusher Server (Port 9999)
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| `GET` | `/health` | Health check |
| `GET` | `/license` | Statut de la licence |
| `GET` | `/license/hostname` | Hostname Splunk |
| `GET` | `/deployer/health` | Santé du SH Deployer |
| `GET` | `/deployer/status` | Statut du SH Deployer |
| `GET` | `/deployer/config` | Configuration du SH Deployer |
| `POST` | `/license/upload` | Uploader une licence |
| `POST` | `/push` | Pousser les applications |
### Deployer Agent (Port 9998)
| Méthode | Endpoint | Description |
|---------|----------|-------------|
| `GET` | `/health` | Health check (pas d'auth) |
| `GET` | `/status` | Statut du déploiement |
| `GET` | `/apps` | Liste des apps |
| `GET` | `/history` | Historique des déploiements |
| `POST` | `/pull` | Git pull |
| `POST` | `/deploy` | Apply shcluster-bundle |
| `POST` | `/pull-and-deploy` | Pull + Deploy en une opération |
### Exemple : Push avec déploiement SH Cluster
```bash
curl -k -X POST "https://localhost:9999/push?git_url=https://github.com/user/repo.git&git_branch=main&git_token=TOKEN&commit_message=Update&apps=[{\"id\":\"my_app\"}]&user=admin&deploy_to_shcluster=true"
```
---
## 📝 Changelog
### Version 2.1.0 (Février 2026)
- 🚀 **Nouveau** : Déploiement automatique vers Search Head Cluster
- 🔧 **Nouveau** : Agent Deployer pour le SH Deployer
- ⚙️ **Nouveau** : Interface de configuration du SH Deployer
- 🔐 **Amélioration** : Gestion des caractères spéciaux dans les mots de passe
- 🛠️ **Correction** : Mode non-interactif pour shcluster-bundle
### Version 2.0.0 (Février 2026)
- ✨ Nouveau système de licence par fichier `.lic`
- 🔐 Credentials Splunk chiffrés
- 🔒 Support HTTPS avec certificats SSL
- 🎨 Interface utilisateur modernisée
- 📊 Badge de licence en temps réel
- 🔧 Script de gestion amélioré
- 🛡️ Options pour masquer les boutons d'édition du dashboard
- 📝 Logs détaillés
- 🐛 Correction des problèmes CORS
### Version 1.0.0
- 🚀 Version initiale
- Push d'applications vers Git
- Interface basique
---
## 📞 Support
### Obtenir de l'aide
- 📧 Email : support@gitpusher.com
- 🌐 Site web : https://gitpusher.com
- 📖 Documentation : https://docs.gitpusher.com
### Signaler un bug
Incluez dans votre rapport :
1. Version de Git Pusher
2. Version de Splunk
3. Configuration (HTTP/HTTPS)
4. Architecture (standalone, SH Cluster)
5. Logs Git Pusher (`/opt/splunk/var/log/splunk/git_pusher.log`)
6. Logs Deployer Agent (`/opt/splunk/var/log/splunk/deployer_agent.log`)
7. Erreurs de la console navigateur (F12)
8. Étapes pour reproduire le problème
---
## 📄 Licence
Git Pusher est un logiciel propriétaire. Une licence valide est requise pour son utilisation.
© 2026 Git Pusher - Tous droits réservés
---
<p align="center">
Made with ❤️ for Splunk administrators
</p>

@ -0,0 +1,391 @@
// ============================================
// GIT PUSHER - CONFIGURATION PAGE
// Version 2.1 - Compatible Splunk
// ============================================
require([
'jquery',
'splunkjs/mvc',
'splunkjs/mvc/simplexml/ready!'
], function($, mvc) {
console.log('Git Pusher Config v2.1 initializing...');
// Configuration par défaut
var DEFAULT_CONFIG = {
api: {
url: '',
port: 9999,
useProxy: true
},
deployer: {
enabled: false,
host: '',
port: 9998,
token: '',
useSSL: true
},
license: {
checkInterval: 24
},
advanced: {
logLevel: 'INFO',
timeout: 30,
gitTimeout: 120
}
};
// URL de l'API pour la config
function getConfigApiUrl() {
var hostname = window.location.hostname;
var protocol = window.location.protocol;
// Essayer de charger depuis localStorage
try {
var stored = localStorage.getItem('git_pusher_config');
if (stored) {
var config = JSON.parse(stored);
if (config.api && config.api.url) {
var url = config.api.url;
if (!config.api.useProxy && config.api.port) {
url = url.replace(/\/$/, '') + ':' + config.api.port;
}
return url;
}
}
} catch(e) {}
// Auto-détection
if (hostname === 'myprivspldev.jp-engineering.fr') {
return protocol + '//myprivspldev-api.jp-engineering.fr';
}
if (hostname === 'myprivspldev-api.jp-engineering.fr') {
return protocol + '//' + hostname;
}
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname) || hostname === 'localhost') {
return protocol + '//' + hostname + ':9999';
}
return protocol + '//' + hostname + ':9999';
}
// ============================================
// CHARGEMENT DE LA CONFIGURATION
// ============================================
function loadConfig() {
console.log('Loading configuration...');
var apiUrl = getConfigApiUrl();
$.ajax({
url: apiUrl + '/config',
method: 'GET',
dataType: 'json',
success: function(config) {
console.log('Config loaded:', config);
applyConfigToForm(config);
showMessage('Configuration chargée', 'success');
},
error: function(xhr, status, error) {
console.log('No server config, using defaults:', error);
applyConfigToForm(DEFAULT_CONFIG);
}
});
loadLicenseStatus();
}
function applyConfigToForm(config) {
// API
$('#api-url').val(config.api ? config.api.url || '' : '');
$('#api-port').val(config.api ? config.api.port || 9999 : 9999);
$('#use-proxy').prop('checked', config.api ? config.api.useProxy !== false : true);
// Deployer
$('#deployer-enabled').prop('checked', config.deployer ? config.deployer.enabled || false : false);
$('#deployer-host').val(config.deployer ? config.deployer.host || '' : '');
$('#deployer-port').val(config.deployer ? config.deployer.port || 9998 : 9998);
$('#deployer-token').val(config.deployer ? config.deployer.token || '' : '');
$('#deployer-use-ssl').prop('checked', config.deployer ? config.deployer.useSSL !== false : true);
// Licence
$('#license-check-interval').val(config.license ? config.license.checkInterval || 24 : 24);
// Avancé
$('#log-level').val(config.advanced ? config.advanced.logLevel || 'INFO' : 'INFO');
$('#timeout').val(config.advanced ? config.advanced.timeout || 30 : 30);
$('#git-timeout').val(config.advanced ? config.advanced.gitTimeout || 120 : 120);
}
function getConfigFromForm() {
return {
api: {
url: $('#api-url').val().trim(),
port: parseInt($('#api-port').val()) || 9999,
useProxy: $('#use-proxy').is(':checked')
},
deployer: {
enabled: $('#deployer-enabled').is(':checked'),
host: $('#deployer-host').val().trim(),
port: parseInt($('#deployer-port').val()) || 9998,
token: $('#deployer-token').val(),
useSSL: $('#deployer-use-ssl').is(':checked')
},
license: {
checkInterval: parseInt($('#license-check-interval').val()) || 24
},
advanced: {
logLevel: $('#log-level').val(),
timeout: parseInt($('#timeout').val()) || 30,
gitTimeout: parseInt($('#git-timeout').val()) || 120
}
};
}
// ============================================
// SAUVEGARDE DE LA CONFIGURATION
// ============================================
function saveConfig() {
console.log('Saving configuration...');
var config = getConfigFromForm();
var apiUrl = getConfigApiUrl();
$.ajax({
url: apiUrl + '/config',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(config),
dataType: 'json',
success: function(result) {
console.log('Save result:', result);
if (result.success) {
showMessage('✅ Configuration sauvegardée avec succès !', 'success');
// Sauvegarder aussi dans localStorage
localStorage.setItem('git_pusher_config', JSON.stringify(config));
} else {
showMessage('❌ Erreur: ' + (result.error || 'Échec de la sauvegarde'), 'error');
}
},
error: function(xhr, status, error) {
console.error('Save error:', error);
showMessage('❌ Erreur de connexion au serveur: ' + error, 'error');
}
});
}
function resetConfig() {
if (confirm('Voulez-vous vraiment réinitialiser la configuration ?')) {
applyConfigToForm(DEFAULT_CONFIG);
showMessage('Configuration réinitialisée (non sauvegardée)', 'success');
}
}
// ============================================
// TESTS DE CONNEXION
// ============================================
function testApiConnection() {
console.log('Testing API connection...');
var $status = $('#api-status');
$status.removeClass('connected disconnected').text('● Test en cours...');
var apiUrl = $('#api-url').val().trim();
if (!apiUrl) {
apiUrl = getConfigApiUrl();
} else if (!$('#use-proxy').is(':checked')) {
var port = $('#api-port').val() || 9999;
if (apiUrl.indexOf(':' + port) === -1) {
apiUrl = apiUrl.replace(/\/$/, '') + ':' + port;
}
}
console.log('Testing URL:', apiUrl);
$.ajax({
url: apiUrl + '/health',
method: 'GET',
dataType: 'json',
timeout: 10000,
success: function(data) {
console.log('API health:', data);
$status.addClass('connected').text('● Connecté');
},
error: function(xhr, status, error) {
console.error('API test failed:', error);
$status.addClass('disconnected').text('● Échec connexion');
}
});
}
function testDeployerConnection() {
console.log('Testing Deployer connection...');
var $status = $('#deployer-status');
$status.removeClass('connected disconnected').text('● Test en cours...');
var host = $('#deployer-host').val().trim();
var port = $('#deployer-port').val() || 9998;
var useSSL = $('#deployer-use-ssl').is(':checked');
var token = $('#deployer-token').val();
if (!host) {
$status.addClass('disconnected').text('● Adresse manquante');
return;
}
var protocol = useSSL ? 'https' : 'http';
var url;
// Si c'est un nom de domaine (contient des lettres et des points, pas une IP)
// Ne pas ajouter le port (le proxy gère)
if (/^[a-zA-Z]/.test(host) && host.indexOf('.') > -1 && !/^(\d{1,3}\.){3}\d{1,3}$/.test(host)) {
// C'est un domaine, pas de port
url = protocol + '://' + host + '/health';
} else {
// C'est une IP ou localhost, ajouter le port
url = protocol + '://' + host + ':' + port + '/health';
}
console.log('Testing Deployer URL:', url);
$.ajax({
url: url,
method: 'GET',
dataType: 'json',
timeout: 10000,
headers: {
'X-Auth-Token': token
},
success: function(data) {
console.log('Deployer health:', data);
$status.addClass('connected').text('● Connecté');
},
error: function(xhr, status, error) {
console.error('Deployer test failed:', error);
$status.addClass('disconnected').text('● Échec connexion');
}
});
}
// ============================================
// STATUT DE LA LICENCE
// ============================================
function loadLicenseStatus() {
var $status = $('#license-status');
try {
var stored = localStorage.getItem('git_pusher_license');
if (stored) {
var parsed = JSON.parse(stored);
var licenseData = parsed.licenseData;
if (licenseData) {
var expires = new Date(licenseData.expires);
var now = new Date();
var daysRemaining = Math.ceil((expires - now) / (1000 * 60 * 60 * 24));
if (daysRemaining > 0) {
$status.html(
'<span class="config-status connected">● Active</span>' +
'<br><small>Type: ' + licenseData.type_name + ' | Expire: ' + licenseData.expires + ' (' + daysRemaining + 'j)</small>'
);
} else {
$status.html(
'<span class="config-status disconnected">● Expirée</span>' +
'<br><small>Expirée le ' + licenseData.expires + '</small>'
);
}
return;
}
}
$status.html('<span class="config-status disconnected">● Non installée</span>');
} catch (error) {
console.error('Erreur lecture licence:', error);
$status.html('<span class="config-status disconnected">● Erreur</span>');
}
}
// ============================================
// UTILITAIRES
// ============================================
function showMessage(message, type) {
var $msg = $('#config-message');
$msg.text(message).removeClass('success error').addClass(type).show();
setTimeout(function() {
$msg.fadeOut();
}, 5000);
}
// ============================================
// ATTACHER LES ÉVÉNEMENTS
// ============================================
function attachEvents() {
console.log('Attaching events...');
// Bouton Test API
$('#test-api-btn').on('click', function(e) {
e.preventDefault();
console.log('Test API clicked');
testApiConnection();
});
// Bouton Test Deployer
$('#test-deployer-btn').on('click', function(e) {
e.preventDefault();
console.log('Test Deployer clicked');
testDeployerConnection();
});
// Bouton Sauvegarder
$('#save-btn').on('click', function(e) {
e.preventDefault();
console.log('Save clicked');
saveConfig();
});
// Bouton Réinitialiser
$('#reset-btn').on('click', function(e) {
e.preventDefault();
console.log('Reset clicked');
resetConfig();
});
console.log('Events attached to buttons');
}
// ============================================
// INITIALISATION
// ============================================
// Attendre que le DOM soit complètement prêt
function init() {
if ($('#api-url').length > 0) {
console.log('DOM ready, initializing...');
attachEvents();
loadConfig();
} else {
console.log('DOM not ready, retrying...');
setTimeout(init, 300);
}
}
setTimeout(init, 500);
// Exposer globalement pour le debug
window.gitPusherConfig = {
saveConfig: saveConfig,
resetConfig: resetConfig,
testApiConnection: testApiConnection,
testDeployerConnection: testDeployerConnection,
loadConfig: loadConfig
};
console.log('Git Pusher Config module loaded');
});

@ -0,0 +1,613 @@
// ============================================
// SYSTÈME DE GESTION DE LICENCE FICHIER .LIC
// ============================================
const LICENSE_FILE_KEY = 'git_pusher_license_file';
const LICENSE_STATUS_ENDPOINT = '/custom/git_pusher/license_status';
let currentLicenseStatus = null;
function initializeLicenseSystem() {
console.log("Initializing file-based license system...");
// Vérifier le statut de la licence
checkLicenseStatus();
}
function checkLicenseStatus() {
console.log("Checking license status...");
// Appeler le backend pour vérifier la licence
fetch(LICENSE_STATUS_ENDPOINT)
.then(response => response.json())
.then(data => {
console.log("License status:", data);
currentLicenseStatus = data;
if (data.licensed) {
// Licence valide
displayLicenseBadge(data.license_info, data.warnings);
} else {
// Pas de licence ou invalide
showLicenseUploadModal(data.errors);
}
})
.catch(error => {
console.error("Error checking license:", error);
// En cas d'erreur, afficher le modal
showLicenseUploadModal(["Impossible de vérifier la licence"]);
});
}
function displayLicenseBadge(licenseInfo, warnings) {
console.log("Displaying license 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;
`;
// Déterminer le texte et la couleur selon le type
let badgeText = '✓ Licence Activée';
let badgeColor = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
if (licenseInfo.type === 'trial') {
badgeText = `⏱️ Essai: ${licenseInfo.days_remaining} jours`;
badgeColor = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';
} else if (licenseInfo.type === 'standard') {
badgeText = `✓ Standard (${licenseInfo.days_remaining}j)`;
} else if (licenseInfo.type === 'enterprise') {
badgeText = `✓ Enterprise (${licenseInfo.days_remaining}j)`;
badgeColor = 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)';
}
// Avertissement si expire bientôt
if (licenseInfo.days_remaining < 30) {
badgeColor = 'linear-gradient(135deg, #ff9800 0%, #f57c00 100%)';
badgeText = `⚠️ Expire dans ${licenseInfo.days_remaining}j`;
}
badge.style.background = badgeColor;
badge.textContent = badgeText;
// Clic pour afficher les détails
badge.onclick = function() {
showLicenseDetailsModal(licenseInfo, warnings);
};
container.appendChild(badge);
// 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 showLicenseUploadModal(errors = []) {
console.log("Showing license upload modal");
// Créer le modal
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.8);
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: 550px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease;
`;
let errorsHtml = '';
if (errors.length > 0) {
errorsHtml = `
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 25px; border-left: 4px solid #ff9800;">
<p style="margin: 0; color: #856404; font-weight: 500; font-size: 13px;">
<strong>Problèmes détectés:</strong>
</p>
<ul style="margin: 10px 0 0 20px; color: #856404; font-size: 12px;">
${errors.map(e => `<li>${e}</li>`).join('')}
</ul>
</div>
`;
}
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>
${errorsHtml}
<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: 25px;">
<label style="display: block; font-weight: 600; color: #333; margin-bottom: 12px; font-size: 14px;">
📂 Choisissez votre fichier de licence (.lic)
</label>
<div id="drop-zone" style="
border: 3px dashed #667eea;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #f8f9ff;
">
<div style="font-size: 48px; margin-bottom: 15px;">📄</div>
<p style="margin: 0 0 10px 0; font-weight: 600; color: #333;">
Glissez votre fichier .lic ici
</p>
<p style="margin: 0; color: #999; font-size: 13px;">
ou cliquez pour parcourir
</p>
<input type="file" id="license-file-input" accept=".lic" style="display: none;">
</div>
<div id="file-info" style="display: none; margin-top: 15px; padding: 12px; background: #e8f5e9; border-radius: 8px; border-left: 4px solid #4caf50;">
<p style="margin: 0; color: #2e7d32; font-size: 13px;">
<strong id="file-name"></strong> (<span id="file-size"></span>)
</p>
</div>
<small style="color: #999; display: block; margin-top: 12px; font-size: 12px;">
💡 Vous n'avez pas de licence? Contactez-nous pour obtenir votre fichier .lic personnalisé
</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 id="upload-btn" onclick="uploadLicenseFile()" disabled style="
flex: 1;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: not-allowed;
font-size: 14px;
transition: all 0.3s ease;
opacity: 0.5;
">📤 Installer la licence</button>
<button onclick="requestTrial()" style="
padding: 14px 20px;
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</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
// Ajouter l'animation CSS
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
document.head.appendChild(style);
// Récupérer et afficher le hostname
getHostname().then(hostname => {
document.getElementById('detected-hostname').textContent = hostname;
});
// Configurer le drag & drop et file input
setupFileUpload();
}
function setupFileUpload() {
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('license-file-input');
const fileInfo = document.getElementById('file-info');
const uploadBtn = document.getElementById('upload-btn');
let selectedFile = null;
// Clic sur la zone pour ouvrir le sélecteur
dropZone.onclick = () => fileInput.click();
// Hover effect
dropZone.addEventListener('mouseenter', function() {
this.style.background = '#eef1ff';
this.style.borderColor = '#5568d3';
});
dropZone.addEventListener('mouseleave', function() {
this.style.background = '#f8f9ff';
this.style.borderColor = '#667eea';
});
// Drag & drop
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.background = '#e3e7ff';
dropZone.style.borderColor = '#5568d3';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.background = '#f8f9ff';
dropZone.style.borderColor = '#667eea';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.background = '#f8f9ff';
dropZone.style.borderColor = '#667eea';
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelection(files[0]);
}
});
// Sélection de fichier
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelection(e.target.files[0]);
}
});
function handleFileSelection(file) {
selectedFile = file;
// Vérifier l'extension
if (!file.name.endsWith('.lic')) {
showLicenseMessage('⚠️ Veuillez sélectionner un fichier .lic', 'warning');
return;
}
// Afficher les infos du fichier
document.getElementById('file-name').textContent = file.name;
document.getElementById('file-size').textContent = formatFileSize(file.size);
fileInfo.style.display = 'block';
// Activer le bouton
uploadBtn.disabled = false;
uploadBtn.style.cursor = 'pointer';
uploadBtn.style.opacity = '1';
// Stocker le fichier pour l'upload
window.selectedLicenseFile = file;
showLicenseMessage('✓ Fichier prêt à être installé', 'success');
}
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function uploadLicenseFile() {
const file = window.selectedLicenseFile;
if (!file) {
showLicenseMessage('❌ Aucun fichier sélectionné', 'error');
return;
}
showLicenseMessage('⏳ Installation de la licence...', 'info');
// Lire le fichier
const reader = new FileReader();
reader.onload = function(e) {
const fileContent = e.target.result;
// Envoyer au backend pour validation et installation
fetch('/custom/git_pusher/install_license', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
license_file: fileContent,
filename: file.name
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showLicenseMessage('✅ Licence installée avec succès !', 'success');
setTimeout(() => {
closeLicenseModal();
// Recharger pour afficher le badge
checkLicenseStatus();
}, 1500);
} else {
showLicenseMessage('❌ ' + (data.message || 'Erreur d\'installation'), 'error');
if (data.errors && data.errors.length > 0) {
const errorList = data.errors.map(e => `${e}`).join('\n');
console.error('License errors:', errorList);
}
}
})
.catch(error => {
console.error('Upload error:', error);
showLicenseMessage('❌ Erreur de connexion au serveur', 'error');
});
};
reader.onerror = function() {
showLicenseMessage('❌ Erreur de lecture du fichier', 'error');
};
reader.readAsText(file);
}
function requestTrial() {
showLicenseMessage('⏳ Demande d\'essai en cours...', 'info');
// Appeler le backend pour créer une licence d'essai
fetch('/custom/git_pusher/request_trial', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showLicenseMessage('✅ Licence d\'essai activée (7 jours) !', 'success');
setTimeout(() => {
closeLicenseModal();
checkLicenseStatus();
}, 1500);
} else {
showLicenseMessage('❌ ' + (data.message || 'Erreur'), 'error');
}
})
.catch(error => {
console.error('Trial request error:', error);
showLicenseMessage('❌ Erreur de connexion', 'error');
});
}
function showLicenseDetailsModal(licenseInfo, warnings) {
// Créer un modal pour afficher les détails de la licence
const modal = document.createElement('div');
modal.id = 'license-details-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: 10001;
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.5);
`;
let warningsHtml = '';
if (warnings && warnings.length > 0) {
warningsHtml = `
<div style="background: #fff3cd; padding: 12px; border-radius: 8px; margin-top: 20px; border-left: 4px solid #ff9800;">
${warnings.map(w => `<p style="margin: 5px 0; color: #856404; font-size: 13px;">${w}</p>`).join('')}
</div>
`;
}
const featuresHtml = Object.entries(licenseInfo.features || {})
.map(([feature, enabled]) => {
const icon = enabled ? '✅' : '❌';
return `<div style="padding: 8px 0;"><span style="font-size: 14px;">${icon} ${feature}</span></div>`;
}).join('');
content.innerHTML = `
<div style="text-align: center; margin-bottom: 25px;">
<h2 style="font-size: 24px; margin: 0 0 5px 0; color: #333;">🔐 Informations de licence</h2>
</div>
<div style="background: #f5f7ff; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
<div style="margin-bottom: 15px;">
<strong style="color: #667eea;">🆔 ID de licence:</strong>
<div style="font-family: monospace; font-size: 12px; margin-top: 5px; color: #555;">${licenseInfo.license_id}</div>
</div>
<div style="margin-bottom: 15px;">
<strong style="color: #667eea;">👤 Client:</strong>
<div style="margin-top: 5px; color: #555;">${licenseInfo.customer}</div>
</div>
<div style="margin-bottom: 15px;">
<strong style="color: #667eea;">🏷 Type:</strong>
<div style="margin-top: 5px; color: #555;">${licenseInfo.type.toUpperCase()}</div>
</div>
<div style="margin-bottom: 15px;">
<strong style="color: #667eea;">📅 Expire le:</strong>
<div style="margin-top: 5px; color: #555;">${new Date(licenseInfo.expires).toLocaleDateString('fr-FR')}</div>
</div>
<div>
<strong style="color: #667eea;"> Jours restants:</strong>
<div style="margin-top: 5px; color: #555; font-size: 18px; font-weight: 600;">${licenseInfo.days_remaining} jours</div>
</div>
</div>
<div style="margin-bottom: 20px;">
<strong style="color: #333; display: block; margin-bottom: 10px;"> Fonctionnalités:</strong>
<div style="background: #f9f9f9; padding: 15px; border-radius: 8px;">
${featuresHtml}
</div>
</div>
${warningsHtml}
<div style="display: flex; gap: 10px; margin-top: 25px;">
<button onclick="showLicenseUploadModal([])" style="
flex: 1;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
">🔄 Changer de licence</button>
<button onclick="closeDetailsModal()" style="
flex: 1;
padding: 12px;
background: #f5f7ff;
color: #667eea;
border: 2px solid #667eea;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
">Fermer</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
}
function closeDetailsModal() {
const modal = document.getElementById('license-details-modal');
if (modal) {
modal.remove();
}
}
function showLicenseMessage(message, type) {
const messageEl = document.getElementById('license-message');
if (!messageEl) return;
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 === 'warning') {
messageEl.style.background = '#fff3cd';
messageEl.style.color = '#856404';
messageEl.style.border = '1px solid #ffeaa7';
} else if (type === 'info') {
messageEl.style.background = '#d1ecf1';
messageEl.style.color = '#0c5460';
messageEl.style.border = '1px solid #bee5eb';
}
}
function closeLicenseModal() {
const modal = document.getElementById('license-modal');
if (modal) {
modal.remove();
}
}
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 checkLicenseBeforePush() {
if (!currentLicenseStatus || !currentLicenseStatus.licensed) {
alert('⚠️ Aucune licence valide détectée. Veuillez installer une licence.');
showLicenseUploadModal([]);
return false;
}
return true;
}
// Initialiser au chargement de la page
document.addEventListener('DOMContentLoaded', function() {
initializeLicenseSystem();
});

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

@ -0,0 +1,271 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Git Pusher - Credentials Manager
Gère les credentials de manière sécurisée via Splunk storage/passwords
ou via un fichier chiffré local.
"""
import os
import sys
import json
import base64
import hashlib
from pathlib import Path
# Chemin vers le fichier de credentials chiffré
CREDENTIALS_FILE = "/opt/splunk/etc/apps/pusher_app_prem/local/.credentials"
ENCRYPTION_KEY_FILE = "/opt/splunk/etc/apps/pusher_app_prem/local/.key"
def get_machine_id():
"""Obtenir un identifiant unique de la machine pour le chiffrement"""
machine_id = ""
# Essayer différentes sources
try:
with open('/etc/machine-id', 'r') as f:
machine_id = f.read().strip()
except:
pass
if not machine_id:
try:
import socket
machine_id = socket.gethostname()
except:
machine_id = "default_key"
return machine_id
def derive_key(password, salt=None):
"""Dériver une clé de chiffrement à partir d'un mot de passe"""
if salt is None:
salt = get_machine_id().encode()
# Utiliser PBKDF2-like avec SHA256
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
100000
)
return base64.b64encode(key).decode()
def simple_encrypt(data, key):
"""Chiffrement simple XOR (pour une sécurité basique)"""
# Pour une vraie sécurité, utiliser cryptography.fernet
key_bytes = key.encode() * (len(data) // len(key) + 1)
encrypted = bytes([a ^ b for a, b in zip(data.encode(), key_bytes[:len(data)])])
return base64.b64encode(encrypted).decode()
def simple_decrypt(encrypted_data, key):
"""Déchiffrement simple XOR"""
try:
data = base64.b64decode(encrypted_data)
key_bytes = key.encode() * (len(data) // len(key) + 1)
decrypted = bytes([a ^ b for a, b in zip(data, key_bytes[:len(data)])])
return decrypted.decode()
except Exception as e:
return None
def get_encryption_key():
"""Obtenir ou créer la clé de chiffrement"""
if os.path.exists(ENCRYPTION_KEY_FILE):
with open(ENCRYPTION_KEY_FILE, 'r') as f:
return f.read().strip()
else:
# Générer une nouvelle clé basée sur la machine
key = derive_key(get_machine_id())
# Sauvegarder la clé
os.makedirs(os.path.dirname(ENCRYPTION_KEY_FILE), exist_ok=True)
with open(ENCRYPTION_KEY_FILE, 'w') as f:
f.write(key)
# Protéger le fichier
os.chmod(ENCRYPTION_KEY_FILE, 0o600)
return key
def save_credentials(username, password):
"""Sauvegarder les credentials de manière chiffrée"""
key = get_encryption_key()
credentials = {
'username': username,
'password': simple_encrypt(password, key)
}
os.makedirs(os.path.dirname(CREDENTIALS_FILE), exist_ok=True)
with open(CREDENTIALS_FILE, 'w') as f:
json.dump(credentials, f)
# Protéger le fichier
os.chmod(CREDENTIALS_FILE, 0o600)
print(f"✓ Credentials saved securely to {CREDENTIALS_FILE}")
def load_credentials():
"""Charger les credentials chiffrés"""
if not os.path.exists(CREDENTIALS_FILE):
return None, None
try:
key = get_encryption_key()
with open(CREDENTIALS_FILE, 'r') as f:
credentials = json.load(f)
username = credentials.get('username')
encrypted_password = credentials.get('password')
password = simple_decrypt(encrypted_password, key)
return username, password
except Exception as e:
print(f"Error loading credentials: {e}", file=sys.stderr)
return None, None
def delete_credentials():
"""Supprimer les credentials stockés"""
if os.path.exists(CREDENTIALS_FILE):
os.remove(CREDENTIALS_FILE)
print("✓ Credentials deleted")
else:
print("No credentials file found")
def get_credentials_for_script():
"""
Fonction utilisée par le script de démarrage pour obtenir les credentials.
Retourne username et password, ou None si non configurés.
"""
username, password = load_credentials()
if username and password:
return username, password
# Fallback sur les variables d'environnement
env_user = os.environ.get('SPLUNK_USERNAME')
env_pass = os.environ.get('SPLUNK_PASSWORD')
if env_user and env_pass:
return env_user, env_pass
return None, None
# ============================================
# CLI
# ============================================
def interactive_setup():
"""Configuration interactive des credentials"""
print("=" * 50)
print("Git Pusher - Credentials Setup")
print("=" * 50)
print()
print("This will securely store your Splunk credentials.")
print(f"Credentials will be encrypted and saved to:")
print(f" {CREDENTIALS_FILE}")
print()
username = input("Splunk Username [admin]: ").strip() or "admin"
import getpass
password = getpass.getpass("Splunk Password: ")
if not password:
print("Error: Password cannot be empty")
return
# Confirmer
password2 = getpass.getpass("Confirm Password: ")
if password != password2:
print("Error: Passwords do not match")
return
save_credentials(username, password)
print()
print("✓ Credentials configured successfully!")
print()
print("You can now start Git Pusher without specifying credentials:")
print(" ./start_git_pusher.sh start")
def show_status():
"""Afficher le statut des credentials"""
print("=" * 50)
print("Credentials Status")
print("=" * 50)
if os.path.exists(CREDENTIALS_FILE):
print(f"✓ Credentials file exists: {CREDENTIALS_FILE}")
username, password = load_credentials()
if username:
print(f" Username: {username}")
print(f" Password: {'*' * len(password) if password else 'ERROR'}")
else:
print(" Error: Could not decrypt credentials")
else:
print("✗ No credentials file found")
print()
print("Run: python credentials_manager.py setup")
if __name__ == '__main__':
if len(sys.argv) < 2:
show_status()
sys.exit(0)
command = sys.argv[1]
if command == 'setup':
interactive_setup()
elif command == 'status':
show_status()
elif command == 'delete':
delete_credentials()
elif command == 'get':
# Pour utilisation dans les scripts bash
username, password = get_credentials_for_script()
if username and password:
print(f"{username}")
print(f"{password}")
else:
sys.exit(1)
elif command == 'export':
# Exporter pour utilisation dans bash
username, password = get_credentials_for_script()
if username and password:
print(f"export SPLUNK_USERNAME='{username}'")
print(f"export SPLUNK_PASSWORD='{password}'")
else:
print("# No credentials found", file=sys.stderr)
sys.exit(1)
else:
print("Usage: python credentials_manager.py [setup|status|delete|get|export]")
print()
print("Commands:")
print(" setup - Configure credentials interactively")
print(" status - Show credentials status")
print(" delete - Delete stored credentials")
print(" get - Output credentials (for scripts)")
print(" export - Output as bash export commands")

@ -0,0 +1,984 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Git Pusher - Main Server
Serveur HTTP pour pousser les applications Splunk vers Git
et déployer vers le Search Head Cluster via le SH Deployer
Avec système de licence par fichier .lic
"""
import sys
import os
import json
import logging
import tempfile
import shutil
import subprocess
import ssl
import urllib.request
import urllib.error
from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse
# Importer le validateur de licence
# En production, ce fichier sera dans le même dossier
try:
from license_validator import (
validate_license,
save_license_file,
check_limits,
increment_usage,
get_splunk_hostname,
get_usage_stats,
parse_license_content
)
except ImportError:
# Fallback pour le développement
print("Warning: license_validator not found, running without license checks")
def validate_license(): return {"valid": True, "type": "dev", "days_remaining": 999}
def save_license_file(c): return {"success": True}
def check_limits(): return {"allowed": True}
def increment_usage(): return {}
def get_splunk_hostname(): return "dev-host"
def get_usage_stats(): return {}
def parse_license_content(c): return {}
# ============================================
# CONFIGURATION
# ============================================
# Chemins Splunk
SPLUNK_HOME = os.environ.get('SPLUNK_HOME', '/opt/splunk')
APP_HOME = os.path.join(SPLUNK_HOME, 'etc', 'apps', 'pusher_app_prem')
CONFIG_FILE = os.path.join(APP_HOME, 'local', 'config.json')
# Configuration par défaut
DEFAULT_CONFIG = {
"api": {
"url": "",
"port": 9999,
"useProxy": True
},
"deployer": {
"enabled": False,
"host": "",
"port": 9998,
"token": "",
"useSSL": True
},
"license": {
"checkInterval": 24
},
"advanced": {
"logLevel": "INFO",
"timeout": 30,
"gitTimeout": 120
}
}
def load_config():
"""Charger la configuration depuis le fichier"""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
# Fusionner avec la config par défaut pour les clés manquantes
return {**DEFAULT_CONFIG, **config}
except Exception as e:
logger.error(f"Erreur chargement config: {e}")
return DEFAULT_CONFIG.copy()
def save_config(config):
"""Sauvegarder la configuration dans le fichier"""
try:
local_dir = os.path.join(APP_HOME, 'local')
os.makedirs(local_dir, exist_ok=True)
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
os.chmod(CONFIG_FILE, 0o600)
logger.info(f"Configuration sauvegardée: {CONFIG_FILE}")
return True
except Exception as e:
logger.error(f"Erreur sauvegarde config: {e}")
return False
# Charger la configuration au démarrage
APP_CONFIG = load_config()
# Configuration du SH Deployer (depuis la config ou valeurs par défaut)
SH_DEPLOYER_CONFIG = {
"enabled": APP_CONFIG.get("deployer", {}).get("enabled", False),
"host": APP_CONFIG.get("deployer", {}).get("host", ""),
"port": APP_CONFIG.get("deployer", {}).get("port", 9998),
"use_ssl": APP_CONFIG.get("deployer", {}).get("useSSL", True),
"token": APP_CONFIG.get("deployer", {}).get("token", ""),
"timeout": APP_CONFIG.get("advanced", {}).get("timeout", 30)
}
# Configuration du logging
log_dir = '/opt/splunk/var/log/splunk'
os.makedirs(log_dir, exist_ok=True)
log_level = getattr(logging, APP_CONFIG.get("advanced", {}).get("logLevel", "INFO"), logging.INFO)
logging.basicConfig(
level=log_level,
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 send_cors_headers(self):
"""Envoyer les headers CORS complets"""
# Permettre toutes les origines
origin = self.headers.get('Origin', '*')
self.send_header('Access-Control-Allow-Origin', origin)
self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, DELETE')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin, X-Splunk-Form-Key, X-Auth-Token')
self.send_header('Access-Control-Allow-Credentials', 'true')
self.send_header('Access-Control-Max-Age', '86400') # Cache preflight 24h
def do_OPTIONS(self):
"""Traiter les requêtes OPTIONS (CORS preflight)"""
logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}")
self.send_response(200)
self.send_cors_headers()
self.end_headers()
# Important: ne rien écrire dans le body pour OPTIONS
return
def do_GET(self):
"""Traiter les requêtes GET"""
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_cors_headers()
self.end_headers()
try:
parsed_url = urlparse(self.path)
path = parsed_url.path
# ============================================
# ENDPOINTS LICENCE
# ============================================
if path == '/license' or path == '/license/status':
# Récupérer le statut de la licence
validation = validate_license()
usage = get_usage_stats()
hostname = get_splunk_hostname()
response = {
"status": "valid" if validation.get("valid") else "invalid",
"hostname": hostname,
"license": validation if validation.get("valid") else None,
"error": validation.get("error") if not validation.get("valid") else None,
"error_code": validation.get("error_code") if not validation.get("valid") else None,
"usage": usage
}
self.wfile.write(json.dumps(response).encode())
elif path == '/license/hostname':
# Juste le hostname
response = {"hostname": get_splunk_hostname()}
self.wfile.write(json.dumps(response).encode())
elif path == '/license/file':
# Charger la licence depuis le fichier sur le serveur
license_file = os.path.join(APP_HOME, 'local', 'license.lic')
if os.path.exists(license_file):
try:
with open(license_file, 'r') as f:
license_content = f.read()
response = {
"success": True,
"content": license_content
}
except Exception as e:
response = {
"success": False,
"error": f"Erreur lecture fichier: {str(e)}"
}
else:
response = {
"success": False,
"error": "Aucun fichier de licence sur le serveur"
}
self.wfile.write(json.dumps(response).encode())
# ============================================
# ENDPOINT CONFIGURATION
# ============================================
elif path == '/config':
# Retourner la configuration actuelle
config = load_config()
# Masquer le token pour la sécurité
if 'deployer' in config and 'token' in config['deployer']:
config['deployer']['token'] = '***' if config['deployer']['token'] else ''
self.wfile.write(json.dumps(config).encode())
elif path == '/health':
# Health check
response = {
"status": "ok",
"service": "git_pusher",
"timestamp": datetime.now().isoformat(),
"sh_deployer": {
"enabled": SH_DEPLOYER_CONFIG.get("enabled", True),
"host": SH_DEPLOYER_CONFIG.get("host"),
"port": SH_DEPLOYER_CONFIG.get("port")
}
}
self.wfile.write(json.dumps(response).encode())
# ============================================
# ENDPOINTS SH DEPLOYER
# ============================================
elif path == '/deployer/health':
# Vérifier la santé du SH Deployer
result = call_deployer_agent("/health")
if result.get("success"):
response = {
"status": "ok",
"deployer": result.get("data"),
"config": {
"host": SH_DEPLOYER_CONFIG.get("host"),
"port": SH_DEPLOYER_CONFIG.get("port")
}
}
else:
response = {
"status": "error",
"error": result.get("error"),
"config": {
"host": SH_DEPLOYER_CONFIG.get("host"),
"port": SH_DEPLOYER_CONFIG.get("port")
}
}
self.wfile.write(json.dumps(response).encode())
elif path == '/deployer/status':
# Statut du SH Deployer
result = get_deployer_status()
self.wfile.write(json.dumps(result).encode())
elif path == '/deployer/config':
# Configuration actuelle du SH Deployer
response = {
"enabled": SH_DEPLOYER_CONFIG.get("enabled", True),
"host": SH_DEPLOYER_CONFIG.get("host"),
"port": SH_DEPLOYER_CONFIG.get("port"),
"use_ssl": SH_DEPLOYER_CONFIG.get("use_ssl", True)
}
self.wfile.write(json.dumps(response).encode())
else:
response = {"error": "Unknown endpoint", "path": path}
self.wfile.write(json.dumps(response).encode())
except Exception as e:
logger.error(f"GET error: {e}")
self.wfile.write(json.dumps({"error": str(e)}).encode())
def do_POST(self):
"""Traiter les requêtes POST"""
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_cors_headers()
self.end_headers()
try:
parsed_url = urlparse(self.path)
path = parsed_url.path
query_params = parse_qs(parsed_url.query)
logger.info(f"POST request to {path}")
# ============================================
# ENDPOINT CONFIGURATION
# ============================================
if path == '/config':
# Sauvegarder la configuration
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length).decode('utf-8')
try:
new_config = json.loads(body)
# Charger la config existante pour préserver le token si masqué
existing_config = load_config()
# Si le token est masqué (***), garder l'ancien
if new_config.get('deployer', {}).get('token') == '***':
new_config['deployer']['token'] = existing_config.get('deployer', {}).get('token', '')
# Sauvegarder
if save_config(new_config):
# Recharger la config globale
global APP_CONFIG, SH_DEPLOYER_CONFIG
APP_CONFIG = load_config()
SH_DEPLOYER_CONFIG = {
"enabled": APP_CONFIG.get("deployer", {}).get("enabled", False),
"host": APP_CONFIG.get("deployer", {}).get("host", ""),
"port": APP_CONFIG.get("deployer", {}).get("port", 9998),
"use_ssl": APP_CONFIG.get("deployer", {}).get("useSSL", True),
"token": APP_CONFIG.get("deployer", {}).get("token", ""),
"timeout": APP_CONFIG.get("advanced", {}).get("timeout", 30)
}
response = {"success": True, "message": "Configuration sauvegardée"}
else:
response = {"success": False, "error": "Erreur lors de la sauvegarde"}
except json.JSONDecodeError as e:
response = {"success": False, "error": f"JSON invalide: {str(e)}"}
except Exception as e:
logger.error(f"Erreur sauvegarde config: {e}")
response = {"success": False, "error": str(e)}
self.wfile.write(json.dumps(response).encode())
return
# ============================================
# ENDPOINTS LICENCE
# ============================================
elif path == '/license/upload' or path == '/license/save':
# Sauvegarder la licence sur le serveur (fichier)
# La validation RSA est faite côté client
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length).decode('utf-8')
try:
data = json.loads(body)
license_content = data.get('license_content', '')
except:
license_content = body
if not license_content:
response = {"success": False, "error": "Contenu de licence vide"}
else:
# Sauvegarder le fichier de licence
try:
local_dir = os.path.join(APP_HOME, 'local')
os.makedirs(local_dir, exist_ok=True)
license_path = os.path.join(local_dir, 'license.lic')
with open(license_path, 'w') as f:
f.write(license_content)
os.chmod(license_path, 0o600)
logger.info(f"Licence sauvegardée: {license_path}")
response = {
"success": True,
"message": "Licence sauvegardée sur le serveur",
"path": license_path
}
except Exception as e:
logger.error(f"Erreur sauvegarde licence: {e}")
response = {
"success": False,
"error": f"Erreur sauvegarde: {str(e)}"
}
self.wfile.write(json.dumps(response).encode())
return
elif path == '/license/delete':
# Supprimer la licence
license_path = os.path.join(APP_HOME, 'local', 'license.lic')
if os.path.exists(license_path):
os.remove(license_path)
logger.info(f"Licence supprimée: {license_path}")
response = {"success": True, "message": "Licence supprimée"}
else:
response = {"success": False, "error": "Aucune licence à supprimer"}
self.wfile.write(json.dumps(response).encode())
return
# ============================================
# ENDPOINT PUSH GIT
# ============================================
elif path == '/push' or path.startswith('/services/'):
# NOTE: La vérification de licence est maintenant faite côté client (JavaScript)
# avec validation RSA. Le serveur fait confiance au client.
# Si vous voulez réactiver la vérification serveur, décommentez le bloc ci-dessous:
#
# license_check = check_limits()
# if not license_check.get("allowed"):
# response = {
# "status": "error",
# "error_code": "LICENSE_ERROR",
# "message": license_check.get("error", "Licence invalide ou limite atteinte")
# }
# self.wfile.write(json.dumps(response).encode())
# return
# Traiter le push Git
self.handle_git_push(query_params)
return
else:
# Traiter comme un push Git (compatibilité)
self.handle_git_push(query_params)
except Exception as e:
logger.error(f"POST error: {str(e)}", exc_info=True)
response = {
"status": "error",
"message": f"Error: {str(e)}"
}
self.wfile.write(json.dumps(response).encode())
def handle_git_push(self, query_params):
"""Gérer le push Git et optionnellement le déploiement vers SH Cluster"""
try:
# 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]
apps_json = query_params.get('apps', query_params.get('dashboards', ['[]']))[0]
shcluster_apps_json = query_params.get('shcluster_apps', ['[]'])[0] # Apps pour le SH Cluster
user = query_params.get('user', ['unknown'])[0]
# Paramètres pour le déploiement SH Cluster
deploy_to_shcluster = query_params.get('deploy_to_shcluster', ['false'])[0].lower() == 'true'
deployer_host = query_params.get('deployer_host', [SH_DEPLOYER_CONFIG.get('host', '')])[0]
deployer_token = query_params.get('deployer_token', [SH_DEPLOYER_CONFIG.get('token', '')])[0]
sh_auth_user = query_params.get('sh_auth_user', [''])[0]
sh_auth_pass = query_params.get('sh_auth_pass', [''])[0]
# Paramètres de licence (envoyés par le client)
license_type = query_params.get('license_type', [''])[0]
license_id = query_params.get('license_id', [''])[0]
logger.info(f"Parameters: git_url={git_url}, branch={git_branch}, user={user}, deploy_to_shcluster={deploy_to_shcluster}")
# Parser les apps pour Git
try:
apps = json.loads(apps_json) if isinstance(apps_json, str) else apps_json
except (json.JSONDecodeError, TypeError) as e:
logger.error(f"JSON parse error for apps: {e}")
apps = []
# Parser les apps pour SH Cluster
try:
shcluster_apps = json.loads(shcluster_apps_json) if isinstance(shcluster_apps_json, str) else shcluster_apps_json
except (json.JSONDecodeError, TypeError) as e:
logger.error(f"JSON parse error for shcluster_apps: {e}")
shcluster_apps = apps # Fallback: utiliser toutes les apps
logger.info(f"Apps for Git: {len(apps)}, Apps for SH Cluster: {len(shcluster_apps)}")
# NOTE: La vérification des limites est maintenant faite côté client
# Le serveur fait confiance aux informations envoyées par le client
# Valider les paramètres
if not git_url or not git_token or not commit_message or not 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("Cloning repository...")
self.clone_repository(temp_dir, git_url_with_token, git_branch)
# Récupérer les applications
logger.info("Fetching applications from Splunk...")
app_directories = 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...")
apps_copied = 0
for app_data in app_directories:
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):
# Supprimer l'ancienne version si elle existe
if os.path.exists(dest_path):
logger.info(f"Removing old version of {app_name}")
shutil.rmtree(dest_path)
# Copier la nouvelle version
shutil.copytree(app_path, dest_path)
apps_copied += 1
# Compter les fichiers copiés
file_count = sum(len(files) for _, _, files in os.walk(dest_path))
logger.info(f"Copied app: {app_name} ({file_count} files)")
else:
logger.warning(f"App path not found: {app_path}")
logger.info(f"Total apps copied: {apps_copied}")
# Configurer 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)
# Ajouter TOUS les fichiers (y compris les suppressions)
subprocess.run(['git', 'add', '--all'], cwd=temp_dir, capture_output=True)
# Afficher le statut Git pour debug
status_check = subprocess.run(['git', 'status', '--short'],
cwd=temp_dir, capture_output=True, text=True)
if status_check.stdout.strip():
logger.info(f"Git changes detected:\n{status_check.stdout[:500]}")
else:
logger.info("No Git changes detected")
# Message de commit avec infos de licence (envoyées par le client)
full_message = f"{commit_message}\n\n"
full_message += f"Pushed by: {user}\n"
full_message += f"License: {license_id or 'N/A'} ({license_type or 'N/A'})\n"
full_message += f"Timestamp: {datetime.now().isoformat()}"
# Vérifier s'il y a des changements à committer
status_result = subprocess.run(['git', 'status', '--porcelain'],
cwd=temp_dir, capture_output=True, text=True)
if status_result.stdout.strip():
# Il y a des changements, faire le commit
result = subprocess.run(['git', 'commit', '-m', full_message],
cwd=temp_dir, capture_output=True, text=True)
if result.returncode != 0:
logger.warning(f"Commit warning: {result.stderr}")
else:
logger.info("Commit created successfully")
else:
logger.info("No changes detected, skipping commit")
logger.info("Pushing to Git...")
result = subprocess.run(['git', 'push', 'origin', git_branch],
cwd=temp_dir, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
# Vérifier si c'est juste "Everything up-to-date"
if "Everything up-to-date" in result.stderr or "Everything up-to-date" in result.stdout:
logger.info("Repository is already up-to-date")
else:
raise Exception(f"Push failed: {result.stderr}")
# NOTE: L'incrémentation des stats est maintenant faite côté client (JavaScript)
logger.info("Git push successful!")
# ============================================
# DÉPLOIEMENT VERS SH CLUSTER (optionnel)
# ============================================
deployer_result = None
shcluster_apps_deployed = 0
if deploy_to_shcluster:
logger.info("Triggering deployment to SH Cluster...")
# Extraire les IDs des apps à déployer sur le SH Cluster
shcluster_app_ids = [app.get('id') or app.get('name') for app in shcluster_apps]
logger.info(f"Apps to deploy to SH Cluster: {shcluster_app_ids}")
shcluster_apps_deployed = len(shcluster_app_ids)
# Configurer le deployer
deployer_config = SH_DEPLOYER_CONFIG.copy()
if deployer_host:
deployer_config["host"] = deployer_host
if deployer_token:
deployer_config["token"] = deployer_token
# Appeler le SH Deployer pour pull + deploy
# Passer la liste des apps à déployer
deployer_result = trigger_deployer_pull_and_deploy(
git_url=git_url,
git_token=git_token,
auth_user=sh_auth_user if sh_auth_user else None,
auth_pass=sh_auth_pass if sh_auth_pass else None,
config=deployer_config,
apps_to_deploy=shcluster_app_ids # Liste des apps à déployer
)
if deployer_result.get("success"):
logger.info(f"SH Cluster deployment triggered successfully for {shcluster_apps_deployed} apps")
else:
logger.error(f"SH Cluster deployment failed: {deployer_result.get('error')}")
# Préparer la réponse
response = {
"status": "success",
"message": f"Successfully pushed {len(app_directories)} application(s) to Git",
"apps_pushed": len(app_directories),
"license_type": license_type or "N/A"
}
# Ajouter les infos de déploiement si activé
if deploy_to_shcluster:
response["shcluster_deployment"] = {
"triggered": True,
"apps_count": shcluster_apps_deployed,
"success": deployer_result.get("success", False) if deployer_result else False,
"message": deployer_result.get("data", {}).get("message") if deployer_result and deployer_result.get("success") else deployer_result.get("error") if deployer_result else "Not triggered"
}
if deployer_result and deployer_result.get("success"):
response["message"] += f" and triggered SH Cluster deployment ({shcluster_apps_deployed} apps)"
else:
response["message"] += " (SH Cluster deployment failed)"
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"Git push 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):
logger.debug(format % args)
@staticmethod
def prepare_git_url(git_url, token):
"""Préparer l'URL Git avec le token"""
if '@' in git_url:
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}"
if git_url.startswith('https://') or git_url.startswith('http://'):
protocol = git_url.split('://')[0]
host_and_path = git_url.split('://', 1)[1]
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 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)
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}")
else:
logger.warning(f"App directory not found: {app_path}")
return app_directories
# ============================================
# FONCTIONS SH DEPLOYER
# ============================================
def call_deployer_agent(endpoint, method="GET", data=None, config=None):
"""
Appeler l'agent SH Deployer
Args:
endpoint: Endpoint à appeler (ex: /health, /pull, /deploy)
method: GET ou POST
data: Données à envoyer (dict)
config: Configuration (override SH_DEPLOYER_CONFIG)
Returns:
dict avec success, data ou error
"""
if config is None:
config = SH_DEPLOYER_CONFIG
if not config.get("enabled", True):
return {"success": False, "error": "SH Deployer is disabled"}
host = config.get("host", "10.10.40.14")
port = config.get("port", 9998)
use_ssl = config.get("use_ssl", True)
token = config.get("token", "")
timeout = config.get("timeout", 30)
protocol = "https" if use_ssl else "http"
# Si c'est un nom de domaine (commence par une lettre et contient un point)
# Ne pas ajouter le port (le proxy gère)
import re
is_domain = bool(re.match(r'^[a-zA-Z]', host)) and '.' in host and not re.match(r'^(\d{1,3}\.){3}\d{1,3}$', host)
if is_domain:
url = f"{protocol}://{host}{endpoint}"
else:
url = f"{protocol}://{host}:{port}{endpoint}"
logger.info(f"Calling SH Deployer: {method} {url}")
try:
# Créer le contexte SSL (ignorer les certificats auto-signés)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# Préparer les données
if data:
json_data = json.dumps(data).encode('utf-8')
else:
json_data = None
# Créer la requête
req = urllib.request.Request(url, data=json_data, method=method)
req.add_header('Content-Type', 'application/json')
req.add_header('X-Auth-Token', token)
# Exécuter la requête
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context) as response:
response_data = json.loads(response.read().decode('utf-8'))
logger.info(f"SH Deployer response: {response_data}")
return {"success": True, "data": response_data}
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else str(e)
logger.error(f"SH Deployer HTTP error {e.code}: {error_body}")
return {"success": False, "error": f"HTTP {e.code}: {error_body}"}
except urllib.error.URLError as e:
logger.error(f"SH Deployer connection error: {e.reason}")
return {"success": False, "error": f"Connection error: {e.reason}"}
except Exception as e:
logger.error(f"SH Deployer error: {str(e)}")
return {"success": False, "error": str(e)}
def check_deployer_health(config=None):
"""Vérifier si l'agent SH Deployer est accessible"""
result = call_deployer_agent("/health", config=config)
return result.get("success", False)
def trigger_deployer_pull(git_url, git_token, config=None):
"""
Déclencher un pull sur le SH Deployer
Args:
git_url: URL du repository Git
git_token: Token Git pour l'authentification
config: Configuration du deployer
"""
data = {
"repo_url": git_url,
"git_token": git_token,
"apps_subdir": "apps"
}
return call_deployer_agent("/pull", method="POST", data=data, config=config)
def trigger_deployer_deploy(target_uri=None, auth_user=None, auth_pass=None, config=None):
"""
Déclencher le déploiement du bundle sur le SH Cluster
Args:
target_uri: URI du captain du SH Cluster (optionnel)
auth_user: Utilisateur Splunk
auth_pass: Mot de passe Splunk
config: Configuration du deployer
"""
data = {}
if target_uri:
data["target_uri"] = target_uri
if auth_user:
data["auth_user"] = auth_user
if auth_pass:
data["auth_pass"] = auth_pass
return call_deployer_agent("/deploy", method="POST", data=data, config=config)
def trigger_deployer_pull_and_deploy(git_url, git_token, target_uri=None, auth_user=None, auth_pass=None, config=None, apps_to_deploy=None):
"""
Déclencher pull + deploy en une seule opération
Args:
git_url: URL du repo Git
git_token: Token d'accès Git
target_uri: URI cible pour le déploiement
auth_user: Utilisateur Splunk pour l'authentification
auth_pass: Mot de passe Splunk
config: Configuration du deployer
apps_to_deploy: Liste des IDs d'apps à déployer (si None, toutes les apps)
"""
data = {
"repo_url": git_url,
"git_token": git_token,
"apps_subdir": "apps"
}
if target_uri:
data["target_uri"] = target_uri
if auth_user:
data["auth_user"] = auth_user
if auth_pass:
data["auth_pass"] = auth_pass
if apps_to_deploy:
data["apps_to_deploy"] = apps_to_deploy # Liste des apps à déployer
return call_deployer_agent("/pull-and-deploy", method="POST", data=data, config=config)
def get_deployer_status(config=None):
"""Récupérer le statut du SH Deployer"""
return call_deployer_agent("/status", config=config)
def start_server(port=9999, use_ssl=True):
"""Démarrer le serveur HTTP/HTTPS"""
import ssl
server = HTTPServer(('0.0.0.0', port), GitPusherRequestHandler)
ssl_enabled = False
if use_ssl:
# Chemins possibles pour les certificats (ordre de priorité)
cert_paths = [
# Certificats dédiés pour Git Pusher (recommandé)
('/opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt',
'/opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key'),
# Certificats splunkweb
('/opt/splunk/etc/auth/splunkweb/cert.pem',
'/opt/splunk/etc/auth/splunkweb/privkey.pem'),
# Autre emplacement splunkweb
('/opt/splunk/etc/auth/splunkweb/splunkweb.pem',
'/opt/splunk/etc/auth/splunkweb/splunkweb.key'),
]
for cert_file, key_file in cert_paths:
logger.info(f"Trying SSL cert: {cert_file}")
if os.path.exists(cert_file) and os.path.exists(key_file):
try:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# Charger le certificat et la clé
ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
server.socket = ssl_context.wrap_socket(server.socket, server_side=True)
ssl_enabled = True
logger.info(f"SSL enabled using: {cert_file}")
break
except Exception as e:
logger.warning(f"Could not load SSL cert {cert_file}: {e}")
continue
else:
logger.debug(f"Cert not found: {cert_file} or {key_file}")
if not ssl_enabled:
logger.error("=" * 60)
logger.error("SSL CERTIFICATES NOT FOUND OR INVALID!")
logger.error("HTTPS requests from browser will fail!")
logger.error("")
logger.error("To fix, run these commands:")
logger.error(" mkdir -p /opt/splunk/etc/apps/pusher_app_prem/local/certs")
logger.error(" openssl req -x509 -newkey rsa:4096 \\")
logger.error(" -keyout /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key \\")
logger.error(" -out /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt \\")
logger.error(" -days 365 -nodes -subj \"/CN=git-pusher\"")
logger.error("=" * 60)
protocol = "HTTPS" if ssl_enabled else "HTTP"
logger.info(f"Git Pusher server listening on 0.0.0.0:{port} ({protocol})")
# Afficher le statut de la licence au démarrage
license_status = validate_license()
if license_status.get("valid"):
logger.info(f"License: {license_status.get('type_name')} - {license_status.get('days_remaining')} days remaining")
else:
logger.warning(f"License: {license_status.get('error', 'Invalid')}")
server.serve_forever()
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Git Pusher Server')
parser.add_argument('--no-ssl', action='store_true', help='Disable SSL/HTTPS')
parser.add_argument('--port', type=int, default=9999, help='Port number (default: 9999)')
args = parser.parse_args()
port = args.port
use_ssl = not args.no_ssl
logger.info(f"Starting Git Pusher on port {port} (SSL: {use_ssl})")
start_server(port, use_ssl)

@ -0,0 +1,191 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Git Pusher - License Endpoints
Endpoints REST pour gérer les licences
"""
import sys
import os
import json
import tempfile
from http.server import BaseHTTPRequestHandler
# Ajouter le chemin du module
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
from license_validator import LicenseValidator, get_license_status, LICENSE_FILE_PATH
class LicenseHandler(BaseHTTPRequestHandler):
"""Handler pour les requêtes de licence"""
def do_OPTIONS(self):
"""Gérer les requêtes OPTIONS (CORS preflight)"""
self.send_response(200)
self.send_headers()
self.end_headers()
def do_GET(self):
"""Gérer les requêtes GET"""
if self.path == '/custom/git_pusher/license_status':
self.handle_license_status()
else:
self.send_error(404)
def do_POST(self):
"""Gérer les requêtes POST"""
if self.path == '/custom/git_pusher/install_license':
self.handle_install_license()
elif self.path == '/custom/git_pusher/request_trial':
self.handle_request_trial()
else:
self.send_error(404)
def send_headers(self):
"""Envoyer les headers CORS"""
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')
def handle_license_status(self):
"""Vérifier le statut de la licence"""
try:
status = get_license_status()
self.send_response(200)
self.send_headers()
self.end_headers()
self.wfile.write(json.dumps(status).encode())
except Exception as e:
self.send_response(500)
self.send_headers()
self.end_headers()
self.wfile.write(json.dumps({
'licensed': False,
'errors': [f'Erreur serveur: {str(e)}']
}).encode())
def handle_install_license(self):
"""Installer un fichier de licence uploadé"""
try:
# Lire le body
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
data = json.loads(body.decode())
license_content = data.get('license_file')
filename = data.get('filename', 'uploaded.lic')
if not license_content:
raise Exception("Aucun contenu de licence fourni")
# Créer un fichier temporaire
with tempfile.NamedTemporaryFile(mode='w', suffix='.lic', delete=False) as temp_file:
temp_file.write(license_content)
temp_path = temp_file.name
try:
# Valider et installer
validator = LicenseValidator()
result = validator.install_license(temp_path, LICENSE_FILE_PATH)
self.send_response(200)
self.send_headers()
self.end_headers()
self.wfile.write(json.dumps(result).encode())
finally:
# Nettoyer le fichier temporaire
if os.path.exists(temp_path):
os.remove(temp_path)
except Exception as e:
self.send_response(400)
self.send_headers()
self.end_headers()
self.wfile.write(json.dumps({
'success': False,
'message': f'Erreur: {str(e)}'
}).encode())
def handle_request_trial(self):
"""Créer une licence d'essai"""
try:
from datetime import datetime, timedelta
import socket
import hashlib
# Générer une licence d'essai basique (7 jours)
hostname = socket.gethostname()
trial_license = {
"license": {
"version": "1.0",
"license_id": "TRIAL-" + hashlib.md5(
f"{hostname}{datetime.now()}".encode()
).hexdigest()[:8].upper(),
"customer": {
"name": "Trial User",
"hostname": hostname
},
"validity": {
"issued": datetime.now().isoformat(),
"expires": (datetime.now() + timedelta(days=7)).isoformat(),
"days": 7
},
"limits": {
"max_pushes": 50,
"max_apps": None
},
"features": {
"git_push": True,
"multi_branch": False,
"auto_commit": False,
"webhooks": False
},
"type": "trial"
},
"signature": "TRIAL-NO-SIGNATURE",
"checksum": "TRIAL"
}
# Créer le dossier si nécessaire
os.makedirs(os.path.dirname(LICENSE_FILE_PATH), exist_ok=True)
# Sauvegarder
with open(LICENSE_FILE_PATH, 'w') as f:
json.dump(trial_license, f, indent=2)
self.send_response(200)
self.send_headers()
self.end_headers()
self.wfile.write(json.dumps({
'success': True,
'message': 'Licence d\'essai créée (7 jours, 50 pushes max)',
'license_info': {
'license_id': trial_license['license']['license_id'],
'type': 'trial',
'expires': trial_license['license']['validity']['expires'],
'days_remaining': 7
}
}).encode())
except Exception as e:
self.send_response(500)
self.send_headers()
self.end_headers()
self.wfile.write(json.dumps({
'success': False,
'message': f'Erreur: {str(e)}'
}).encode())
def log_message(self, format, *args):
"""Override pour éviter les logs HTTP par défaut"""
pass

@ -0,0 +1,468 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Git Pusher - License Validator (RSA)
Validation des licences avec vérification de signature RSA
Ce fichier est distribué avec l'application client.
La clé publique peut être visible - seul le vendeur avec la clé privée peut signer.
"""
import os
import sys
import json
import base64
import socket
from datetime import datetime
# ============================================
# CLÉ PUBLIQUE RSA
# ============================================
# Cette clé est générée par le vendeur avec license_generator_rsa.py
# Commande: python license_generator_rsa.py export-key
#
# REMPLACEZ CETTE CLÉ PAR VOTRE CLÉ PUBLIQUE !
PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnj2hOg61Q9k9iz4U5F7I
RdaJrpLTG+orz0/Kpbz2HSxbAVXkvL5GvYVfxROjy0UgxOZFycZAaGN2am+5CDHA
D1dTL9KCPhEaPqw4XTFnf6Ur5VG0+SftugTdTcyxRe614Z+i61/2ahk/vKG9D4kB
j4qV4se4lLk993lEaQrOXkXCbZ8royB5MPeOchPxZd7SDzoovEcyUmf2Fa2eYk6U
WmbrymCnJRsfxEVZofQQyp1ILS8KuSxaquXvMWm3cXV2Krs/3E5ax0vBPMrZRL+o
Vn7/dVnzbOlbifeosTYaad1DLd7NEgst3OFUv+dSH5hcCCc36IHMSvxcJ9l7s2kv
KbEFPeh582JNmRoMMNPRbd+/ZVDeJ/oB344+TtB6VeQ2GQyOyoggiLryZujg3WDE
shwLkFwiYGa/zEct0qs2/HBS1FOAqrLiPdQYJTx+RhrXhTni/p3H42L+2xJcbnki
fAgn8ND5k2yQw2gk4AlLqq2y01m0jsHdjaOfhKzzPLyzt1En/KAnCWJSzB+jur4z
fd70R4pLTYfawr2NTvTAhOtiOIjWj380oGmxeKCNJT1P4Dq8Yl+OxKLBy4cnUlgV
sbpKJug7Goth4g7bmCVMhC4bf7JB/iTrrS8DhMaWaZX/FeFloM3yYyo/gzmAY19z
sUdQuBpxCwIf/J7Q4dYsDkMCAwEAAQ==
-----END PUBLIC KEY-----'''
# ============================================
# CONFIGURATION
# ============================================
# Chemin de l'application
APP_HOME = os.environ.get('SPLUNK_HOME', '/opt/splunk') + '/etc/apps/pusher_app_prem'
LICENSE_FILE = os.path.join(APP_HOME, 'local', 'license.lic')
USAGE_FILE = os.path.join(APP_HOME, 'local', '.usage_stats')
# ============================================
# FONCTIONS UTILITAIRES
# ============================================
def get_splunk_hostname():
"""Récupérer le hostname du serveur Splunk"""
try:
# Essayer via servername de Splunk
server_conf = os.path.join(
os.environ.get('SPLUNK_HOME', '/opt/splunk'),
'etc', 'system', 'local', 'server.conf'
)
if os.path.exists(server_conf):
with open(server_conf, 'r') as f:
for line in f:
if line.strip().startswith('serverName'):
return line.split('=')[1].strip().lower()
except:
pass
# Fallback sur le hostname système
return socket.gethostname().lower()
def load_public_key():
"""Charger la clé publique RSA"""
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
public_key = serialization.load_pem_public_key(
PUBLIC_KEY.encode('utf-8'),
backend=default_backend()
)
return public_key
except ImportError:
# Fallback si cryptography n'est pas installé
return None
except Exception as e:
print(f"Erreur chargement clé publique: {e}")
return None
def verify_rsa_signature(data, signature, public_key):
"""Vérifier la signature RSA"""
try:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
public_key.verify(
signature,
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except Exception:
return False
# ============================================
# PARSING DU FICHIER .LIC
# ============================================
def parse_license_file(filepath=None):
"""Parser un fichier de licence .lic"""
if filepath is None:
filepath = LICENSE_FILE
if not os.path.exists(filepath):
return None
try:
with open(filepath, 'r') as f:
content = f.read()
return parse_license_content(content)
except Exception as e:
print(f"Erreur lecture licence: {e}")
return None
def parse_license_content(content):
"""Parser le contenu d'une licence"""
try:
# Extraire le payload (ignorer les lignes de commentaires)
lines = content.strip().split('\n')
payload_b64 = None
for line in lines:
line = line.strip()
if line and not line.startswith('#'):
payload_b64 = line
break
if not payload_b64:
return None
# Décoder le payload
payload = json.loads(base64.b64decode(payload_b64).decode('utf-8'))
return {
"license_b64": payload.get("license"),
"signature_b64": payload.get("signature"),
"raw_payload": payload_b64
}
except Exception as e:
print(f"Erreur parsing licence: {e}")
return None
# ============================================
# VALIDATION DE LA LICENCE
# ============================================
def validate_license(filepath=None):
"""
Valider une licence
Returns:
dict avec:
- valid: bool
- error: str (si invalide)
- error_code: str (si invalide)
- license_id, type, type_name, expires, days_remaining, limits, features (si valide)
"""
# Parser le fichier
parsed = parse_license_file(filepath)
if not parsed:
return {
"valid": False,
"error": "Fichier de licence non trouvé ou invalide",
"error_code": "NO_LICENSE"
}
license_b64 = parsed.get("license_b64")
signature_b64 = parsed.get("signature_b64")
if not license_b64 or not signature_b64:
return {
"valid": False,
"error": "Format de licence invalide",
"error_code": "INVALID_FORMAT"
}
try:
# Décoder la licence et la signature
license_json = base64.b64decode(license_b64).decode('utf-8')
signature = base64.b64decode(signature_b64)
# Charger la clé publique
public_key = load_public_key()
if public_key is None:
# Mode dégradé sans cryptography - vérification basique
print("⚠️ Module cryptography non installé - vérification basique uniquement")
else:
# Vérifier la signature RSA
if not verify_rsa_signature(license_json.encode('utf-8'), signature, public_key):
return {
"valid": False,
"error": "Signature de licence invalide",
"error_code": "INVALID_SIGNATURE"
}
# Parser les données de licence
license_data = json.loads(license_json)
# Vérifier le hostname
expected_hostname = license_data.get("hostname", "").lower()
current_hostname = get_splunk_hostname()
if expected_hostname and expected_hostname != current_hostname:
return {
"valid": False,
"error": f"Licence non valide pour ce serveur. Attendu: {expected_hostname}, Actuel: {current_hostname}",
"error_code": "HOSTNAME_MISMATCH"
}
# Vérifier la date d'expiration
expires_str = license_data.get("expires", "")
if expires_str:
try:
expires_date = datetime.strptime(expires_str, "%Y-%m-%d")
now = datetime.now()
if now > expires_date:
return {
"valid": False,
"error": f"Licence expirée le {expires_str}",
"error_code": "LICENSE_EXPIRED"
}
days_remaining = (expires_date - now).days
except ValueError:
days_remaining = 0
else:
days_remaining = 999 # Pas d'expiration
# Licence valide !
return {
"valid": True,
"license_id": license_data.get("license_id"),
"type": license_data.get("type"),
"type_name": license_data.get("type_name"),
"customer": license_data.get("customer", {}),
"hostname": expected_hostname,
"issued": license_data.get("issued"),
"expires": expires_str,
"days_remaining": days_remaining,
"limits": license_data.get("limits", {}),
"features": license_data.get("features", [])
}
except json.JSONDecodeError:
return {
"valid": False,
"error": "Données de licence corrompues",
"error_code": "CORRUPTED_DATA"
}
except Exception as e:
return {
"valid": False,
"error": f"Erreur de validation: {str(e)}",
"error_code": "VALIDATION_ERROR"
}
# ============================================
# SAUVEGARDE DE LICENCE
# ============================================
def save_license_file(content):
"""Sauvegarder un fichier de licence uploadé"""
try:
# Créer le dossier local si nécessaire
local_dir = os.path.join(APP_HOME, 'local')
os.makedirs(local_dir, exist_ok=True)
# Valider le contenu avant de sauvegarder
parsed = parse_license_content(content)
if not parsed:
return {
"success": False,
"error": "Format de licence invalide"
}
# Valider la licence
# Sauvegarder temporairement pour valider
temp_file = LICENSE_FILE + '.tmp'
with open(temp_file, 'w') as f:
f.write(content)
validation = validate_license(temp_file)
if not validation.get("valid"):
os.remove(temp_file)
return {
"success": False,
"error": validation.get("error"),
"error_code": validation.get("error_code")
}
# Renommer le fichier temporaire
os.rename(temp_file, LICENSE_FILE)
os.chmod(LICENSE_FILE, 0o600)
return {
"success": True,
"license": validation
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
# ============================================
# GESTION DES LIMITES D'UTILISATION
# ============================================
def get_usage_stats():
"""Récupérer les statistiques d'utilisation"""
try:
if os.path.exists(USAGE_FILE):
with open(USAGE_FILE, 'r') as f:
return json.load(f)
except:
pass
return {
"total_pushes": 0,
"pushes_today": 0,
"last_push_date": None,
"apps_pushed": []
}
def save_usage_stats(stats):
"""Sauvegarder les statistiques d'utilisation"""
try:
local_dir = os.path.join(APP_HOME, 'local')
os.makedirs(local_dir, exist_ok=True)
with open(USAGE_FILE, 'w') as f:
json.dump(stats, f, indent=2)
os.chmod(USAGE_FILE, 0o600)
except Exception as e:
print(f"Erreur sauvegarde stats: {e}")
def increment_usage():
"""Incrémenter le compteur d'utilisation"""
stats = get_usage_stats()
today = datetime.now().strftime("%Y-%m-%d")
# Reset compteur quotidien si nouveau jour
if stats.get("last_push_date") != today:
stats["pushes_today"] = 0
stats["last_push_date"] = today
stats["total_pushes"] = stats.get("total_pushes", 0) + 1
stats["pushes_today"] = stats.get("pushes_today", 0) + 1
save_usage_stats(stats)
return stats
def check_limits():
"""Vérifier si les limites de la licence sont respectées"""
license_info = validate_license()
if not license_info.get("valid"):
return {
"allowed": False,
"error": license_info.get("error"),
"error_code": license_info.get("error_code")
}
limits = license_info.get("limits", {})
max_pushes = limits.get("max_pushes_per_day", -1)
if max_pushes > 0:
stats = get_usage_stats()
today = datetime.now().strftime("%Y-%m-%d")
# Reset si nouveau jour
if stats.get("last_push_date") != today:
stats["pushes_today"] = 0
if stats.get("pushes_today", 0) >= max_pushes:
return {
"allowed": False,
"error": f"Limite quotidienne atteinte ({max_pushes} pushes/jour)",
"error_code": "DAILY_LIMIT_REACHED"
}
return {
"allowed": True,
"license_type": license_info.get("type_name"),
"remaining_today": max_pushes - get_usage_stats().get("pushes_today", 0) if max_pushes > 0 else -1
}
def has_feature(feature_name):
"""Vérifier si une fonctionnalité est disponible dans la licence"""
license_info = validate_license()
if not license_info.get("valid"):
return False
features = license_info.get("features", [])
return feature_name in features
# ============================================
# CLI POUR TESTS
# ============================================
if __name__ == "__main__":
if len(sys.argv) > 1:
if sys.argv[1] == "status":
result = validate_license()
if result.get("valid"):
print("✅ Licence valide")
print(f" ID: {result.get('license_id')}")
print(f" Type: {result.get('type_name')}")
print(f" Hostname: {result.get('hostname')}")
print(f" Expire: {result.get('expires')} ({result.get('days_remaining')} jours)")
print(f" Features: {', '.join(result.get('features', []))}")
else:
print(f"❌ Licence invalide: {result.get('error')}")
elif sys.argv[1] == "hostname":
print(f"Hostname: {get_splunk_hostname()}")
elif sys.argv[1] == "usage":
stats = get_usage_stats()
print(f"Total pushes: {stats.get('total_pushes', 0)}")
print(f"Pushes aujourd'hui: {stats.get('pushes_today', 0)}")
elif sys.argv[1] == "check":
result = check_limits()
if result.get("allowed"):
print(f"✅ Push autorisé ({result.get('license_type')})")
else:
print(f"❌ Push refusé: {result.get('error')}")
else:
print("Usage: python license_validator.py [status|hostname|usage|check]")
else:
result = validate_license()
print(json.dumps(result, indent=2))

@ -0,0 +1,354 @@
#!/bin/bash
# ============================================
# Git Pusher - Start Script
# Version 2.0 avec credentials sécurisés
# ============================================
# Configuration
SPLUNK_HOME=${SPLUNK_HOME:-/opt/splunk}
APP_NAME="pusher_app_prem"
APP_HOME="${SPLUNK_HOME}/etc/apps/${APP_NAME}"
BIN_DIR="${APP_HOME}/bin"
LOG_DIR="${SPLUNK_HOME}/var/log/splunk"
PID_FILE="${BIN_DIR}/git_pusher.pid"
CREDENTIALS_MANAGER="${BIN_DIR}/credentials_manager.py"
# Couleurs pour les logs
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Fonction de logging
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_debug() {
echo -e "${BLUE}[DEBUG]${NC} $1"
}
# Charger les credentials de manière sécurisée
load_credentials() {
# Méthode 1: Utiliser le credentials manager (recommandé)
if [ -f "$CREDENTIALS_MANAGER" ]; then
CREDS=$(python3 "$CREDENTIALS_MANAGER" get 2>/dev/null)
if [ $? -eq 0 ]; then
export SPLUNK_USERNAME=$(echo "$CREDS" | head -1)
export SPLUNK_PASSWORD=$(echo "$CREDS" | tail -1)
log_info "Credentials loaded from secure storage"
return 0
fi
fi
# Méthode 2: Variables d'environnement déjà définies
if [ -n "$SPLUNK_USERNAME" ] && [ -n "$SPLUNK_PASSWORD" ]; then
log_info "Using credentials from environment variables"
return 0
fi
# Méthode 3: Fichier .env (moins sécurisé mais pratique)
ENV_FILE="${APP_HOME}/local/.env"
if [ -f "$ENV_FILE" ]; then
source "$ENV_FILE"
if [ -n "$SPLUNK_USERNAME" ] && [ -n "$SPLUNK_PASSWORD" ]; then
log_warn "Using credentials from .env file (consider using 'credentials setup' for better security)"
return 0
fi
fi
# Aucune credential trouvée
log_error "No credentials found!"
log_error "Please run: $0 credentials setup"
return 1
}
# Vérifier si le serveur est déjà en cours d'exécution
check_running() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
return 0 # Running
fi
fi
return 1 # Not running
}
# Configurer les credentials
setup_credentials() {
if [ -f "$CREDENTIALS_MANAGER" ]; then
python3 "$CREDENTIALS_MANAGER" setup
else
log_error "Credentials manager not found at $CREDENTIALS_MANAGER"
exit 1
fi
}
# Afficher le statut des credentials
credentials_status() {
if [ -f "$CREDENTIALS_MANAGER" ]; then
python3 "$CREDENTIALS_MANAGER" status
else
log_error "Credentials manager not found"
fi
}
# Supprimer les credentials
delete_credentials() {
if [ -f "$CREDENTIALS_MANAGER" ]; then
python3 "$CREDENTIALS_MANAGER" delete
else
log_error "Credentials manager not found"
fi
}
# Démarrer le serveur
start_server() {
log_info "Starting Git Pusher server..."
# Vérifier si déjà en cours
if check_running; then
log_warn "Git Pusher is already running (PID: $(cat $PID_FILE))"
return 1
fi
# Charger les credentials
if ! load_credentials; then
exit 1
fi
# Créer le répertoire de logs
mkdir -p "$LOG_DIR"
# Démarrer le serveur Python
cd "$BIN_DIR"
python3 git_pusher.py > "${LOG_DIR}/git_pusher_startup.log" 2>&1 &
# Sauvegarder le PID
echo $! > "$PID_FILE"
# Attendre un peu et vérifier
sleep 2
if check_running; then
log_info "Git Pusher started successfully (PID: $(cat $PID_FILE))"
log_info "Server listening on port 9999"
# Vérifier le statut de la licence
HOSTNAME=$(hostname)
log_info "Hostname: $HOSTNAME"
if [ -f "${APP_HOME}/local/license.lic" ]; then
log_info "License file found"
else
log_warn "No license file found - activation required"
fi
return 0
else
log_error "Failed to start Git Pusher"
log_error "Check logs at ${LOG_DIR}/git_pusher.log"
return 1
fi
}
# Arrêter le serveur
stop_server() {
log_info "Stopping Git Pusher server..."
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
kill $PID
sleep 2
# Force kill si nécessaire
if ps -p $PID > /dev/null 2>&1; then
log_warn "Force killing process..."
kill -9 $PID
fi
rm -f "$PID_FILE"
log_info "Git Pusher stopped"
return 0
else
log_warn "Process not running, cleaning up PID file"
rm -f "$PID_FILE"
return 0
fi
else
log_warn "PID file not found, Git Pusher may not be running"
return 1
fi
}
# Redémarrer le serveur
restart_server() {
log_info "Restarting Git Pusher server..."
stop_server
sleep 1
start_server
}
# Afficher le statut
show_status() {
echo "============================================"
echo "Git Pusher Status"
echo "============================================"
if check_running; then
PID=$(cat "$PID_FILE")
echo -e "Server Status: ${GREEN}RUNNING${NC}"
echo "PID: $PID"
echo "Port: 9999"
else
echo -e "Server Status: ${RED}STOPPED${NC}"
fi
echo ""
echo "Paths:"
echo " App Home: $APP_HOME"
echo " Bin Dir: $BIN_DIR"
echo " Log Dir: $LOG_DIR"
echo ""
# Statut des credentials
echo "Credentials:"
if [ -f "${APP_HOME}/local/.credentials" ]; then
echo -e " Secure storage: ${GREEN}Configured${NC}"
else
echo -e " Secure storage: ${YELLOW}Not configured${NC}"
fi
echo ""
# Statut de la licence
echo "License:"
if [ -f "${APP_HOME}/local/license.lic" ]; then
echo -e " File: ${GREEN}Present${NC}"
# Essayer de lire quelques infos
if command -v python3 &> /dev/null; then
python3 -c "
import sys
sys.path.insert(0, '$BIN_DIR')
try:
from license_validator import validate_license
result = validate_license()
if result.get('valid'):
print(f\" Type: {result.get('type_name', 'N/A')}\")
print(f\" Expires: {result.get('expires', 'N/A')}\")
print(f\" Days remaining: {result.get('days_remaining', 'N/A')}\")
else:
print(f\" Status: Invalid - {result.get('error', 'Unknown error')}\")
except Exception as e:
print(f' Unable to read license: {e}')
" 2>/dev/null || echo " Unable to read license details"
fi
else
echo -e " File: ${YELLOW}Not found${NC}"
fi
echo ""
echo "Hostname: $(hostname)"
echo "============================================"
}
# Afficher les logs
show_logs() {
LOG_FILE="${LOG_DIR}/git_pusher.log"
if [ -f "$LOG_FILE" ]; then
if [ "$1" == "-f" ]; then
tail -f "$LOG_FILE"
else
tail -n 50 "$LOG_FILE"
fi
else
log_warn "Log file not found at $LOG_FILE"
fi
}
# Menu d'aide
show_help() {
echo "Git Pusher - Server Management Script v2.0"
echo ""
echo "Usage: $0 {command} [options]"
echo ""
echo "Server Commands:"
echo " start Start the Git Pusher server"
echo " stop Stop the Git Pusher server"
echo " restart Restart the Git Pusher server"
echo " status Show the current status"
echo " logs [-f] Show recent logs (-f for follow)"
echo ""
echo "Credentials Commands:"
echo " credentials setup Configure Splunk credentials securely"
echo " credentials status Show credentials status"
echo " credentials delete Delete stored credentials"
echo ""
echo "Other:"
echo " help Show this help message"
echo ""
echo "Examples:"
echo " $0 credentials setup # First time setup"
echo " $0 start # Start server"
echo " $0 logs -f # Follow logs"
}
# Main
case "$1" in
start)
start_server
;;
stop)
stop_server
;;
restart)
restart_server
;;
status)
show_status
;;
logs)
show_logs "$2"
;;
credentials)
case "$2" in
setup)
setup_credentials
;;
status)
credentials_status
;;
delete)
delete_credentials
;;
*)
echo "Usage: $0 credentials {setup|status|delete}"
;;
esac
;;
help|--help|-h)
show_help
;;
*)
if [ -z "$1" ]; then
show_help
else
echo "Unknown command: $1"
echo ""
show_help
exit 1
fi
;;
esac

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

@ -0,0 +1,4 @@
<nav search_view="git_pusher_dashboard">
<view name="git_pusher_dashboard" default='true' />
<view name="git_pusher_config" />
</nav>

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

@ -0,0 +1 @@
{"username": "admin", "password": "VgNoQiAmDzgUO3NQ"}

@ -0,0 +1 @@
d0YpjIlHuVRowV+y4yQh+VuqDhq7QikC3qepdfLE9+o=

@ -0,0 +1,6 @@
{
"total_pushes": 21,
"pushes_today": 6,
"last_push_date": "2026-02-21",
"apps_pushed": []
}

@ -0,0 +1,6 @@
[ui]
[launcher]
[install]
state = enabled

@ -0,0 +1,49 @@
-----BEGIN CERTIFICATE-----
MIIDxTCCA0ugAwIBAgISBXu55+97HBg/V5v9p1wKdygbMAoGCCqGSM49BAMDMDIx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
ODAeFw0yNjAyMTcxMzU5MDRaFw0yNjA1MTgxMzU5MDNaMCkxJzAlBgNVBAMTHm15
cHJpdnNwbGRldi5qcC1lbmdpbmVlcmluZy5mcjB2MBAGByqGSM49AgEGBSuBBAAi
A2IABOLi1mN0uYVlBILGpDZDyk1wmGPgXklxm7E2Zn/Nswn54qsHJcSW8gSJDHyE
GbqFcc3wTomzFHa73aPxF+/8bTX281VkKjABfNPFSu83qmVcDZ35VT4bF1zbDOES
280Kq6OCAiswggInMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD
ATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTq3jD3pSEI3y4olRyu1I/aK5sGDDAf
BgNVHSMEGDAWgBSPDROi9i5+0VBsMxg4XVmOI3KRyjAyBggrBgEFBQcBAQQmMCQw
IgYIKwYBBQUHMAKGFmh0dHA6Ly9lOC5pLmxlbmNyLm9yZy8wKQYDVR0RBCIwIIIe
bXlwcml2c3BsZGV2LmpwLWVuZ2luZWVyaW5nLmZyMBMGA1UdIAQMMAowCAYGZ4EM
AQIBMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHA6Ly9lOC5jLmxlbmNyLm9yZy8yOC5j
cmwwggENBgorBgEEAdZ5AgQCBIH+BIH7APkAfwBxfpXzwjiKbbHjhEk9MeFaqWII
di1CAOAFDNBntaZh4gAAAZxsG05YAAgAAAUACa6ZQgQDAEgwRgIhAIichDr9Q2CB
AXzXmEbKjiO9qSAUXE7s8IgBXw8BfjluAiEAyjAyAc2I4hCjco4FLp7+y0nmeElH
yl4BBAaCEsOFK3AAdgAWgy2r8KklDw/wOqVF/8i/yCPQh0v2BCkn+OcfMxP1+gAA
AZxsG1XXAAAEAwBHMEUCIE7uCd4jXwCcTGuqR3ThJm0Zms0U7tAJcuhnR2F0syZJ
AiEAj06LtXnMoYEN6wvfgFUVd4zekUUaNJlJ1TK0HcMMlNQwCgYIKoZIzj0EAwMD
aAAwZQIwZaVb5NEYfUrGirhaFV9eohbjDY7YLzaJ5dIxH+Jt78o+GMfypoFc6Rc1
S2FPNCJsAjEAphAhfBmVN1ASp3JENib1t12TuEVaTNHdNlzzuFE4s3z+z0cp6je4
MmgQpu/hgcn3
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEVjCCAj6gAwIBAgIQY5WTY8JOcIJxWRi/w9ftVjANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQswCQYDVQQDEwJFODB2MBAGByqGSM49AgEGBSuBBAAiA2IABNFl8l7c
S7QMApzSsvru6WyrOq44ofTUOTIzxULUzDMMNMchIJBwXOhiLxxxs0LXeb5GDcHb
R6EToMffgSZjO9SNHfY9gjMy9vQr5/WWOrQTZxh7az6NSNnq3u2ubT6HTKOB+DCB
9TAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI8NE6L2Ln7RUGwzGDhdWY4j
cpHKMB8GA1UdIwQYMBaAFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEB
BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzATBgNVHSAE
DDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veDEuYy5sZW5j
ci5vcmcvMA0GCSqGSIb3DQEBCwUAA4ICAQBnE0hGINKsCYWi0Xx1ygxD5qihEjZ0
RI3tTZz1wuATH3ZwYPIp97kWEayanD1j0cDhIYzy4CkDo2jB8D5t0a6zZWzlr98d
AQFNh8uKJkIHdLShy+nUyeZxc5bNeMp1Lu0gSzE4McqfmNMvIpeiwWSYO9w82Ob8
otvXcO2JUYi3svHIWRm3+707DUbL51XMcY2iZdlCq4Wa9nbuk3WTU4gr6LY8MzVA
aDQG2+4U3eJ6qUF10bBnR1uuVyDYs9RhrwucRVnfuDj29CMLTsplM5f5wSV5hUpm
Uwp/vV7M4w4aGunt74koX71n4EdagCsL/Yk5+mAQU0+tue0JOfAV/R6t1k+Xk9s2
HMQFeoxppfzAVC04FdG9M+AC2JWxmFSt6BCuh3CEey3fE52Qrj9YM75rtvIjsm/1
Hl+u//Wqxnu1ZQ4jpa+VpuZiGOlWrqSP9eogdOhCGisnyewWJwRQOqK16wiGyZeR
xs/Bekw65vwSIaVkBruPiTfMOo0Zh4gVa8/qJgMbJbyrwwG97z/PRgmLKCDl8z3d
tA0Z7qq7fta0Gl24uyuB05dqI5J1LvAzKuWdIjT1tP8qCoxSE/xpix8hX2dt3h+/
jujUgFPFZ0EVZ0xSyBNRF3MboGZnYXFUxpNjTWPKpagDHJQmqrAcDmWJnMsFY3jS
u1igv3OefnWjSQ==
-----END CERTIFICATE-----

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCzCCAvOgAwIBAgIUIkJeLpRn6wfN8VbsMrPdFAl/x7swDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAwwKZ2l0LXB1c2hlcjAeFw0yNjAyMDExOTM1MzNaFw0yNzAy
MDExOTM1MzNaMBUxEzARBgNVBAMMCmdpdC1wdXNoZXIwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQDsTH5H+OD27yXIFz4MvNrpmpNgYly6QYUukJO8FI/f
unBe4N5mfOTZ/WrBeR5KoBOS3FV8VWIH7ShXIzWBpollmIz9+jlPyftawXyiSnSX
rwLBdHc8gjVRZf1H2U0E191v9z0oaXApy8d7E0Pw+i4odoGHOX5Ix89s0DJrG5UP
LrLr9OHlyMCC0D2QVp0wqFGkIXXv/cyYwlcGPACvFhE/fWUazC4AEImJhXypOfQZ
h57SoldKQWwW7BZdGmnbSqeG2lq7KFUow0sie4KzRPLPXrIGdKQbPAKDKcQ7MYlG
9bfgsmM1Rr6klRAmO/e4w1HRHSHUetmFBYDJ5MYNdeddYDfIVwjgwfDvy1i+ojRl
/viywW3ONXR9rUcx/nGGc8UJTjJAAaAaTj6UMzn40ltLYmiymhj6mWFNiuGwDxas
YTxA/3i62pGlbC8s1ZSXtdCYsEN4+W3N5CSadHdVtAIIEc6OyQ2mRa3v4oeZlyzt
1mk9j9oChxi5r74ujUmjNHxpawWG5wmnRy3b9ABxcivESEuXIbjBXrQ+RCmuQ8Dp
5Asa7iede7iEPkhGw3Es3uVdk4s3/OlS6o+1F+rG9qZr3aTtJrciVSLofbrSNtFF
MERip+VBwIdBofVzS6GUhnLjjdhfu/kdzsqrTDDFan4NX1IyC+zBL90DPQ8SoIOg
owIDAQABo1MwUTAdBgNVHQ4EFgQUe6KibB+U4yTq9/pwf6C4JeoPQo0wHwYDVR0j
BBgwFoAUe6KibB+U4yTq9/pwf6C4JeoPQo0wDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAgEA12Y7N6iLOtRAt+xD3B5eIdJtPnFclhmy8FCd2+SdnF/K
zWVdzTsWYKzZ4TZ08aMOh2+H2vDf/ZTG+gryCob28QVy0sRxbpA4WJMI6zBfZSKr
Raudh5Gql/CO28gN4k6EGgNAn1S02NkvhZP4FokfXwkY4CIReRECJzws0gCyMZto
wLfYEVyvN9NYQf1xoJHaozGghA0FADQrr6pQsl1Ek4bkrvb8/L8gWeT6lgny0ZcI
1UEW9lkx7pc0dRDOjUjVzlHYzAf53xEslontihQcOoE/Uj3Wsmi1V/lPm+Pi0Ml0
msMUkvMZP6Y8lnvRT4625YKR6L3ibuQxlXGtKHvu7eJdcOnW5IeVmg5vsH2xoVaF
4L0zBPpA6CBzsfZPgLJ9odD10nRVx8bh0b555/CUP5WIVHvLkPgp+NptJAoKah7Q
PkI5OKZWGjrJGiV7BWx5HIB5TX/w0PCjMA4ce3yVRNs7poUzR4PKgohrp94cIDFa
m+K4rfBmeMwVuuJZIuYpye80dcdsiFPrIZuA+hbP7tGlrwBKKMzpB3BEDIFTPb2b
hUq1TJCcHffFqxpRia+L/MMI8RpaEwzW9hghQB/LiPG0UqJycNpuo8cnggx+bsop
0ZXB68cW6K1UWLo1qLSz7/0+65PrWRkJisbjtmpKRDJlB72yIxlsZmBucxBy3II=
-----END CERTIFICATE-----

@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDQfw7eesJcJWzoV0AL
EJtevZRBdfNTKjceiNAfFoc/2bKN5pKU3n17/ZuJGGWFGNOhZANiAATi4tZjdLmF
ZQSCxqQ2Q8pNcJhj4F5JcZuxNmZ/zbMJ+eKrByXElvIEiQx8hBm6hXHN8E6JsxR2
u92j8Rfv/G019vNVZCowAXzTxUrvN6plXA2d+VU+Gxdc2wzhEtvNCqs=
-----END PRIVATE KEY-----

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDsTH5H+OD27yXI
Fz4MvNrpmpNgYly6QYUukJO8FI/funBe4N5mfOTZ/WrBeR5KoBOS3FV8VWIH7ShX
IzWBpollmIz9+jlPyftawXyiSnSXrwLBdHc8gjVRZf1H2U0E191v9z0oaXApy8d7
E0Pw+i4odoGHOX5Ix89s0DJrG5UPLrLr9OHlyMCC0D2QVp0wqFGkIXXv/cyYwlcG
PACvFhE/fWUazC4AEImJhXypOfQZh57SoldKQWwW7BZdGmnbSqeG2lq7KFUow0si
e4KzRPLPXrIGdKQbPAKDKcQ7MYlG9bfgsmM1Rr6klRAmO/e4w1HRHSHUetmFBYDJ
5MYNdeddYDfIVwjgwfDvy1i+ojRl/viywW3ONXR9rUcx/nGGc8UJTjJAAaAaTj6U
Mzn40ltLYmiymhj6mWFNiuGwDxasYTxA/3i62pGlbC8s1ZSXtdCYsEN4+W3N5CSa
dHdVtAIIEc6OyQ2mRa3v4oeZlyzt1mk9j9oChxi5r74ujUmjNHxpawWG5wmnRy3b
9ABxcivESEuXIbjBXrQ+RCmuQ8Dp5Asa7iede7iEPkhGw3Es3uVdk4s3/OlS6o+1
F+rG9qZr3aTtJrciVSLofbrSNtFFMERip+VBwIdBofVzS6GUhnLjjdhfu/kdzsqr
TDDFan4NX1IyC+zBL90DPQ8SoIOgowIDAQABAoICABEB7HDns+FyEwkUyy2Fhkgc
DRF54uyw/JH+a/O0kypqM95QVxGPWbVq7P0h55E9dksyuqBpUNX7NtUWvqonP2pl
kXhSQz+/7Ox6Uqsnqr6kJRGhfVeIk6fZLGK4fDemBdUiOW+oLx+DAEeWemRkzV+y
L954v+MjJoXRcl+NK6xdExmylXPBgEGqFVRHN6ch5kZm9iMg5FH2Yuca+H2hm/oy
300PdxwgFJYmWnOfrTcNMNw+PQQmM05vDakD1qym8end23vvCjoV3FmOBDk89DEC
wtN+H7WqGxAvuGT+SsAlvWdZz5QtFFmqNPBbjpfozwG7FA1EDlXpsHxXj/22B8Ho
XAPQ0ZeEJv4ZDbLs7AM/ofpNaitYcE0po2W7dlMqb1so3hK+eCPpVfos8mxlrkPV
sq2nFzVtl7V3MeipGw/MixDZZz6LDX0M1tk60a+4zAbDJaht1dS4SLfhygdQM+/m
OEAWyRAT7TDhCB+F474/G8PM1PnKmps5gV0X5jkqjQBCterBRwVcTI+ewNxUz7+Q
f4iN6sO8+Ihf94sr0YwaaIFyEetxTWBjevqWXpZ7fmdVbAmD5Z7WN8nvuARyXYdd
xOfXYA0mv0gtF1RDFV69lsMD6yPIGYdYIVd50r6P0VPQK+yjDQM3Pn3tVyLBmwqK
smKGCvkpZ92UvTsR3L/BAoIBAQD3w2RgKg3qVUeuJgbFDCRS/AkF1mkI4PMY5WLc
BHGa9q/9LucYTpZ60Pq/t9cNdDrEY/wdGvS23shWsAN3XGnj3m9FCCyuEZojmOkO
kNL51afhJ4h7r0yy4oPuXhQ5V860PuC3xv1QiBpxvKsod+KwiJI7O49dNxCr5dGk
RRdAHJEm73O2EmiTFHOZ3oGPoZ5SGtYxurVd3MNptiuKQYLA4+2i/P439TbCrh7E
0dxW9bZRLTygA77HLMA5suHKR4q6DS0HlpY9Cba2CxxIh/4xJRSQHOgiamNtYLeH
PnrXjyQVblf09j34YMc1K0A4R91CI74BYe5MwCptRSOIer6DAoIBAQD0J4g6F01m
26EqLhszq3n4jVroqmdB+oQ7q+yclrpjjPkwYhKZmXVrA+eNavBuMdLNv+50eqg4
9RW8H+GBwzhA+CWbZCFJ3Ad3e7jcE6QJVMjNIe6sZ15KF0IvYJA3B1lSsuK1jGga
eOFQZG+nIqpH58iowJt0wUTuiehf2xRaEhVkQn+PvIriPAmTlI3xME7ehctxFm8C
ZqFODfXsh0bLMx/pWVKDKYNh6WOst3k6/CUt4vN/PlPVvnNLMtbFQWyn1p5LhvvZ
xVlFBtobkaGUEjFYoQTwzl3s566f2anMa2Slhz0uopTttkvd3rXyPHm4guNiN/CN
ohW534tkPfthAoIBAFsBIfVQfRv9hv6oaQQnmZABky7ZumrQdXpHhzBZUYEh6zKL
78Y113/1EqUo2YzPjGZmc0wdgpVI7z0oGZ3WC+7u3N/2SLMHNB6vI6t99oBdwfQp
mTAVC48JNHxxgewuHHaIQfI+3PyfgVcVfai3oERHZa7sCZSrjSwWlhJIbmnWFFrA
yTevO0oK0QtLdztSmdx+jv5lHgkD9aL2jreRqH1BOyAK3TWglCSd4B9bFhu61OSs
QQBlX8W44kJPOjAaZxI/lLKc1UJGNx5WpmTdzrgubocglwNNIIgkZkT+5hAXO6HD
jfskF08L/R/CayxA+Tw59Kh9WBJI40yPgKW4sBECggEBAIdk0MeeGn86tnIEpXMO
2ZG7GbnCnYZaHTBWE912PKBuEdYB3Nyu3A1fWe3zaqdBG+ybTensBxOm3cm4SD7E
epKUyY4VhdxGlyFsS8RHZAUErmILOicDH6eopDxPqUnK2n7g0pXo6eYcOJ5zQ/OE
Zrd/Uqg6PzsM3mQFuAZIIE4ejxxNQB3+aWox7wGXNOuWZXZC7eGlliPXtAXr+f+T
uO+AR2cI8Jfp0oDegzbJfAH4x8ldfLiIYMc8WQVPiQhUUqP0gU3S6iEGro130kXN
ibPqLtE+YdYEKtPwWscsVlwVBfhBOe19nWcBW6sLEQzm+n0WoG/cI5r3UmMEE3Gg
aaECggEAJBcLJKp4WEz69qQbvV15HrEgsctIswUoaRsF0r0qnCAJoySG0mX2rGWR
Wmf8I2rw5hARLR5BFa3YtBPj0doL2mSxLOuH5tKdqthMai+B7K2J/YTPh8xOKoQd
2HcAoh1zxT2oA1Fj7BwzpvLTWwJEfsWzcOM+2ApxzLXkmgF90TBHoeCvlbfzdZ99
Ni/yj7tRpK1LoJYCkUHRspWwHB8DIagohG8tCtu4Bb5NdogtfaNeQ/BrgnZj77kf
FHu59hzsQQzkplCOnwJuT/Y6iqZuuJgPiESprUxHUz6aesySgcL1tuEq1GmB6O6U
eiDRzM4++dQGm7osCy1NRbPGxdUyhw==
-----END PRIVATE KEY-----

@ -0,0 +1,22 @@
{
"api": {
"url": "https://myprivspldev-api.jp-engineering.fr",
"port": 9999,
"useProxy": true
},
"deployer": {
"enabled": true,
"host": "myprivspldev-shdp-api.jp-engineering.fr",
"port": 9998,
"token": "bc2564e5a885d49ac3811dc946ca5620da24da19b8a8f5c5fdfcd7c07a241688",
"useSSL": true
},
"license": {
"checkInterval": 24
},
"advanced": {
"logLevel": "INFO",
"timeout": 30,
"gitTimeout": 120
}
}

@ -0,0 +1,572 @@
<dashboard version="1.1" script="license_validation.js, git_pusher.js" hideEdit="true" hideExport="true">
<label>Git Pusher - Deploy Applications</label>
<description>Push Splunk applications to Git repository and deploy to SH Cluster</description>
<search id="dsearch">
<query>| rest /services/apps/local | search disabled=0 | table title, label, description | rename title as name | sort label</query>
<earliest>-1m</earliest>
<latest>now</latest>
</search>
<row>
<panel>
<html>
<style>
/* ============================================ */
/* GIT PUSHER STYLES - VERSION 2.2 */
/* ============================================ */
.git-pusher-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Header avec badge de licence */
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header-title {
display: flex;
align-items: center;
gap: 15px;
}
.header-title h1 {
margin: 0;
font-size: 28px;
color: #333;
}
.version-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
/* Container pour le badge de licence */
#license-badge-container {
min-width: 200px;
text-align: right;
}
/* Grille principale */
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
@media (max-width: 1200px) {
.main-grid {
grid-template-columns: 1fr;
}
}
/* Sections */
.section {
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 16px;
padding: 25px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid #e8e8e8;
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 20px 0;
padding-bottom: 15px;
border-bottom: 2px solid #e0e0e0;
color: #333;
font-size: 18px;
}
.section-title::before {
content: '';
display: block;
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
/* Formulaire */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #444;
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group textarea {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-hint {
font-size: 12px;
color: #888;
margin-top: 5px;
}
/* Checkbox personnalisé */
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #667eea;
}
.checkbox-group label {
margin: 0;
font-weight: normal;
color: #555;
}
/* Liste des applications */
#dashboard-list {
max-height: 400px;
overflow-y: auto;
padding-right: 10px;
}
#dashboard-list::-webkit-scrollbar {
width: 8px;
}
#dashboard-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#dashboard-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
#dashboard-list::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
margin-bottom: 15px;
color: white;
}
.app-header label {
color: white;
font-weight: 600;
}
.app-count {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
font-size: 13px;
}
.app-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
margin-bottom: 8px;
background: white;
border: 1px solid #e8e8e8;
border-radius: 8px;
transition: all 0.2s ease;
}
.app-item:hover {
border-color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
}
.app-item input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #667eea;
}
.app-item label {
flex: 1;
cursor: pointer;
font-size: 14px;
}
.app-badge {
background: #e8f0fe;
color: #1a73e8;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
}
/* Section SH Deployer */
.deployer-section {
margin-top: 20px;
padding: 20px;
background: linear-gradient(145deg, #fff8e1 0%, #ffecb3 100%);
border-radius: 12px;
border: 2px solid #ffd54f;
}
.deployer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.deployer-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #f57c00;
}
.deployer-status {
font-size: 13px;
}
.deployer-config-btn {
background: none;
border: 1px solid #f57c00;
color: #f57c00;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.deployer-config-btn:hover {
background: #f57c00;
color: white;
}
.deployer-checkbox {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.deployer-checkbox input {
width: 20px;
height: 20px;
accent-color: #f57c00;
}
.deployer-auth {
display: none;
margin-top: 15px;
padding-top: 15px;
border-top: 1px dashed #ffd54f;
}
.deployer-auth.visible {
display: block;
}
.deployer-auth-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.deployer-auth input {
width: 100%;
padding: 10px;
border: 1px solid #ffd54f;
border-radius: 6px;
background: white;
}
/* Boutons */
.button-group {
display: flex;
gap: 15px;
margin-top: 25px;
}
.btn {
padding: 14px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
flex: 2;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f5f5f5;
color: #333;
flex: 1;
}
.btn-secondary:hover {
background: #e0e0e0;
}
/* Messages */
.message {
padding: 15px 20px;
border-radius: 8px;
margin-top: 20px;
display: none;
align-items: center;
gap: 10px;
white-space: pre-line;
}
.message.active {
display: flex;
}
.message.success {
background: linear-gradient(145deg, #e8f5e9 0%, #c8e6c9 100%);
color: #2e7d32;
border: 1px solid #a5d6a7;
}
.message.error {
background: linear-gradient(145deg, #ffebee 0%, #ffcdd2 100%);
color: #c62828;
border: 1px solid #ef9a9a;
}
/* Loading */
.loading {
display: none;
align-items: center;
gap: 15px;
padding: 20px;
background: linear-gradient(145deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 8px;
margin-top: 20px;
border: 1px solid #90caf9;
}
.loading.active {
display: flex;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #90caf9;
border-top-color: #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #1565c0;
font-weight: 500;
}
</style>
<div class="git-pusher-container">
<!-- Header -->
<div class="header-section">
<div class="header-title">
<h1>🚀 Git Pusher</h1>
<span class="version-badge">v2.2</span>
</div>
<div id="license-badge-container">
<!-- Le badge de licence sera inséré ici par JavaScript -->
</div>
</div>
<!-- Grille principale -->
<div class="main-grid">
<!-- Colonne gauche: Applications -->
<div class="section">
<h2 class="section-title">📦 Applications</h2>
<div id="dashboard-list">
<!-- Liste générée par JavaScript -->
<p style="color: #888; text-align: center; padding: 20px;">Loading applications...</p>
</div>
</div>
<!-- Colonne droite: Configuration -->
<div class="section">
<h2 class="section-title">⚙️ Git Configuration</h2>
<div class="form-group">
<label for="git-url">Repository URL</label>
<input type="text" id="git-url" placeholder="https://github.com/user/repo.git" />
<div class="form-hint">HTTPS URL of your Git repository</div>
</div>
<div class="form-group">
<label for="git-branch">Branch</label>
<input type="text" id="git-branch" value="main" placeholder="main" />
</div>
<div class="form-group">
<label for="git-token">Access Token / Password</label>
<input type="password" id="git-token" placeholder="ghp_xxxx or personal access token" />
<div class="form-hint">Personal access token with write permissions</div>
</div>
<div class="form-group">
<label for="commit-message">Commit Message</label>
<textarea id="commit-message" placeholder="Describe your changes..."></textarea>
</div>
<div class="checkbox-group">
<input type="checkbox" id="save-credentials" />
<label for="save-credentials">Remember credentials for next time</label>
</div>
<!-- Section SH Deployer -->
<div class="deployer-section" id="deployer-section">
<div class="deployer-header">
<div class="deployer-title">
🎯 Deploy to Search Head Cluster
<span class="deployer-status" id="deployer-status">
<span style="color: #888;">● Checking...</span>
</span>
</div>
<button class="deployer-config-btn" onclick="showDeployerConfigModal()">⚙️ Configure</button>
</div>
<div class="deployer-checkbox">
<input type="checkbox" id="deploy-to-shcluster" onchange="toggleDeployerAuth()" />
<label for="deploy-to-shcluster">
<strong>Enable automatic deployment</strong><br/>
<small style="color: #888;">After pushing to Git, pull and apply bundle to SH Cluster</small>
</label>
</div>
<div class="deployer-auth" id="deployer-auth">
<div class="form-hint" style="margin-bottom: 10px;">
Splunk credentials for applying shcluster-bundle (optional if using default)
</div>
<div class="deployer-auth-grid">
<input type="text" id="sh-auth-user" placeholder="Splunk username (optional)" />
<input type="password" id="sh-auth-pass" placeholder="Splunk password (optional)" />
</div>
</div>
</div>
<!-- Boutons -->
<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>
<!-- Messages -->
<div class="loading" id="loading">
<div class="spinner"></div>
<span class="loading-text">Deploying applications to Git... Please wait</span>
</div>
<div class="message success" id="success-message"></div>
<div class="message error" id="error-message"></div>
</div>
</div>
</div>
<script>
function toggleDeployerAuth() {
var checkbox = document.getElementById('deploy-to-shcluster');
var authSection = document.getElementById('deployer-auth');
if (checkbox) {
if (authSection) {
if (checkbox.checked) {
authSection.classList.add('visible');
} else {
authSection.classList.remove('visible');
}
}
}
}
</script>
</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 @@
{"pushes_today": 1, "pushes_total": 47, "last_push_date": "2026-02-14", "apps_pushed": []}

@ -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,30 @@
[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 = 1769976365.099941000
[app/install/state]
version = 10.0.2
modtime = 1771702401.801159000

@ -10,7 +10,7 @@ is_visible = true
label = Pusher Premium label = Pusher Premium
[launcher] [launcher]
author = author = Jocelyn PAMPHILE
description = description = Application Splunk pour déployer vos applications vers Git et le Search Head Cluster
version = 2.0.0 version = 2.1.0

@ -0,0 +1,311 @@
<dashboard version="1.1" script="git_pusher_config.js" hideEdit="true">
<label>Git Pusher - Configuration</label>
<description>Configuration de l'application Git Pusher</description>
<row>
<panel>
<html>
<style>
.config-container {
max-width: 900px;
margin: 0 auto;
padding: 30px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.config-header {
text-align: center;
margin-bottom: 40px;
}
.config-header h1 {
font-size: 28px;
color: #333;
margin-bottom: 10px;
}
.config-header p {
color: #666;
font-size: 14px;
}
.config-section {
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.config-section h2 {
font-size: 18px;
color: #333;
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
display: flex;
align-items: center;
gap: 10px;
}
.config-section h2 .icon {
font-size: 24px;
}
.config-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 15px;
align-items: center;
}
.config-grid label {
font-weight: 600;
color: #555;
}
.config-grid input, .config-grid select {
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.config-grid input:focus, .config-grid select:focus {
outline: none;
border-color: #667eea;
}
.config-grid input[type="checkbox"] {
width: 20px;
height: 20px;
}
.config-hint {
grid-column: 2;
font-size: 12px;
color: #888;
margin-top: -10px;
}
.config-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.config-message {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.config-message.success {
background: #e8f5e9;
color: #2e7d32;
border-left: 4px solid #4caf50;
display: block;
}
.config-message.error {
background: #ffebee;
color: #c62828;
border-left: 4px solid #f44336;
display: block;
}
.config-status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.config-status.connected {
background: #e8f5e9;
color: #2e7d32;
}
.config-status.disconnected {
background: #ffebee;
color: #c62828;
}
.test-btn {
padding: 8px 15px;
font-size: 12px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.test-btn:hover {
background: #1976D2;
}
.version-badge {
position: fixed;
bottom: 20px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
</style>
<div class="config-container">
<div class="config-header">
<h1>⚙️ Configuration Git Pusher</h1>
<p>Configurez les paramètres de l'application</p>
</div>
<div id="config-message" class="config-message"></div>
<!-- Section API -->
<div class="config-section">
<h2><span class="icon">🌐</span> Configuration API</h2>
<div class="config-grid">
<label for="api-url">URL de l'API Git Pusher</label>
<input type="text" id="api-url" placeholder="https://myprivspldev-api.example.com" />
<span class="config-hint">L'URL du serveur Git Pusher (sans le port si proxy)</span>
<label for="api-port">Port (si accès direct)</label>
<input type="number" id="api-port" placeholder="9999" value="9999" />
<span class="config-hint">Port utilisé si accès direct par IP (ignoré si URL de domaine)</span>
<label for="use-proxy">Utiliser un proxy</label>
<input type="checkbox" id="use-proxy" checked="checked" />
<span class="config-hint">Coché si vous utilisez un reverse proxy (Nginx, etc.)</span>
<label>Test de connexion</label>
<div>
<button class="test-btn" id="test-api-btn">Tester la connexion</button>
<span id="api-status" class="config-status disconnected">● Non testé</span>
</div>
</div>
</div>
<!-- Section SH Deployer -->
<div class="config-section">
<h2><span class="icon">🚀</span> Configuration SH Deployer</h2>
<div class="config-grid">
<label for="deployer-enabled">Activer SH Deployer</label>
<input type="checkbox" id="deployer-enabled" />
<span class="config-hint">Activer le déploiement automatique vers le Search Head Cluster</span>
<label for="deployer-host">Adresse du SH Deployer</label>
<input type="text" id="deployer-host" placeholder="10.10.40.14" />
<span class="config-hint">Adresse IP ou hostname du serveur SH Deployer</span>
<label for="deployer-port">Port</label>
<input type="number" id="deployer-port" placeholder="9998" value="9998" />
<label for="deployer-token">Token d'authentification</label>
<input type="password" id="deployer-token" placeholder="Token secret" />
<span class="config-hint">Token configuré dans deployer_agent.py</span>
<label for="deployer-use-ssl">Utiliser SSL</label>
<input type="checkbox" id="deployer-use-ssl" checked="checked" />
<label>Test de connexion</label>
<div>
<button class="test-btn" id="test-deployer-btn">Tester la connexion</button>
<span id="deployer-status" class="config-status disconnected">● Non testé</span>
</div>
</div>
</div>
<!-- Section Licence -->
<div class="config-section">
<h2><span class="icon">🔐</span> Configuration Licence</h2>
<div class="config-grid">
<label>Statut de la licence</label>
<div id="license-status">Chargement...</div>
<label for="license-check-interval">Intervalle de vérification (heures)</label>
<input type="number" id="license-check-interval" value="24" min="1" max="168" />
<span class="config-hint">Fréquence de revalidation de la licence</span>
</div>
</div>
<!-- Section Avancée -->
<div class="config-section">
<h2><span class="icon">🔧</span> Paramètres avancés</h2>
<div class="config-grid">
<label for="log-level">Niveau de log</label>
<select id="log-level">
<option value="INFO">INFO</option>
<option value="DEBUG">DEBUG</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
</select>
<label for="timeout">Timeout des requêtes (secondes)</label>
<input type="number" id="timeout" value="30" min="5" max="300" />
<label for="git-timeout">Timeout Git (secondes)</label>
<input type="number" id="git-timeout" value="120" min="30" max="600" />
</div>
</div>
<!-- Actions -->
<div class="config-actions">
<button class="btn btn-secondary" id="reset-btn">Réinitialiser</button>
<button class="btn btn-primary" id="save-btn">💾 Sauvegarder</button>
</div>
</div>
<div class="version-badge">Git Pusher v2.1</div>
</html>
</panel>
</row>
</dashboard>

@ -0,0 +1,661 @@
<dashboard version="1.1" script="license_validation.js, git_pusher.js" hideEdit="true" hideExport="true">
<label>Git Pusher - Deploy Applications</label>
<description>Push Splunk applications to Git repository and deploy to SH Cluster</description>
<search id="dsearch">
<query>| rest /services/apps/local | search disabled=0 | table title, label, description | rename title as name | sort label</query>
<earliest>-1m</earliest>
<latest>now</latest>
</search>
<row>
<panel>
<html>
<style>
/* ============================================ */
/* GIT PUSHER STYLES - VERSION 2.2 */
/* ============================================ */
.git-pusher-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Header avec badge de licence */
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header-title {
display: flex;
align-items: center;
gap: 15px;
}
.header-title h1 {
margin: 0;
font-size: 28px;
color: #333;
}
.version-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
/* Container pour le badge de licence */
#license-badge-container {
min-width: 200px;
text-align: right;
}
/* Grille principale */
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
@media (max-width: 1200px) {
.main-grid {
grid-template-columns: 1fr;
}
}
/* Sections */
.section {
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 16px;
padding: 25px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid #e8e8e8;
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 20px 0;
padding-bottom: 15px;
border-bottom: 2px solid #e0e0e0;
color: #333;
font-size: 18px;
}
.section-title::before {
content: '';
display: block;
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
/* Formulaire */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #444;
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group textarea {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-hint {
font-size: 12px;
color: #888;
margin-top: 5px;
}
/* Checkbox personnalisé */
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #667eea;
}
.checkbox-group label {
margin: 0;
font-weight: normal;
color: #555;
}
/* Liste des applications */
#dashboard-list {
max-height: 400px;
overflow-y: auto;
padding-right: 10px;
}
#dashboard-list::-webkit-scrollbar {
width: 8px;
}
#dashboard-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#dashboard-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
#dashboard-list::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
margin-bottom: 15px;
color: white;
}
.app-header label {
color: white;
font-weight: 600;
}
.app-count {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
font-size: 13px;
}
.app-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 15px;
margin-bottom: 8px;
background: white;
border: 1px solid #e8e8e8;
border-radius: 8px;
transition: all 0.2s ease;
}
.app-item:hover {
border-color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
}
.app-item input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #667eea;
}
.app-item label {
flex: 1;
cursor: pointer;
font-size: 14px;
}
.app-badge {
background: #e8f0fe;
color: #1a73e8;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
}
/* Section SH Deployer */
.deployer-section {
margin-top: 20px;
padding: 20px;
background: linear-gradient(145deg, #fff8e1 0%, #ffecb3 100%);
border-radius: 12px;
border: 2px solid #ffd54f;
}
.deployer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.deployer-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #f57c00;
}
.deployer-status {
font-size: 13px;
}
.deployer-config-btn {
background: none;
border: 1px solid #f57c00;
color: #f57c00;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.deployer-config-btn:hover {
background: #f57c00;
color: white;
}
.deployer-checkbox {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.deployer-checkbox input {
width: 20px;
height: 20px;
accent-color: #f57c00;
}
/* Section sélection apps SH Cluster */
.deployer-apps-section {
display: none;
margin-top: 15px;
padding: 15px;
background: rgba(255, 255, 255, 0.7);
border-radius: 8px;
border: 1px dashed #ffd54f;
}
.deployer-apps-section.visible {
display: block;
}
.deployer-apps-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.deployer-apps-header label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.deployer-apps-list {
max-height: 200px;
overflow-y: auto;
padding: 10px;
background: white;
border-radius: 6px;
border: 1px solid #ddd;
margin-top: 10px;
}
.shcluster-app-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
margin-bottom: 5px;
background: #fff8e1;
border-radius: 4px;
transition: background 0.2s;
}
.shcluster-app-item:hover {
background: #ffecb3;
}
.shcluster-app-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #f57c00;
}
.shcluster-app-item label {
flex: 1;
cursor: pointer;
font-size: 13px;
}
.shcluster-app-item .app-badge {
font-size: 11px;
}
.deployer-auth {
display: none;
margin-top: 15px;
padding-top: 15px;
border-top: 1px dashed #ffd54f;
}
.deployer-auth.visible {
display: block;
}
.deployer-auth-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.deployer-auth input {
width: 100%;
padding: 10px;
border: 1px solid #ffd54f;
border-radius: 6px;
background: white;
}
/* Boutons */
.button-group {
display: flex;
gap: 15px;
margin-top: 25px;
}
.btn {
padding: 14px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
flex: 2;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f5f5f5;
color: #333;
flex: 1;
}
.btn-secondary:hover {
background: #e0e0e0;
}
/* Messages */
.message {
padding: 15px 20px;
border-radius: 8px;
margin-top: 20px;
display: none;
align-items: center;
gap: 10px;
white-space: pre-line;
}
.message.active {
display: flex;
}
.message.success {
background: linear-gradient(145deg, #e8f5e9 0%, #c8e6c9 100%);
color: #2e7d32;
border: 1px solid #a5d6a7;
}
.message.error {
background: linear-gradient(145deg, #ffebee 0%, #ffcdd2 100%);
color: #c62828;
border: 1px solid #ef9a9a;
}
/* Loading */
.loading {
display: none;
align-items: center;
gap: 15px;
padding: 20px;
background: linear-gradient(145deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 8px;
margin-top: 20px;
border: 1px solid #90caf9;
}
.loading.active {
display: flex;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #90caf9;
border-top-color: #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #1565c0;
font-weight: 500;
}
</style>
<div class="git-pusher-container">
<!-- Header -->
<div class="header-section">
<div class="header-title">
<h1>🚀 Git Pusher</h1>
<span class="version-badge">v2.2</span>
</div>
<div id="license-badge-container">
<!-- Le badge de licence sera inséré ici par JavaScript -->
</div>
</div>
<!-- Grille principale -->
<div class="main-grid">
<!-- Colonne gauche: Applications -->
<div class="section">
<h2 class="section-title">📦 Applications</h2>
<div id="dashboard-list">
<!-- Liste générée par JavaScript -->
<p style="color: #888; text-align: center; padding: 20px;">Loading applications...</p>
</div>
</div>
<!-- Colonne droite: Configuration -->
<div class="section">
<h2 class="section-title">⚙️ Git Configuration</h2>
<div class="form-group">
<label for="git-url">Repository URL</label>
<input type="text" id="git-url" placeholder="https://github.com/user/repo.git" />
<div class="form-hint">HTTPS URL of your Git repository</div>
</div>
<div class="form-group">
<label for="git-branch">Branch</label>
<input type="text" id="git-branch" value="main" placeholder="main" />
</div>
<div class="form-group">
<label for="git-token">Access Token / Password</label>
<input type="password" id="git-token" placeholder="ghp_xxxx or personal access token" />
<div class="form-hint">Personal access token with write permissions</div>
</div>
<div class="form-group">
<label for="commit-message">Commit Message</label>
<textarea id="commit-message" placeholder="Describe your changes..."></textarea>
</div>
<div class="checkbox-group">
<input type="checkbox" id="save-credentials" />
<label for="save-credentials">Remember credentials for next time</label>
</div>
<!-- Section SH Deployer -->
<div class="deployer-section" id="deployer-section">
<div class="deployer-header">
<div class="deployer-title">
🎯 Deploy to Search Head Cluster
<span class="deployer-status" id="deployer-status">
<span style="color: #888;">● Checking...</span>
</span>
</div>
<button class="deployer-config-btn" id="deployer-config-btn">⚙️ Configure</button>
</div>
<div class="deployer-checkbox">
<input type="checkbox" id="deploy-to-shcluster" />
<label for="deploy-to-shcluster">
<strong>Enable automatic deployment</strong><br/>
<small style="color: #888;">After pushing to Git, pull and apply bundle to SH Cluster</small>
</label>
</div>
<!-- Sélection des apps pour le SH Cluster -->
<div class="deployer-apps-section" id="deployer-apps-section">
<div class="deployer-apps-header">
<label>
<input type="checkbox" id="shcluster-all-apps" checked="checked" />
<strong>Deploy all selected apps to SH Cluster</strong>
</label>
</div>
<div class="deployer-apps-list" id="shcluster-apps-list" style="display: none;">
<div class="form-hint" style="margin-bottom: 10px;">
Select which apps to deploy to the Search Head Cluster:
</div>
<div id="shcluster-apps-container">
<!-- Apps will be populated by JavaScript -->
<p style="color: #888; font-style: italic;">Select apps from the left panel first</p>
</div>
</div>
</div>
<div class="deployer-auth" id="deployer-auth">
<div class="form-hint" style="margin-bottom: 10px;">
Splunk credentials for applying shcluster-bundle (optional if using default)
</div>
<div class="deployer-auth-grid">
<input type="text" id="sh-auth-user" placeholder="Splunk username (optional)" />
<input type="password" id="sh-auth-pass" placeholder="Splunk password (optional)" />
</div>
</div>
</div>
<!-- Boutons -->
<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>
<!-- Messages -->
<div class="loading" id="loading">
<div class="spinner"></div>
<span class="loading-text">Deploying applications to Git... Please wait</span>
</div>
<div class="message success" id="success-message"></div>
<div class="message error" id="error-message"></div>
</div>
</div>
</div>
<script>
function toggleDeployerAuth() {
var checkbox = document.getElementById('deploy-to-shcluster');
var authSection = document.getElementById('deployer-auth');
if (checkbox) {
if (authSection) {
if (checkbox.checked) {
authSection.classList.add('visible');
} else {
authSection.classList.remove('visible');
}
}
}
}
</script>
</html>
</panel>
</row>
</dashboard>
Loading…
Cancel
Save