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.
@@ -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
+ };