@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/src/core/ai.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zentry — Moteur IA v3.0
|
|
3
|
+
*
|
|
4
|
+
* Améliorations :
|
|
5
|
+
* - Prompt système ULTRA-STRICT avec scoring CVSS-like (0-10)
|
|
6
|
+
* - Nouveau provider : Mistral (meilleur rapport qualité/prix sur le code)
|
|
7
|
+
* - Retry automatique avec backoff exponentiel sur les erreurs réseau
|
|
8
|
+
* - Chunking intelligent : si le code dépasse la fenêtre, découpe proprement
|
|
9
|
+
* - Cache de résultats (évite de re-payer pour un fichier déjà analysé)
|
|
10
|
+
* - Parsing structuré de la réponse IA (JSON forcé)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ─── PROMPT SYSTÈME ULTRA-STRICT ───────────────────────────────────────────
|
|
14
|
+
export const STRICT_SYSTEM_PROMPT = `Tu es un Auditeur de Sécurité et de Qualité de Code de niveau Industriel (OWASP Top 10, CWE, CVE).
|
|
15
|
+
|
|
16
|
+
══════════════════════════════════════════════════════════
|
|
17
|
+
RÈGLES ABSOLUES — VIOLATION = RÉSULTAT INUTILISABLE
|
|
18
|
+
══════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
RÈGLE 1 — ZÉRO FAUX POSITIF (la plus importante)
|
|
21
|
+
• Ne signale une faille que si tu as VU la ligne de code vulnérable.
|
|
22
|
+
• "Potentiellement" ou "il serait bon de" → ce n'est PAS une faille, c'est un conseil.
|
|
23
|
+
• Si le code fait la même chose de façon différente mais sécurisée → [SAFE]
|
|
24
|
+
• Si une lib gère la sécurité en interne (ex: Supabase RLS, Prisma paramètres auto) → valide [SAFE]
|
|
25
|
+
|
|
26
|
+
RÈGLE 2 — FORMAT DE SORTIE STRICT JSON
|
|
27
|
+
Retourne UNIQUEMENT du JSON valide, sans markdown, sans texte avant/après.
|
|
28
|
+
|
|
29
|
+
Si le code est sain :
|
|
30
|
+
{ "status": "SAFE", "findings": [] }
|
|
31
|
+
|
|
32
|
+
Si des failles existent :
|
|
33
|
+
{
|
|
34
|
+
"status": "FINDINGS",
|
|
35
|
+
"findings": [
|
|
36
|
+
{
|
|
37
|
+
"file": "chemin/vers/fichier.ts",
|
|
38
|
+
"line": 42,
|
|
39
|
+
"severity": "CRITICAL|HIGH|MEDIUM|LOW",
|
|
40
|
+
"cvss": 9.1,
|
|
41
|
+
"cwe": "CWE-89",
|
|
42
|
+
"category": "Injection|Auth|XSS|IDOR|Config|Deps|Perf|Architecture",
|
|
43
|
+
"title": "Injection SQL via interpolation de chaîne",
|
|
44
|
+
"description": "La requête SQL à la ligne 42 concatène directement req.body.id sans paramètre préparé.",
|
|
45
|
+
"vulnerable_code": "const q = \`SELECT * FROM users WHERE id = \${req.body.id}\`",
|
|
46
|
+
"fix": "Utilise des paramètres préparés : db.query('SELECT * FROM users WHERE id = $1', [req.body.id])",
|
|
47
|
+
"auto_fixable": true
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
RÈGLE 3 — SCORING CVSS OBLIGATOIRE
|
|
53
|
+
• CRITICAL : 9.0-10.0 — RCE, fuite de base de données complète, contournement d'auth total
|
|
54
|
+
• HIGH : 7.0-8.9 — IDOR, XSS stocké, injection SQL, secrets exposés
|
|
55
|
+
• MEDIUM : 4.0-6.9 — Rate limiting absent, informations de debug exposées, CSRF
|
|
56
|
+
• LOW : 0.1-3.9 — Problème de configuration mineur, code mort, mauvaise pratique sans impact direct
|
|
57
|
+
|
|
58
|
+
RÈGLE 4 — CHAMPS CWE ET AUTO-FIX
|
|
59
|
+
• Toujours référencer le CWE (Common Weakness Enumeration) pertinent
|
|
60
|
+
• "auto_fixable": true UNIQUEMENT si tu fournis le correctif complet et applicable directement
|
|
61
|
+
• "auto_fixable": false si le correctif nécessite une décision architecturale
|
|
62
|
+
|
|
63
|
+
RÈGLE 5 — CONTEXTE DU PROJET
|
|
64
|
+
Tu analyses du code de production réel. Chaque faille non signalée = risque réel pour les utilisateurs finaux.
|
|
65
|
+
Chaque faux positif = perte de confiance dans l'outil = outil abandonné.`;
|
|
66
|
+
|
|
67
|
+
// ─── CACHE EN MÉMOIRE ──────────────────────────────────────────────────────
|
|
68
|
+
const analysisCache = new Map();
|
|
69
|
+
|
|
70
|
+
function cacheKey(provider, model, content) {
|
|
71
|
+
// Hash simple basé sur la longueur + premiers 200 chars + derniers 200 chars
|
|
72
|
+
const sig = `${provider}:${model}:${content.length}:${content.slice(0, 200)}:${content.slice(-200)}`;
|
|
73
|
+
return sig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── RETRY AVEC BACKOFF ────────────────────────────────────────────────────
|
|
77
|
+
async function withRetry(fn, maxRetries = 3) {
|
|
78
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
return await fn();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (attempt === maxRetries) throw err;
|
|
83
|
+
// Backoff exponentiel : 1s, 2s, 4s
|
|
84
|
+
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── CHUNKING INTELLIGENT ──────────────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Si le code dépasse le seuil, le découpe par fonctions/classes/lignes
|
|
92
|
+
* pour ne jamais tronquer au milieu d'un contexte important.
|
|
93
|
+
*/
|
|
94
|
+
function chunkContent(content, maxChars = 12000) {
|
|
95
|
+
if (content.length <= maxChars) return [content];
|
|
96
|
+
|
|
97
|
+
const chunks = [];
|
|
98
|
+
const lines = content.split('\n');
|
|
99
|
+
let currentChunk = '';
|
|
100
|
+
let lineIndex = 0;
|
|
101
|
+
|
|
102
|
+
while (lineIndex < lines.length) {
|
|
103
|
+
const line = lines[lineIndex];
|
|
104
|
+
// Coupe proprement sur une ligne vide ou une déclaration de fonction
|
|
105
|
+
if (currentChunk.length + line.length > maxChars) {
|
|
106
|
+
if (currentChunk.trim()) chunks.push(currentChunk);
|
|
107
|
+
currentChunk = line + '\n';
|
|
108
|
+
} else {
|
|
109
|
+
currentChunk += line + '\n';
|
|
110
|
+
}
|
|
111
|
+
lineIndex++;
|
|
112
|
+
}
|
|
113
|
+
if (currentChunk.trim()) chunks.push(currentChunk);
|
|
114
|
+
return chunks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── PARSING SÉCURISÉ DE LA RÉPONSE ───────────────────────────────────────
|
|
118
|
+
export function parseAIResponse(raw) {
|
|
119
|
+
try {
|
|
120
|
+
// Cherche le JSON dans la réponse (au cas où il y a du texte parasite)
|
|
121
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
122
|
+
if (!jsonMatch) return { status: 'SAFE', findings: [] };
|
|
123
|
+
|
|
124
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
125
|
+
|
|
126
|
+
// Validation du format
|
|
127
|
+
if (!parsed.status || !Array.isArray(parsed.findings)) {
|
|
128
|
+
return { status: 'SAFE', findings: [] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Enrichit chaque finding avec un ID unique
|
|
132
|
+
parsed.findings = parsed.findings.map((f, i) => ({
|
|
133
|
+
id: `F${Date.now()}-${i}`,
|
|
134
|
+
...f,
|
|
135
|
+
cvss: Number(f.cvss) || 5.0,
|
|
136
|
+
auto_fixable: Boolean(f.auto_fixable),
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
return parsed;
|
|
140
|
+
} catch {
|
|
141
|
+
// Si l'IA a quand même répondu "SAFE" en texte libre
|
|
142
|
+
if (raw.includes('[SAFE]') || raw.trim() === 'SAFE') {
|
|
143
|
+
return { status: 'SAFE', findings: [] };
|
|
144
|
+
}
|
|
145
|
+
return { status: 'PARSE_ERROR', findings: [], raw };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── PROVIDERS ─────────────────────────────────────────────────────────────
|
|
150
|
+
export const AI = {
|
|
151
|
+
anthropic: {
|
|
152
|
+
label: "Claude (Anthropic)",
|
|
153
|
+
models: ["claude-opus-4-5", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"],
|
|
154
|
+
call: async (key, model, prompt) => {
|
|
155
|
+
const r = await fetch("https://api.anthropic.com/v1/messages", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
"x-api-key": key,
|
|
160
|
+
"anthropic-version": "2023-06-01"
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
model,
|
|
164
|
+
system: STRICT_SYSTEM_PROMPT,
|
|
165
|
+
max_tokens: 4096,
|
|
166
|
+
messages: [{ role: "user", content: prompt }]
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
if (!r.ok) throw new Error(`Anthropic API error ${r.status}`);
|
|
170
|
+
const d = await r.json();
|
|
171
|
+
return (d.content && d.content[0] && d.content[0].text) || '{"status":"SAFE","findings":[]}';
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
openai: {
|
|
176
|
+
label: "GPT-4 (OpenAI)",
|
|
177
|
+
models: ["gpt-4o", "gpt-4o-mini", "o4-mini"],
|
|
178
|
+
call: async (key, model, prompt) => {
|
|
179
|
+
const r = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
model,
|
|
184
|
+
response_format: { type: "json_object" },
|
|
185
|
+
messages: [
|
|
186
|
+
{ role: "system", content: STRICT_SYSTEM_PROMPT },
|
|
187
|
+
{ role: "user", content: prompt }
|
|
188
|
+
]
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
if (!r.ok) throw new Error(`OpenAI API error ${r.status}`);
|
|
192
|
+
const d = await r.json();
|
|
193
|
+
return (d.choices && d.choices[0]?.message?.content) || '{"status":"SAFE","findings":[]}';
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
groq: {
|
|
198
|
+
label: "Llama 3.3 (Groq — Ultra-rapide)",
|
|
199
|
+
models: ["llama-3.3-70b-versatile", "llama3-70b-8192", "mixtral-8x7b-32768"],
|
|
200
|
+
call: async (key, model, prompt) => {
|
|
201
|
+
const r = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
model,
|
|
206
|
+
response_format: { type: "json_object" },
|
|
207
|
+
messages: [
|
|
208
|
+
{ role: "system", content: STRICT_SYSTEM_PROMPT },
|
|
209
|
+
{ role: "user", content: prompt }
|
|
210
|
+
]
|
|
211
|
+
})
|
|
212
|
+
});
|
|
213
|
+
if (!r.ok) throw new Error(`Groq API error ${r.status}`);
|
|
214
|
+
const d = await r.json();
|
|
215
|
+
return (d.choices && d.choices[0]?.message?.content) || '{"status":"SAFE","findings":[]}';
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
gemini: {
|
|
220
|
+
label: "Gemini 2.0 (Google)",
|
|
221
|
+
models: ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"],
|
|
222
|
+
call: async (key, model, prompt) => {
|
|
223
|
+
const r = await fetch(
|
|
224
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`,
|
|
225
|
+
{
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/json" },
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
systemInstruction: { parts: [{ text: STRICT_SYSTEM_PROMPT }] },
|
|
230
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
231
|
+
generationConfig: {
|
|
232
|
+
maxOutputTokens: 4096,
|
|
233
|
+
responseMimeType: "application/json"
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
if (!r.ok) throw new Error(`Gemini API error ${r.status}`);
|
|
239
|
+
const d = await r.json();
|
|
240
|
+
return (d.candidates?.[0]?.content?.parts?.[0]?.text) || '{"status":"SAFE","findings":[]}';
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
mistral: {
|
|
245
|
+
label: "Mistral Large (Meilleur code)",
|
|
246
|
+
models: ["mistral-large-latest", "codestral-latest", "mistral-small-latest"],
|
|
247
|
+
call: async (key, model, prompt) => {
|
|
248
|
+
const r = await fetch("https://api.mistral.ai/v1/chat/completions", {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
model,
|
|
253
|
+
response_format: { type: "json_object" },
|
|
254
|
+
messages: [
|
|
255
|
+
{ role: "system", content: STRICT_SYSTEM_PROMPT },
|
|
256
|
+
{ role: "user", content: prompt }
|
|
257
|
+
]
|
|
258
|
+
})
|
|
259
|
+
});
|
|
260
|
+
if (!r.ok) throw new Error(`Mistral API error ${r.status}`);
|
|
261
|
+
const d = await r.json();
|
|
262
|
+
return (d.choices && d.choices[0]?.message?.content) || '{"status":"SAFE","findings":[]}';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ─── FONCTION D'APPEL PRINCIPALE ───────────────────────────────────────────
|
|
268
|
+
/**
|
|
269
|
+
* Appel IA avec cache, retry et chunking automatiques.
|
|
270
|
+
* Retourne toujours un objet structuré { status, findings }.
|
|
271
|
+
*/
|
|
272
|
+
export async function callAI(provider, key, model, prompt, useCache = true) {
|
|
273
|
+
const key_ = cacheKey(provider, model, prompt);
|
|
274
|
+
|
|
275
|
+
// Vérifie le cache
|
|
276
|
+
if (useCache && analysisCache.has(key_)) {
|
|
277
|
+
return { ...analysisCache.get(key_), fromCache: true };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const providerObj = AI[provider];
|
|
281
|
+
if (!providerObj) throw new Error(`Provider inconnu: ${provider}`);
|
|
282
|
+
|
|
283
|
+
// Chunking si le prompt est trop long
|
|
284
|
+
const promptChunks = chunkContent(prompt, 15000);
|
|
285
|
+
|
|
286
|
+
let allFindings = [];
|
|
287
|
+
|
|
288
|
+
for (const chunk of promptChunks) {
|
|
289
|
+
const raw = await withRetry(() => providerObj.call(key, model, chunk));
|
|
290
|
+
const parsed = parseAIResponse(raw);
|
|
291
|
+
if (parsed.findings) allFindings = allFindings.concat(parsed.findings);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = {
|
|
295
|
+
status: allFindings.length > 0 ? 'FINDINGS' : 'SAFE',
|
|
296
|
+
findings: allFindings,
|
|
297
|
+
provider,
|
|
298
|
+
model,
|
|
299
|
+
timestamp: new Date().toISOString()
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Met en cache
|
|
303
|
+
if (useCache) analysisCache.set(key_, result);
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Vide le cache (utile entre deux scans complets)
|
|
310
|
+
*/
|
|
311
|
+
export function clearAnalysisCache() {
|
|
312
|
+
analysisCache.clear();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Génère le prompt complet pour un bloc de fichiers
|
|
317
|
+
*/
|
|
318
|
+
export function buildAuditPrompt(files, stack = null) {
|
|
319
|
+
const stackContext = stack
|
|
320
|
+
? `\n\nCONTEXTE STACK : ${stack.label} (${stack.db}, auth: ${stack.auth})\nRÈGLES SPÉCIFIQUES:\n${stack.rules}`
|
|
321
|
+
: '';
|
|
322
|
+
|
|
323
|
+
const filesContent = files.map(({ path, content, truncated }) =>
|
|
324
|
+
`\n${'='.repeat(60)}\nFICHIER: ${path}${truncated ? ' [TRONQUÉ > 200kb]' : ''}\n${'='.repeat(60)}\n${content || '// Fichier vide ou illisible'}`
|
|
325
|
+
).join('\n');
|
|
326
|
+
|
|
327
|
+
return `Analyse de sécurité et qualité pour les fichiers suivants.${stackContext}\n\nRetourne UNIQUEMENT du JSON valide selon le format spécifié.\n${filesContent}`;
|
|
328
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lance le profiler dans le répertoire passé en paramètre (ou process.cwd() par défaut).
|
|
6
|
+
* Accepter un `cwd` explicite permet de tester la fonction sans `process.chdir()`.
|
|
7
|
+
*/
|
|
8
|
+
export async function runProfiler(cwd = process.cwd()) {
|
|
9
|
+
console.log('🔍 Zentry — Démarrage du Profiler IA de Sécurité...');
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
project: 'Unknown',
|
|
13
|
+
stackProfile: 'node',
|
|
14
|
+
compliance: [],
|
|
15
|
+
sensitiveData: [],
|
|
16
|
+
sla: '99.9%',
|
|
17
|
+
customRules: [
|
|
18
|
+
{ category: "SÉCURITÉ", rule: "Toutes les requêtes externes doivent inclure un timeout." }
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// 1. Lire package.json
|
|
23
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
24
|
+
if (fs.existsSync(pkgPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
27
|
+
config.project = pkg.name || path.basename(cwd);
|
|
28
|
+
|
|
29
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
30
|
+
|
|
31
|
+
// Détection Stack
|
|
32
|
+
if (deps['next']) config.stackProfile = 'njs-supa'; // ou njs-prisma
|
|
33
|
+
if (deps['express']) config.stackProfile = 'node-express';
|
|
34
|
+
if (deps['@supabase/supabase-js']) config.stackProfile = 'njs-supa';
|
|
35
|
+
if (deps['prisma']) config.stackProfile = 'njs-prisma';
|
|
36
|
+
|
|
37
|
+
// Détection Conformité
|
|
38
|
+
if (deps['stripe']) {
|
|
39
|
+
config.compliance.push('PCI-DSS');
|
|
40
|
+
config.sensitiveData.push('paiements', 'cartes bancaires');
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.warn('⚠️ Impossible de parser package.json');
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
config.project = path.basename(cwd);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Détection Prisma (Santé, RGPD)
|
|
50
|
+
const prismaPath = path.join(cwd, 'prisma', 'schema.prisma');
|
|
51
|
+
if (fs.existsSync(prismaPath)) {
|
|
52
|
+
const schema = fs.readFileSync(prismaPath, 'utf8').toLowerCase();
|
|
53
|
+
if (schema.includes('patient') || schema.includes('health') || schema.includes('medical')) {
|
|
54
|
+
config.compliance.push('HIPAA');
|
|
55
|
+
config.sensitiveData.push('données médicales');
|
|
56
|
+
}
|
|
57
|
+
if (schema.includes('user') || schema.includes('email') || schema.includes('password')) {
|
|
58
|
+
config.compliance.push('RGPD');
|
|
59
|
+
config.sensitiveData.push('données personnelles (PII)');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Dédoublonnage
|
|
64
|
+
config.compliance = [...new Set(config.compliance)];
|
|
65
|
+
config.sensitiveData = [...new Set(config.sensitiveData)];
|
|
66
|
+
|
|
67
|
+
if (config.compliance.length === 0) {
|
|
68
|
+
config.compliance.push('RGPD'); // Base
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Sauvegarde de la configuration globale
|
|
72
|
+
const configPath = path.join(cwd, 'zentry.config.json');
|
|
73
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
74
|
+
console.log(`✅ Fichier de configuration généré : ${configPath}`);
|
|
75
|
+
|
|
76
|
+
// 4. Génération des "System Prompts" IDE (Cursor, Windsurf, Cline)
|
|
77
|
+
const rulesContent = buildIdeRules(config);
|
|
78
|
+
|
|
79
|
+
const ideFiles = [
|
|
80
|
+
'.cursorrules',
|
|
81
|
+
'.windsurfrules',
|
|
82
|
+
'.clinerules',
|
|
83
|
+
'.github/copilot-instructions.md'
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const file of ideFiles) {
|
|
87
|
+
const filePath = path.join(cwd, file);
|
|
88
|
+
// Créer le dossier parent si nécessaire (.github)
|
|
89
|
+
const dir = path.dirname(filePath);
|
|
90
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(filePath, rulesContent);
|
|
93
|
+
console.log(`✅ Règles IDE de sécurité générées : ${file}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('🎉 Initialisation Zentry terminée. Les IA de votre IDE sont désormais verrouillées sur ces règles de sécurité.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildIdeRules(config) {
|
|
100
|
+
return `---
|
|
101
|
+
description: "Règles globales de sécurité et conformité générées par Zentry"
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# CONTEXTE PROJET ET CONFORMITÉ (ZENTRY)
|
|
105
|
+
Tu travailles sur le projet "${config.project}".
|
|
106
|
+
Ce projet est soumis à des contraintes de sécurité STRICTES.
|
|
107
|
+
|
|
108
|
+
## NORMES DE CONFORMITÉ ACTIVES
|
|
109
|
+
${config.compliance.map(c => `- ${c}`).join('\n')}
|
|
110
|
+
|
|
111
|
+
## DONNÉES SENSIBLES MANIPULÉES
|
|
112
|
+
${config.sensitiveData.length > 0 ? config.sensitiveData.map(d => `- ${d}`).join('\n') : '- Aucune donnée hautement sensible détectée par défaut.'}
|
|
113
|
+
SLA cible : ${config.sla}
|
|
114
|
+
|
|
115
|
+
# 🔴 RÈGLE ABSOLUE : NON-RÉGRESSION ET RESPECT DE L'EXISTANT
|
|
116
|
+
1. AVANT toute modification, tu DOIS analyser le graphe d'import et l'architecture locale.
|
|
117
|
+
2. Tu ne dois JAMAIS casser le code existant qui fonctionne. Tes modifications doivent s'intégrer harmonieusement sans causer d'effets de bord.
|
|
118
|
+
3. Si ta modification risque d'impacter un autre fichier, demande confirmation à l'utilisateur ou assure-toi de mettre à jour le fichier impacté.
|
|
119
|
+
|
|
120
|
+
# 🛡️ DIRECTIVES DE SÉCURITÉ ("Security By Design")
|
|
121
|
+
Lors de la génération de nouveau code ou de la modification de code existant, tu dois OBLIGATOIREMENT respecter les règles suivantes :
|
|
122
|
+
|
|
123
|
+
1. **Injection (SQL/NoSQL/Commandes) :**
|
|
124
|
+
- Utilise toujours des requêtes préparées ou l'ORM officiel (ex: Prisma).
|
|
125
|
+
- NE JAMAIS concaténer de chaînes de caractères contenant des inputs utilisateurs dans des requêtes.
|
|
126
|
+
|
|
127
|
+
2. **Authentification et Autorisation :**
|
|
128
|
+
- Chaque nouvelle route d'API ou action serveur DOIT vérifier l'authentification ET l'autorisation (rôles/permissions) avant d'exécuter la moindre logique métier.
|
|
129
|
+
- Ne jamais faire confiance au client (valider systématiquement les IDs côté serveur).
|
|
130
|
+
|
|
131
|
+
3. **Protection des Données (RGPD / HIPAA / PCI-DSS) :**
|
|
132
|
+
- Ne jamais logger (console.log) de mots de passe, tokens, clés d'API, ou données de cartes bancaires.
|
|
133
|
+
- Ne jamais renvoyer le mot de passe hashé au client.
|
|
134
|
+
|
|
135
|
+
4. **Résilience (SLA ${config.sla}) :**
|
|
136
|
+
- Toutes les requêtes réseau externes (fetch, axios) doivent avoir un try/catch ET un Timeout défini.
|
|
137
|
+
- Gérer toutes les erreurs gracieusement sans exposer la stack trace au client.
|
|
138
|
+
|
|
139
|
+
5. **Règles Personnalisées du Projet :**
|
|
140
|
+
${config.customRules.map(r => `- [${r.category}] ${r.rule}`).join('\n')}
|
|
141
|
+
|
|
142
|
+
Si tu détectes que l'utilisateur te demande d'écrire du code qui viole ces règles, tu dois REFUSER et proposer la version sécurisée.
|
|
143
|
+
`;
|
|
144
|
+
}
|