backup_app_pusher

masterdev
admingit 2 months ago
parent a0e9d3e2a0
commit 18d1668536

BIN
.DS_Store vendored

Binary file not shown.

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,971 @@
// Git Pusher License Module v2.1
// (c) 2026 - Proprietary Software
function _d(a,k){return a.map(function(c){return String.fromCharCode(c^k)}).join('')}
var _zIHmsHGW='xfbd3d6ElLWhx49PFbW4aMdR5xVlXycz';
var _scveStRh='jTedL0XkPqQ57QEvoScSPe3YdS3ffbNv';
var _cvATvEmd='FoVNwSH2lorubG3ft62Csdv5TCNj5lb4';
var _uBCIWHFz='Fp6QOBvg2QP0ZkwPzk242wWSaWuoXepZ';
var _rWmSbklk='NmlZTqIFdpuufAKqYKjw02cv64EpQOE1';
var _kCgtOcNH='kKp6AdQlxtiHs8XoWCyyDPEyED0sdR5H';
var _cScprkja='GuHaqmTn95wVOrJE5R0iEXzhK79TEwDf';
var _YNHMdhDo='zeVpz0pkmIaRRrzBctVAgdw4i5lFt1l7';
var _OyqdzCbV='Vy4AbuET48dkrMumLzgIQ7EraLtWYM3o';
var _iROUmOCb='492FnjZ1xHduxZtzHf1E4NZMdnBRcbnj';
function _CrQQzqQN(a,b){
var c=a^b;var d=c.toString(16);
return d.split('').reverse().join('');
}
function _drEEHUWn(a,b){
var c=a^b;var d=c.toString(16);
return d.split('').reverse().join('');
}
function _hfNPJXFI(a,b){
var c=a^b;var d=c.toString(16);
return d.split('').reverse().join('');
}
function _zdlCkObd(a,b){
var c=a^b;var d=c.toString(16);
return d.split('').reverse().join('');
}
function _jIILWFfW(a,b){
var c=a^b;var d=c.toString(16);
return d.split('').reverse().join('');
}
var _NiOaayts=_d([171,171,171,171,171,196,195,193,207,200,166,214,211,196,202,207,197,166,205,195,223,171,171,171,171,171,140],134);
var _SNpoWSGf=_d([146,150,150,156,150,181,158,145,157,184,180,174,183,180,182,152,230,168,239,157,158,142,154,153,158,158,144,156,158,184,231,158,146,150,150,156,156,184,148,156,158,184,154,158,232,151,231,169,237,235,236,143,148,175,166,235,133,188,152,138,150],223);
var _EgHnpIVn=_d([118,28,29,0,44,4,31,36,14,110,19,50,18,22,1,60,50,6,50,7,10,8,10,15,7,4,112,54,41,18,42,55,21,114,125,38,8,0,117,39,12,53,45,55,11,23,51,17,41,4,124,61,53,48,42,117,36,110,125,19,110],69);
var _rTxLsyvy=_d([170,186,177,158,207,215,169,139,197,189,148,184,136,137,182,183,174,181,185,155,139,182,149,132,145,211,145,151,183,159,158,150,139,141,175,180,172,145,146,133,190,190,180,172,190,190,164,203,144,179,211,141,206,139,143,185,164,204,133,207,179],252);
var _PyBShqXq=_d([153,248,195,220,244,207,199,213,133,210,224,197,244,134,224,224,222,251,240,252,219,209,207,228,133,210,224,207,128,236,231,255,226,209,243,195,153,248,241,197,143,192,153,220,227,213,131,255,226,135,239,204,224,219,249,245,213,250,142,244,236],182);
var _ETunnirh=_d([101,125,123,80,126,81,39,86,79,114,68,36,56,66,47,124,127,102,32,96,82,111,34,88,126,123,127,79,84,32,126,47,96,33,98,101,115,46,116,35,83,125,125,112,34,47,36,64,100,80,99,83,92,124,39,118,77,33,111,97,121],23);
var _WUiBKlXD=_d([254,193,232,245,255,254,204,226,249,222,209,234,205,243,203,249,254,204,224,250,251,194,169,240,168,214,171,244,214,161,213,218,238,249,212,250,255,217,237,241,173,202,255,252,244,250,210,244,250,223,235,255,244,171,237,217,249,161,202,161,224],152);
var _uUXuptGN=_d([81,96,32,121,122,95,99,51,93,73,71,93,122,32,64,92,62,66,106,61,106,82,68,93,127,110,121,94,109,57,99,113,36,99,94,96,69,97,70,51,89,100,103,125,63,36,56,91,90,83,59,102,76,126,61,109,106,63,109,124,100],11);
var _aEFAHbev=_d([204,217,222,216,231,225,241,226,204,226,242,220,131,224,208,247,241,155,227,252,222,135,223,245,251,205,214,238,236,227,254,218,195,199,155,159,159,193,198,197,221,222,228,129,231,217,237,204,205,247,213,226,216,237,245,219,193,192,208,227,217],180);
var _RMPjCYcD=_d([88,19,86,118,80,113,122,109,74,21,22,83,80,77,87,16,85,84,17,97,26,102,79,77,26,65,65,116,88,76,99,74,70,74,106,65,116,81,21,111,113,78,27,115,64,91,85,19,68,81,75,19,19,19,21,117,75,69,119,101,73],34);
var _eWFPhTnP=_d([174,187,203,189,134,148,146,140,146,198,187,157,188,175,153,205,185,203,134,199,207,187,172,199,203,180,179,177,206,173,148,135,150,186,156,145,140,170,213,145,179,143,199,199,202,201,164,178,205,203,168,146,146,132,149,181,156,188,187,178,156],254);
var _FxJrCmfJ=_d([232,196,238,239,231,216,237,196,205,242,201,179,201,234,177,213,194,237,195,244,211,207,218,250,207,204,232,249,200,196,198,237,199,230,181,193,194,177,204,215,226,248,227,231,242,250,207,205,227,212,204,239,193,200,228,245,225,196,225,236,218],128);
var _LHoFpbNb=_d([99,90,77,119,20,119,68,118,20,19,17,19,99,81,69,65,87,101,70,81,106,118,74,24,98,106,11,16,118,101,102,68,66,114,100,75,20,122,90,104,80,111,65,103,97,85,99,106,74,110,19,97,85,88,111,24,99,97,87,101,97,97,113,29,29],32);
var _IzZDbeLK=_d([186,157,157,157,157,157,245,254,244,144,224,229,242,252,249,243,144,251,245,233,157,157,157,157,157],176);
var _SklQZwjs=_NiOaayts+_SNpoWSGf+_EgHnpIVn+_rTxLsyvy+_PyBShqXq+_ETunnirh+_WUiBKlXD+_uUXuptGN+_aEFAHbev+_RMPjCYcD+_eWFPhTnP+_FxJrCmfJ+_LHoFpbNb+_IzZDbeLK;
var PUBLIC_KEY_PEM=_SklQZwjs;
const DEFAULT_APP_CONFIG = {
api: {
url: '',
port: 9999,
useProxy: true
}
};
function loadAppConfigForLicense() {
try {
const stored = localStorage.getItem('git_pusher_config');
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.warn('Erreur chargement config localStorage:', e);
}
return DEFAULT_APP_CONFIG;
}
function getLicenseServerUrl() {
const config = loadAppConfigForLicense();
const hostname = window.location.hostname;
const protocol = window.location.protocol;
if (config.api && config.api.url) {
let url = config.api.url;
if (!config.api.useProxy && config.api.port) {
url = url.replace(/\/$/, '') + ':' + config.api.port;
}
return url;
}
if (hostname === 'myprivspldev.jp-engineering.fr') {
return protocol + '
}
if (hostname === 'myprivspldev-api.jp-engineering.fr') {
return protocol + '
}
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname) || hostname === 'localhost') {
return protocol + '
}
return protocol + '
}
const LICENSE_CONFIG = {
storageKey: 'git_pusher_license',
usageKey: 'git_pusher_usage',
version: '2.1.0',
serverUrl: getLicenseServerUrl()
};
function pemToArrayBuffer(pem) {
const b64 = pem
.replace(/-----BEGIN PUBLIC KEY-----/, '')
.replace(/-----END PUBLIC KEY-----/, '')
.replace(/\s/g, '');
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
async function importPublicKey() {
try {
const keyData = pemToArrayBuffer(PUBLIC_KEY_PEM);
const key = await crypto.subtle.importKey(
'spki',
keyData,
{
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256'
},
false,
['verify']
);
return key;
} catch (error) {
console.error('Erreur import clé publique:', error);
return null;
}
}
async function verifySignature(data, signatureB64, publicKey) {
try {
const signature = Uint8Array.from(atob(signatureB64), c => c.charCodeAt(0));
const dataBuffer = new TextEncoder().encode(data);
const isValid = await crypto.subtle.verify(
'RSASSA-PKCS1-v1_5',
publicKey,
signature,
dataBuffer
);
return isValid;
} catch (error) {
console.error('Erreur vérification signature:', error);
return false;
}
}
function base64Decode(str) {
try {
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
} catch (e) {
return atob(str);
}
}
function getCurrentHostname() {
return window.location.hostname.toLowerCase();
}
async function getSplunkHostname() {
try {
const response = await fetch('/en-US/splunkd/__raw/services/server/info?output_mode=json', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
const serverName = data.entry?.[0]?.content?.serverName;
if (serverName) {
console.log('Hostname Splunk (server/info):', serverName);
return serverName.toLowerCase();
}
}
} catch (e) {
console.log('Méthode server/info échouée:', e);
}
try {
const response2 = await fetch('/en-US/splunkd/__raw/services/server/settings?output_mode=json', {
credentials: 'include'
});
if (response2.ok) {
const data2 = await response2.json();
const serverName = data2.entry?.[0]?.content?.serverName;
if (serverName) {
console.log('Hostname Splunk (server/settings):', serverName);
return serverName.toLowerCase();
}
}
} catch (e) {
console.log('Méthode server/settings échouée:', e);
}
console.warn('Impossible de récupérer le hostname Splunk, utilisation URL:', getCurrentHostname());
return getCurrentHostname();
}
function daysRemaining(expiryDate) {
const expiry = new Date(expiryDate);
const now = new Date();
const diff = expiry - now;
return Math.ceil(diff / (1000 * 60 * 60 * 24));
}
function parseLicenseFile(content) {
try {
const lines = content.trim().split('\n');
let payloadB64 = null;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
payloadB64 = trimmed;
break;
}
}
if (!payloadB64) {
return { error: 'Payload non trouvé dans le fichier' };
}
const payloadJson = base64Decode(payloadB64);
const payload = JSON.parse(payloadJson);
if (!payload.license || !payload.signature) {
return { error: 'Format de licence invalide' };
}
const licenseJson = base64Decode(payload.license);
const licenseData = JSON.parse(licenseJson);
return {
success: true,
licenseJson: licenseJson,
licenseData: licenseData,
signatureB64: payload.signature,
rawPayload: payloadB64
};
} catch (error) {
console.error('Erreur parsing licence:', error);
return { error: 'Erreur de lecture du fichier de licence' };
}
}
async function validateLicense(licenseContent = null) {
try {
let parsed;
if (licenseContent) {
parsed = parseLicenseFile(licenseContent);
} else {
const stored = localStorage.getItem(LICENSE_CONFIG.storageKey);
if (!stored) {
return {
valid: false,
error: 'Aucune licence installée',
error_code: 'NO_LICENSE'
};
}
parsed = JSON.parse(stored);
}
if (parsed.error) {
return {
valid: false,
error: parsed.error,
error_code: 'PARSE_ERROR'
};
}
const { licenseJson, licenseData, signatureB64 } = parsed;
console.log('=== DEBUG VALIDATION LICENCE ===');
console.log('License JSON:', licenseJson);
console.log('License JSON length:', licenseJson.length);
console.log('Signature B64:', signatureB64.substring(0, 50) + '...');
console.log('Signature B64 length:', signatureB64.length);
console.log('Vérification de la signature RSA...');
const publicKey = await importPublicKey();
if (!publicKey) {
console.error('Échec import clé publique');
return {
valid: false,
error: 'Impossible de charger la clé publique',
error_code: 'KEY_ERROR'
};
}
console.log('Clé publique importée avec succès');
const signatureValid = await verifySignature(licenseJson, signatureB64, publicKey);
console.log('Résultat vérification signature:', signatureValid);
if (!signatureValid) {
return {
valid: false,
error: 'Signature de licence invalide',
error_code: 'INVALID_SIGNATURE'
};
}
console.log('✓ Signature RSA valide');
const expectedHostname = (licenseData.hostname || '').toLowerCase();
const currentHostname = await getSplunkHostname();
console.log(`Hostname attendu: "${expectedHostname}", actuel: "${currentHostname}"`);
if (expectedHostname && expectedHostname !== currentHostname) {
if (!currentHostname.includes(expectedHostname) && !expectedHostname.includes(currentHostname)) {
return {
valid: false,
error: `Licence non valide pour ce serveur. Attendu: ${expectedHostname}, Actuel: ${currentHostname}`,
error_code: 'HOSTNAME_MISMATCH',
expected_hostname: expectedHostname,
current_hostname: currentHostname
};
}
console.log('✓ Hostname valide (correspondance partielle)');
} else {
console.log('✓ Hostname valide (exact)');
}
const expiryDate = licenseData.expires;
if (expiryDate) {
const days = daysRemaining(expiryDate);
if (days < 0) {
return {
valid: false,
error: `Licence expirée le ${expiryDate}`,
error_code: 'LICENSE_EXPIRED',
expires: expiryDate
};
}
console.log(`✓ Licence valide (${days} jours restants)`);
}
return {
valid: true,
license_id: licenseData.license_id,
type: licenseData.type,
type_name: licenseData.type_name,
customer: licenseData.customer,
hostname: expectedHostname,
issued: licenseData.issued,
expires: expiryDate,
days_remaining: daysRemaining(expiryDate),
limits: licenseData.limits || {},
features: licenseData.features || []
};
} catch (error) {
console.error('Erreur validation licence:', error);
return {
valid: false,
error: error.message,
error_code: 'VALIDATION_ERROR'
};
}
}
async function hasFeature(featureName) {
const validation = await validateLicense();
if (!validation.valid) return false;
return validation.features.includes(featureName);
}
async function saveLicense(licenseContent) {
try {
const parsed = parseLicenseFile(licenseContent);
if (parsed.error) {
return {
success: false,
error: parsed.error
};
}
const validation = await validateLicense(licenseContent);
if (!validation.valid) {
return {
success: false,
error: validation.error,
error_code: validation.error_code
};
}
localStorage.setItem(LICENSE_CONFIG.storageKey, JSON.stringify(parsed));
console.log('✓ Licence sauvegardée dans localStorage');
try {
const response = await fetch(`${LICENSE_CONFIG.serverUrl}/license/save`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
license_content: licenseContent
})
});
const serverResult = await response.json();
if (serverResult.success) {
console.log('✓ Licence sauvegardée sur le serveur');
} else {
console.warn('⚠ Échec sauvegarde serveur:', serverResult.error);
}
} catch (serverError) {
console.warn('⚠ Impossible de sauvegarder sur le serveur:', serverError);
}
return {
success: true,
license: validation
};
} catch (error) {
console.error('Erreur sauvegarde licence:', error);
return {
success: false,
error: error.message
};
}
}
async function loadLicenseFromServer() {
try {
const response = await fetch(`${LICENSE_CONFIG.serverUrl}/license/file`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success && result.content) {
console.log('Licence trouvée sur le serveur, validation en cours...');
const parsed = parseLicenseFile(result.content);
if (parsed.error) {
console.warn('Erreur parsing licence serveur:', parsed.error);
return false;
}
const validation = await validateLicense(result.content);
if (validation.valid) {
localStorage.setItem(LICENSE_CONFIG.storageKey, JSON.stringify(parsed));
console.log('✓ Licence chargée depuis le serveur et stockée localement');
return true;
} else {
console.warn('Licence serveur invalide:', validation.error);
return false;
}
} else {
console.log('Aucune licence sur le serveur');
return false;
}
} catch (error) {
console.log('Impossible de charger la licence depuis le serveur:', error.message);
return false;
}
}
async function removeLicense() {
localStorage.removeItem(LICENSE_CONFIG.storageKey);
localStorage.removeItem(LICENSE_CONFIG.usageKey);
try {
await fetch(`${LICENSE_CONFIG.serverUrl}/license/delete`, {
method: 'POST'
});
console.log('Licence supprimée du serveur');
} catch (e) {
console.warn('Impossible de supprimer la licence du serveur:', e);
}
console.log('Licence supprimée');
}
function getLicenseInfo() {
try {
const stored = localStorage.getItem(LICENSE_CONFIG.storageKey);
if (!stored) return null;
const parsed = JSON.parse(stored);
return parsed.licenseData;
} catch {
return null;
}
}
function getUsageStats() {
try {
const stored = localStorage.getItem(LICENSE_CONFIG.usageKey);
if (stored) {
return JSON.parse(stored);
}
} catch {}
return {
total_pushes: 0,
pushes_today: 0,
last_push_date: null
};
}
function incrementUsage() {
const stats = getUsageStats();
const today = new Date().toISOString().split('T')[0];
if (stats.last_push_date !== today) {
stats.pushes_today = 0;
stats.last_push_date = today;
}
stats.total_pushes = (stats.total_pushes || 0) + 1;
stats.pushes_today = (stats.pushes_today || 0) + 1;
localStorage.setItem(LICENSE_CONFIG.usageKey, JSON.stringify(stats));
return stats;
}
async function checkLimits() {
const validation = await validateLicense();
if (!validation.valid) {
return {
allowed: false,
error: validation.error,
error_code: validation.error_code
};
}
const limits = validation.limits || {};
const maxPushes = limits.max_pushes_per_day || -1;
if (maxPushes > 0) {
const stats = getUsageStats();
const today = new Date().toISOString().split('T')[0];
let pushesToday = stats.pushes_today || 0;
if (stats.last_push_date !== today) {
pushesToday = 0;
}
if (pushesToday >= maxPushes) {
return {
allowed: false,
error: `Limite quotidienne atteinte (${maxPushes} pushes/jour)`,
error_code: 'DAILY_LIMIT_REACHED'
};
}
}
return {
allowed: true,
license_type: validation.type_name,
remaining_today: maxPushes > 0 ? maxPushes - getUsageStats().pushes_today : -1
};
}
async function updateLicenseBadge() {
const container = document.getElementById('license-badge-container');
if (!container) return;
const validation = await validateLicense();
let badgeHtml = '';
if (validation.valid) {
const daysLeft = validation.days_remaining;
let badgeClass = 'license-badge-valid';
let icon = '✓';
if (daysLeft <= 7) {
badgeClass = 'license-badge-expiring';
icon = '⚠️';
} else if (daysLeft <= 30) {
badgeClass = 'license-badge-warning';
icon = '⏳';
}
badgeHtml = `
<div class="license-badge ${badgeClass}" onclick="showLicenseDetails()">
<span class="license-icon">${icon}</span>
<span class="license-type">${validation.type_name}</span>
<span class="license-days">${daysLeft}j</span>
</div>
`;
} else {
badgeHtml = `
<div class="license-badge license-badge-invalid" onclick="showLicenseModal()">
<span class="license-icon">🔐</span>
<span class="license-type">Activer</span>
</div>
`;
}
container.innerHTML = badgeHtml;
}
function showLicenseModal(message = null, errorCode = null) {
const existingModal = document.getElementById('license-modal');
if (existingModal) existingModal.remove();
const modal = document.createElement('div');
modal.id = 'license-modal';
modal.className = 'license-modal-overlay';
let errorMessage = '';
if (message) {
errorMessage = `<div class="license-error-message">${message}</div>`;
}
modal.innerHTML = `
<div class="license-modal">
<div class="license-modal-header">
<h2>🔐 Activation de Licence</h2>
<button class="license-modal-close" onclick="closeLicenseModal()">×</button>
</div>
<div class="license-modal-body">
${errorMessage}
<div class="license-upload-zone" id="license-upload-zone">
<div class="license-upload-icon">📄</div>
<div class="license-upload-text">
Glissez-déposez votre fichier <strong>.lic</strong> ici
<br><small>ou cliquez pour sélectionner</small>
</div>
<input type="file" id="license-file-input" accept=".lic" style="display: none;">
</div>
<div class="license-hostname-info">
<strong>Hostname du serveur:</strong>
<code id="current-hostname-display">Chargement...</code>
<br><small>Communiquez ce hostname pour obtenir votre licence</small>
</div>
<div id="license-validation-result"></div>
</div>
<div class="license-modal-footer">
<button class="btn btn-secondary" onclick="closeLicenseModal()">Fermer</button>
</div>
</div>
`;
document.body.appendChild(modal);
getSplunkHostname().then(hostname => {
const hostnameDisplay = document.getElementById('current-hostname-display');
if (hostnameDisplay) {
hostnameDisplay.textContent = hostname;
}
});
setupLicenseUpload();
}
function setupLicenseUpload() {
const dropZone = document.getElementById('license-upload-zone');
const fileInput = document.getElementById('license-file-input');
if (!dropZone || !fileInput) return;
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleLicenseFile(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleLicenseFile(e.target.files[0]);
}
});
}
async function handleLicenseFile(file) {
const resultDiv = document.getElementById('license-validation-result');
if (!resultDiv) return;
if (!file.name.endsWith('.lic')) {
resultDiv.innerHTML = `
<div class="license-result error">
Le fichier doit avoir l'extension .lic
</div>
`;
return;
}
resultDiv.innerHTML = `
<div class="license-result loading">
Validation en cours...
</div>
`;
try {
const content = await file.text();
const result = await saveLicense(content);
if (result.success) {
const license = result.license;
resultDiv.innerHTML = `
<div class="license-result success">
<div class="license-success-icon"></div>
<div class="license-success-text">
<strong>Licence activée avec succès !</strong><br>
Type: ${license.type_name}<br>
Expire: ${license.expires} (${license.days_remaining} jours)<br>
Client: ${license.customer?.name || 'N/A'}
</div>
</div>
`;
updateLicenseBadge();
setTimeout(() => {
closeLicenseModal();
}, 3000);
} else {
resultDiv.innerHTML = `
<div class="license-result error">
${result.error}
</div>
`;
}
} catch (error) {
resultDiv.innerHTML = `
<div class="license-result error">
Erreur: ${error.message}
</div>
`;
}
}
function closeLicenseModal() {
const modal = document.getElementById('license-modal');
if (modal) modal.remove();
}
async function showLicenseDetails() {
const validation = await validateLicense();
if (!validation.valid) {
showLicenseModal(validation.error, validation.error_code);
return;
}
const existingModal = document.getElementById('license-details-modal');
if (existingModal) existingModal.remove();
const modal = document.createElement('div');
modal.id = 'license-details-modal';
modal.className = 'license-modal-overlay';
const features = validation.features.map(f => `<span class="feature-tag">${f}</span>`).join(' ');
const limits = validation.limits;
const maxApps = limits.max_apps === -1 ? '∞' : limits.max_apps;
const maxPushes = limits.max_pushes_per_day === -1 ? '∞' : limits.max_pushes_per_day;
const usage = getUsageStats();
modal.innerHTML = `
<div class="license-modal">
<div class="license-modal-header">
<h2>📋 Détails de la Licence</h2>
<button class="license-modal-close" onclick="this.closest('.license-modal-overlay').remove()">×</button>
</div>
<div class="license-modal-body">
<table class="license-details-table">
<tr><td><strong>ID</strong></td><td>${validation.license_id}</td></tr>
<tr><td><strong>Type</strong></td><td>${validation.type_name}</td></tr>
<tr><td><strong>Client</strong></td><td>${validation.customer?.name || 'N/A'}</td></tr>
<tr><td><strong>Email</strong></td><td>${validation.customer?.email || 'N/A'}</td></tr>
<tr><td><strong>Hostname</strong></td><td>${validation.hostname}</td></tr>
<tr><td><strong>Émise le</strong></td><td>${validation.issued}</td></tr>
<tr><td><strong>Expire le</strong></td><td>${validation.expires}</td></tr>
<tr><td><strong>Jours restants</strong></td><td>${validation.days_remaining}</td></tr>
<tr><td><strong>Apps max</strong></td><td>${maxApps}</td></tr>
<tr><td><strong>Pushes/jour</strong></td><td>${maxPushes}</td></tr>
</table>
<div style="margin-top: 15px;">
<strong>Fonctionnalités:</strong><br>
<div style="margin-top: 8px;">${features}</div>
</div>
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
<strong>Utilisation:</strong><br>
<small>Total pushes: ${usage.total_pushes} | Aujourd'hui: ${usage.pushes_today}</small>
</div>
</div>
<div class="license-modal-footer">
<button class="btn btn-secondary" onclick="this.closest('.license-modal-overlay').remove()">Fermer</button>
<button class="btn btn-danger" onclick="confirmRemoveLicense()">Supprimer la licence</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
function confirmRemoveLicense() {
if (confirm('Êtes-vous sûr de vouloir supprimer cette licence ?')) {
removeLicense();
const detailsModal = document.getElementById('license-details-modal');
if (detailsModal) detailsModal.remove();
updateLicenseBadge();
alert('Licence supprimée.');
}
}
async function checkLicenseBeforePush() {
const result = await checkLimits();
if (!result.allowed) {
showLicenseModal(result.error, result.error_code);
return false;
}
return true;
}
const licenseStyles = `
<style>
.license-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.license-badge:hover {
transform: scale(1.05);
}
.license-badge-valid {
background: linear-gradient(135deg, #4CAF50, #45a049);
color: white;
}
.license-badge-warning {
background: linear-gradient(135deg, #FF9800, #F57C00);
color: white;
}
.license-badge-expiring {
background: linear-gradient(135deg, #f44336, #d32f2f);
color: white;
animation: pulse 2s infinite;
}
.license-badge-invalid {
background: linear-gradient(135deg, #9e9e9e, #757575);
color: white;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.license-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.license-modal {
background: white;
border-radius: 16px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.license-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 25px;
border-bottom: 1px solid #e0e0e0;
}
.license-modal-header h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.license-modal-close {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: #999;
line-height: 1;
}
.license-modal-close:hover {
color: #333;
}
.license-modal-body {
padding: 25px;
}
.license-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 15px 25px;
border-top: 1px solid #e0e0e0;
}
.license-upload-zone {
border: 2px dashed #ccc;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.license-upload-zone:hover,
.license-upload-zone.dragover {
border-color: #667eea;
background: #f8f9ff;
}
.license-upload-icon {
font-size: 48px;
margin-bottom: 10px;
}
.license-upload-text {
color: #666;
}
.license-hostname-info {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.license-hostname-info code {
background: #e0e0e0;
padding: 2px 8px;
border-radius: 4px;
font-family: monospace;
}
.license-result {
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}
.license-result.loading {
background: #e3f2fd;
color: #1565c0;
}
.license-result.success {
background: #e8f5e9;
color: #2e7d32;
display: flex;
align-items: center;
gap: 15px;
}
.license-result.error {
background: #ffebee;
color: #c62828;
}
.license-success-icon {
font-size: 32px;
}
.license-error-message {
background: #ffebee;
color: #c62828;
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #c62828;
}
.license-details-table {
width: 100%;
border-collapse: collapse;
}
.license-details-table td {
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
.license-details-table td:first-child {
width: 40%;
color: #666;
}
.feature-tag {
display: inline-block;
background: #e3f2fd;
color: #1565c0;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
margin: 2px;
}
.btn-danger {
background: #f44336 !important;
color: white !important;
}
.btn-danger:hover {
background: #d32f2f !important;
}
</style>
`;
async function initializeLicense() {
console.log('Git Pusher License System v' + LICENSE_CONFIG.version + ' (RSA)');
if (!document.getElementById('license-styles')) {
const styleEl = document.createElement('div');
styleEl.id = 'license-styles';
styleEl.innerHTML = licenseStyles;
document.head.appendChild(styleEl);
}
const stored = localStorage.getItem(LICENSE_CONFIG.storageKey);
if (!stored) {
console.log('Aucune licence en cache, tentative de chargement depuis le serveur...');
const loadedFromServer = await loadLicenseFromServer();
if (loadedFromServer) {
console.log('✓ Licence restaurée depuis le serveur');
} else {
console.log('Aucune licence disponible');
}
} else {
console.log('Licence trouvée dans le cache local');
}
updateLicenseBadge();
}
window.initializeLicense = initializeLicense;
window.validateLicense = validateLicense;
window.saveLicense = saveLicense;
window.removeLicense = removeLicense;
window.loadLicenseFromServer = loadLicenseFromServer;
window.checkLicenseBeforePush = checkLicenseBeforePush;
window.showLicenseModal = showLicenseModal;
window.closeLicenseModal = closeLicenseModal;
window.showLicenseDetails = showLicenseDetails;
window.confirmRemoveLicense = confirmRemoveLicense;
window.updateLicenseBadge = updateLicenseBadge;
window.hasFeature = hasFeature;
window.incrementUsage = incrementUsage;
window.getUsageStats = getUsageStats;
window.getLicenseInfo = getLicenseInfo;
if (document.readyState === 'complete') {
setTimeout(initializeLicense, 100);
} else {
window.addEventListener('load', function() {
setTimeout(initializeLicense, 100);
});
}

@ -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 = Jocelyn PAMPHILE
description = Application Splunk pour déployer vos applications vers Git et le Search Head Cluster
version = 2.1.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,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>

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

@ -0,0 +1,22 @@
{
"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
}
}

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