recker 1.0.15-next.eb07368 → 1.0.16
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/README.md +10 -1
- package/dist/ai/providers/anthropic.d.ts.map +1 -1
- package/dist/ai/providers/anthropic.js +4 -1
- package/dist/ai/providers/base.d.ts.map +1 -1
- package/dist/ai/providers/base.js +7 -2
- package/dist/ai/rate-limiter.d.ts.map +1 -1
- package/dist/ai/rate-limiter.js +4 -1
- package/dist/bench/generator.d.ts.map +1 -1
- package/dist/bench/generator.js +7 -3
- package/dist/bench/stats.d.ts.map +1 -1
- package/dist/bench/stats.js +43 -10
- package/dist/cache/memory-storage.d.ts.map +1 -1
- package/dist/cache/memory-storage.js +3 -2
- package/dist/cli/handler.js +14 -14
- package/dist/cli/index.js +533 -79
- package/dist/cli/presets.js +5 -5
- package/dist/cli/tui/ai-chat.js +10 -10
- package/dist/cli/tui/load-dashboard.d.ts.map +1 -1
- package/dist/cli/tui/load-dashboard.js +96 -55
- package/dist/cli/tui/shell.d.ts +3 -0
- package/dist/cli/tui/shell.d.ts.map +1 -1
- package/dist/cli/tui/shell.js +163 -1
- package/dist/cli/tui/websocket.js +17 -17
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +18 -26
- package/dist/core/errors.d.ts +109 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +214 -1
- package/dist/core/request-promise.d.ts.map +1 -1
- package/dist/core/request-promise.js +5 -6
- package/dist/core/response.d.ts.map +1 -1
- package/dist/core/response.js +5 -6
- package/dist/dns/propagation.d.ts +3 -1
- package/dist/dns/propagation.d.ts.map +1 -1
- package/dist/dns/propagation.js +99 -59
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +10 -11
- package/dist/mcp/embeddings-loader.d.ts.map +1 -1
- package/dist/mcp/embeddings-loader.js +12 -2
- package/dist/mcp/geoip-loader.d.ts +11 -0
- package/dist/mcp/geoip-loader.d.ts.map +1 -0
- package/dist/mcp/geoip-loader.js +107 -0
- package/dist/mcp/ip-intel.d.ts +28 -0
- package/dist/mcp/ip-intel.d.ts.map +1 -0
- package/dist/mcp/ip-intel.js +209 -0
- package/dist/mcp/search/hybrid-search.d.ts.map +1 -1
- package/dist/mcp/search/hybrid-search.js +5 -1
- package/dist/mcp/search/math.d.ts.map +1 -1
- package/dist/mcp/search/math.js +5 -1
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +114 -1
- package/dist/plugins/compression.js +4 -2
- package/dist/plugins/har-player.d.ts.map +1 -1
- package/dist/plugins/har-player.js +8 -11
- package/dist/plugins/odata.d.ts.map +1 -1
- package/dist/plugins/odata.js +5 -2
- package/dist/protocols/ftp.d.ts.map +1 -1
- package/dist/protocols/ftp.js +69 -16
- package/dist/protocols/sftp.d.ts.map +1 -1
- package/dist/protocols/sftp.js +13 -3
- package/dist/protocols/telnet.d.ts.map +1 -1
- package/dist/protocols/telnet.js +25 -6
- package/dist/transport/base-udp.d.ts.map +1 -1
- package/dist/transport/base-udp.js +7 -4
- package/dist/transport/udp-response.d.ts.map +1 -1
- package/dist/transport/udp-response.js +10 -3
- package/dist/transport/udp.d.ts.map +1 -1
- package/dist/transport/udp.js +5 -1
- package/dist/transport/undici.d.ts.map +1 -1
- package/dist/transport/undici.js +3 -11
- package/dist/utils/agent-manager.d.ts +1 -0
- package/dist/utils/agent-manager.d.ts.map +1 -1
- package/dist/utils/agent-manager.js +11 -0
- package/dist/utils/client-pool.d.ts.map +1 -1
- package/dist/utils/client-pool.js +4 -1
- package/dist/utils/dns-toolkit.d.ts +88 -1
- package/dist/utils/dns-toolkit.d.ts.map +1 -1
- package/dist/utils/dns-toolkit.js +704 -6
- package/dist/utils/doh.d.ts.map +1 -1
- package/dist/utils/doh.js +13 -16
- package/dist/utils/download.d.ts.map +1 -1
- package/dist/utils/download.js +10 -11
- package/dist/utils/rdap.d.ts +9 -0
- package/dist/utils/rdap.d.ts.map +1 -1
- package/dist/utils/rdap.js +78 -9
- package/dist/utils/security-grader.d.ts +33 -0
- package/dist/utils/security-grader.d.ts.map +1 -1
- package/dist/utils/security-grader.js +548 -43
- package/dist/utils/sparkline.d.ts +18 -0
- package/dist/utils/sparkline.d.ts.map +1 -0
- package/dist/utils/sparkline.js +55 -0
- package/dist/utils/sse.d.ts.map +1 -1
- package/dist/utils/sse.js +5 -6
- package/dist/utils/system-metrics.d.ts +26 -0
- package/dist/utils/system-metrics.d.ts.map +1 -0
- package/dist/utils/system-metrics.js +81 -0
- package/dist/webrtc/index.d.ts.map +1 -1
- package/dist/webrtc/index.js +21 -7
- package/dist/websocket/client.d.ts.map +1 -1
- package/dist/websocket/client.js +13 -16
- package/package.json +3 -2
- package/dist/utils/ip-intel.d.ts +0 -15
- package/dist/utils/ip-intel.d.ts.map +0 -1
- package/dist/utils/ip-intel.js +0 -30
|
@@ -1,91 +1,556 @@
|
|
|
1
|
+
const CSP_FETCH_DIRECTIVES = [
|
|
2
|
+
'default-src', 'script-src', 'style-src', 'img-src', 'font-src',
|
|
3
|
+
'connect-src', 'media-src', 'object-src', 'frame-src', 'child-src',
|
|
4
|
+
'worker-src', 'manifest-src', 'prefetch-src'
|
|
5
|
+
];
|
|
6
|
+
const CSP_DOCUMENT_DIRECTIVES = [
|
|
7
|
+
'base-uri', 'sandbox', 'form-action', 'frame-ancestors',
|
|
8
|
+
'navigate-to'
|
|
9
|
+
];
|
|
10
|
+
const CSP_REPORTING_DIRECTIVES = [
|
|
11
|
+
'report-uri', 'report-to'
|
|
12
|
+
];
|
|
13
|
+
const CSP_SPECIAL_DIRECTIVES = [
|
|
14
|
+
'upgrade-insecure-requests', 'block-all-mixed-content',
|
|
15
|
+
'require-trusted-types-for', 'trusted-types'
|
|
16
|
+
];
|
|
17
|
+
const DANGEROUS_CSP_VALUES = {
|
|
18
|
+
"'unsafe-inline'": 'Allows inline scripts/styles. Use nonces or hashes instead.',
|
|
19
|
+
"'unsafe-eval'": 'Allows eval() and similar. Major XSS risk.',
|
|
20
|
+
"'unsafe-hashes'": 'Allows specific inline event handlers. Prefer nonces.',
|
|
21
|
+
'*': 'Wildcard allows loading from any source.',
|
|
22
|
+
'data:': 'Data URIs can be used for XSS attacks.',
|
|
23
|
+
'blob:': 'Blob URLs can bypass CSP in some cases.',
|
|
24
|
+
'http:': 'Allows insecure HTTP sources. Use HTTPS only.',
|
|
25
|
+
};
|
|
26
|
+
const SAFE_CSP_VALUES = new Set([
|
|
27
|
+
"'self'", "'none'", "'strict-dynamic'", "'report-sample'",
|
|
28
|
+
'https:', "'wasm-unsafe-eval'"
|
|
29
|
+
]);
|
|
30
|
+
export function analyzeCSP(cspValue) {
|
|
31
|
+
const directives = [];
|
|
32
|
+
const issues = [];
|
|
33
|
+
let score = 100;
|
|
34
|
+
let hasUnsafeInline = false;
|
|
35
|
+
let hasUnsafeEval = false;
|
|
36
|
+
let hasWildcard = false;
|
|
37
|
+
const parts = cspValue.split(';').map(p => p.trim()).filter(Boolean);
|
|
38
|
+
const directiveNames = new Set();
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
const [name, ...values] = part.split(/\s+/);
|
|
41
|
+
const directiveName = name.toLowerCase();
|
|
42
|
+
directiveNames.add(directiveName);
|
|
43
|
+
const directive = {
|
|
44
|
+
name: directiveName,
|
|
45
|
+
values,
|
|
46
|
+
issues: [],
|
|
47
|
+
severity: 'safe'
|
|
48
|
+
};
|
|
49
|
+
for (const val of values) {
|
|
50
|
+
const lowerVal = val.toLowerCase();
|
|
51
|
+
if (lowerVal === "'unsafe-inline'") {
|
|
52
|
+
hasUnsafeInline = true;
|
|
53
|
+
directive.issues.push(DANGEROUS_CSP_VALUES["'unsafe-inline'"]);
|
|
54
|
+
directive.severity = 'dangerous';
|
|
55
|
+
}
|
|
56
|
+
else if (lowerVal === "'unsafe-eval'") {
|
|
57
|
+
hasUnsafeEval = true;
|
|
58
|
+
directive.issues.push(DANGEROUS_CSP_VALUES["'unsafe-eval'"]);
|
|
59
|
+
directive.severity = 'dangerous';
|
|
60
|
+
}
|
|
61
|
+
else if (val === '*') {
|
|
62
|
+
hasWildcard = true;
|
|
63
|
+
directive.issues.push(DANGEROUS_CSP_VALUES['*']);
|
|
64
|
+
directive.severity = 'dangerous';
|
|
65
|
+
}
|
|
66
|
+
else if (lowerVal === 'data:' && ['script-src', 'default-src'].includes(directiveName)) {
|
|
67
|
+
directive.issues.push(DANGEROUS_CSP_VALUES['data:']);
|
|
68
|
+
directive.severity = 'warn';
|
|
69
|
+
}
|
|
70
|
+
else if (lowerVal === 'http:') {
|
|
71
|
+
directive.issues.push(DANGEROUS_CSP_VALUES['http:']);
|
|
72
|
+
directive.severity = 'warn';
|
|
73
|
+
}
|
|
74
|
+
else if (val.startsWith('*.') || val.includes('*')) {
|
|
75
|
+
directive.issues.push(`Wildcard domain "${val}" is overly permissive.`);
|
|
76
|
+
directive.severity = directive.severity === 'dangerous' ? 'dangerous' : 'warn';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
directives.push(directive);
|
|
80
|
+
}
|
|
81
|
+
const missingDirectives = [];
|
|
82
|
+
if (!directiveNames.has('default-src')) {
|
|
83
|
+
missingDirectives.push('default-src');
|
|
84
|
+
issues.push('Missing default-src directive. Fallback behavior is unpredictable.');
|
|
85
|
+
score -= 10;
|
|
86
|
+
}
|
|
87
|
+
if (!directiveNames.has('object-src') && !directiveNames.has('default-src')) {
|
|
88
|
+
missingDirectives.push('object-src');
|
|
89
|
+
issues.push("Missing object-src. Consider adding object-src 'none' to block plugins.");
|
|
90
|
+
score -= 5;
|
|
91
|
+
}
|
|
92
|
+
if (!directiveNames.has('base-uri')) {
|
|
93
|
+
missingDirectives.push('base-uri');
|
|
94
|
+
issues.push("Missing base-uri. Consider adding base-uri 'self' to prevent base tag injection.");
|
|
95
|
+
score -= 5;
|
|
96
|
+
}
|
|
97
|
+
if (!directiveNames.has('form-action')) {
|
|
98
|
+
missingDirectives.push('form-action');
|
|
99
|
+
issues.push('Missing form-action. Form submissions can be hijacked.');
|
|
100
|
+
score -= 5;
|
|
101
|
+
}
|
|
102
|
+
if (!directiveNames.has('frame-ancestors')) {
|
|
103
|
+
missingDirectives.push('frame-ancestors');
|
|
104
|
+
issues.push('Missing frame-ancestors. Use this instead of X-Frame-Options for modern browsers.');
|
|
105
|
+
score -= 5;
|
|
106
|
+
}
|
|
107
|
+
if (hasUnsafeInline) {
|
|
108
|
+
issues.push("'unsafe-inline' allows inline scripts. Major XSS vulnerability.");
|
|
109
|
+
score -= 20;
|
|
110
|
+
}
|
|
111
|
+
if (hasUnsafeEval) {
|
|
112
|
+
issues.push("'unsafe-eval' allows eval(). Dangerous for code injection.");
|
|
113
|
+
score -= 20;
|
|
114
|
+
}
|
|
115
|
+
if (hasWildcard) {
|
|
116
|
+
issues.push('Wildcard (*) source allows loading from any origin.');
|
|
117
|
+
score -= 15;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
raw: cspValue,
|
|
121
|
+
directives,
|
|
122
|
+
issues,
|
|
123
|
+
score: Math.max(0, score),
|
|
124
|
+
hasUnsafeInline,
|
|
125
|
+
hasUnsafeEval,
|
|
126
|
+
hasWildcard,
|
|
127
|
+
missingDirectives
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function generateRecommendedCSP(options = {}) {
|
|
131
|
+
const { strictMode = true, allowInlineStyles = false, trustedDomains = [] } = options;
|
|
132
|
+
const directives = [];
|
|
133
|
+
directives.push("default-src 'self'");
|
|
134
|
+
if (strictMode) {
|
|
135
|
+
directives.push("script-src 'self' 'strict-dynamic'");
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
directives.push("script-src 'self'" + (trustedDomains.length ? ' ' + trustedDomains.join(' ') : ''));
|
|
139
|
+
}
|
|
140
|
+
if (allowInlineStyles) {
|
|
141
|
+
directives.push("style-src 'self' 'unsafe-inline'");
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
directives.push("style-src 'self'");
|
|
145
|
+
}
|
|
146
|
+
directives.push("img-src 'self' data: https:");
|
|
147
|
+
directives.push("font-src 'self'");
|
|
148
|
+
directives.push("connect-src 'self'" + (trustedDomains.length ? ' ' + trustedDomains.join(' ') : ''));
|
|
149
|
+
directives.push("object-src 'none'");
|
|
150
|
+
directives.push("base-uri 'self'");
|
|
151
|
+
directives.push("form-action 'self'");
|
|
152
|
+
directives.push("frame-ancestors 'none'");
|
|
153
|
+
directives.push('upgrade-insecure-requests');
|
|
154
|
+
return directives.join('; ');
|
|
155
|
+
}
|
|
1
156
|
const HEADERS_CHECKS = [
|
|
2
157
|
{
|
|
3
158
|
header: 'strict-transport-security',
|
|
4
159
|
weight: 25,
|
|
160
|
+
category: 'transport',
|
|
5
161
|
check: (val) => {
|
|
6
|
-
if (!val)
|
|
7
|
-
return {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
162
|
+
if (!val) {
|
|
163
|
+
return {
|
|
164
|
+
status: 'fail',
|
|
165
|
+
message: 'HSTS not enabled. Vulnerable to SSL stripping attacks.',
|
|
166
|
+
recommendation: 'Add: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload'
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const maxAge = parseInt(val.match(/max-age=(\d+)/i)?.[1] || '0');
|
|
170
|
+
const hasSubDomains = val.toLowerCase().includes('includesubdomains');
|
|
171
|
+
const hasPreload = val.toLowerCase().includes('preload');
|
|
172
|
+
if (maxAge < 86400) {
|
|
173
|
+
return {
|
|
174
|
+
status: 'fail',
|
|
175
|
+
message: `HSTS max-age too short (${maxAge}s). Minimum 1 day recommended.`,
|
|
176
|
+
recommendation: 'Set max-age to at least 31536000 (1 year)'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (maxAge < 15552000) {
|
|
180
|
+
return {
|
|
181
|
+
status: 'warn',
|
|
182
|
+
message: `HSTS max-age is ${Math.floor(maxAge / 86400)} days. 6+ months recommended.`,
|
|
183
|
+
recommendation: 'Increase max-age to 31536000 (1 year) for better security'
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!hasSubDomains) {
|
|
187
|
+
return {
|
|
188
|
+
status: 'warn',
|
|
189
|
+
message: 'HSTS does not include subdomains.',
|
|
190
|
+
recommendation: 'Add includeSubDomains to protect all subdomains'
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (hasPreload) {
|
|
194
|
+
return { status: 'pass', message: 'HSTS enabled with preload. Excellent!' };
|
|
195
|
+
}
|
|
196
|
+
return { status: 'pass', message: 'HSTS enabled with good configuration.' };
|
|
16
197
|
}
|
|
17
198
|
},
|
|
18
199
|
{
|
|
19
200
|
header: 'content-security-policy',
|
|
20
201
|
weight: 25,
|
|
202
|
+
category: 'content',
|
|
21
203
|
check: (val) => {
|
|
22
|
-
if (!val)
|
|
23
|
-
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
204
|
+
if (!val) {
|
|
205
|
+
return {
|
|
206
|
+
status: 'fail',
|
|
207
|
+
message: 'CSP is missing. Site is vulnerable to XSS attacks.',
|
|
208
|
+
recommendation: "Add a CSP. Start with: Content-Security-Policy: default-src 'self'"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const analysis = analyzeCSP(val);
|
|
212
|
+
if (analysis.hasUnsafeEval && analysis.hasUnsafeInline) {
|
|
213
|
+
return {
|
|
214
|
+
status: 'fail',
|
|
215
|
+
message: 'CSP has both unsafe-inline and unsafe-eval. Provides minimal protection.',
|
|
216
|
+
recommendation: 'Remove unsafe directives. Use nonces or hashes for inline scripts.'
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (analysis.hasWildcard) {
|
|
220
|
+
return {
|
|
221
|
+
status: 'warn',
|
|
222
|
+
message: 'CSP contains wildcard (*). Too permissive.',
|
|
223
|
+
recommendation: 'Replace wildcards with specific trusted domains.'
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (analysis.hasUnsafeInline) {
|
|
227
|
+
return {
|
|
228
|
+
status: 'warn',
|
|
229
|
+
message: "CSP has 'unsafe-inline'. Consider using nonces or hashes.",
|
|
230
|
+
recommendation: "Use 'strict-dynamic' with nonces for script-src."
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (analysis.hasUnsafeEval) {
|
|
234
|
+
return {
|
|
235
|
+
status: 'warn',
|
|
236
|
+
message: "CSP has 'unsafe-eval'. Allows eval() which is dangerous.",
|
|
237
|
+
recommendation: 'Remove unsafe-eval. Refactor code to avoid eval().'
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (analysis.missingDirectives.length > 2) {
|
|
241
|
+
return {
|
|
242
|
+
status: 'warn',
|
|
243
|
+
message: `CSP missing important directives: ${analysis.missingDirectives.join(', ')}`,
|
|
244
|
+
recommendation: `Add: ${analysis.missingDirectives.map(d => `${d} 'self'`).join('; ')}`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return { status: 'pass', message: 'CSP is well configured.' };
|
|
29
248
|
}
|
|
30
249
|
},
|
|
31
250
|
{
|
|
32
251
|
header: 'x-frame-options',
|
|
33
|
-
weight:
|
|
252
|
+
weight: 10,
|
|
253
|
+
category: 'framing',
|
|
34
254
|
check: (val) => {
|
|
35
|
-
if (!val)
|
|
36
|
-
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
255
|
+
if (!val) {
|
|
256
|
+
return {
|
|
257
|
+
status: 'warn',
|
|
258
|
+
message: 'Missing X-Frame-Options. Use CSP frame-ancestors instead.',
|
|
259
|
+
recommendation: 'Add X-Frame-Options: DENY or use CSP frame-ancestors'
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const upper = val.toUpperCase();
|
|
263
|
+
if (upper === 'DENY') {
|
|
264
|
+
return { status: 'pass', message: 'Clickjacking protection: DENY (strictest).' };
|
|
265
|
+
}
|
|
266
|
+
if (upper === 'SAMEORIGIN') {
|
|
267
|
+
return { status: 'pass', message: 'Clickjacking protection: SAMEORIGIN.' };
|
|
268
|
+
}
|
|
269
|
+
if (upper.startsWith('ALLOW-FROM')) {
|
|
270
|
+
return {
|
|
271
|
+
status: 'warn',
|
|
272
|
+
message: 'ALLOW-FROM is deprecated and not supported in modern browsers.',
|
|
273
|
+
recommendation: 'Use CSP frame-ancestors instead of ALLOW-FROM'
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return { status: 'warn', message: `Unknown X-Frame-Options value: ${val}` };
|
|
40
277
|
}
|
|
41
278
|
},
|
|
42
279
|
{
|
|
43
280
|
header: 'x-content-type-options',
|
|
44
281
|
weight: 10,
|
|
282
|
+
category: 'content',
|
|
45
283
|
check: (val) => {
|
|
46
|
-
if (!val)
|
|
47
|
-
return {
|
|
48
|
-
|
|
284
|
+
if (!val) {
|
|
285
|
+
return {
|
|
286
|
+
status: 'fail',
|
|
287
|
+
message: 'Missing X-Content-Type-Options. MIME sniffing attacks possible.',
|
|
288
|
+
recommendation: 'Add: X-Content-Type-Options: nosniff'
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (val.toLowerCase() === 'nosniff') {
|
|
49
292
|
return { status: 'pass', message: 'MIME sniffing disabled.' };
|
|
50
|
-
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
status: 'fail',
|
|
296
|
+
message: `Invalid value "${val}". Must be "nosniff".`,
|
|
297
|
+
recommendation: 'Set value to exactly: nosniff'
|
|
298
|
+
};
|
|
51
299
|
}
|
|
52
300
|
},
|
|
53
301
|
{
|
|
54
302
|
header: 'referrer-policy',
|
|
55
303
|
weight: 10,
|
|
304
|
+
category: 'content',
|
|
56
305
|
check: (val) => {
|
|
57
|
-
if (!val)
|
|
58
|
-
return {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
306
|
+
if (!val) {
|
|
307
|
+
return {
|
|
308
|
+
status: 'warn',
|
|
309
|
+
message: 'Missing Referrer-Policy. URL may leak to third parties.',
|
|
310
|
+
recommendation: 'Add: Referrer-Policy: strict-origin-when-cross-origin'
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const policies = val.toLowerCase().split(',').map(p => p.trim());
|
|
314
|
+
const safePolicies = ['no-referrer', 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin'];
|
|
315
|
+
const unsafePolicies = ['unsafe-url', 'no-referrer-when-downgrade'];
|
|
316
|
+
for (const policy of policies) {
|
|
317
|
+
if (unsafePolicies.includes(policy)) {
|
|
318
|
+
return {
|
|
319
|
+
status: 'warn',
|
|
320
|
+
message: `"${policy}" may leak referrer to third parties.`,
|
|
321
|
+
recommendation: 'Use strict-origin-when-cross-origin for balanced privacy'
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (policies.some(p => safePolicies.includes(p))) {
|
|
326
|
+
return { status: 'pass', message: 'Referrer leakage properly restricted.' };
|
|
327
|
+
}
|
|
328
|
+
return { status: 'warn', message: `Unknown policy: ${val}` };
|
|
62
329
|
}
|
|
63
330
|
},
|
|
64
331
|
{
|
|
65
332
|
header: 'permissions-policy',
|
|
66
333
|
weight: 10,
|
|
334
|
+
category: 'content',
|
|
67
335
|
check: (val) => {
|
|
68
|
-
if (!val)
|
|
69
|
-
return {
|
|
336
|
+
if (!val) {
|
|
337
|
+
return {
|
|
338
|
+
status: 'warn',
|
|
339
|
+
message: 'Missing Permissions-Policy. Browser features unrestricted.',
|
|
340
|
+
recommendation: 'Add: Permissions-Policy: geolocation=(), camera=(), microphone=()'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (val.includes('*')) {
|
|
344
|
+
return {
|
|
345
|
+
status: 'warn',
|
|
346
|
+
message: 'Permissions-Policy allows all origins for some features.',
|
|
347
|
+
recommendation: 'Restrict features to self or specific origins'
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const features = val.split(',').length;
|
|
351
|
+
if (features >= 5) {
|
|
352
|
+
return { status: 'pass', message: `Permissions-Policy restricts ${features} features.` };
|
|
353
|
+
}
|
|
70
354
|
return { status: 'pass', message: 'Permissions-Policy enabled.' };
|
|
71
355
|
}
|
|
72
356
|
},
|
|
73
357
|
{
|
|
74
|
-
header: '
|
|
358
|
+
header: 'cross-origin-opener-policy',
|
|
359
|
+
weight: 8,
|
|
360
|
+
category: 'isolation',
|
|
361
|
+
check: (val) => {
|
|
362
|
+
if (!val) {
|
|
363
|
+
return {
|
|
364
|
+
status: 'warn',
|
|
365
|
+
message: 'Missing COOP. Site may be vulnerable to cross-origin attacks.',
|
|
366
|
+
recommendation: 'Add: Cross-Origin-Opener-Policy: same-origin'
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const lower = val.toLowerCase();
|
|
370
|
+
if (lower === 'same-origin') {
|
|
371
|
+
return { status: 'pass', message: 'COOP: same-origin. Strong isolation enabled.' };
|
|
372
|
+
}
|
|
373
|
+
if (lower === 'same-origin-allow-popups') {
|
|
374
|
+
return { status: 'pass', message: 'COOP allows popups but maintains isolation.' };
|
|
375
|
+
}
|
|
376
|
+
if (lower === 'unsafe-none') {
|
|
377
|
+
return {
|
|
378
|
+
status: 'warn',
|
|
379
|
+
message: 'COOP is unsafe-none. No isolation.',
|
|
380
|
+
recommendation: 'Use same-origin for better security'
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return { status: 'warn', message: `Unknown COOP value: ${val}` };
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
header: 'cross-origin-embedder-policy',
|
|
388
|
+
weight: 8,
|
|
389
|
+
category: 'isolation',
|
|
390
|
+
check: (val) => {
|
|
391
|
+
if (!val) {
|
|
392
|
+
return {
|
|
393
|
+
status: 'warn',
|
|
394
|
+
message: 'Missing COEP. Required for SharedArrayBuffer and high-resolution timers.',
|
|
395
|
+
recommendation: 'Add: Cross-Origin-Embedder-Policy: require-corp'
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const lower = val.toLowerCase();
|
|
399
|
+
if (lower === 'require-corp') {
|
|
400
|
+
return { status: 'pass', message: 'COEP: require-corp. Cross-origin isolation enabled.' };
|
|
401
|
+
}
|
|
402
|
+
if (lower === 'credentialless') {
|
|
403
|
+
return { status: 'pass', message: 'COEP: credentialless. Good isolation with flexibility.' };
|
|
404
|
+
}
|
|
405
|
+
if (lower === 'unsafe-none') {
|
|
406
|
+
return {
|
|
407
|
+
status: 'warn',
|
|
408
|
+
message: 'COEP is unsafe-none. No cross-origin isolation.',
|
|
409
|
+
recommendation: 'Use require-corp or credentialless'
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return { status: 'warn', message: `Unknown COEP value: ${val}` };
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
header: 'cross-origin-resource-policy',
|
|
417
|
+
weight: 5,
|
|
418
|
+
category: 'isolation',
|
|
419
|
+
check: (val) => {
|
|
420
|
+
if (!val) {
|
|
421
|
+
return {
|
|
422
|
+
status: 'warn',
|
|
423
|
+
message: 'Missing CORP. Resources can be embedded by any site.',
|
|
424
|
+
recommendation: 'Add: Cross-Origin-Resource-Policy: same-origin'
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const lower = val.toLowerCase();
|
|
428
|
+
if (lower === 'same-origin') {
|
|
429
|
+
return { status: 'pass', message: 'CORP: same-origin. Strictest embedding restriction.' };
|
|
430
|
+
}
|
|
431
|
+
if (lower === 'same-site') {
|
|
432
|
+
return { status: 'pass', message: 'CORP: same-site. Allows same-site embedding.' };
|
|
433
|
+
}
|
|
434
|
+
if (lower === 'cross-origin') {
|
|
435
|
+
return {
|
|
436
|
+
status: 'warn',
|
|
437
|
+
message: 'CORP allows cross-origin embedding.',
|
|
438
|
+
recommendation: 'Use same-origin or same-site for sensitive resources'
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return { status: 'warn', message: `Unknown CORP value: ${val}` };
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
header: 'x-xss-protection',
|
|
75
446
|
weight: 0,
|
|
447
|
+
category: 'content',
|
|
76
448
|
check: (val) => {
|
|
77
|
-
if (val)
|
|
78
|
-
return { status: '
|
|
79
|
-
|
|
449
|
+
if (!val) {
|
|
450
|
+
return { status: 'pass', message: 'X-XSS-Protection not set (deprecated header).' };
|
|
451
|
+
}
|
|
452
|
+
if (val === '0') {
|
|
453
|
+
return { status: 'pass', message: 'XSS filter disabled (recommended for CSP sites).' };
|
|
454
|
+
}
|
|
455
|
+
if (val.includes('1') && val.includes('mode=block')) {
|
|
456
|
+
return {
|
|
457
|
+
status: 'warn',
|
|
458
|
+
message: 'X-XSS-Protection is deprecated. Use CSP instead.',
|
|
459
|
+
recommendation: 'Remove this header and rely on CSP'
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
status: 'warn',
|
|
464
|
+
message: 'X-XSS-Protection is deprecated.',
|
|
465
|
+
recommendation: 'Remove this header. Use Content-Security-Policy instead.'
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
header: 'server',
|
|
471
|
+
weight: 2,
|
|
472
|
+
category: 'info-leak',
|
|
473
|
+
check: (val) => {
|
|
474
|
+
if (!val) {
|
|
475
|
+
return { status: 'pass', message: 'Server header hidden.' };
|
|
476
|
+
}
|
|
477
|
+
if (/\d+\.\d+/.test(val)) {
|
|
478
|
+
return {
|
|
479
|
+
status: 'warn',
|
|
480
|
+
message: `Server header exposes version: "${val}"`,
|
|
481
|
+
recommendation: 'Remove version numbers from Server header'
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
status: 'warn',
|
|
486
|
+
message: `Server header reveals: "${val}"`,
|
|
487
|
+
recommendation: 'Consider hiding or minimizing Server header'
|
|
488
|
+
};
|
|
80
489
|
}
|
|
81
490
|
},
|
|
82
491
|
{
|
|
83
492
|
header: 'x-powered-by',
|
|
84
493
|
weight: 5,
|
|
494
|
+
category: 'info-leak',
|
|
495
|
+
check: (val) => {
|
|
496
|
+
if (!val) {
|
|
497
|
+
return { status: 'pass', message: 'Technology stack hidden.' };
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
status: 'fail',
|
|
501
|
+
message: `X-Powered-By exposes: "${val}"`,
|
|
502
|
+
recommendation: 'Remove X-Powered-By header (e.g., app.disable("x-powered-by") in Express)'
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
header: 'x-dns-prefetch-control',
|
|
508
|
+
weight: 2,
|
|
509
|
+
category: 'content',
|
|
510
|
+
check: (val) => {
|
|
511
|
+
if (!val) {
|
|
512
|
+
return { status: 'pass', message: 'DNS prefetch uses browser default.' };
|
|
513
|
+
}
|
|
514
|
+
if (val.toLowerCase() === 'off') {
|
|
515
|
+
return { status: 'pass', message: 'DNS prefetching disabled for privacy.' };
|
|
516
|
+
}
|
|
517
|
+
if (val.toLowerCase() === 'on') {
|
|
518
|
+
return {
|
|
519
|
+
status: 'warn',
|
|
520
|
+
message: 'DNS prefetching enabled. May leak browsing intent.',
|
|
521
|
+
recommendation: 'Set to "off" for privacy-sensitive sites'
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return { status: 'warn', message: `Unknown value: ${val}` };
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
header: 'cache-control',
|
|
529
|
+
weight: 5,
|
|
530
|
+
category: 'content',
|
|
85
531
|
check: (val) => {
|
|
86
|
-
if (val)
|
|
87
|
-
return {
|
|
88
|
-
|
|
532
|
+
if (!val) {
|
|
533
|
+
return {
|
|
534
|
+
status: 'warn',
|
|
535
|
+
message: 'No Cache-Control header. Browser may cache sensitive content.',
|
|
536
|
+
recommendation: 'Add: Cache-Control: no-store for sensitive pages'
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const lower = val.toLowerCase();
|
|
540
|
+
if (lower.includes('no-store')) {
|
|
541
|
+
return { status: 'pass', message: 'Cache-Control prevents caching of sensitive data.' };
|
|
542
|
+
}
|
|
543
|
+
if (lower.includes('private')) {
|
|
544
|
+
return { status: 'pass', message: 'Cache-Control set to private.' };
|
|
545
|
+
}
|
|
546
|
+
if (lower.includes('public') && !lower.includes('no-cache')) {
|
|
547
|
+
return {
|
|
548
|
+
status: 'warn',
|
|
549
|
+
message: 'Public caching enabled. May expose sensitive data.',
|
|
550
|
+
recommendation: 'Use no-store for sensitive content'
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { status: 'pass', message: 'Cache-Control configured.' };
|
|
89
554
|
}
|
|
90
555
|
}
|
|
91
556
|
];
|
|
@@ -93,15 +558,27 @@ export function analyzeSecurityHeaders(headers) {
|
|
|
93
558
|
let totalScore = 100;
|
|
94
559
|
let penalty = 0;
|
|
95
560
|
const details = [];
|
|
561
|
+
let passed = 0;
|
|
562
|
+
let warnings = 0;
|
|
563
|
+
let failed = 0;
|
|
564
|
+
let cspAnalysis;
|
|
96
565
|
for (const check of HEADERS_CHECKS) {
|
|
97
566
|
const value = headers.get(check.header);
|
|
98
567
|
const result = check.check(value || undefined);
|
|
568
|
+
if (check.header === 'content-security-policy' && value) {
|
|
569
|
+
cspAnalysis = analyzeCSP(value);
|
|
570
|
+
}
|
|
99
571
|
let itemPenalty = 0;
|
|
100
572
|
if (result.status === 'fail') {
|
|
101
573
|
itemPenalty = check.weight;
|
|
574
|
+
failed++;
|
|
102
575
|
}
|
|
103
576
|
else if (result.status === 'warn') {
|
|
104
577
|
itemPenalty = Math.ceil(check.weight / 2);
|
|
578
|
+
warnings++;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
passed++;
|
|
105
582
|
}
|
|
106
583
|
penalty += itemPenalty;
|
|
107
584
|
details.push({
|
|
@@ -109,7 +586,8 @@ export function analyzeSecurityHeaders(headers) {
|
|
|
109
586
|
value: value || undefined,
|
|
110
587
|
status: result.status,
|
|
111
588
|
score: -itemPenalty,
|
|
112
|
-
message: result.message
|
|
589
|
+
message: result.message,
|
|
590
|
+
recommendation: result.recommendation
|
|
113
591
|
});
|
|
114
592
|
}
|
|
115
593
|
const finalScore = Math.max(0, totalScore - penalty);
|
|
@@ -127,6 +605,33 @@ export function analyzeSecurityHeaders(headers) {
|
|
|
127
605
|
return {
|
|
128
606
|
grade,
|
|
129
607
|
score: finalScore,
|
|
130
|
-
details
|
|
608
|
+
details,
|
|
609
|
+
csp: cspAnalysis,
|
|
610
|
+
summary: { passed, warnings, failed }
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
export function quickSecurityCheck(headers) {
|
|
614
|
+
const criticalIssues = [];
|
|
615
|
+
if (!headers.get('strict-transport-security')) {
|
|
616
|
+
criticalIssues.push('No HSTS - vulnerable to SSL stripping');
|
|
617
|
+
}
|
|
618
|
+
const csp = headers.get('content-security-policy');
|
|
619
|
+
if (!csp) {
|
|
620
|
+
criticalIssues.push('No CSP - vulnerable to XSS');
|
|
621
|
+
}
|
|
622
|
+
else if (csp.includes("'unsafe-inline'") && csp.includes("'unsafe-eval'")) {
|
|
623
|
+
criticalIssues.push('CSP too permissive (unsafe-inline + unsafe-eval)');
|
|
624
|
+
}
|
|
625
|
+
const xfo = headers.get('x-frame-options');
|
|
626
|
+
const frameAncestors = csp?.includes('frame-ancestors');
|
|
627
|
+
if (!xfo && !frameAncestors) {
|
|
628
|
+
criticalIssues.push('No clickjacking protection');
|
|
629
|
+
}
|
|
630
|
+
if (!headers.get('x-content-type-options')) {
|
|
631
|
+
criticalIssues.push('MIME sniffing not disabled');
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
secure: criticalIssues.length === 0,
|
|
635
|
+
criticalIssues
|
|
131
636
|
};
|
|
132
637
|
}
|