securenow 5.10.2 → 5.11.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.
@@ -1,186 +1,186 @@
1
- /**
2
- * SecureNow Next.js Middleware for Body Capture
3
- *
4
- * OPTIONAL: Import this in your Next.js app to enable automatic body capture
5
- *
6
- * Usage:
7
- *
8
- * Create middleware.ts in your Next.js app root:
9
- *
10
- * export { middleware } from 'securenow/nextjs-middleware';
11
- * export const config = {
12
- * matcher: '/api/:path*', // Apply to API routes only
13
- * };
14
- *
15
- * That's it! Bodies are now captured with sensitive data redacted.
16
- */
17
-
18
- const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
19
-
20
- // Default sensitive fields to redact
21
- const DEFAULT_SENSITIVE_FIELDS = [
22
- 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
23
- 'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
24
- 'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
25
- ];
26
-
27
- /**
28
- * Redact sensitive fields from an object
29
- */
30
- function escapeRegex(str) {
31
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
- }
33
-
34
- function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
35
- if (!obj || typeof obj !== 'object') return obj;
36
-
37
- const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
38
-
39
- for (const key of Object.keys(redacted)) {
40
- const lowerKey = key.toLowerCase();
41
-
42
- if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
43
- redacted[key] = '[REDACTED]';
44
- } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
45
- redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
46
- }
47
- }
48
-
49
- return redacted;
50
- }
51
-
52
- /**
53
- * Redact sensitive data from GraphQL query strings
54
- */
55
- function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
56
- if (!query || typeof query !== 'string') return query;
57
-
58
- let redacted = query;
59
-
60
- sensitiveFields.forEach(field => {
61
- const escaped = escapeRegex(field);
62
- const patterns = [
63
- new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
64
- new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
65
- ];
66
-
67
- patterns.forEach(pattern => {
68
- redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
69
- return suffix ? `${prefix}[REDACTED]${suffix}` : `${prefix}[REDACTED]`;
70
- });
71
- });
72
- });
73
-
74
- return redacted;
75
- }
76
-
77
- /**
78
- * Next.js Middleware for Body Capture
79
- */
80
- async function middleware(request) {
81
- const { NextResponse } = require('next/server');
82
-
83
- // Only capture for POST/PUT/PATCH
84
- if (!['POST', 'PUT', 'PATCH'].includes(request.method)) {
85
- return NextResponse.next();
86
- }
87
-
88
- // Get or create a tracer
89
- const tracer = trace.getTracer('securenow-middleware');
90
- let span = trace.getActiveSpan();
91
- let createdSpan = false;
92
-
93
- // If no active span, create one for this middleware
94
- if (!span) {
95
- const url = new URL(request.url);
96
- span = tracer.startSpan(`middleware ${request.method} ${url.pathname}`);
97
- createdSpan = true;
98
- }
99
-
100
- try {
101
- const contentType = request.headers.get('content-type') || '';
102
- const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
103
- const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
104
- const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
105
-
106
- // Only capture supported types
107
- if (contentType.includes('application/json') ||
108
- contentType.includes('application/graphql')) {
109
-
110
- // Clone the request to read body without consuming the original
111
- const clonedRequest = request.clone();
112
- const bodyText = await clonedRequest.text();
113
-
114
- if (bodyText.length <= maxBodySize) {
115
- let redactedBody;
116
-
117
- if (contentType.includes('application/graphql')) {
118
- // GraphQL: redact query string
119
- redactedBody = redactGraphQLQuery(bodyText, allSensitiveFields);
120
- } else {
121
- // JSON: parse and redact
122
- try {
123
- const parsed = JSON.parse(bodyText);
124
- const redacted = redactSensitiveData(parsed, allSensitiveFields);
125
- redactedBody = JSON.stringify(redacted);
126
- } catch (e) {
127
- redactedBody = '[UNPARSEABLE - REDACTED FOR SAFETY]';
128
- }
129
- }
130
-
131
- span.setAttributes({
132
- 'http.request.body': redactedBody.substring(0, maxBodySize),
133
- 'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
134
- 'http.request.body.size': bodyText.length,
135
- });
136
- } else {
137
- span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
138
- }
139
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
140
- const clonedRequest = request.clone();
141
- const formData = await clonedRequest.formData();
142
- const parsed = Object.fromEntries(formData);
143
- const redacted = redactSensitiveData(parsed, allSensitiveFields);
144
-
145
- span.setAttributes({
146
- 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
147
- 'http.request.body.type': 'form',
148
- 'http.request.body.size': JSON.stringify(parsed).length,
149
- });
150
- } else if (contentType.includes('multipart/form-data')) {
151
- span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
152
- span.setAttribute('http.request.body.type', 'multipart');
153
- }
154
-
155
- // End span if we created it
156
- if (createdSpan) {
157
- span.setStatus({ code: SpanStatusCode.OK });
158
- span.end();
159
- }
160
- } catch (error) {
161
- // Silently fail - don't break the request
162
- console.debug('[securenow] Body capture failed:', error.message);
163
-
164
- // End span with error if we created it
165
- if (createdSpan && span) {
166
- span.setStatus({
167
- code: SpanStatusCode.ERROR,
168
- message: error.message
169
- });
170
- span.end();
171
- }
172
- }
173
-
174
- return NextResponse.next();
175
- }
176
-
177
- module.exports = {
178
- middleware,
179
- redactSensitiveData,
180
- redactGraphQLQuery,
181
- DEFAULT_SENSITIVE_FIELDS,
182
- };
183
-
184
-
185
-
186
-
1
+ /**
2
+ * SecureNow Next.js Middleware for Body Capture
3
+ *
4
+ * OPTIONAL: Import this in your Next.js app to enable automatic body capture
5
+ *
6
+ * Usage:
7
+ *
8
+ * Create middleware.ts in your Next.js app root:
9
+ *
10
+ * export { middleware } from 'securenow/nextjs-middleware';
11
+ * export const config = {
12
+ * matcher: '/api/:path*', // Apply to API routes only
13
+ * };
14
+ *
15
+ * That's it! Bodies are now captured with sensitive data redacted.
16
+ */
17
+
18
+ const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
19
+
20
+ // Default sensitive fields to redact
21
+ const DEFAULT_SENSITIVE_FIELDS = [
22
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
23
+ 'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
24
+ 'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
25
+ ];
26
+
27
+ /**
28
+ * Redact sensitive fields from an object
29
+ */
30
+ function escapeRegex(str) {
31
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+
34
+ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
35
+ if (!obj || typeof obj !== 'object') return obj;
36
+
37
+ const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
38
+
39
+ for (const key of Object.keys(redacted)) {
40
+ const lowerKey = key.toLowerCase();
41
+
42
+ if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
43
+ redacted[key] = '[REDACTED]';
44
+ } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
45
+ redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
46
+ }
47
+ }
48
+
49
+ return redacted;
50
+ }
51
+
52
+ /**
53
+ * Redact sensitive data from GraphQL query strings
54
+ */
55
+ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
56
+ if (!query || typeof query !== 'string') return query;
57
+
58
+ let redacted = query;
59
+
60
+ sensitiveFields.forEach(field => {
61
+ const escaped = escapeRegex(field);
62
+ const patterns = [
63
+ new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
64
+ new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
65
+ ];
66
+
67
+ patterns.forEach(pattern => {
68
+ redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
69
+ return suffix ? `${prefix}[REDACTED]${suffix}` : `${prefix}[REDACTED]`;
70
+ });
71
+ });
72
+ });
73
+
74
+ return redacted;
75
+ }
76
+
77
+ /**
78
+ * Next.js Middleware for Body Capture
79
+ */
80
+ async function middleware(request) {
81
+ const { NextResponse } = require('next/server');
82
+
83
+ // Only capture for POST/PUT/PATCH
84
+ if (!['POST', 'PUT', 'PATCH'].includes(request.method)) {
85
+ return NextResponse.next();
86
+ }
87
+
88
+ // Get or create a tracer
89
+ const tracer = trace.getTracer('securenow-middleware');
90
+ let span = trace.getActiveSpan();
91
+ let createdSpan = false;
92
+
93
+ // If no active span, create one for this middleware
94
+ if (!span) {
95
+ const url = new URL(request.url);
96
+ span = tracer.startSpan(`middleware ${request.method} ${url.pathname}`);
97
+ createdSpan = true;
98
+ }
99
+
100
+ try {
101
+ const contentType = request.headers.get('content-type') || '';
102
+ const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
103
+ const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
104
+ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
105
+
106
+ // Only capture supported types
107
+ if (contentType.includes('application/json') ||
108
+ contentType.includes('application/graphql')) {
109
+
110
+ // Clone the request to read body without consuming the original
111
+ const clonedRequest = request.clone();
112
+ const bodyText = await clonedRequest.text();
113
+
114
+ if (bodyText.length <= maxBodySize) {
115
+ let redactedBody;
116
+
117
+ if (contentType.includes('application/graphql')) {
118
+ // GraphQL: redact query string
119
+ redactedBody = redactGraphQLQuery(bodyText, allSensitiveFields);
120
+ } else {
121
+ // JSON: parse and redact
122
+ try {
123
+ const parsed = JSON.parse(bodyText);
124
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
125
+ redactedBody = JSON.stringify(redacted);
126
+ } catch (e) {
127
+ redactedBody = '[UNPARSEABLE - REDACTED FOR SAFETY]';
128
+ }
129
+ }
130
+
131
+ span.setAttributes({
132
+ 'http.request.body': redactedBody.substring(0, maxBodySize),
133
+ 'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
134
+ 'http.request.body.size': bodyText.length,
135
+ });
136
+ } else {
137
+ span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
138
+ }
139
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
140
+ const clonedRequest = request.clone();
141
+ const formData = await clonedRequest.formData();
142
+ const parsed = Object.fromEntries(formData);
143
+ const redacted = redactSensitiveData(parsed, allSensitiveFields);
144
+
145
+ span.setAttributes({
146
+ 'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
147
+ 'http.request.body.type': 'form',
148
+ 'http.request.body.size': JSON.stringify(parsed).length,
149
+ });
150
+ } else if (contentType.includes('multipart/form-data')) {
151
+ span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
152
+ span.setAttribute('http.request.body.type', 'multipart');
153
+ }
154
+
155
+ // End span if we created it
156
+ if (createdSpan) {
157
+ span.setStatus({ code: SpanStatusCode.OK });
158
+ span.end();
159
+ }
160
+ } catch (error) {
161
+ // Silently fail - don't break the request
162
+ console.debug('[securenow] Body capture failed:', error.message);
163
+
164
+ // End span with error if we created it
165
+ if (createdSpan && span) {
166
+ span.setStatus({
167
+ code: SpanStatusCode.ERROR,
168
+ message: error.message
169
+ });
170
+ span.end();
171
+ }
172
+ }
173
+
174
+ return NextResponse.next();
175
+ }
176
+
177
+ module.exports = {
178
+ middleware,
179
+ redactSensitiveData,
180
+ redactGraphQLQuery,
181
+ DEFAULT_SENSITIVE_FIELDS,
182
+ };
183
+
184
+
185
+
186
+