@fkom13/mcp-sftp-orchestrator 6.0.0 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +21 -15
- package/.gencodedoc.yaml +76 -0
- package/README.fr.md +369 -0
- package/README.md +333 -61
- package/ROADMAP.md +21 -0
- package/apis.js +17 -0
- package/compareEngine.js +132 -0
- package/config.js +35 -4
- package/diagnose.js +89 -0
- package/diffEngine.js +141 -0
- package/diffFormatter.js +114 -0
- package/fileOps.js +275 -0
- package/guide.js +236 -0
- package/history.js +8 -1
- package/notes.js +87 -0
- package/package.json +5 -22
- package/policies.js +76 -0
- package/queue.js +107 -121
- package/server.js +1460 -108
- package/sftp.js +131 -41
- package/shellSessions.js +237 -0
- package/snapshotManager.js +324 -0
- package/sourceAdapter.js +357 -0
- package/ssh.js +168 -84
- package/sshPool.js +52 -56
- package/test_mcp.js +55 -0
- package/tunnels.js +224 -0
- package/utils.js +6 -0
- package/publish1github.md +0 -216
package/guide.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guide — Manuel intégré pour l'IA pilote d'orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Retourne des sections structurées (workflows, cheatsheet, pièges) pour que
|
|
5
|
+
* l'agent charge les bonnes pratiques en début de session, sans avoir à
|
|
6
|
+
* connaître les ~44 outils par cœur. Inspiré de gencodedoc_guide.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const SECTIONS = {
|
|
10
|
+
index: `# Guide Orchestrator — Index
|
|
11
|
+
|
|
12
|
+
Serveur MCP d'orchestration d'infrastructure (SSH/SFTP, fichiers, diff, sessions, snapshots).
|
|
13
|
+
|
|
14
|
+
## Catégories d'outils
|
|
15
|
+
- **Serveurs** : server_add/list/remove, server_note_* (contexte du parc), infra_overview
|
|
16
|
+
- **Exécution** : task_exec (one-shot), task_exec_sequence, shell_* (sessions persistantes)
|
|
17
|
+
- **Fichiers** : file_read, file_write, file_edit (local + remote unifiés)
|
|
18
|
+
- **Comparaison** : diff_files, diff_folders, compare_all_sources
|
|
19
|
+
- **Versioning** : snapshot_create/list/diff/restore/delete
|
|
20
|
+
- **Monitoring** : get_system_resources, get_services_status, get_pm2_logs, get_docker_logs, tail_file
|
|
21
|
+
- **Transferts** : task_transfer, task_transfer_multi
|
|
22
|
+
|
|
23
|
+
## Sections détaillées (guide section:<nom>)
|
|
24
|
+
- workflows : recettes copier-coller
|
|
25
|
+
- file-editing : éditer un fichier en sécurité
|
|
26
|
+
- sources : concept local vs remote
|
|
27
|
+
- cheatsheet : tableau outil → usage
|
|
28
|
+
- audit : audit complet du parc (recette copier-coller)
|
|
29
|
+
- security : politique de sécurité (blocklist + tunnels)
|
|
30
|
+
- pitfalls : pièges à éviter
|
|
31
|
+
|
|
32
|
+
Astuce : appelle 'infra_overview' en début de session pour situer le parc.`,
|
|
33
|
+
|
|
34
|
+
sources: `# Concept clé : les "sources" (local vs remote)
|
|
35
|
+
|
|
36
|
+
Tous les outils fichiers/diff/snapshot acceptent une source unifiée :
|
|
37
|
+
|
|
38
|
+
{ type: 'local', path: '/chemin' } → PC hôte du MCP, SANS SSH
|
|
39
|
+
{ type: 'remote', alias: 'vpsparfait', path: '/chemin' } → serveur distant via SFTP (pool SSH)
|
|
40
|
+
|
|
41
|
+
- 'local' = agit sur la machine où tourne le MCP (fs direct, rapide)
|
|
42
|
+
- 'remote' = agit sur un serveur configuré (server_list pour les alias)
|
|
43
|
+
- label optionnel : nom lisible pour un affichage clair quand les chemins varient
|
|
44
|
+
|
|
45
|
+
Puissance : on peut mixer (diff local↔remote, snapshot remote→restore local).`,
|
|
46
|
+
|
|
47
|
+
'file-editing': `# Éditer un fichier en sécurité
|
|
48
|
+
|
|
49
|
+
## Workflow recommandé
|
|
50
|
+
1. file_read { source } → récupère 'content' + 'hash'
|
|
51
|
+
2. file_edit { source, oldString, newString, expectedHash: <hash> }
|
|
52
|
+
- mode chirurgical : remplace un bout précis (économe en tokens)
|
|
53
|
+
- expectedHash : refuse l'édition si le fichier a changé entre-temps (HASH_MISMATCH)
|
|
54
|
+
|
|
55
|
+
## Options utiles
|
|
56
|
+
- dryRun:true → prévisualise le diff SANS écrire (valider avant d'appliquer)
|
|
57
|
+
- backup:true → snapshot auto avant modif → undo via snapshot_restore
|
|
58
|
+
- replaceAll:true → remplace toutes les occurrences (sinon erreur si multiples)
|
|
59
|
+
|
|
60
|
+
## Mode complet
|
|
61
|
+
file_edit { source, newContent: "<tout le fichier>" } → remplace tout.
|
|
62
|
+
|
|
63
|
+
## Erreurs
|
|
64
|
+
- OLDSTRING_NOT_FOUND : le texte exact n'existe pas (vérifie espaces/indentation)
|
|
65
|
+
- MULTIPLE_MATCHES : oldString présent N fois → ajoute du contexte ou replaceAll
|
|
66
|
+
- HASH_MISMATCH : fichier modifié depuis la lecture → relire, réanalyser`,
|
|
67
|
+
|
|
68
|
+
workflows: `# Recettes
|
|
69
|
+
|
|
70
|
+
## Éditer une config distante en sécurité
|
|
71
|
+
file_read {source:{type:'remote',alias:'vpsparfait',path:'/etc/nginx/nginx.conf'}}
|
|
72
|
+
→ file_edit {source, oldString:'...', newString:'...', expectedHash, backup:true}
|
|
73
|
+
|
|
74
|
+
## Détecter un drift de config sur le parc
|
|
75
|
+
compare_all_sources { sources:[
|
|
76
|
+
{type:'local', path:'/ref.conf', label:'ref'},
|
|
77
|
+
{type:'remote', alias:'vps1', path:'/etc/x.conf', label:'vps1'},
|
|
78
|
+
{type:'remote', alias:'vps2', path:'/etc/x.conf', label:'vps2'}
|
|
79
|
+
]}
|
|
80
|
+
→ drift:true + diff référence→divergences
|
|
81
|
+
|
|
82
|
+
## Workflow multi-étapes avec état (build)
|
|
83
|
+
shell_create {alias:'vps1', workdir:'/app'} → sessionId
|
|
84
|
+
shell_exec {sessionId, command:'npm install'} # cd/env persistent
|
|
85
|
+
shell_exec {sessionId, command:'npm run build'}
|
|
86
|
+
shell_close {sessionId}
|
|
87
|
+
|
|
88
|
+
## Sécuriser avant une modif risquée
|
|
89
|
+
snapshot_create {source:{type:'remote',alias:'vps1'}, paths:['/etc/nginx/'], tag:'before-fix'}
|
|
90
|
+
... modifs ...
|
|
91
|
+
snapshot_restore {snapshotId, target:{type:'remote',alias:'vps1'}, dryRun:true} # preview
|
|
92
|
+
snapshot_restore {snapshotId, target:{type:'remote',alias:'vps1'}, dryRun:false, force:true}`,
|
|
93
|
+
|
|
94
|
+
cheatsheet: `# Cheatsheet
|
|
95
|
+
|
|
96
|
+
| Besoin | Outil |
|
|
97
|
+
|--------|-------|
|
|
98
|
+
| Lire un fichier | file_read |
|
|
99
|
+
| Modifier un bout précis | file_edit (oldString/newString) |
|
|
100
|
+
| Créer/écraser | file_write |
|
|
101
|
+
| Comparer 2 fichiers | diff_files |
|
|
102
|
+
| Comparer 2 dossiers | diff_folders |
|
|
103
|
+
| Vérifier cohérence parc | compare_all_sources |
|
|
104
|
+
| Commande one-shot | task_exec |
|
|
105
|
+
| Suite de commandes avec état | shell_create + shell_exec |
|
|
106
|
+
| Sauvegarder avant modif | snapshot_create / backup:true |
|
|
107
|
+
| Annuler une modif | snapshot_restore |
|
|
108
|
+
| Contexte d'un serveur | server_note_get |
|
|
109
|
+
| Vue du parc | infra_overview |
|
|
110
|
+
| CPU/RAM/Disque | get_system_resources |
|
|
111
|
+
| Logs PM2/Docker | get_pm2_logs / get_docker_logs |`,
|
|
112
|
+
|
|
113
|
+
audit: `# Audit complet du parc — recette copier-coller
|
|
114
|
+
|
|
115
|
+
## 1. Vue d'ensemble
|
|
116
|
+
\`\`\`
|
|
117
|
+
infra_overview → liste des serveurs + rôles + services + warnings
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
## 2. Ressources de chaque serveur
|
|
121
|
+
\`\`\`
|
|
122
|
+
get_system_resources {alias:'vps_contabo'}
|
|
123
|
+
get_system_resources {alias:'vpsparfait_fkomp'}
|
|
124
|
+
get_system_resources {alias:'vpsparfait2_fkomp'}
|
|
125
|
+
get_system_resources {alias:'fkomprodmini1_prod'}
|
|
126
|
+
get_system_resources {alias:'fkomprodmini2_prod'}
|
|
127
|
+
get_system_resources {alias:'pc2'}
|
|
128
|
+
\`\`\`
|
|
129
|
+
|
|
130
|
+
## 3. Services (systemd, Docker, PM2) par serveur
|
|
131
|
+
\`\`\`
|
|
132
|
+
get_services_status {alias:'vps_contabo'}
|
|
133
|
+
# ... idem pour chaque alias
|
|
134
|
+
\`\`\`
|
|
135
|
+
|
|
136
|
+
## 4. Scan Nginx (domaines et proxy_pass)
|
|
137
|
+
\`\`\`
|
|
138
|
+
task_exec {alias:'<alias>', cmd: "for f in /etc/nginx/sites-enabled/*; do echo \"--- \$f ---\"; cat \"\$f\" 2>/dev/null | grep -E \"server_name|listen|proxy_pass|return \" | head -20; done"}
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
## 5. Ports ouverts
|
|
142
|
+
\`\`\`
|
|
143
|
+
task_exec {alias:'<alias>', cmd: "ss -tlnp 2>/dev/null | sort -n -k4"}
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
## 6. Contexte enregistré
|
|
147
|
+
\`\`\`
|
|
148
|
+
server_note_get {alias:'<alias>'} → avant d'intervenir sur un serveur
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
## 7. Notes de tout le parc
|
|
152
|
+
\`\`\`
|
|
153
|
+
server_note_list
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
## 8. Après une modification risquée, vérifier les drifts
|
|
157
|
+
\`\`\`
|
|
158
|
+
compare_all_sources {sources: [
|
|
159
|
+
{type:'remote', alias:'vps1', path:'/etc/nginx/nginx.conf', label:'vps1'},
|
|
160
|
+
{type:'remote', alias:'vps2', path:'/etc/nginx/nginx.conf', label:'vps2'}
|
|
161
|
+
]}
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
Astuce: lancer les appels des étapes 2-5 en parallèle pour un audit complet en ~30s.`,
|
|
165
|
+
|
|
166
|
+
security: `# Politique de sécurité
|
|
167
|
+
|
|
168
|
+
## Command Blocklist
|
|
169
|
+
Les commandes dangereuses sont bloquées par défaut avant exécution SSH.
|
|
170
|
+
|
|
171
|
+
## Gérer la blocklist
|
|
172
|
+
\`\`\`
|
|
173
|
+
policy_blocklist_list → lister les patterns
|
|
174
|
+
policy_blocklist_add {pattern:"rm -rf /"} → ajouter un pattern
|
|
175
|
+
policy_blocklist_remove {pattern:"rm -rf /"} → retirer un pattern
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
## Contournement conscient
|
|
179
|
+
\`\`\`
|
|
180
|
+
task_exec {alias:"vps", cmd:"...", skip_policy:true}
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
183
|
+
## Patterns supportés
|
|
184
|
+
- Littéral : "rm -rf /" bloque exactement cette commande
|
|
185
|
+
- Wildcard : "wget * | sh" bloque tout wget pipé vers sh
|
|
186
|
+
- Les patterns sont testés en regex insensible à la casse
|
|
187
|
+
|
|
188
|
+
La blocklist est stockée dans data/policies.json et persiste entre les redémarrages.
|
|
189
|
+
|
|
190
|
+
## Tunnels SSH
|
|
191
|
+
\`\`\`
|
|
192
|
+
tunnel_create {name:"crm", type:"local", listen_port:8080, target:"127.0.0.1:3100", via:"vps_contabo"}
|
|
193
|
+
→ http://localhost:8080 → vps_contabo:3100
|
|
194
|
+
|
|
195
|
+
tunnel_create {name:"proxy", type:"socks", listen_port:1080, via:"vpsparfait"}
|
|
196
|
+
→ Proxy SOCKS5 sur localhost:1080 via vpsparfait
|
|
197
|
+
|
|
198
|
+
tunnel_create {name:"reverse", type:"remote", listen_port:9090, target:"127.0.0.1:3000", via:"vps_contabo", source:"fkomprodmini2"}
|
|
199
|
+
→ fkomprodmini2:9090 → localhost:3000 (reverse)
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
## Allowlist de ports
|
|
203
|
+
Seuls les ports autorisés peuvent être utilisés pour les tunnels.
|
|
204
|
+
\`\`\`
|
|
205
|
+
tunnel_allowlist_add {port:3306} → ouvrir un port
|
|
206
|
+
tunnel_allowlist_remove {port:3306} → le fermer
|
|
207
|
+
tunnel_list → voir les ports autorisés
|
|
208
|
+
\`\`\`
|
|
209
|
+
|
|
210
|
+
## Sécurité des tunnels
|
|
211
|
+
- Ports < 1024 bloqués (root requis)
|
|
212
|
+
- Allowlist obligatoire : un port doit être explicitement autorisé
|
|
213
|
+
- Les tunnels via tmux survivent au redémarrage du MCP
|
|
214
|
+
- Les tunnels locaux (source=null) meurent avec le MCP et sont auto-restaurés au redémarrage
|
|
215
|
+
- Warning émis à la création`,
|
|
216
|
+
|
|
217
|
+
pitfalls: `# Pièges à éviter
|
|
218
|
+
|
|
219
|
+
1. **Toujours dryRun avant snapshot_restore réel** (écrase des fichiers).
|
|
220
|
+
2. **expectedHash sur file_edit** : sans lui, pas de protection contre écrasement concurrent.
|
|
221
|
+
3. **type:'local' agit sur l'hôte du MCP**, pas sur un serveur — vérifie bien le type.
|
|
222
|
+
4. **Gros fichiers** : file_read avec offset/limit pour consulter sans tout charger.
|
|
223
|
+
5. **Binaires** : détection auto (base64), mais file_edit ne s'applique qu'au texte.
|
|
224
|
+
6. **shell_exec** : une commande à la fois par session ; ferme les sessions (shell_close).
|
|
225
|
+
7. **task_exec ≠ shell_exec** : task_exec isole chaque commande (cd perdu), shell_exec persiste.
|
|
226
|
+
8. **Consulte server_note_get avant d'intervenir** sur un serveur inconnu.`
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export default {
|
|
230
|
+
get(section = 'index') {
|
|
231
|
+
return SECTIONS[section] || `Section '${section}' inconnue. Sections : ${Object.keys(SECTIONS).join(', ')}.`;
|
|
232
|
+
},
|
|
233
|
+
sections() {
|
|
234
|
+
return Object.keys(SECTIONS);
|
|
235
|
+
}
|
|
236
|
+
};
|
package/history.js
CHANGED
|
@@ -14,7 +14,14 @@ async function readHistory() {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
async function writeHistory(history) {
|
|
17
|
-
|
|
17
|
+
try {
|
|
18
|
+
await fs.writeFile(HISTORY_FILE_PATH, JSON.stringify(history, null, 2));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// Silently fail or log if possible, but don't crash
|
|
21
|
+
if (process.env.MCP_DEBUG === 'true') {
|
|
22
|
+
console.error(`[ERROR] Failed to write history: ${error.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
async function logTask(jobDetails) {
|
package/notes.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import config from './config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* notes — Mémoire contextuelle par serveur (protocole/particularités).
|
|
7
|
+
*
|
|
8
|
+
* Permet à l'IA pilote de ne pas se perdre dans un parc de nombreux serveurs :
|
|
9
|
+
* chaque serveur peut porter une description, la liste de ses services critiques,
|
|
10
|
+
* des avertissements (ex: "RAM 1Go, ne pas rebuild en prod"), des conventions
|
|
11
|
+
* (chemins standards), et la trace de la dernière intervention.
|
|
12
|
+
*
|
|
13
|
+
* Stockage : <dataDir>/server_notes.json (même pattern que servers.json).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const NOTES_FILE = path.join(config.dataDir, 'server_notes.json');
|
|
17
|
+
|
|
18
|
+
async function readAll() {
|
|
19
|
+
try {
|
|
20
|
+
const data = await fs.readFile(NOTES_FILE, 'utf-8');
|
|
21
|
+
return JSON.parse(data);
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function writeAll(notes) {
|
|
28
|
+
await fs.mkdir(path.dirname(NOTES_FILE), { recursive: true });
|
|
29
|
+
await fs.writeFile(NOTES_FILE, JSON.stringify(notes, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
/**
|
|
34
|
+
* Définit/met à jour la note d'un serveur (merge partiel : seuls les champs
|
|
35
|
+
* fournis sont modifiés). Champs libres :
|
|
36
|
+
* description, services[], warnings[], conventions[], note (texte libre)
|
|
37
|
+
* `last_intervention` est mis à jour automatiquement (timestamp + résumé).
|
|
38
|
+
*/
|
|
39
|
+
async set(alias, fields = {}) {
|
|
40
|
+
const notes = await readAll();
|
|
41
|
+
const existing = notes[alias] || {};
|
|
42
|
+
const merged = { ...existing };
|
|
43
|
+
|
|
44
|
+
// Champs structurés (remplacés si fournis)
|
|
45
|
+
for (const key of ['description', 'services', 'warnings', 'conventions', 'note']) {
|
|
46
|
+
if (fields[key] !== undefined) merged[key] = fields[key];
|
|
47
|
+
}
|
|
48
|
+
// Trace d'intervention
|
|
49
|
+
if (fields.intervention) {
|
|
50
|
+
merged.last_intervention = {
|
|
51
|
+
at: new Date().toISOString(),
|
|
52
|
+
summary: fields.intervention
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
merged.updatedAt = new Date().toISOString();
|
|
56
|
+
|
|
57
|
+
notes[alias] = merged;
|
|
58
|
+
await writeAll(notes);
|
|
59
|
+
return { alias, note: merged };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Récupère la note d'un serveur. Retourne null si absente.
|
|
64
|
+
*/
|
|
65
|
+
async get(alias) {
|
|
66
|
+
const notes = await readAll();
|
|
67
|
+
return notes[alias] || null;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Liste toutes les notes (vue d'ensemble du parc).
|
|
72
|
+
*/
|
|
73
|
+
async list() {
|
|
74
|
+
return await readAll();
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Supprime la note d'un serveur.
|
|
79
|
+
*/
|
|
80
|
+
async remove(alias) {
|
|
81
|
+
const notes = await readAll();
|
|
82
|
+
if (!notes[alias]) return { removed: false, alias };
|
|
83
|
+
delete notes[alias];
|
|
84
|
+
await writeAll(notes);
|
|
85
|
+
return { removed: true, alias };
|
|
86
|
+
}
|
|
87
|
+
};
|
package/package.json
CHANGED
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fkom13/mcp-sftp-orchestrator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "Un serveur MCP stdio pour l'orchestration de tâches distantes avec exécution hybride (synchrone/asynchrone) et configuration flexible.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"
|
|
8
|
-
"mcp-sftp-orchestrator": "server.js"
|
|
9
|
-
},
|
|
7
|
+
"mcpName": "io.github.fkom13/mcp-sftp-orchestrator",
|
|
10
8
|
"scripts": {
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/fkom13/mcp-sftp-orchestrator.git"
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"mcp",
|
|
19
|
-
"orchestrator",
|
|
20
|
-
"ssh",
|
|
21
|
-
"sftp",
|
|
22
|
-
"automation"
|
|
23
|
-
],
|
|
24
|
-
"author": "fkom13",
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"bugs": {
|
|
27
|
-
"url": "https://github.com/fkom13/mcp-sftp-orchestrator/issues"
|
|
9
|
+
"start": "node server.js",
|
|
10
|
+
"test": "node test_mcp.js && node test_features.js",
|
|
11
|
+
"test:integration": "node test_features.js"
|
|
28
12
|
},
|
|
29
|
-
"homepage": "https://github.com/fkom13/mcp-sftp-orchestrator#readme",
|
|
30
13
|
"dependencies": {
|
|
31
14
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
32
15
|
"dotenv": "^16.4.5",
|
package/policies.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import config from './config.js';
|
|
4
|
+
|
|
5
|
+
const POLICIES_PATH = path.join(config.dataDir, 'policies.json');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_POLICIES = {
|
|
8
|
+
command_blocklist: [
|
|
9
|
+
"rm -rf /",
|
|
10
|
+
"rm -rf /*",
|
|
11
|
+
"mkfs*",
|
|
12
|
+
":(){:|:&};:",
|
|
13
|
+
"dd if=/dev/zero",
|
|
14
|
+
"> /dev/sda",
|
|
15
|
+
"chmod -R 000 /",
|
|
16
|
+
"mv / /dev/null",
|
|
17
|
+
"wget -O- http://* | sh",
|
|
18
|
+
"curl http://* | sh",
|
|
19
|
+
"eval $(curl",
|
|
20
|
+
"eval $(wget",
|
|
21
|
+
"git clone http://* | sh"
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function loadPolicies() {
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(POLICIES_PATH);
|
|
28
|
+
const data = await fs.readFile(POLICIES_PATH, 'utf-8');
|
|
29
|
+
return JSON.parse(data);
|
|
30
|
+
} catch {
|
|
31
|
+
await fs.writeFile(POLICIES_PATH, JSON.stringify(DEFAULT_POLICIES, null, 2));
|
|
32
|
+
return { ...DEFAULT_POLICIES };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function savePolicies(policies) {
|
|
37
|
+
await fs.writeFile(POLICIES_PATH, JSON.stringify(policies, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function checkCommand(cmd, blocklist) {
|
|
41
|
+
const cmdTrimmed = cmd.trim();
|
|
42
|
+
for (const pattern of blocklist) {
|
|
43
|
+
const regex = pattern.includes('*')
|
|
44
|
+
? new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i')
|
|
45
|
+
: new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
46
|
+
if (regex.test(cmdTrimmed)) {
|
|
47
|
+
return { blocked: true, pattern };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { blocked: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default {
|
|
54
|
+
async list() {
|
|
55
|
+
return await loadPolicies();
|
|
56
|
+
},
|
|
57
|
+
async add(pattern) {
|
|
58
|
+
const policies = await loadPolicies();
|
|
59
|
+
if (!policies.command_blocklist.includes(pattern)) {
|
|
60
|
+
policies.command_blocklist.push(pattern);
|
|
61
|
+
await savePolicies(policies);
|
|
62
|
+
}
|
|
63
|
+
return policies;
|
|
64
|
+
},
|
|
65
|
+
async remove(pattern) {
|
|
66
|
+
const policies = await loadPolicies();
|
|
67
|
+
policies.command_blocklist = policies.command_blocklist.filter(p => p !== pattern);
|
|
68
|
+
await savePolicies(policies);
|
|
69
|
+
return policies;
|
|
70
|
+
},
|
|
71
|
+
async check(cmd) {
|
|
72
|
+
const policies = await loadPolicies();
|
|
73
|
+
return checkCommand(cmd, policies.command_blocklist);
|
|
74
|
+
},
|
|
75
|
+
checkCommand
|
|
76
|
+
};
|