cipher-shield 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/LICENSE +21 -0
- package/README.md +666 -0
- package/index.js +75 -0
- package/package.json +57 -0
- package/src/core/aesEngine.js +369 -0
- package/src/core/blacklistMem.js +510 -0
- package/src/core/defconSystem.js +301 -0
- package/src/magicAuth.js +272 -0
- package/src/modules/aiScanner.js +482 -0
- package/src/modules/ghostHandler.js +279 -0
- package/src/modules/shadowHandler.js +266 -0
- package/src/shield.js +694 -0
- package/src/smartLogger.js +268 -0
- package/src/sslManager.js +345 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Security Scanner v2.1
|
|
3
|
+
*
|
|
4
|
+
* Advanced AI-powered payload analysis using Google Gemini and OpenAI.
|
|
5
|
+
* Performs deep inspection of request payloads for malicious intent,
|
|
6
|
+
* injection attacks, and security threats.
|
|
7
|
+
*
|
|
8
|
+
* @module aiScanner
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
|
13
|
+
const OpenAI = require('openai');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default configuration constants
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
19
|
+
REQUEST_TIMEOUT: 10000,
|
|
20
|
+
MAX_PAYLOAD_SIZE: 10000,
|
|
21
|
+
MAX_RETRIES: 2,
|
|
22
|
+
CACHE_TTL: 300000,
|
|
23
|
+
SENSITIVE_FIELDS: Object.freeze([
|
|
24
|
+
'password', 'token', 'secret', 'apikey', 'api_key',
|
|
25
|
+
'authorization', 'auth', 'bearer', 'jwt', 'session',
|
|
26
|
+
'credit_card', 'card_number', 'cvv', 'pin'
|
|
27
|
+
])
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* AI system prompt for consistent threat analysis
|
|
32
|
+
*/
|
|
33
|
+
const SYSTEM_PROMPT = Object.freeze(`You are a cybersecurity analyst specializing in web application security.
|
|
34
|
+
|
|
35
|
+
Analyze the following JSON payload for potential security threats including:
|
|
36
|
+
- SQL injection attempts
|
|
37
|
+
- Cross-site scripting (XSS)
|
|
38
|
+
- Command injection
|
|
39
|
+
- Path traversal attacks
|
|
40
|
+
- Server-side request forgery (SSRF)
|
|
41
|
+
- Deserialization attacks
|
|
42
|
+
- Phishing or social engineering attempts
|
|
43
|
+
- Obfuscated malicious code
|
|
44
|
+
- Suspicious encoding or evasion techniques
|
|
45
|
+
|
|
46
|
+
IMPORTANT: Focus on the SECURITY IMPLICATIONS, not the content appropriateness.
|
|
47
|
+
|
|
48
|
+
Return ONLY valid JSON with this exact structure:
|
|
49
|
+
{
|
|
50
|
+
"safe": boolean,
|
|
51
|
+
"threatLevel": number (0-10, where 0=no threat, 10=maximum threat),
|
|
52
|
+
"reason": "brief explanation of findings"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Do not include any other text or formatting.`);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Simple in-memory cache for AI responses
|
|
59
|
+
*/
|
|
60
|
+
const responseCache = new Map();
|
|
61
|
+
const cacheTimestamps = new Map();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Performance tracking statistics
|
|
65
|
+
*/
|
|
66
|
+
let stats = {
|
|
67
|
+
totalScans: 0,
|
|
68
|
+
cacheHits: 0,
|
|
69
|
+
apiCalls: 0,
|
|
70
|
+
errors: 0,
|
|
71
|
+
averageResponseTime: 0,
|
|
72
|
+
lastScanTime: 0
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generates secure cache key for payload with integrity validation
|
|
77
|
+
* @private
|
|
78
|
+
* @param {Object} payload - Request payload
|
|
79
|
+
* @returns {string} Secure cache key
|
|
80
|
+
*/
|
|
81
|
+
function generateCacheKey(payload) {
|
|
82
|
+
const sortedJson = JSON.stringify(payload, Object.keys(payload).sort());
|
|
83
|
+
return require('crypto').createHash('sha256').update(sortedJson).digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Checks cache for existing AI analysis with integrity validation
|
|
88
|
+
* @private
|
|
89
|
+
* @param {string} cacheKey - Cache key
|
|
90
|
+
* @returns {Object|null} Validated cached result or null
|
|
91
|
+
*/
|
|
92
|
+
function getCachedResult(cacheKey) {
|
|
93
|
+
const cached = responseCache.get(cacheKey);
|
|
94
|
+
const timestamp = cacheTimestamps.get(cacheKey);
|
|
95
|
+
|
|
96
|
+
if (!cached || !timestamp) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ((Date.now() - timestamp) > DEFAULT_CONFIG.CACHE_TTL) {
|
|
101
|
+
responseCache.delete(cacheKey);
|
|
102
|
+
cacheTimestamps.delete(cacheKey);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!isValidCachedResponse(cached)) {
|
|
107
|
+
responseCache.delete(cacheKey);
|
|
108
|
+
cacheTimestamps.delete(cacheKey);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stats.cacheHits++;
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validates cached response structure to prevent cache poisoning
|
|
118
|
+
* @private
|
|
119
|
+
* @param {Object} response - Cached response to validate
|
|
120
|
+
* @returns {boolean} True if response is valid
|
|
121
|
+
*/
|
|
122
|
+
function isValidCachedResponse(response) {
|
|
123
|
+
return response &&
|
|
124
|
+
typeof response.safe === 'boolean' &&
|
|
125
|
+
typeof response.threatLevel === 'number' &&
|
|
126
|
+
response.threatLevel >= 0 && response.threatLevel <= 10 &&
|
|
127
|
+
typeof response.reason === 'string' &&
|
|
128
|
+
response.reason.length > 0 &&
|
|
129
|
+
response.reason.length < 500;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stores result in cache
|
|
134
|
+
* @private
|
|
135
|
+
* @param {string} cacheKey - Cache key
|
|
136
|
+
* @param {Object} result - AI analysis result
|
|
137
|
+
*/
|
|
138
|
+
function setCachedResult(cacheKey, result) {
|
|
139
|
+
responseCache.set(cacheKey, result);
|
|
140
|
+
cacheTimestamps.set(cacheKey, Date.now());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Masks sensitive fields in payload before AI analysis
|
|
145
|
+
* @param {*} obj - Object, array, or primitive to sanitize
|
|
146
|
+
* @returns {*} Sanitized copy with sensitive fields masked
|
|
147
|
+
*/
|
|
148
|
+
function maskSensitiveFields(obj) {
|
|
149
|
+
if (obj === null || typeof obj !== 'object') {
|
|
150
|
+
return obj;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (Array.isArray(obj)) {
|
|
154
|
+
return obj.map(maskSensitiveFields);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const sanitized = {};
|
|
158
|
+
|
|
159
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
160
|
+
const isSensitive = DEFAULT_CONFIG.SENSITIVE_FIELDS.some(field =>
|
|
161
|
+
key.toLowerCase().includes(field.toLowerCase())
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (isSensitive) {
|
|
165
|
+
sanitized[key] = '***REDACTED***';
|
|
166
|
+
} else {
|
|
167
|
+
sanitized[key] = maskSensitiveFields(value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return sanitized;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Validates payload structure to prevent malformed data attacks
|
|
176
|
+
* @private
|
|
177
|
+
* @param {Object} payload - Payload to validate
|
|
178
|
+
* @returns {boolean} True if payload structure is valid
|
|
179
|
+
*/
|
|
180
|
+
function isValidPayload(payload) {
|
|
181
|
+
if (payload.__proto__ !== Object.prototype || payload.constructor !== Object) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
JSON.stringify(payload);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const maxDepth = 10;
|
|
192
|
+
function checkDepth(obj, depth = 0) {
|
|
193
|
+
if (depth > maxDepth) return false;
|
|
194
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
195
|
+
for (const key in obj) {
|
|
196
|
+
if (!checkDepth(obj[key], depth + 1)) return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return checkDepth(payload);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Checks for obvious attack patterns that don't need AI analysis
|
|
207
|
+
* @private
|
|
208
|
+
* @param {Object} payload - Payload to check
|
|
209
|
+
* @returns {boolean} True if attack patterns detected
|
|
210
|
+
*/
|
|
211
|
+
function containsAttackPatterns(payload) {
|
|
212
|
+
const payloadStr = JSON.stringify(payload).toLowerCase();
|
|
213
|
+
|
|
214
|
+
const attackPatterns = [
|
|
215
|
+
/\bunion\s+select\b/i, // SQL injection
|
|
216
|
+
/\bscript\b.*\bsrc\b/i, // XSS with script tags
|
|
217
|
+
/\bon\w+\s*=/i, // Event handlers (XSS)
|
|
218
|
+
/\bjavascript:/i, // JavaScript URLs
|
|
219
|
+
/\bdata:\s*text\/html/i, // Data URLs
|
|
220
|
+
/\beval\s*\(/i, // Code injection
|
|
221
|
+
/\bexec\s*\(/i, // Command injection
|
|
222
|
+
/<\s*script[^>]*>.*<\s*\/\s*script\s*>/i, // Script tags
|
|
223
|
+
/\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE)\b.*\bFROM\b/i, // SQL keywords
|
|
224
|
+
/\$\{.*\}/g, // Template injection
|
|
225
|
+
/process\.env|process\.argv|require\s*\(/i, // Node.js injection
|
|
226
|
+
/__proto__|constructor|prototype/i // Prototype pollution
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
return attackPatterns.some(pattern => pattern.test(payloadStr));
|
|
230
|
+
}
|
|
231
|
+
function parseAIResponse(aiText) {
|
|
232
|
+
if (!aiText || typeof aiText !== 'string') {
|
|
233
|
+
throw new Error('Empty or invalid AI response');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let cleanedText = aiText.trim();
|
|
237
|
+
|
|
238
|
+
if (cleanedText.startsWith('```json')) {
|
|
239
|
+
cleanedText = cleanedText.replace(/```json\n?/g, '').replace(/```\n?/g, '');
|
|
240
|
+
} else if (cleanedText.startsWith('```')) {
|
|
241
|
+
cleanedText = cleanedText.replace(/```\n?/g, '');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let aiResult;
|
|
245
|
+
try {
|
|
246
|
+
aiResult = JSON.parse(cleanedText);
|
|
247
|
+
|
|
248
|
+
if (aiResult === null || typeof aiResult !== 'object') {
|
|
249
|
+
throw new Error('AI response must be a valid object');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if ('__proto__' in aiResult || 'constructor' in aiResult || 'prototype' in aiResult) {
|
|
253
|
+
throw new Error('AI response contains prototype pollution attempt');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
} catch (parseError) {
|
|
257
|
+
throw new Error(`Failed to parse AI response as JSON: ${parseError.message}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (typeof aiResult.safe !== 'boolean') {
|
|
261
|
+
throw new Error('AI response missing required "safe" boolean field');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (typeof aiResult.threatLevel !== 'number' ||
|
|
265
|
+
aiResult.threatLevel < 0 || aiResult.threatLevel > 10) {
|
|
266
|
+
throw new Error('AI response "threatLevel" must be a number between 0-10');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
safe: aiResult.safe,
|
|
271
|
+
threatLevel: aiResult.threatLevel,
|
|
272
|
+
reason: aiResult.reason || 'No reason provided'
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Scans payload using Google Gemini AI
|
|
278
|
+
* @private
|
|
279
|
+
* @param {Object} sanitizedPayload - Payload with sensitive data masked
|
|
280
|
+
* @param {Object} geminiConfig - Gemini configuration
|
|
281
|
+
* @returns {Promise<Object>} Scan result
|
|
282
|
+
*/
|
|
283
|
+
async function scanWithGemini(sanitizedPayload, geminiConfig) {
|
|
284
|
+
const genAI = new GoogleGenerativeAI(geminiConfig.apiKey);
|
|
285
|
+
const model = genAI.getGenerativeModel({
|
|
286
|
+
model: geminiConfig.model || 'gemini-2.5-flash'
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const prompt = `${SYSTEM_PROMPT}\n\nPayload to analyze:\n${JSON.stringify(sanitizedPayload, null, 2)}`;
|
|
290
|
+
|
|
291
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
292
|
+
setTimeout(() => reject(new Error('Gemini API request timeout')), DEFAULT_CONFIG.REQUEST_TIMEOUT);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const apiPromise = model.generateContent({
|
|
296
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
297
|
+
generationConfig: {
|
|
298
|
+
temperature: 0.1,
|
|
299
|
+
maxOutputTokens: 500,
|
|
300
|
+
topP: 1,
|
|
301
|
+
topK: 1
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const result = await Promise.race([apiPromise, timeoutPromise]);
|
|
306
|
+
const response = await result.response;
|
|
307
|
+
const aiText = response.text();
|
|
308
|
+
|
|
309
|
+
if (!aiText) {
|
|
310
|
+
throw new Error('Empty response from Gemini API');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return parseAIResponse(aiText);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Scans payload using OpenAI
|
|
318
|
+
* @private
|
|
319
|
+
* @param {Object} sanitizedPayload - Payload with sensitive data masked
|
|
320
|
+
* @param {Object} openaiConfig - OpenAI configuration
|
|
321
|
+
* @returns {Promise<Object>} Scan result
|
|
322
|
+
*/
|
|
323
|
+
async function scanWithOpenAI(sanitizedPayload, openaiConfig) {
|
|
324
|
+
const openai = new OpenAI({
|
|
325
|
+
apiKey: openaiConfig.apiKey,
|
|
326
|
+
organization: openaiConfig.organization || undefined,
|
|
327
|
+
timeout: DEFAULT_CONFIG.REQUEST_TIMEOUT,
|
|
328
|
+
maxRetries: DEFAULT_CONFIG.MAX_RETRIES
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const userPrompt = `Payload to analyze:\n${JSON.stringify(sanitizedPayload, null, 2)}`;
|
|
332
|
+
|
|
333
|
+
const completion = await openai.chat.completions.create({
|
|
334
|
+
model: openaiConfig.model || 'gpt-4o-mini',
|
|
335
|
+
messages: [
|
|
336
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
337
|
+
{ role: 'user', content: userPrompt }
|
|
338
|
+
],
|
|
339
|
+
temperature: 0.1,
|
|
340
|
+
max_tokens: 500,
|
|
341
|
+
response_format: { type: 'json_object' }
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const aiText = completion.choices[0]?.message?.content;
|
|
345
|
+
|
|
346
|
+
if (!aiText) {
|
|
347
|
+
throw new Error('Empty response from OpenAI API');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return parseAIResponse(aiText);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Main AI scanning function with caching and error handling
|
|
355
|
+
*
|
|
356
|
+
* @param {Object} payload - Request body/payload to analyze
|
|
357
|
+
* @param {Object} aiConfig - AI provider configuration
|
|
358
|
+
* @returns {Promise<Object>} Scan result with safe, threatLevel, and reason
|
|
359
|
+
* @throws {Error} If AI provider is unsupported or API call fails
|
|
360
|
+
*/
|
|
361
|
+
async function scan(payload, aiConfig) {
|
|
362
|
+
const startTime = process.hrtime.bigint();
|
|
363
|
+
stats.totalScans++;
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
if (!payload || typeof payload !== 'object') {
|
|
367
|
+
return { safe: true, threatLevel: 0, reason: 'No payload to scan' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!isValidPayload(payload)) {
|
|
371
|
+
return { safe: false, threatLevel: 2, reason: 'Invalid payload structure' };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const payloadSize = JSON.stringify(payload).length;
|
|
375
|
+
if (payloadSize > DEFAULT_CONFIG.MAX_PAYLOAD_SIZE) {
|
|
376
|
+
return { safe: false, threatLevel: 3, reason: 'Payload size exceeds security limits' };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (containsAttackPatterns(payload)) {
|
|
380
|
+
return { safe: false, threatLevel: 8, reason: 'Detected obvious attack patterns' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const cacheKey = generateCacheKey(payload);
|
|
384
|
+
const cachedResult = getCachedResult(cacheKey);
|
|
385
|
+
if (cachedResult) {
|
|
386
|
+
return cachedResult;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const sanitizedPayload = maskSensitiveFields(payload);
|
|
390
|
+
let result;
|
|
391
|
+
|
|
392
|
+
if (aiConfig.provider === 'gemini') {
|
|
393
|
+
if (!aiConfig.gemini?.apiKey) {
|
|
394
|
+
throw new Error('Gemini API key is required');
|
|
395
|
+
}
|
|
396
|
+
result = await scanWithGemini(sanitizedPayload, aiConfig.gemini);
|
|
397
|
+
} else if (aiConfig.provider === 'openai') {
|
|
398
|
+
if (!aiConfig.openai?.apiKey) {
|
|
399
|
+
throw new Error('OpenAI API key is required');
|
|
400
|
+
}
|
|
401
|
+
result = await scanWithOpenAI(sanitizedPayload, aiConfig.openai);
|
|
402
|
+
} else {
|
|
403
|
+
throw new Error(`Unsupported AI provider: ${aiConfig.provider}. Use 'gemini' or 'openai'`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
setCachedResult(cacheKey, result);
|
|
407
|
+
stats.apiCalls++;
|
|
408
|
+
|
|
409
|
+
const scanTime = Number(process.hrtime.bigint() - startTime) / 1000000;
|
|
410
|
+
stats.lastScanTime = scanTime;
|
|
411
|
+
stats.averageResponseTime = ((stats.averageResponseTime * (stats.apiCalls - 1)) + scanTime) / stats.apiCalls;
|
|
412
|
+
|
|
413
|
+
return result;
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
stats.errors++;
|
|
417
|
+
throw new Error(`AI scan failed: ${error.message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Retrieves AI scanner performance statistics
|
|
423
|
+
* @returns {Object} Performance statistics
|
|
424
|
+
*/
|
|
425
|
+
function getStats() {
|
|
426
|
+
return {
|
|
427
|
+
...stats,
|
|
428
|
+
cacheHitRate: stats.totalScans > 0 ? (stats.cacheHits / stats.totalScans * 100).toFixed(1) : 0,
|
|
429
|
+
config: DEFAULT_CONFIG
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Clears the response cache
|
|
435
|
+
* @returns {number} Number of cached entries cleared
|
|
436
|
+
*/
|
|
437
|
+
function clearCache() {
|
|
438
|
+
const cacheSize = responseCache.size;
|
|
439
|
+
responseCache.clear();
|
|
440
|
+
cacheTimestamps.clear();
|
|
441
|
+
return cacheSize;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Validates AI configuration parameters
|
|
446
|
+
* @param {Object} aiConfig - AI configuration to validate
|
|
447
|
+
* @returns {Object} Validation result with valid boolean and errors array
|
|
448
|
+
*/
|
|
449
|
+
function validateConfig(aiConfig) {
|
|
450
|
+
const errors = [];
|
|
451
|
+
|
|
452
|
+
if (!aiConfig || typeof aiConfig !== 'object') {
|
|
453
|
+
errors.push('AI configuration must be an object');
|
|
454
|
+
return { valid: false, errors };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!['gemini', 'openai'].includes(aiConfig.provider)) {
|
|
458
|
+
errors.push('Provider must be either "gemini" or "openai"');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (aiConfig.provider === 'gemini' && !aiConfig.gemini?.apiKey) {
|
|
462
|
+
errors.push('Gemini API key is required when using Gemini provider');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (aiConfig.provider === 'openai' && !aiConfig.openai?.apiKey) {
|
|
466
|
+
errors.push('OpenAI API key is required when using OpenAI provider');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
valid: errors.length === 0,
|
|
471
|
+
errors
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
module.exports = {
|
|
476
|
+
scan,
|
|
477
|
+
maskSensitiveFields,
|
|
478
|
+
getStats,
|
|
479
|
+
clearCache,
|
|
480
|
+
validateConfig,
|
|
481
|
+
DEFAULT_CONFIG
|
|
482
|
+
};
|