@yonathan124/zentry 1.0.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/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/package.json +62 -0
- package/src/cli/server.js +170 -0
- package/src/cli/zentry-cli.js +263 -0
- package/src/components/ZentryDashboard.jsx +908 -0
- package/src/config/constants.js +1232 -0
- package/src/core/ai.js +328 -0
- package/src/core/profiler.js +144 -0
- package/src/core/scanner.js +323 -0
- package/src/core/utils.js +3 -0
- package/src/index.js +7 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Guide de Contribution - Zentry
|
|
2
|
+
|
|
3
|
+
Merci de votre intérêt pour améliorer Zentry !
|
|
4
|
+
|
|
5
|
+
## Architecture du projet
|
|
6
|
+
|
|
7
|
+
Zentry est découpé en plusieurs modules internes :
|
|
8
|
+
- \`src/cli/zentry-cli.js\` : Point d'entrée du CLI (Pre-commit, init, ui).
|
|
9
|
+
- \`src/cli/server.js\` : Mini-serveur Express qui expose le Dashboard React et les routes locales d'auto-remédiation.
|
|
10
|
+
- \`src/core/profiler.js\` : Le moteur d'auto-détection (lit le package.json, les schémas, génère les `.cursorrules`).
|
|
11
|
+
- \`src/core/scanner.js\` : Moteur RAG et construction du Graphe d'Impact (parse les imports).
|
|
12
|
+
- \`src/core/ai.js\` : Wrapper autour des LLMs (Groq, OpenAI, Gemini).
|
|
13
|
+
- \`src/components/ZentryDashboard.jsx\` : Interface React standalone (chargée via Babel in-browser pour éviter un pipeline de build lourd).
|
|
14
|
+
|
|
15
|
+
## Comment ajouter un nouveau moteur LLM (Provider) ?
|
|
16
|
+
|
|
17
|
+
1. Ouvrez \`src/core/ai.js\`.
|
|
18
|
+
2. Ajoutez la logique d'appel fetch() dans la fonction \`callAI\`.
|
|
19
|
+
3. Assurez-vous que l'output JSON respecte le format interne de Zentry.
|
|
20
|
+
4. Mettez à jour \`src/components/ZentryDashboard.jsx\` pour ajouter le logo et le modèle dans la liste des fournisseurs.
|
|
21
|
+
|
|
22
|
+
## Comment ajouter une nouvelle règle de conformité automatique ?
|
|
23
|
+
|
|
24
|
+
Si vous souhaitez que \`zentry init\` détecte un nouveau standard (par exemple SOC2) :
|
|
25
|
+
1. Allez dans \`src/core/profiler.js\`.
|
|
26
|
+
2. Ajoutez un test de détection (ex: vérifier la présence de certains mots-clés dans les dépendances ou fichiers de configuration).
|
|
27
|
+
3. Poussez la chaîne `"SOC2"` dans le tableau \`config.compliance\`.
|
|
28
|
+
|
|
29
|
+
## Pull Requests
|
|
30
|
+
Nous privilégions les PRs courtes et testées localement.
|
|
31
|
+
Avant de soumettre :
|
|
32
|
+
\`\`\`bash
|
|
33
|
+
# 1. Vérifiez que le profiler marche
|
|
34
|
+
node src/cli/zentry-cli.js init
|
|
35
|
+
|
|
36
|
+
# 2. Vérifiez que l'UI se lance
|
|
37
|
+
node src/cli/zentry-cli.js ui
|
|
38
|
+
\`\`\`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orvia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Zentry Security Scanner 🛡️
|
|
2
|
+
|
|
3
|
+
Zentry est un moteur DevSecOps (Sécurité & Qualité) piloté par l'IA, conçu pour sécuriser votre code à la source de manière autonome. Il intègre une approche de "Security By Design", intercepte les commits non sécurisés, et propose une auto-remédiation locale via un Dashboard visuel.
|
|
4
|
+
|
|
5
|
+
## ✨ Fonctionnalités V6 (Universal Architecture)
|
|
6
|
+
|
|
7
|
+
- **Proactif (IDE Guardrails) :** Génère automatiquement les règles `.cursorrules`, `.clinerules`, `.windsurfrules` pour forcer l'IA de votre IDE à respecter vos contraintes de sécurité *avant* l'écriture du code.
|
|
8
|
+
- **Auto-Détection de Conformité :** Analyse votre `package.json` et vos schémas (Prisma) pour activer automatiquement les règles RGPD, PCI-DSS, ou HIPAA.
|
|
9
|
+
- **Serveur UI Local Indépendant :** Pas besoin d'un backend Next.js. Zentry lance son propre serveur Express local pour son Dashboard.
|
|
10
|
+
- **Graphe d'Impact (--diff) :** En pre-commit, il scanne les fichiers modifiés *ainsi que tous les fichiers qui en dépendent*, garantissant une non-régression absolue.
|
|
11
|
+
- **Preuves d'Audit :** Génère des rapports Markdown persistants dans `.zentry/reports/`.
|
|
12
|
+
|
|
13
|
+
## 📦 Installation
|
|
14
|
+
|
|
15
|
+
\`\`\`bash
|
|
16
|
+
npm install -g @orvia/zentry
|
|
17
|
+
# ou l'ajouter à votre projet local :
|
|
18
|
+
npm install --save-dev @orvia/zentry
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
## 🚀 Utilisation Rapide
|
|
22
|
+
|
|
23
|
+
### 1. Initialiser le profil de sécurité
|
|
24
|
+
Dans le dossier racine de votre projet, lancez :
|
|
25
|
+
\`\`\`bash
|
|
26
|
+
npx zentry init
|
|
27
|
+
\`\`\`
|
|
28
|
+
*Cela génère `zentry.config.json` et sécurise vos IDE (Cursor, Windsurf, Cline).*
|
|
29
|
+
|
|
30
|
+
### 2. Lancer le Dashboard Visuel (Serveur Local)
|
|
31
|
+
\`\`\`bash
|
|
32
|
+
npx zentry ui
|
|
33
|
+
\`\`\`
|
|
34
|
+
*Ouvre un serveur Express sur http://localhost:8080 avec l'interface d'audit et la fonctionnalité de correction automatique (God Mode).*
|
|
35
|
+
|
|
36
|
+
### 3. Intégration Git (Pre-commit)
|
|
37
|
+
Ajoutez cette ligne dans votre script de pre-commit (`.husky/pre-commit` ou `.git/hooks/pre-commit`) :
|
|
38
|
+
\`\`\`bash
|
|
39
|
+
npx zentry --diff
|
|
40
|
+
\`\`\`
|
|
41
|
+
*Bloquera les commits si une régression de sécurité est détectée.*
|
|
42
|
+
|
|
43
|
+
### 4. Audit Global et Rapport
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
npx zentry
|
|
46
|
+
\`\`\`
|
|
47
|
+
*Scanne l'intégralité du projet et génère un rapport Markdown dans `.zentry/reports/`.*
|
|
48
|
+
|
|
49
|
+
## ⚙️ Configuration (zentry.config.json)
|
|
50
|
+
|
|
51
|
+
Généré automatiquement par `init`, vous pouvez le modifier manuellement pour ajouter des règles métier :
|
|
52
|
+
\`\`\`json
|
|
53
|
+
{
|
|
54
|
+
"project": "MonProjet",
|
|
55
|
+
"compliance": ["RGPD", "PCI-DSS"],
|
|
56
|
+
"sla": "99.9%",
|
|
57
|
+
"customRules": [
|
|
58
|
+
{ "category": "SÉCURITÉ", "rule": "Toutes les requêtes externes doivent inclure un timeout." }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
## 🔑 Clés API
|
|
64
|
+
Zentry utilise les modèles LLM de pointe. Assurez-vous d'avoir au moins l'une de ces variables d'environnement (selon votre choix dans l'UI ou le CLI) :
|
|
65
|
+
- \`GROQ_API_KEY\` (Recommandé, super rapide avec LLaMA 3.3)
|
|
66
|
+
- \`GEMINI_API_KEY\`
|
|
67
|
+
- \`OPENAI_API_KEY\`
|
|
68
|
+
- \`ANTHROPIC_API_KEY\`
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yonathan124/zentry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zentry — AI-powered DevSecOps engine. Auto-detects vulnerabilities, generates IDE security guardrails, and auto-remediates code.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"zentry": "src/cli/zentry-cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CONTRIBUTING.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"test:coverage": "vitest run --coverage"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"security",
|
|
23
|
+
"audit",
|
|
24
|
+
"devsecops",
|
|
25
|
+
"cli",
|
|
26
|
+
"ai",
|
|
27
|
+
"llm",
|
|
28
|
+
"vulnerability",
|
|
29
|
+
"sast",
|
|
30
|
+
"pre-commit",
|
|
31
|
+
"compliance",
|
|
32
|
+
"rgpd",
|
|
33
|
+
"pci-dss"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/yonathan124/zentry.git"
|
|
38
|
+
},
|
|
39
|
+
"author": "Orvia",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"express": "^5.2.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"vitest": "^1.6.1"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=18.0.0",
|
|
49
|
+
"react-dom": ">=18.0.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"react": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"react-dom": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
export function startUIServer(port = 8080) {
|
|
11
|
+
// Racine du projet = là où l'utilisateur a lancé la commande, jamais ailleurs
|
|
12
|
+
const rootDir = path.resolve(process.cwd());
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
app.use(express.json({ limit: '5mb' })); // Réduit de 50mb → 5mb (50mb = DoS local possible)
|
|
16
|
+
|
|
17
|
+
// ─── SÉCURITÉ : Uniquement localhost, jamais le réseau externe ───────────
|
|
18
|
+
// On bind explicitement sur 127.0.0.1 dans le listen() plus bas.
|
|
19
|
+
|
|
20
|
+
// ─── API Auto-Remediation (God Mode) ─────────────────────────────────────
|
|
21
|
+
app.post('/api/zentry/fix', (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const { filePath, content } = req.body;
|
|
24
|
+
|
|
25
|
+
// Validation : les deux champs sont requis et non-vides
|
|
26
|
+
if (typeof filePath !== 'string' || !filePath.trim()) {
|
|
27
|
+
return res.status(400).json({ success: false, error: 'filePath manquant ou invalide.' });
|
|
28
|
+
}
|
|
29
|
+
if (typeof content !== 'string') {
|
|
30
|
+
return res.status(400).json({ success: false, error: 'content manquant ou invalide.' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Résolution sécurisée du chemin
|
|
34
|
+
const absolutePath = path.resolve(rootDir, filePath);
|
|
35
|
+
|
|
36
|
+
// ─── SÉCURITÉ : Protection Path Traversal ──────────────────────────
|
|
37
|
+
// path.resolve() normalise les `../../` avant la comparaison
|
|
38
|
+
// On vérifie avec un séparateur trailing pour éviter les faux positifs
|
|
39
|
+
// ex: rootDir='/home/project', absolutePath='/home/project-evil/...' serait capturé
|
|
40
|
+
const safeRoot = rootDir.endsWith(path.sep) ? rootDir : rootDir + path.sep;
|
|
41
|
+
if (!absolutePath.startsWith(safeRoot) && absolutePath !== rootDir) {
|
|
42
|
+
console.error(`[SECURITY] Tentative d'accès hors workspace: ${absolutePath}`);
|
|
43
|
+
return res.status(403).json({ success: false, error: 'Accès refusé. Le chemin est hors du workspace.' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// On refuse d'écraser les fichiers de config critiques du projet Zentry
|
|
47
|
+
const PROTECTED = ['.cursorrules', '.clinerules', '.windsurfrules', 'zentry.config.json'];
|
|
48
|
+
if (PROTECTED.some(p => absolutePath.endsWith(p))) {
|
|
49
|
+
return res.status(403).json({ success: false, error: `Fichier protégé: ${path.basename(absolutePath)}` });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Écriture atomique : on écrit dans un fichier temp puis on le renomme
|
|
53
|
+
const tmpPath = absolutePath + '.zentry.tmp';
|
|
54
|
+
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
55
|
+
fs.renameSync(tmpPath, absolutePath);
|
|
56
|
+
|
|
57
|
+
console.log(`✅ [God Mode] Fichier corrigé: ${path.relative(rootDir, absolutePath)}`);
|
|
58
|
+
return res.json({ success: true, message: 'Fichier corrigé avec succès.' });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Ne pas exposer le stack trace au client
|
|
61
|
+
console.error('[Server Error]', e.message);
|
|
62
|
+
return res.status(500).json({ success: false, error: 'Erreur interne du serveur.' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ─── Fichiers statiques du package Zentry ─────────────────────────────
|
|
67
|
+
const srcPath = path.join(__dirname, '..');
|
|
68
|
+
app.use('/src', express.static(srcPath, {
|
|
69
|
+
// Empêche de traverser hors du dossier src
|
|
70
|
+
dotfiles: 'deny',
|
|
71
|
+
index: false
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// ─── Interface HTML ───────────────────────────────────────────────────────
|
|
75
|
+
app.get('/', (_req, res) => {
|
|
76
|
+
res.send(`
|
|
77
|
+
<!DOCTYPE html>
|
|
78
|
+
<html lang="fr">
|
|
79
|
+
<head>
|
|
80
|
+
<meta charset="UTF-8">
|
|
81
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
82
|
+
<title>Zentry Pro Dashboard</title>
|
|
83
|
+
<!-- CSP strict : pas d'inline eval arbitraire depuis une source externe -->
|
|
84
|
+
<meta http-equiv="Content-Security-Policy"
|
|
85
|
+
content="default-src 'self' https://unpkg.com https://fonts.googleapis.com; script-src 'self' 'unsafe-eval' https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com;">
|
|
86
|
+
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin="anonymous"></script>
|
|
87
|
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin="anonymous"></script>
|
|
88
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin="anonymous"></script>
|
|
89
|
+
<style>
|
|
90
|
+
body { margin: 0; padding: 0; background: #0A0A12; color: #fff; font-family: sans-serif; }
|
|
91
|
+
#root { height: 100vh; overflow: hidden; }
|
|
92
|
+
#loader { display:flex; align-items:center; justify-content:center; height:100vh; flex-direction:column; gap:12px; }
|
|
93
|
+
#loader p { color: #8B5CF6; font-size: 14px; }
|
|
94
|
+
.spinner { width:32px; height:32px; border:3px solid #2A2A42; border-top-color:#8B5CF6; border-radius:50%; animation: spin .8s linear infinite; }
|
|
95
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
<body>
|
|
99
|
+
<div id="root">
|
|
100
|
+
<div id="loader">
|
|
101
|
+
<div class="spinner"></div>
|
|
102
|
+
<p>Chargement de Zentry Pro…</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<script type="text/babel">
|
|
106
|
+
// Note : 'unsafe-eval' est requis pour @babel/standalone (transpilation in-browser).
|
|
107
|
+
// Le code chargé vient uniquement de localhost (même origine), ce qui est sûr.
|
|
108
|
+
async function boot() {
|
|
109
|
+
const [constantsRes, dashboardRes] = await Promise.all([
|
|
110
|
+
fetch('/src/config/constants.js'),
|
|
111
|
+
fetch('/src/components/ZentryDashboard.jsx'),
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
if (!constantsRes.ok || !dashboardRes.ok) {
|
|
115
|
+
document.getElementById('root').innerHTML = '<div style="color:red;padding:40px">Erreur de chargement des fichiers Zentry. Vérifiez le serveur.</div>';
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const constantsCode = await constantsRes.text();
|
|
120
|
+
const dashboardCode = await dashboardRes.text();
|
|
121
|
+
|
|
122
|
+
// Transpile les deux fichiers
|
|
123
|
+
const transpileOptions = { presets: ['react'], plugins: ['transform-modules-commonjs'] };
|
|
124
|
+
const transpiledConstants = Babel.transform(constantsCode, transpileOptions).code;
|
|
125
|
+
const transpiledDashboard = Babel.transform(dashboardCode, transpileOptions).code;
|
|
126
|
+
|
|
127
|
+
// Évaluation sécurisée dans un contexte fermé
|
|
128
|
+
const moduleConstants = {};
|
|
129
|
+
const exportsConstants = {};
|
|
130
|
+
new Function('exports', 'module', transpiledConstants)(exportsConstants, { exports: moduleConstants });
|
|
131
|
+
const constants = Object.keys(exportsConstants).length > 0 ? exportsConstants : moduleConstants;
|
|
132
|
+
|
|
133
|
+
const require = (name) => {
|
|
134
|
+
if (name === 'react') return window.React;
|
|
135
|
+
if (name.includes('constants')) return constants;
|
|
136
|
+
return {};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const exportsApp = {};
|
|
140
|
+
new Function('require', 'exports', 'React', transpiledDashboard)(require, exportsApp, window.React);
|
|
141
|
+
|
|
142
|
+
const Zentry = exportsApp.default;
|
|
143
|
+
if (!Zentry) {
|
|
144
|
+
document.getElementById('root').innerHTML = '<div style="color:orange;padding:40px">Composant Zentry introuvable. Vérifiez ZentryDashboard.jsx.</div>';
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
149
|
+
root.render(React.createElement(Zentry));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
boot().catch(err => {
|
|
153
|
+
document.getElementById('root').innerHTML = '<div style="color:red;padding:40px">Erreur critique: ' + err.message + '</div>';
|
|
154
|
+
});
|
|
155
|
+
</script>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
158
|
+
`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ─── SÉCURITÉ : Bind UNIQUEMENT sur localhost ─────────────────────────────
|
|
162
|
+
// Personne sur le réseau externe ne peut atteindre ce serveur
|
|
163
|
+
app.listen(port, '127.0.0.1', () => {
|
|
164
|
+
const url = `http://localhost:${port}`;
|
|
165
|
+
console.log(`\n🛡️ Zentry UI Server — Écoute uniquement sur localhost (réseau local protégé)`);
|
|
166
|
+
console.log(`✅ Dashboard disponible : ${url}\n`);
|
|
167
|
+
const startCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
168
|
+
exec(`${startCmd} ${url}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zentry CLI v5.0 - Pre-commit & CI Scanner (Fully Wired)
|
|
5
|
+
* Usage: node zentry-cli.js [--all] [--diff] [--auto-fix] [--provider groq]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { buildBlocks, buildImportGraph } from '../core/scanner.js';
|
|
12
|
+
import { callAI, buildAuditPrompt } from '../core/ai.js';
|
|
13
|
+
import { runProfiler } from '../core/profiler.js';
|
|
14
|
+
import { startUIServer } from './server.js';
|
|
15
|
+
|
|
16
|
+
const IGNORE_LIST = ['.git', 'node_modules', '.next', 'dist', 'build', 'public'];
|
|
17
|
+
const EXT_REGEX = /\.(ts|tsx|js|jsx|py|go|rs|php|java|rb|sql|prisma)$/i;
|
|
18
|
+
|
|
19
|
+
function scanLocalDirectory(dir) {
|
|
20
|
+
let results = [];
|
|
21
|
+
try {
|
|
22
|
+
const list = fs.readdirSync(dir);
|
|
23
|
+
for (const file of list) {
|
|
24
|
+
if (IGNORE_LIST.includes(file)) continue;
|
|
25
|
+
const fullPath = path.join(dir, file);
|
|
26
|
+
const stat = fs.statSync(fullPath);
|
|
27
|
+
if (stat && stat.isDirectory()) {
|
|
28
|
+
results = results.concat(scanLocalDirectory(fullPath));
|
|
29
|
+
} else if (EXT_REGEX.test(file)) {
|
|
30
|
+
results.push(fullPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Ignore access errors
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getGitDiffFiles() {
|
|
40
|
+
try {
|
|
41
|
+
const output = execSync('git diff --name-only --cached', { encoding: 'utf-8' });
|
|
42
|
+
return output.split('\n')
|
|
43
|
+
.map(l => l.trim())
|
|
44
|
+
.filter(l => l && EXT_REGEX.test(l) && fs.existsSync(l));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function applyAutoFix(filePath, finding) {
|
|
51
|
+
if (!finding.auto_fixable || !finding.vulnerable_code || !finding.fix) return false;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
55
|
+
if (!content.includes(finding.vulnerable_code)) {
|
|
56
|
+
console.warn(`⚠️ Le code vulnérable exact n'a pas été trouvé dans ${filePath}. Auto-fix ignoré.`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const newContent = content.replace(finding.vulnerable_code, finding.fix);
|
|
60
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
61
|
+
console.log(`✅ [FIXED] ${finding.title} dans ${filePath}`);
|
|
62
|
+
return true;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`❌ Erreur lors de l'application du fix dans ${filePath}:`, err.message);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function run() {
|
|
70
|
+
const args = process.argv.slice(2);
|
|
71
|
+
|
|
72
|
+
// Commande UI (Serveur local indépendant)
|
|
73
|
+
if (args.includes('ui')) {
|
|
74
|
+
startUIServer();
|
|
75
|
+
return; // Ne pas process.exit() pour laisser le serveur tourner
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Commande d'initialisation (Profile & IDE Guardrails)
|
|
79
|
+
if (args.includes('init')) {
|
|
80
|
+
await runProfiler();
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isDiff = args.includes('--diff');
|
|
85
|
+
const isAutoFix = args.includes('--auto-fix');
|
|
86
|
+
const isAutoPR = args.includes('--auto-pr');
|
|
87
|
+
|
|
88
|
+
// Lecture du provider
|
|
89
|
+
const providerIndex = args.indexOf('--provider');
|
|
90
|
+
const provider = providerIndex !== -1 ? args[providerIndex + 1] : 'groq';
|
|
91
|
+
const model = provider === 'groq' ? 'llama-3.3-70b-versatile' : (provider === 'gemini' ? 'gemini-2.0-flash' : 'gpt-4o');
|
|
92
|
+
|
|
93
|
+
// Récupération de la clé API
|
|
94
|
+
const envVarName = `${provider.toUpperCase()}_API_KEY`;
|
|
95
|
+
const apiKey = process.env[envVarName];
|
|
96
|
+
|
|
97
|
+
console.log("🚀 Zentry CLI v5.0 — God Mode Activé");
|
|
98
|
+
console.log(`🧠 Provider: ${provider} (Modèle: ${model})`);
|
|
99
|
+
|
|
100
|
+
if (!apiKey) {
|
|
101
|
+
console.error(`❌ ERREUR: La variable d'environnement ${envVarName} n'est pas définie.`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let diffFiles = [];
|
|
106
|
+
let filePaths = [];
|
|
107
|
+
|
|
108
|
+
if (isDiff) {
|
|
109
|
+
console.log("🔍 Mode Incrémental: Scan via Graphe d'Impact...");
|
|
110
|
+
diffFiles = getGitDiffFiles();
|
|
111
|
+
if (diffFiles.length === 0) {
|
|
112
|
+
console.log("✅ Aucun fichier modifié à auditer. Terminé.");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
console.log("🔍 Mode Global: Scan complet du répertoire local...");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Pour construire un graphe d'impact fiable, on a besoin de tous les fichiers locaux
|
|
120
|
+
filePaths = scanLocalDirectory(process.cwd());
|
|
121
|
+
|
|
122
|
+
if (filePaths.length === 0) {
|
|
123
|
+
console.log("✅ Aucun fichier correspondant à auditer. Terminé.");
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(`📌 Lecture de ${filePaths.length} fichier(s). Préparation du contexte (RAG)...`);
|
|
128
|
+
|
|
129
|
+
// Lire le contenu des fichiers
|
|
130
|
+
const filesWithContent = [];
|
|
131
|
+
for (const fp of filePaths) {
|
|
132
|
+
try {
|
|
133
|
+
const content = fs.readFileSync(fp, 'utf8');
|
|
134
|
+
filesWithContent.push({ path: fp, content });
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Ignorer
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const importGraph = buildImportGraph(filesWithContent);
|
|
141
|
+
let blocks = buildBlocks(filePaths, importGraph);
|
|
142
|
+
|
|
143
|
+
if (isDiff) {
|
|
144
|
+
// Filtrage Graphe d'Impact : on ne garde que les blocs contenant au moins un fichier modifié
|
|
145
|
+
const initialBlockCount = blocks.length;
|
|
146
|
+
blocks = blocks.filter(b => b.files.some(f => diffFiles.includes(f) || diffFiles.some(df => f.includes(df))));
|
|
147
|
+
console.log(`📦 Impact Graph: ${blocks.length} bloc(s) impacté(s) (sur ${initialBlockCount} blocs au total).`);
|
|
148
|
+
} else {
|
|
149
|
+
console.log(`📦 Regroupement terminé: ${blocks.length} bloc(s) d'analyse prêt(s).`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let branchName = `zentry-security-fixes-${Date.now()}`;
|
|
153
|
+
if (isAutoPR) {
|
|
154
|
+
console.log(`🌿 Création de la branche: ${branchName}`);
|
|
155
|
+
try {
|
|
156
|
+
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error("❌ Impossible de créer la branche Git.");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let totalFindings = [];
|
|
164
|
+
let fixedCount = 0;
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
167
|
+
const block = blocks[i];
|
|
168
|
+
console.log(`\n⏳ Analyse du bloc ${i + 1}/${blocks.length}: ${block.name} (${block.files.length} fichiers)`);
|
|
169
|
+
|
|
170
|
+
const blockFiles = block.files.map(fPath => filesWithContent.find(fwc => fwc.path === fPath)).filter(Boolean);
|
|
171
|
+
const prompt = buildAuditPrompt(blockFiles);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const aiResult = await callAI(provider, apiKey, model, prompt, false);
|
|
175
|
+
if (aiResult.status === 'FINDINGS') {
|
|
176
|
+
totalFindings = totalFindings.concat(aiResult.findings);
|
|
177
|
+
console.log(` 🔴 Faille(s) détectée(s) : ${aiResult.findings.length}`);
|
|
178
|
+
|
|
179
|
+
for (const finding of aiResult.findings) {
|
|
180
|
+
console.log(` - [${finding.severity}] ${finding.title} (Fichier: ${finding.file})`);
|
|
181
|
+
|
|
182
|
+
if ((isAutoFix || isAutoPR) && finding.auto_fixable) {
|
|
183
|
+
const absolutePath = path.resolve(process.cwd(), finding.file);
|
|
184
|
+
if (fs.existsSync(absolutePath)) {
|
|
185
|
+
const success = await applyAutoFix(absolutePath, finding);
|
|
186
|
+
if (success) fixedCount++;
|
|
187
|
+
} else {
|
|
188
|
+
console.warn(` ⚠️ Fichier introuvable pour fix: ${absolutePath}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
console.log(" ✅ Aucune faille détectée (SAFE).");
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error(` ❌ Échec de l'analyse du bloc:`, err.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log("\n=======================================================");
|
|
201
|
+
console.log("🏁 RAPPORT FINAL ZENTRY");
|
|
202
|
+
console.log("=======================================================");
|
|
203
|
+
console.log(`Total des fichiers audités: ${filePaths.length}`);
|
|
204
|
+
console.log(`Failles totales détectées : ${totalFindings.length}`);
|
|
205
|
+
if (isAutoFix || isAutoPR) {
|
|
206
|
+
console.log(`Failles corrigées auto. : ${fixedCount}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Export du rapport en mode Global
|
|
210
|
+
if (!isDiff) {
|
|
211
|
+
try {
|
|
212
|
+
const reportDir = path.join(process.cwd(), '.zentry', 'reports');
|
|
213
|
+
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
214
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
215
|
+
const reportPath = path.join(reportDir, `audit-${dateStr}.md`);
|
|
216
|
+
|
|
217
|
+
let md = `# Rapport d'Audit de Sécurité Zentry\n> Date: ${new Date().toLocaleString()}\n> Mode: Global (--all)\n\n`;
|
|
218
|
+
md += `**Résumé :** ${filePaths.length} fichiers audités, ${totalFindings.length} failles détectées.\n\n`;
|
|
219
|
+
|
|
220
|
+
if (totalFindings.length === 0) {
|
|
221
|
+
md += `✅ **Aucune vulnérabilité détectée. Le code est sécurisé.**\n`;
|
|
222
|
+
} else {
|
|
223
|
+
md += `## Failles Détectées\n`;
|
|
224
|
+
totalFindings.forEach((f, idx) => {
|
|
225
|
+
md += `### ${idx + 1}. [${f.severity}] ${f.title}\n`;
|
|
226
|
+
md += `- **Fichier :** \`${f.file}\`\n`;
|
|
227
|
+
md += `- **Description :** ${f.description}\n`;
|
|
228
|
+
if (f.vulnerable_code) md += `- **Code vulnérable :**\n\`\`\`\n${f.vulnerable_code}\n\`\`\`\n`;
|
|
229
|
+
if (f.fix) md += `- **Solution proposée :**\n\`\`\`\n${f.fix}\n\`\`\`\n\n`;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fs.writeFileSync(reportPath, md, 'utf8');
|
|
234
|
+
console.log(`\n📄 Rapport de preuve généré : ${reportPath}`);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error(`⚠️ Impossible d'écrire le rapport Markdown :`, e.message);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (isAutoPR && fixedCount > 0) {
|
|
241
|
+
console.log(`\n🚀 Commit et Push automatique...`);
|
|
242
|
+
try {
|
|
243
|
+
execSync(`git add .`, { stdio: 'inherit' });
|
|
244
|
+
execSync(`git commit -m "[SECURITY] Zentry Auto-Remediation: ${fixedCount} fix(es)"`, { stdio: 'inherit' });
|
|
245
|
+
execSync(`git push origin ${branchName}`, { stdio: 'inherit' });
|
|
246
|
+
console.log(`✅ Pull Request prête sur la branche ${branchName} !`);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.log("⚠️ Git push a échoué (vérifiez vos droits ou l'état du repo).");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (totalFindings.length > 0 && !(isAutoPR && fixedCount === totalFindings.length)) {
|
|
253
|
+
// Il reste des failles non corrigées (ou le push auto n'était pas demandé)
|
|
254
|
+
process.exit(1);
|
|
255
|
+
} else {
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
run().catch(err => {
|
|
261
|
+
console.error("Critical Error:", err);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|