securenow 8.11.2 → 8.11.3
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/SKILL-CLI.md +623 -632
- package/TELEMETRY-SQL.md +6 -1
- package/cidr.js +83 -83
- package/cli/client.js +102 -102
- package/cli/diagnostics.js +4 -0
- package/cli/firewall.js +141 -141
- package/cli/fp.js +638 -638
- package/cli/monitor.js +398 -398
- package/cli/run.js +132 -132
- package/cli/security.js +1866 -1856
- package/cli/ui.js +374 -374
- package/cli.js +923 -922
- package/firewall-cloud.js +202 -202
- package/firewall-iptables.js +139 -139
- package/firewall-only.js +46 -46
- package/firewall-tcp.js +74 -74
- package/firewall.js +1470 -1470
- package/mcp/catalog.js +2358 -2346
- package/nextjs-auto-capture.d.ts +29 -29
- package/nextjs-auto-capture.js +273 -273
- package/nextjs-middleware.d.ts +57 -57
- package/nextjs-middleware.js +267 -267
- package/nextjs-webpack-config.js +79 -79
- package/nextjs-wrapper.d.ts +95 -95
- package/nextjs-wrapper.js +233 -233
- package/nuxt.mjs +74 -74
- package/package.json +1 -1
- package/register.d.ts +42 -42
- package/register.js +44 -44
- package/resolve-ip.js +13 -13
- package/telemetry-schema.json +3 -1
- package/tracing.js +853 -853
package/nextjs-middleware.d.ts
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SecureNow Next.js Middleware TypeScript Declarations
|
|
3
|
-
*
|
|
4
|
-
* Provides middleware for capturing request bodies in Next.js API routes.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { NextRequest, NextResponse } from 'next/server';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Next.js middleware function for request body capture
|
|
11
|
-
*
|
|
12
|
-
* @param request - Next.js request object
|
|
13
|
-
* @returns Promise<NextResponse> - Passes through to next handler
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```typescript
|
|
17
|
-
* // middleware.ts
|
|
18
|
-
* import { NextRequest, NextResponse } from 'next/server';
|
|
19
|
-
* import { middleware as securenowMiddleware } from 'securenow/nextjs-middleware';
|
|
20
|
-
*
|
|
21
|
-
* export async function middleware(request: NextRequest) {
|
|
22
|
-
* // Capture body with SecureNow
|
|
23
|
-
* await securenowMiddleware(request);
|
|
24
|
-
*
|
|
25
|
-
* // Your custom middleware logic
|
|
26
|
-
* return NextResponse.next();
|
|
27
|
-
* }
|
|
28
|
-
*
|
|
29
|
-
* export const config = {
|
|
30
|
-
* matcher: '/api/:path*',
|
|
31
|
-
* };
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export function middleware(request: NextRequest): Promise<NextResponse>;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Configuration options for body capture middleware
|
|
38
|
-
*/
|
|
39
|
-
export interface BodyCaptureOptions {
|
|
40
|
-
/**
|
|
41
|
-
* Maximum body size to capture in bytes
|
|
42
|
-
* @default 10240 (10KB)
|
|
43
|
-
*/
|
|
44
|
-
maxBodySize?: number;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Additional sensitive field names to redact
|
|
48
|
-
* @default []
|
|
49
|
-
*/
|
|
50
|
-
sensitiveFields?: string[];
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Enable body capture
|
|
54
|
-
* @default true
|
|
55
|
-
*/
|
|
56
|
-
enabled?: boolean;
|
|
57
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Next.js Middleware TypeScript Declarations
|
|
3
|
+
*
|
|
4
|
+
* Provides middleware for capturing request bodies in Next.js API routes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Next.js middleware function for request body capture
|
|
11
|
+
*
|
|
12
|
+
* @param request - Next.js request object
|
|
13
|
+
* @returns Promise<NextResponse> - Passes through to next handler
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // middleware.ts
|
|
18
|
+
* import { NextRequest, NextResponse } from 'next/server';
|
|
19
|
+
* import { middleware as securenowMiddleware } from 'securenow/nextjs-middleware';
|
|
20
|
+
*
|
|
21
|
+
* export async function middleware(request: NextRequest) {
|
|
22
|
+
* // Capture body with SecureNow
|
|
23
|
+
* await securenowMiddleware(request);
|
|
24
|
+
*
|
|
25
|
+
* // Your custom middleware logic
|
|
26
|
+
* return NextResponse.next();
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* export const config = {
|
|
30
|
+
* matcher: '/api/:path*',
|
|
31
|
+
* };
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function middleware(request: NextRequest): Promise<NextResponse>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configuration options for body capture middleware
|
|
38
|
+
*/
|
|
39
|
+
export interface BodyCaptureOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Maximum body size to capture in bytes
|
|
42
|
+
* @default 10240 (10KB)
|
|
43
|
+
*/
|
|
44
|
+
maxBodySize?: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Additional sensitive field names to redact
|
|
48
|
+
* @default []
|
|
49
|
+
*/
|
|
50
|
+
sensitiveFields?: string[];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Enable body capture
|
|
54
|
+
* @default true
|
|
55
|
+
*/
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
}
|
package/nextjs-middleware.js
CHANGED
|
@@ -1,274 +1,274 @@
|
|
|
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
|
-
const appConfig = require('./app-config');
|
|
20
|
-
|
|
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
|
+
const appConfig = require('./app-config');
|
|
20
|
+
|
|
21
21
|
const { OTP_SENSITIVE_FIELDS, isOtpCodeField, matchesSensitiveField, redactOtpGraphQL, redactOtpQuery } = require('./otp-redaction');
|
|
22
22
|
|
|
23
|
-
// Default sensitive fields to redact from request bodies.
|
|
23
|
+
// Default sensitive fields to redact from request bodies.
|
|
24
24
|
// OTP/code aliases use exact normalized matching; other fields are matched
|
|
25
|
-
// substring-wise against lowercased keys (see redactSensitiveData),
|
|
26
|
-
// so e.g. 'card' also catches 'creditCard' and 'account' also catches
|
|
27
|
-
// 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
|
|
28
|
-
// telemetry value is always safer than a leaked secret. Entries are kept
|
|
29
|
-
// specific enough to avoid nuking broad benign keys (e.g. we use
|
|
30
|
-
// 'firstname'/'lastname'/'fullname', never bare 'name').
|
|
25
|
+
// substring-wise against lowercased keys (see redactSensitiveData),
|
|
26
|
+
// so e.g. 'card' also catches 'creditCard' and 'account' also catches
|
|
27
|
+
// 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
|
|
28
|
+
// telemetry value is always safer than a leaked secret. Entries are kept
|
|
29
|
+
// specific enough to avoid nuking broad benign keys (e.g. we use
|
|
30
|
+
// 'firstname'/'lastname'/'fullname', never bare 'name').
|
|
31
31
|
const DEFAULT_SENSITIVE_FIELDS = [
|
|
32
|
-
...OTP_SENSITIVE_FIELDS,
|
|
33
|
-
// credentials / auth
|
|
34
|
-
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
35
|
-
'access_token', 'auth', 'authorization', 'bearer', 'credentials',
|
|
36
|
-
'mysql_pwd', 'otp', 'mfa', 'totp', 'sessionid', 'session_id',
|
|
37
|
-
'cookie', 'set-cookie',
|
|
38
|
-
// financial
|
|
39
|
-
'stripeToken', 'card', 'cardnumber', 'ccv', 'cvc', 'cvv',
|
|
40
|
-
'iban', 'account', 'accountnumber', 'routing', 'sortcode', 'taxid',
|
|
41
|
-
// PII
|
|
42
|
-
'ssn', 'pin', 'email', 'e_mail', 'phone', 'mobile', 'dob', 'birthdate',
|
|
43
|
-
'firstname', 'lastname', 'fullname', 'address', 'postcode', 'zip',
|
|
44
|
-
'passport', 'license',
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
// Conservative value-shape redactors. Key-name matching misses secrets that
|
|
48
|
-
// land in free-form string values (GraphQL bodies, message fields, etc.), so
|
|
49
|
-
// as a second layer we scrub string VALUES that *look like* a secret/PII.
|
|
50
|
-
// These are intentionally precise/bounded so they don't garble normal prose,
|
|
51
|
-
// and they only ever transform captured telemetry strings (read-only) — never
|
|
52
|
-
// the actual request/response stream. Compiled once at module load.
|
|
53
|
-
const VALUE_REDACTORS = [
|
|
54
|
-
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
55
|
-
// match arbitrary dotted tokens.
|
|
56
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
57
|
-
// Bearer/Basic auth header value embedded in a body string.
|
|
58
|
-
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
59
|
-
// Stripe-style / SecureNow live+test API keys.
|
|
60
|
-
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
61
|
-
// Email addresses (bounded local/domain parts).
|
|
62
|
-
{ name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
|
|
63
|
-
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
64
|
-
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
65
|
-
{ name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
69
|
-
const MAX_VALUE_SCAN_LENGTH = 16384;
|
|
70
|
-
|
|
71
|
-
function luhnValid(digits) {
|
|
72
|
-
let sum = 0;
|
|
73
|
-
let alt = false;
|
|
74
|
-
for (let i = digits.length - 1; i >= 0; i--) {
|
|
75
|
-
let d = digits.charCodeAt(i) - 48;
|
|
76
|
-
if (d < 0 || d > 9) return false;
|
|
77
|
-
if (alt) { d *= 2; if (d > 9) d -= 9; }
|
|
78
|
-
sum += d;
|
|
79
|
-
alt = !alt;
|
|
80
|
-
}
|
|
81
|
-
return sum % 10 === 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Redact obvious secret/PII shapes inside a captured string VALUE.
|
|
86
|
-
* Returns the input unchanged when nothing matches (cheap common case).
|
|
87
|
-
*/
|
|
88
|
-
function redactSensitiveValue(value) {
|
|
89
|
-
if (typeof value !== 'string' || value.length === 0) return value;
|
|
32
|
+
...OTP_SENSITIVE_FIELDS,
|
|
33
|
+
// credentials / auth
|
|
34
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
35
|
+
'access_token', 'auth', 'authorization', 'bearer', 'credentials',
|
|
36
|
+
'mysql_pwd', 'otp', 'mfa', 'totp', 'sessionid', 'session_id',
|
|
37
|
+
'cookie', 'set-cookie',
|
|
38
|
+
// financial
|
|
39
|
+
'stripeToken', 'card', 'cardnumber', 'ccv', 'cvc', 'cvv',
|
|
40
|
+
'iban', 'account', 'accountnumber', 'routing', 'sortcode', 'taxid',
|
|
41
|
+
// PII
|
|
42
|
+
'ssn', 'pin', 'email', 'e_mail', 'phone', 'mobile', 'dob', 'birthdate',
|
|
43
|
+
'firstname', 'lastname', 'fullname', 'address', 'postcode', 'zip',
|
|
44
|
+
'passport', 'license',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Conservative value-shape redactors. Key-name matching misses secrets that
|
|
48
|
+
// land in free-form string values (GraphQL bodies, message fields, etc.), so
|
|
49
|
+
// as a second layer we scrub string VALUES that *look like* a secret/PII.
|
|
50
|
+
// These are intentionally precise/bounded so they don't garble normal prose,
|
|
51
|
+
// and they only ever transform captured telemetry strings (read-only) — never
|
|
52
|
+
// the actual request/response stream. Compiled once at module load.
|
|
53
|
+
const VALUE_REDACTORS = [
|
|
54
|
+
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
55
|
+
// match arbitrary dotted tokens.
|
|
56
|
+
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
57
|
+
// Bearer/Basic auth header value embedded in a body string.
|
|
58
|
+
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
59
|
+
// Stripe-style / SecureNow live+test API keys.
|
|
60
|
+
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
61
|
+
// Email addresses (bounded local/domain parts).
|
|
62
|
+
{ name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
|
|
63
|
+
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
64
|
+
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
65
|
+
{ name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
69
|
+
const MAX_VALUE_SCAN_LENGTH = 16384;
|
|
70
|
+
|
|
71
|
+
function luhnValid(digits) {
|
|
72
|
+
let sum = 0;
|
|
73
|
+
let alt = false;
|
|
74
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
75
|
+
let d = digits.charCodeAt(i) - 48;
|
|
76
|
+
if (d < 0 || d > 9) return false;
|
|
77
|
+
if (alt) { d *= 2; if (d > 9) d -= 9; }
|
|
78
|
+
sum += d;
|
|
79
|
+
alt = !alt;
|
|
80
|
+
}
|
|
81
|
+
return sum % 10 === 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Redact obvious secret/PII shapes inside a captured string VALUE.
|
|
86
|
+
* Returns the input unchanged when nothing matches (cheap common case).
|
|
87
|
+
*/
|
|
88
|
+
function redactSensitiveValue(value) {
|
|
89
|
+
if (typeof value !== 'string' || value.length === 0) return value;
|
|
90
90
|
// Credential assignments in GraphQL/query strings need field-aware
|
|
91
91
|
// redaction even though a short numeric OTP has no unique value shape.
|
|
92
92
|
value = redactOtpQuery(redactOtpGraphQL(value));
|
|
93
|
-
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
94
|
-
let out = value;
|
|
95
|
-
for (const r of VALUE_REDACTORS) {
|
|
96
|
-
r.re.lastIndex = 0;
|
|
97
|
-
if (r.luhn) {
|
|
98
|
-
out = out.replace(r.re, (m) => {
|
|
99
|
-
const digits = m.replace(/[ -]/g, '');
|
|
100
|
-
if (digits.length < 13 || digits.length > 19) return m;
|
|
101
|
-
return luhnValid(digits) ? '[REDACTED]' : m;
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
out = out.replace(r.re, '[REDACTED]');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Redact sensitive fields from an object
|
|
112
|
-
*/
|
|
113
|
-
function escapeRegex(str) {
|
|
114
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
118
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
119
|
-
|
|
120
|
-
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
121
|
-
|
|
122
|
-
for (const key of Object.keys(redacted)) {
|
|
123
|
-
|
|
124
|
-
if (matchesSensitiveField(key, sensitiveFields)) {
|
|
125
|
-
redacted[key] = '[REDACTED]';
|
|
126
|
-
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
127
|
-
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
128
|
-
} else if (typeof redacted[key] === 'string') {
|
|
129
|
-
// Second layer: even if the key name looks benign, scrub values that
|
|
130
|
-
// *look like* a secret/PII (JWT, bearer token, API key, email, card).
|
|
131
|
-
redacted[key] = redactSensitiveValue(redacted[key]);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return redacted;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Redact sensitive data from GraphQL query strings
|
|
140
|
-
*/
|
|
141
|
-
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
142
|
-
if (!query || typeof query !== 'string') return query;
|
|
143
|
-
|
|
144
|
-
let redacted = query;
|
|
145
|
-
|
|
146
|
-
sensitiveFields.filter(field => field === 'otp' || !isOtpCodeField(field)).forEach(field => {
|
|
147
|
-
const escaped = escapeRegex(field);
|
|
148
|
-
const patterns = [
|
|
149
|
-
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
150
|
-
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
patterns.forEach(pattern => {
|
|
154
|
-
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
155
|
-
return suffix ? `${prefix}[REDACTED]${suffix}` : `${prefix}[REDACTED]`;
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
|
|
161
|
-
// body — catches values the key-name pass above can't see.
|
|
162
|
-
redacted = redactOtpGraphQL(redactSensitiveValue(redacted));
|
|
163
|
-
|
|
164
|
-
return redacted;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Next.js Middleware for Body Capture
|
|
169
|
-
*/
|
|
170
|
-
async function middleware(request) {
|
|
171
|
-
const { NextResponse } = require('next/server');
|
|
172
|
-
|
|
173
|
-
// Only capture for POST/PUT/PATCH
|
|
174
|
-
if (!['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
175
|
-
return NextResponse.next();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Get or create a tracer
|
|
179
|
-
const tracer = trace.getTracer('securenow-middleware');
|
|
180
|
-
let span = trace.getActiveSpan();
|
|
181
|
-
let createdSpan = false;
|
|
182
|
-
|
|
183
|
-
// If no active span, create one for this middleware
|
|
184
|
-
if (!span) {
|
|
185
|
-
const url = new URL(request.url);
|
|
186
|
-
span = tracer.startSpan(`middleware ${request.method} ${url.pathname}`);
|
|
187
|
-
createdSpan = true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const contentType = request.headers.get('content-type') || '';
|
|
192
|
-
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
193
|
-
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
194
|
-
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
195
|
-
|
|
196
|
-
// Only capture supported types
|
|
197
|
-
if (contentType.includes('application/json') ||
|
|
198
|
-
contentType.includes('application/graphql')) {
|
|
199
|
-
|
|
200
|
-
// Clone the request to read body without consuming the original
|
|
201
|
-
const clonedRequest = request.clone();
|
|
202
|
-
const bodyText = await clonedRequest.text();
|
|
203
|
-
|
|
204
|
-
if (bodyText.length <= maxBodySize) {
|
|
205
|
-
let redactedBody;
|
|
206
|
-
|
|
207
|
-
if (contentType.includes('application/graphql')) {
|
|
208
|
-
// GraphQL: redact query string
|
|
209
|
-
redactedBody = redactGraphQLQuery(bodyText, allSensitiveFields);
|
|
210
|
-
} else {
|
|
211
|
-
// JSON: parse and redact
|
|
212
|
-
try {
|
|
213
|
-
const parsed = JSON.parse(bodyText);
|
|
214
|
-
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
215
|
-
redactedBody = JSON.stringify(redacted);
|
|
216
|
-
} catch (e) {
|
|
217
|
-
redactedBody = '[UNPARSEABLE - REDACTED FOR SAFETY]';
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
span.setAttributes({
|
|
222
|
-
'http.request.body': redactedBody.substring(0, maxBodySize),
|
|
223
|
-
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
224
|
-
'http.request.body.size': bodyText.length,
|
|
225
|
-
});
|
|
226
|
-
} else {
|
|
227
|
-
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
228
|
-
}
|
|
229
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
230
|
-
const clonedRequest = request.clone();
|
|
231
|
-
const formData = await clonedRequest.formData();
|
|
232
|
-
const parsed = Object.fromEntries(formData);
|
|
233
|
-
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
234
|
-
|
|
235
|
-
span.setAttributes({
|
|
236
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
237
|
-
'http.request.body.type': 'form',
|
|
238
|
-
'http.request.body.size': JSON.stringify(parsed).length,
|
|
239
|
-
});
|
|
240
|
-
} else if (contentType.includes('multipart/form-data')) {
|
|
241
|
-
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
242
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// End span if we created it
|
|
246
|
-
if (createdSpan) {
|
|
247
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
248
|
-
span.end();
|
|
249
|
-
}
|
|
250
|
-
} catch (error) {
|
|
251
|
-
// Silently fail - don't break the request
|
|
252
|
-
console.debug('[securenow] Body capture failed:', error.message);
|
|
253
|
-
|
|
254
|
-
// End span with error if we created it
|
|
255
|
-
if (createdSpan && span) {
|
|
256
|
-
span.setStatus({
|
|
257
|
-
code: SpanStatusCode.ERROR,
|
|
258
|
-
message: error.message
|
|
259
|
-
});
|
|
260
|
-
span.end();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return NextResponse.next();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
module.exports = {
|
|
268
|
-
middleware,
|
|
269
|
-
redactSensitiveData,
|
|
270
|
-
redactGraphQLQuery,
|
|
271
|
-
DEFAULT_SENSITIVE_FIELDS,
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
|
|
93
|
+
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
94
|
+
let out = value;
|
|
95
|
+
for (const r of VALUE_REDACTORS) {
|
|
96
|
+
r.re.lastIndex = 0;
|
|
97
|
+
if (r.luhn) {
|
|
98
|
+
out = out.replace(r.re, (m) => {
|
|
99
|
+
const digits = m.replace(/[ -]/g, '');
|
|
100
|
+
if (digits.length < 13 || digits.length > 19) return m;
|
|
101
|
+
return luhnValid(digits) ? '[REDACTED]' : m;
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
out = out.replace(r.re, '[REDACTED]');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Redact sensitive fields from an object
|
|
112
|
+
*/
|
|
113
|
+
function escapeRegex(str) {
|
|
114
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
118
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
119
|
+
|
|
120
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
121
|
+
|
|
122
|
+
for (const key of Object.keys(redacted)) {
|
|
123
|
+
|
|
124
|
+
if (matchesSensitiveField(key, sensitiveFields)) {
|
|
125
|
+
redacted[key] = '[REDACTED]';
|
|
126
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
127
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
128
|
+
} else if (typeof redacted[key] === 'string') {
|
|
129
|
+
// Second layer: even if the key name looks benign, scrub values that
|
|
130
|
+
// *look like* a secret/PII (JWT, bearer token, API key, email, card).
|
|
131
|
+
redacted[key] = redactSensitiveValue(redacted[key]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return redacted;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Redact sensitive data from GraphQL query strings
|
|
140
|
+
*/
|
|
141
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
142
|
+
if (!query || typeof query !== 'string') return query;
|
|
143
|
+
|
|
144
|
+
let redacted = query;
|
|
145
|
+
|
|
146
|
+
sensitiveFields.filter(field => field === 'otp' || !isOtpCodeField(field)).forEach(field => {
|
|
147
|
+
const escaped = escapeRegex(field);
|
|
148
|
+
const patterns = [
|
|
149
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
150
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
patterns.forEach(pattern => {
|
|
154
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
155
|
+
return suffix ? `${prefix}[REDACTED]${suffix}` : `${prefix}[REDACTED]`;
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
|
|
161
|
+
// body — catches values the key-name pass above can't see.
|
|
162
|
+
redacted = redactOtpGraphQL(redactSensitiveValue(redacted));
|
|
163
|
+
|
|
164
|
+
return redacted;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Next.js Middleware for Body Capture
|
|
169
|
+
*/
|
|
170
|
+
async function middleware(request) {
|
|
171
|
+
const { NextResponse } = require('next/server');
|
|
172
|
+
|
|
173
|
+
// Only capture for POST/PUT/PATCH
|
|
174
|
+
if (!['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
175
|
+
return NextResponse.next();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get or create a tracer
|
|
179
|
+
const tracer = trace.getTracer('securenow-middleware');
|
|
180
|
+
let span = trace.getActiveSpan();
|
|
181
|
+
let createdSpan = false;
|
|
182
|
+
|
|
183
|
+
// If no active span, create one for this middleware
|
|
184
|
+
if (!span) {
|
|
185
|
+
const url = new URL(request.url);
|
|
186
|
+
span = tracer.startSpan(`middleware ${request.method} ${url.pathname}`);
|
|
187
|
+
createdSpan = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const contentType = request.headers.get('content-type') || '';
|
|
192
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
193
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
194
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
195
|
+
|
|
196
|
+
// Only capture supported types
|
|
197
|
+
if (contentType.includes('application/json') ||
|
|
198
|
+
contentType.includes('application/graphql')) {
|
|
199
|
+
|
|
200
|
+
// Clone the request to read body without consuming the original
|
|
201
|
+
const clonedRequest = request.clone();
|
|
202
|
+
const bodyText = await clonedRequest.text();
|
|
203
|
+
|
|
204
|
+
if (bodyText.length <= maxBodySize) {
|
|
205
|
+
let redactedBody;
|
|
206
|
+
|
|
207
|
+
if (contentType.includes('application/graphql')) {
|
|
208
|
+
// GraphQL: redact query string
|
|
209
|
+
redactedBody = redactGraphQLQuery(bodyText, allSensitiveFields);
|
|
210
|
+
} else {
|
|
211
|
+
// JSON: parse and redact
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(bodyText);
|
|
214
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
215
|
+
redactedBody = JSON.stringify(redacted);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
redactedBody = '[UNPARSEABLE - REDACTED FOR SAFETY]';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
span.setAttributes({
|
|
222
|
+
'http.request.body': redactedBody.substring(0, maxBodySize),
|
|
223
|
+
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
224
|
+
'http.request.body.size': bodyText.length,
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
228
|
+
}
|
|
229
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
230
|
+
const clonedRequest = request.clone();
|
|
231
|
+
const formData = await clonedRequest.formData();
|
|
232
|
+
const parsed = Object.fromEntries(formData);
|
|
233
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
234
|
+
|
|
235
|
+
span.setAttributes({
|
|
236
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
237
|
+
'http.request.body.type': 'form',
|
|
238
|
+
'http.request.body.size': JSON.stringify(parsed).length,
|
|
239
|
+
});
|
|
240
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
241
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
242
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// End span if we created it
|
|
246
|
+
if (createdSpan) {
|
|
247
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
248
|
+
span.end();
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
// Silently fail - don't break the request
|
|
252
|
+
console.debug('[securenow] Body capture failed:', error.message);
|
|
253
|
+
|
|
254
|
+
// End span with error if we created it
|
|
255
|
+
if (createdSpan && span) {
|
|
256
|
+
span.setStatus({
|
|
257
|
+
code: SpanStatusCode.ERROR,
|
|
258
|
+
message: error.message
|
|
259
|
+
});
|
|
260
|
+
span.end();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return NextResponse.next();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
middleware,
|
|
269
|
+
redactSensitiveData,
|
|
270
|
+
redactGraphQLQuery,
|
|
271
|
+
DEFAULT_SENSITIVE_FIELDS,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
|