securenow 5.10.2 → 5.11.1
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/CONSUMING-APPS-GUIDE.md +30 -0
- package/NPM_README.md +65 -0
- package/README.md +13 -0
- package/cidr.js +83 -0
- package/cli/auth.js +208 -208
- package/cli/config.js +117 -117
- package/cli/firewall.js +81 -0
- package/cli/fp.js +638 -0
- package/cli/security.js +4 -8
- package/cli.js +28 -1
- package/console-instrumentation.js +147 -147
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +40 -1
- package/docs/API-KEYS-GUIDE.md +215 -0
- package/docs/ENVIRONMENT-VARIABLES.md +880 -697
- package/docs/FIREWALL-GUIDE.md +388 -0
- package/docs/INDEX.md +8 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-tcp.js +58 -0
- package/firewall.js +235 -0
- package/free-trial-banner.js +174 -174
- package/nextjs-auto-capture.js +199 -199
- package/nextjs-middleware.js +186 -186
- package/nextjs-wrapper.js +158 -158
- package/nextjs.js +22 -2
- package/nuxt-server-plugin.mjs +400 -400
- package/package.json +30 -3
- package/resolve-ip.js +77 -0
- package/tracing.js +31 -56
- package/web-vite.mjs +239 -239
package/nextjs-middleware.js
CHANGED
|
@@ -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
|
+
|