@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
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zentry — Moteur de Découverte & Scanner RAG v2.0
|
|
3
|
+
*
|
|
4
|
+
* Fonctionnalités :
|
|
5
|
+
* - Découverte récursive (FileSystem API navigateur & Node.js)
|
|
6
|
+
* - Parseur de graphe d'imports : regroupe les fichiers dépendants ensemble
|
|
7
|
+
* - Filtrage intelligent par criticité (routes, auth, DB, config en priorité)
|
|
8
|
+
* - Tri prioritaire : les fichiers les plus critiques sont audités en premier
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── CONSTANTES ────────────────────────────────────────────────────────────
|
|
12
|
+
const IGNORE_LIST = [
|
|
13
|
+
'.git', 'node_modules', '.next', 'dist', 'build', 'public',
|
|
14
|
+
'.vscode', 'coverage', 'storybook-static', '.turbo', '__pycache__',
|
|
15
|
+
'.pytest_cache', 'target', 'vendor', '.cargo'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const EXT_REGEX = /\.(ts|tsx|js|jsx|py|go|rs|php|java|rb|sql|prisma|yml|yaml|env\.example)$/i;
|
|
19
|
+
|
|
20
|
+
// Fichiers à exclure malgré l'extension correcte
|
|
21
|
+
const SKIP_PATTERNS = /\.(test|spec|stories|d)\.(ts|tsx|js|jsx)$/i;
|
|
22
|
+
|
|
23
|
+
// Groupes de criticité : les fichiers correspondants sont traités en priorité
|
|
24
|
+
const CRITICALITY = [
|
|
25
|
+
{ pattern: /(middleware|auth|session|jwt|token|permission|role|policy|guard)\./i, score: 100 },
|
|
26
|
+
{ pattern: /(route|api|handler|controller|endpoint)\./i, score: 90 },
|
|
27
|
+
{ pattern: /(db|database|prisma|supabase|mongoose|sequelize|query|sql)\./i, score: 85 },
|
|
28
|
+
{ pattern: /(config|env|secret|key|credential|vault)\./i, score: 80 },
|
|
29
|
+
{ pattern: /(payment|stripe|webhook|billing|invoice)\./i, score: 75 },
|
|
30
|
+
{ pattern: /(schema|validator|zod|yup|validation)\./i, score: 70 },
|
|
31
|
+
{ pattern: /(upload|file|storage|s3|bucket)\./i, score: 65 },
|
|
32
|
+
{ pattern: /(email|sms|notification|mailer)\./i, score: 60 },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const MAX_FILES_PER_BLOCK = 4;
|
|
36
|
+
|
|
37
|
+
// ─── SCORING ───────────────────────────────────────────────────────────────
|
|
38
|
+
function getCriticalityScore(filePath) {
|
|
39
|
+
const filename = filePath.split('/').pop() || '';
|
|
40
|
+
for (const { pattern, score } of CRITICALITY) {
|
|
41
|
+
if (pattern.test(filename) || pattern.test(filePath)) return score;
|
|
42
|
+
}
|
|
43
|
+
return 30;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── GRAPHE D'IMPORTS (RAG) ────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Extrait les chemins importés depuis le contenu d'un fichier.
|
|
49
|
+
* Supporte : import/export ES6, require() CommonJS, dynamic import()
|
|
50
|
+
*/
|
|
51
|
+
export function extractImports(content, filePath) {
|
|
52
|
+
const imports = new Set();
|
|
53
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
54
|
+
|
|
55
|
+
const patterns = [
|
|
56
|
+
/import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g, // import ... from '...'
|
|
57
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g, // require('...')
|
|
58
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g, // import('...')
|
|
59
|
+
/export\s+.*?\s+from\s+['"]([^'"]+)['"]/g, // export ... from '...'
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const pattern of patterns) {
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
65
|
+
const imp = match[1];
|
|
66
|
+
// On ne garde que les imports relatifs (pas npm packages)
|
|
67
|
+
if (imp.startsWith('.')) {
|
|
68
|
+
// Normalise le chemin relatif
|
|
69
|
+
const normalized = resolveRelativePath(dir, imp);
|
|
70
|
+
if (normalized) imports.add(normalized);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...imports];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveRelativePath(fromDir, relativePath) {
|
|
78
|
+
const parts = (fromDir + '/' + relativePath).split('/');
|
|
79
|
+
const resolved = [];
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (part === '..') resolved.pop();
|
|
82
|
+
else if (part !== '.') resolved.push(part);
|
|
83
|
+
}
|
|
84
|
+
return resolved.join('/');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Construit un graphe d'imports : { filePath: [importedFiles] }
|
|
89
|
+
*/
|
|
90
|
+
export function buildImportGraph(filesWithContent) {
|
|
91
|
+
const graph = {};
|
|
92
|
+
for (const { path, content } of filesWithContent) {
|
|
93
|
+
graph[path] = extractImports(content, path);
|
|
94
|
+
}
|
|
95
|
+
return graph;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Algorithme de regroupement par dépendances.
|
|
100
|
+
* Les fichiers fortement connectés (qui s'importent mutuellement) sont mis dans le même bloc.
|
|
101
|
+
*
|
|
102
|
+
* Algorithme : Union-Find sur le graphe d'imports (non orienté)
|
|
103
|
+
*/
|
|
104
|
+
export function groupByDependencies(files, importGraph) {
|
|
105
|
+
// Union-Find
|
|
106
|
+
const parent = {};
|
|
107
|
+
const find = (x) => {
|
|
108
|
+
if (parent[x] !== x) parent[x] = find(parent[x]);
|
|
109
|
+
return parent[x];
|
|
110
|
+
};
|
|
111
|
+
const union = (a, b) => {
|
|
112
|
+
const pa = find(a), pb = find(b);
|
|
113
|
+
if (pa !== pb) parent[pa] = pb;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Initialise chaque fichier comme son propre parent
|
|
117
|
+
files.forEach(f => { parent[f] = f; });
|
|
118
|
+
|
|
119
|
+
// Unit les fichiers liés par import
|
|
120
|
+
for (const [file, imports] of Object.entries(importGraph)) {
|
|
121
|
+
for (const imp of imports) {
|
|
122
|
+
// Essaie plusieurs extensions pour trouver une correspondance
|
|
123
|
+
const candidates = [imp, imp + '.ts', imp + '.tsx', imp + '.js', imp + '/index.ts', imp + '/index.tsx'];
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
if (files.includes(candidate)) {
|
|
126
|
+
union(file, candidate);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Regroupe par composante connexe
|
|
134
|
+
const groups = {};
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const root = find(file);
|
|
137
|
+
if (!groups[root]) groups[root] = [];
|
|
138
|
+
groups[root].push(file);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Object.values(groups);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── CONSTRUCTION DES BLOCS ────────────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Construit les blocs d'audit depuis le graphe de dépendances.
|
|
147
|
+
*
|
|
148
|
+
* Stratégie :
|
|
149
|
+
* 1. Regrouper les fichiers par composante de dépendance (RAG)
|
|
150
|
+
* 2. Trier les groupes par score de criticité (décroissant)
|
|
151
|
+
* 3. Découper en blocs de MAX_FILES_PER_BLOCK fichiers max
|
|
152
|
+
*
|
|
153
|
+
* Résultat : les fichiers liés sont audités ensemble, les plus critiques d'abord
|
|
154
|
+
*/
|
|
155
|
+
export function buildBlocks(files, importGraph = {}) {
|
|
156
|
+
if (Object.keys(importGraph).length > 0) {
|
|
157
|
+
// Mode RAG : regroupement par dépendances
|
|
158
|
+
const dependencyGroups = groupByDependencies(files, importGraph);
|
|
159
|
+
|
|
160
|
+
const blocks = [];
|
|
161
|
+
let bId = 1;
|
|
162
|
+
|
|
163
|
+
// Trie les groupes : les plus critiques (score moyen élevé) en premier
|
|
164
|
+
const scoredGroups = dependencyGroups.map(group => ({
|
|
165
|
+
group,
|
|
166
|
+
score: group.reduce((s, f) => s + getCriticalityScore(f), 0) / group.length
|
|
167
|
+
}));
|
|
168
|
+
scoredGroups.sort((a, b) => b.score - a.score);
|
|
169
|
+
|
|
170
|
+
for (const { group } of scoredGroups) {
|
|
171
|
+
// Trie les fichiers dans chaque groupe par criticité aussi
|
|
172
|
+
group.sort((a, b) => getCriticalityScore(b) - getCriticalityScore(a));
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < group.length; i += MAX_FILES_PER_BLOCK) {
|
|
175
|
+
const slice = group.slice(i, i + MAX_FILES_PER_BLOCK);
|
|
176
|
+
const critScore = Math.round(slice.reduce((s, f) => s + getCriticalityScore(f), 0) / slice.length);
|
|
177
|
+
blocks.push({
|
|
178
|
+
id: `B${bId.toString().padStart(3, '0')}`,
|
|
179
|
+
name: `Groupe ${bId} (Criticité: ${critScore}/100)`,
|
|
180
|
+
files: slice,
|
|
181
|
+
criticalityScore: critScore,
|
|
182
|
+
// Indique si ce bloc contient des fichiers fortement liés
|
|
183
|
+
isRelated: slice.length > 1 && group.length > 1
|
|
184
|
+
});
|
|
185
|
+
bId++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return blocks;
|
|
190
|
+
|
|
191
|
+
} else {
|
|
192
|
+
// Mode dossier (fallback sans graphe d'imports) : regroupement par répertoire
|
|
193
|
+
const dirs = {};
|
|
194
|
+
files.forEach(f => {
|
|
195
|
+
const dir = f.substring(0, f.lastIndexOf('/')) || '/root';
|
|
196
|
+
if (!dirs[dir]) dirs[dir] = [];
|
|
197
|
+
dirs[dir].push(f);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const blocks = [];
|
|
201
|
+
let bId = 1;
|
|
202
|
+
|
|
203
|
+
// Trie les dossiers par criticité moyenne
|
|
204
|
+
const sortedDirs = Object.entries(dirs).sort((a, b) => {
|
|
205
|
+
const scoreA = a[1].reduce((s, f) => s + getCriticalityScore(f), 0) / a[1].length;
|
|
206
|
+
const scoreB = b[1].reduce((s, f) => s + getCriticalityScore(f), 0) / b[1].length;
|
|
207
|
+
return scoreB - scoreA;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
for (const [dir, dirFiles] of sortedDirs) {
|
|
211
|
+
dirFiles.sort((a, b) => getCriticalityScore(b) - getCriticalityScore(a));
|
|
212
|
+
for (let i = 0; i < dirFiles.length; i += MAX_FILES_PER_BLOCK) {
|
|
213
|
+
const slice = dirFiles.slice(i, i + MAX_FILES_PER_BLOCK);
|
|
214
|
+
blocks.push({
|
|
215
|
+
id: `B${bId.toString().padStart(3, '0')}`,
|
|
216
|
+
name: `${dir.split('/').pop() || 'root'} (${Math.floor(i / MAX_FILES_PER_BLOCK) + 1})`,
|
|
217
|
+
files: slice,
|
|
218
|
+
criticalityScore: Math.round(slice.reduce((s, f) => s + getCriticalityScore(f), 0) / slice.length),
|
|
219
|
+
isRelated: false
|
|
220
|
+
});
|
|
221
|
+
bId++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return blocks;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── SCANNER NAVIGATEUR ────────────────────────────────────────────────────
|
|
229
|
+
/**
|
|
230
|
+
* Scan via l'API File System Access du navigateur (handle directory).
|
|
231
|
+
* Retourne les fichiers avec leur contenu pour le graphe d'imports.
|
|
232
|
+
*/
|
|
233
|
+
export async function scanBrowserDirectory(dirHandle) {
|
|
234
|
+
const filesWithContent = [];
|
|
235
|
+
|
|
236
|
+
async function walk(handle, currentPath = '') {
|
|
237
|
+
for await (const entry of handle.values()) {
|
|
238
|
+
if (IGNORE_LIST.includes(entry.name)) continue;
|
|
239
|
+
|
|
240
|
+
const fullPath = currentPath + entry.name;
|
|
241
|
+
if (entry.kind === 'directory') {
|
|
242
|
+
await walk(entry, fullPath + '/');
|
|
243
|
+
} else if (entry.kind === 'file' && EXT_REGEX.test(entry.name) && !SKIP_PATTERNS.test(entry.name)) {
|
|
244
|
+
try {
|
|
245
|
+
const file = await entry.getFile();
|
|
246
|
+
// Limite à 200kb par fichier pour éviter de saturer le contexte IA
|
|
247
|
+
if (file.size < 204800) {
|
|
248
|
+
const content = await file.text();
|
|
249
|
+
filesWithContent.push({ path: fullPath, content, size: file.size });
|
|
250
|
+
} else {
|
|
251
|
+
// Fichier trop grand : on le garde mais sans contenu pour le graphe
|
|
252
|
+
filesWithContent.push({ path: fullPath, content: '', size: file.size, truncated: true });
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
filesWithContent.push({ path: fullPath, content: '', size: 0, error: true });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await walk(dirHandle);
|
|
262
|
+
return filesWithContent;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Lit le contenu d'un fichier via FileList (input[type=file]) ou File System Access API
|
|
267
|
+
*/
|
|
268
|
+
export async function readBrowserFile(source, filePath) {
|
|
269
|
+
try {
|
|
270
|
+
// Source = FileList (input[type=file])
|
|
271
|
+
if (source && source[0] && source[0].webkitRelativePath !== undefined) {
|
|
272
|
+
const f = [...source].find(f => f.webkitRelativePath === filePath || f.name === filePath.split('/').pop());
|
|
273
|
+
if (f) return await f.text();
|
|
274
|
+
}
|
|
275
|
+
// Source = Directory Handle
|
|
276
|
+
if (source && source.kind === 'directory') {
|
|
277
|
+
const parts = filePath.split('/');
|
|
278
|
+
let h = source;
|
|
279
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
280
|
+
h = await h.getDirectoryHandle(parts[i]);
|
|
281
|
+
}
|
|
282
|
+
const fh = await h.getFileHandle(parts[parts.length - 1]);
|
|
283
|
+
const file = await fh.getFile();
|
|
284
|
+
return await file.text();
|
|
285
|
+
}
|
|
286
|
+
return `// ⚠️ ERREUR DE LECTURE: ${filePath}`;
|
|
287
|
+
} catch (e) {
|
|
288
|
+
return `// ⚠️ ERREUR DE LECTURE: ${filePath} — ${e.message}`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Construit le graphe d'imports et les blocs RAG depuis FileList (input[type=file])
|
|
294
|
+
*/
|
|
295
|
+
export async function buildRAGBlocksFromFileList(fileList) {
|
|
296
|
+
const filesWithContent = [];
|
|
297
|
+
|
|
298
|
+
for (const file of fileList) {
|
|
299
|
+
const path = file.webkitRelativePath || file.name;
|
|
300
|
+
if (!EXT_REGEX.test(file.name) || SKIP_PATTERNS.test(file.name)) continue;
|
|
301
|
+
|
|
302
|
+
const IGNORE = /node_modules|\.next|dist|\.git/;
|
|
303
|
+
if (IGNORE.test(path)) continue;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Limite à 200kb
|
|
307
|
+
if (file.size < 204800) {
|
|
308
|
+
const content = await file.text();
|
|
309
|
+
filesWithContent.push({ path, content, size: file.size });
|
|
310
|
+
} else {
|
|
311
|
+
filesWithContent.push({ path, content: '', size: file.size, truncated: true });
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
filesWithContent.push({ path, content: '', size: 0, error: true });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const files = filesWithContent.map(f => f.path);
|
|
319
|
+
const importGraph = buildImportGraph(filesWithContent);
|
|
320
|
+
const blocks = buildBlocks(files, importGraph);
|
|
321
|
+
|
|
322
|
+
return { files, filesWithContent, importGraph, blocks };
|
|
323
|
+
}
|
package/src/index.js
ADDED