secure-api-guard 0.0.0-stage → 1.1.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,215 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * List of sensitive key substrings or names that must always be redacted.
7
+ */
8
+ const DEFAULT_SENSITIVE_KEYS = [
9
+ 'password',
10
+ 'pass',
11
+ 'secret',
12
+ 'token',
13
+ 'api_key',
14
+ 'apikey',
15
+ 'authorization',
16
+ 'bearer',
17
+ 'auth',
18
+ 'cookie',
19
+ 'set-cookie',
20
+ 'access_token',
21
+ 'refresh_token',
22
+ 'private_key',
23
+ 'session_id',
24
+ 'credit_card',
25
+ 'card_number',
26
+ 'cvv',
27
+ 'ssn'
28
+ ];
29
+
30
+ /**
31
+ * Redact sensitive fields from any data structure.
32
+ * Handles objects, arrays, circular references, and string representations.
33
+ *
34
+ * @param {any} value - Value to redact.
35
+ * @param {string[]} [customSensitiveKeys] - Additional keys to redact.
36
+ * @param {Set<any>} [seen] - Visited objects to prevent cyclic recursion.
37
+ * @returns {any} Redacted data.
38
+ */
39
+ function redact(value, customSensitiveKeys = [], seen = new Set()) {
40
+ if (value === null || value === undefined) {
41
+ return value;
42
+ }
43
+
44
+ if (typeof value !== 'object') {
45
+ return value;
46
+ }
47
+
48
+ if (seen.has(value)) {
49
+ return '[Circular]';
50
+ }
51
+ seen.add(value);
52
+
53
+ const sensitiveKeys = [...DEFAULT_SENSITIVE_KEYS, ...customSensitiveKeys].map((k) => k.toLowerCase());
54
+
55
+ if (Array.isArray(value)) {
56
+ return value.map((item) => redact(item, customSensitiveKeys, seen));
57
+ }
58
+
59
+ const result = {};
60
+ for (const [key, val] of Object.entries(value)) {
61
+ const lowerKey = key.toLowerCase();
62
+ const isSensitive = sensitiveKeys.some((s) => lowerKey.includes(s));
63
+
64
+ if (typeof val === 'object' && val !== null) {
65
+ // Recurse into nested objects and arrays so child properties are inspected and redacted
66
+ result[key] = redact(val, customSensitiveKeys, seen);
67
+ } else if (isSensitive) {
68
+ result[key] = '[REDACTED]';
69
+ } else {
70
+ result[key] = val;
71
+ }
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Retrieve or generate a unique request identifier.
79
+ *
80
+ * @param {import('express').Request} req - Express request object.
81
+ * @returns {string} Request ID.
82
+ */
83
+ function getRequestId(req) {
84
+ if (!req) {
85
+ return crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
86
+ }
87
+
88
+ if (req.id && typeof req.id === 'string') {
89
+ return req.id;
90
+ }
91
+
92
+ const headerId = req.headers && (req.headers['x-request-id'] || req.headers['x-correlation-id']);
93
+ if (headerId && typeof headerId === 'string') {
94
+ return headerId;
95
+ }
96
+
97
+ const generated = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
98
+ req.id = generated;
99
+ return generated;
100
+ }
101
+
102
+ /**
103
+ * Security Logger wrapper.
104
+ */
105
+ class SecurityLogger {
106
+ /**
107
+ * @param {Object} [options]
108
+ * @param {boolean} [options.enabled=true]
109
+ * @param {'info'|'warn'|'error'|'debug'|'silent'} [options.level='warn']
110
+ * @param {Function} [options.customLogger] - Custom logger callback (event => void)
111
+ * @param {string[]} [options.redactFields=[]] - Custom fields to redact
112
+ */
113
+ constructor(options = {}) {
114
+ this.enabled = options.enabled !== false;
115
+ this.level = options.level || 'warn';
116
+ this.customLogger = typeof options.customLogger === 'function' ? options.customLogger : null;
117
+ this.redactFields = Array.isArray(options.redactFields) ? options.redactFields : [];
118
+
119
+ this.levelWeights = {
120
+ debug: 10,
121
+ info: 20,
122
+ warn: 30,
123
+ error: 40,
124
+ silent: 99
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Check if a log level is enabled.
130
+ * @param {'debug'|'info'|'warn'|'error'} level
131
+ * @returns {boolean}
132
+ */
133
+ shouldLog(level) {
134
+ if (!this.enabled || this.level === 'silent') {
135
+ return false;
136
+ }
137
+ const currentWeight = this.levelWeights[this.level] || 30;
138
+ const targetWeight = this.levelWeights[level] || 30;
139
+ return targetWeight >= currentWeight;
140
+ }
141
+
142
+ /**
143
+ * Log a security event.
144
+ *
145
+ * @param {'debug'|'info'|'warn'|'error'} level - Log severity level.
146
+ * @param {string} eventType - Type of security event (e.g. 'RATE_LIMIT_EXCEEDED', 'API_KEY_AUTH_FAILED').
147
+ * @param {string} message - Descriptive event message.
148
+ * @param {Object} [context] - Context data (req, ip, details).
149
+ */
150
+ log(level, eventType, message, context = {}) {
151
+ if (!this.shouldLog(level)) {
152
+ return;
153
+ }
154
+
155
+ const req = context.req;
156
+ const reqId = getRequestId(req);
157
+
158
+ const event = {
159
+ timestamp: new Date().toISOString(),
160
+ level: level.toUpperCase(),
161
+ eventType,
162
+ message,
163
+ requestId: reqId,
164
+ ip: context.ip || (req && (req.ip || req.socket?.remoteAddress)) || 'unknown',
165
+ method: req ? req.method : undefined,
166
+ path: req ? (req.originalUrl || req.url) : undefined,
167
+ details: redact(context.details || {}, this.redactFields)
168
+ };
169
+
170
+ // Remove undefined fields
171
+ Object.keys(event).forEach((k) => event[k] === undefined && delete event[k]);
172
+
173
+ if (this.customLogger) {
174
+ try {
175
+ this.customLogger(event);
176
+ } catch (err) {
177
+ // Fallback to console.error if custom logger throws
178
+ console.error('[secure-api-guard] Custom logger error:', err.message);
179
+ }
180
+ return;
181
+ }
182
+
183
+ const formatted = `[secure-api-guard] [${event.timestamp}] [${event.level}] [${event.eventType}] ${event.message} (reqId=${event.requestId}, ip=${event.ip})`;
184
+
185
+ const logFn = typeof console[level] === 'function' ? console[level] : console.log;
186
+ logFn(formatted, event.details);
187
+ }
188
+
189
+ debug(eventType, message, context) {
190
+ this.log('debug', eventType, message, context);
191
+ }
192
+
193
+ info(eventType, message, context) {
194
+ this.log('info', eventType, message, context);
195
+ }
196
+
197
+ warn(eventType, message, context) {
198
+ this.log('warn', eventType, message, context);
199
+ }
200
+
201
+ error(eventType, message, context) {
202
+ this.log('error', eventType, message, context);
203
+ }
204
+ }
205
+
206
+ // Default singleton logger instance with silent/warn behavior
207
+ const defaultLogger = new SecurityLogger({ level: 'warn' });
208
+
209
+ module.exports = {
210
+ SecurityLogger,
211
+ defaultLogger,
212
+ redact,
213
+ getRequestId,
214
+ DEFAULT_SENSITIVE_KEYS
215
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { SecurityError } = require('./errors');
4
+
5
+ /**
6
+ * Send a standardized security rejection response.
7
+ *
8
+ * @param {import('express').Request} req - Express request object.
9
+ * @param {import('express').Response} res - Express response object.
10
+ * @param {SecurityError|Error|string} error - The error or message to respond with.
11
+ * @param {number} [statusCode] - Optional status code override.
12
+ * @param {Function} [customHandler] - Optional custom rejection handler (req, res, error).
13
+ */
14
+ function sendSecurityResponse(req, res, error, statusCode, customHandler) {
15
+ if (res.headersSent) {
16
+ return;
17
+ }
18
+
19
+ if (typeof customHandler === 'function') {
20
+ return customHandler(req, res, error);
21
+ }
22
+
23
+ let status = statusCode;
24
+ let code = 'SECURITY_ERROR';
25
+ let message = 'Access denied by security policy.';
26
+ let details = null;
27
+
28
+ if (error instanceof SecurityError) {
29
+ status = status || error.statusCode;
30
+ code = error.code;
31
+ message = error.message;
32
+ details = error.details;
33
+ } else if (error instanceof Error) {
34
+ status = status || 500;
35
+ message = error.message;
36
+ } else if (typeof error === 'string') {
37
+ status = status || 400;
38
+ message = error;
39
+ }
40
+
41
+ status = status || 403;
42
+
43
+ const payload = {
44
+ success: false,
45
+ error: {
46
+ code,
47
+ message,
48
+ ...(details ? { details } : {})
49
+ }
50
+ };
51
+
52
+ res.status(status).json(payload);
53
+ }
54
+
55
+ module.exports = {
56
+ sendSecurityResponse
57
+ };
58
+