recker 1.0.28 → 1.0.29
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/dist/cli/tui/shell.d.ts +1 -0
- package/dist/cli/tui/shell.js +339 -5
- package/dist/scrape/index.d.ts +2 -0
- package/dist/scrape/index.js +1 -0
- package/dist/scrape/spider.d.ts +61 -0
- package/dist/scrape/spider.js +250 -0
- package/dist/seo/analyzer.js +27 -0
- package/dist/seo/index.d.ts +3 -1
- package/dist/seo/index.js +1 -0
- package/dist/seo/rules/accessibility.js +620 -54
- package/dist/seo/rules/best-practices.d.ts +2 -0
- package/dist/seo/rules/best-practices.js +188 -0
- package/dist/seo/rules/crawl.d.ts +2 -0
- package/dist/seo/rules/crawl.js +307 -0
- package/dist/seo/rules/cwv.d.ts +2 -0
- package/dist/seo/rules/cwv.js +337 -0
- package/dist/seo/rules/ecommerce.d.ts +2 -0
- package/dist/seo/rules/ecommerce.js +252 -0
- package/dist/seo/rules/i18n.d.ts +2 -0
- package/dist/seo/rules/i18n.js +222 -0
- package/dist/seo/rules/index.d.ts +32 -0
- package/dist/seo/rules/index.js +71 -0
- package/dist/seo/rules/internal-linking.d.ts +2 -0
- package/dist/seo/rules/internal-linking.js +375 -0
- package/dist/seo/rules/local.d.ts +2 -0
- package/dist/seo/rules/local.js +265 -0
- package/dist/seo/rules/pwa.d.ts +2 -0
- package/dist/seo/rules/pwa.js +302 -0
- package/dist/seo/rules/readability.d.ts +2 -0
- package/dist/seo/rules/readability.js +255 -0
- package/dist/seo/rules/security.js +406 -28
- package/dist/seo/rules/social.d.ts +2 -0
- package/dist/seo/rules/social.js +373 -0
- package/dist/seo/rules/types.d.ts +155 -0
- package/dist/seo/seo-spider.d.ts +47 -0
- package/dist/seo/seo-spider.js +362 -0
- package/dist/seo/types.d.ts +24 -0
- package/package.json +1 -1
|
@@ -8,7 +8,15 @@ export const securityRules = [
|
|
|
8
8
|
description: 'Page must be served over HTTPS',
|
|
9
9
|
check: (ctx) => {
|
|
10
10
|
if (ctx.isHttps === false) {
|
|
11
|
-
return createResult({ id: 'https-required', name: 'HTTPS', category: 'security', severity: 'error' }, 'fail', 'Page is not served over HTTPS', {
|
|
11
|
+
return createResult({ id: 'https-required', name: 'HTTPS', category: 'security', severity: 'error' }, 'fail', 'Page is not served over HTTPS', {
|
|
12
|
+
recommendation: 'Enable HTTPS for all pages',
|
|
13
|
+
evidence: {
|
|
14
|
+
found: 'HTTP',
|
|
15
|
+
expected: 'HTTPS',
|
|
16
|
+
impact: 'Browsers show "Not Secure" warning, affects SEO ranking',
|
|
17
|
+
learnMore: 'https://web.dev/why-https-matters/',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
12
20
|
}
|
|
13
21
|
if (ctx.isHttps === true) {
|
|
14
22
|
return createResult({ id: 'https-required', name: 'HTTPS', category: 'security', severity: 'error' }, 'pass', 'Page is served over HTTPS');
|
|
@@ -24,11 +32,41 @@ export const securityRules = [
|
|
|
24
32
|
description: 'HTTPS pages should not load HTTP resources',
|
|
25
33
|
check: (ctx) => {
|
|
26
34
|
if (ctx.hasMixedContent) {
|
|
27
|
-
return createResult({ id: 'mixed-content', name: 'Mixed Content', category: 'security', severity: 'error' }, 'fail', 'Page has mixed content (HTTP resources on HTTPS page)', {
|
|
35
|
+
return createResult({ id: 'mixed-content', name: 'Mixed Content', category: 'security', severity: 'error' }, 'fail', 'Page has mixed content (HTTP resources on HTTPS page)', {
|
|
36
|
+
recommendation: 'Update all resources to use HTTPS',
|
|
37
|
+
evidence: {
|
|
38
|
+
found: 'Mixed content detected',
|
|
39
|
+
expected: 'All resources over HTTPS',
|
|
40
|
+
impact: 'Browsers may block HTTP resources, breaking functionality',
|
|
41
|
+
learnMore: 'https://web.dev/what-is-mixed-content/',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
28
44
|
}
|
|
29
45
|
return null;
|
|
30
46
|
},
|
|
31
47
|
},
|
|
48
|
+
{
|
|
49
|
+
id: 'http-redirect',
|
|
50
|
+
name: 'HTTP to HTTPS Redirect',
|
|
51
|
+
category: 'security',
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
description: 'HTTP traffic should redirect to HTTPS',
|
|
54
|
+
check: (ctx) => {
|
|
55
|
+
if (ctx.httpRedirectsToHttps === undefined)
|
|
56
|
+
return null;
|
|
57
|
+
if (!ctx.httpRedirectsToHttps) {
|
|
58
|
+
return createResult({ id: 'http-redirect', name: 'HTTP to HTTPS Redirect', category: 'security', severity: 'warning' }, 'warn', 'HTTP does not redirect to HTTPS', {
|
|
59
|
+
recommendation: 'Configure server to redirect all HTTP traffic to HTTPS',
|
|
60
|
+
evidence: {
|
|
61
|
+
expected: '301/302 redirect from HTTP to HTTPS',
|
|
62
|
+
impact: 'Users accessing via HTTP may stay on insecure connection',
|
|
63
|
+
learnMore: 'https://web.dev/redirect-http-to-https/',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return createResult({ id: 'http-redirect', name: 'HTTP to HTTPS Redirect', category: 'security', severity: 'warning' }, 'pass', 'HTTP redirects to HTTPS');
|
|
68
|
+
},
|
|
69
|
+
},
|
|
32
70
|
{
|
|
33
71
|
id: 'security-csp-exists',
|
|
34
72
|
name: 'Content Security Policy (CSP)',
|
|
@@ -40,44 +78,115 @@ export const securityRules = [
|
|
|
40
78
|
return null;
|
|
41
79
|
const cspHeader = ctx.responseHeaders['content-security-policy'] || ctx.responseHeaders['Content-Security-Policy'];
|
|
42
80
|
if (!cspHeader) {
|
|
43
|
-
return createResult({ id: 'security-csp-exists', name: 'Content Security Policy', category: 'security', severity: 'warning' }, 'warn', 'Content-Security-Policy header is missing', {
|
|
81
|
+
return createResult({ id: 'security-csp-exists', name: 'Content Security Policy', category: 'security', severity: 'warning' }, 'warn', 'Content-Security-Policy header is missing', {
|
|
82
|
+
recommendation: 'Implement a strong Content-Security-Policy to prevent XSS attacks',
|
|
83
|
+
evidence: {
|
|
84
|
+
expected: 'Content-Security-Policy header',
|
|
85
|
+
impact: 'Page is vulnerable to XSS and data injection attacks',
|
|
86
|
+
learnMore: 'https://web.dev/csp/',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
44
89
|
}
|
|
45
90
|
return createResult({ id: 'security-csp-exists', name: 'Content Security Policy', category: 'security', severity: 'warning' }, 'pass', 'Content-Security-Policy header is present');
|
|
46
91
|
},
|
|
47
92
|
},
|
|
48
93
|
{
|
|
49
|
-
id: 'security-
|
|
50
|
-
name: '
|
|
94
|
+
id: 'security-csp-xss-effective',
|
|
95
|
+
name: 'CSP XSS Effectiveness',
|
|
51
96
|
category: 'security',
|
|
52
97
|
severity: 'warning',
|
|
53
|
-
description: '
|
|
98
|
+
description: 'CSP should be effective against XSS attacks',
|
|
54
99
|
check: (ctx) => {
|
|
55
100
|
if (!ctx.responseHeaders)
|
|
56
101
|
return null;
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
59
|
-
return
|
|
102
|
+
const cspHeader = ctx.responseHeaders['content-security-policy'] || ctx.responseHeaders['Content-Security-Policy'];
|
|
103
|
+
if (!cspHeader)
|
|
104
|
+
return null;
|
|
105
|
+
const csp = String(cspHeader).toLowerCase();
|
|
106
|
+
const weaknesses = [];
|
|
107
|
+
if (csp.includes("'unsafe-inline'") && !csp.includes("'strict-dynamic'") && !csp.includes("'nonce-")) {
|
|
108
|
+
weaknesses.push("unsafe-inline without nonce/strict-dynamic");
|
|
60
109
|
}
|
|
61
|
-
|
|
110
|
+
if (csp.includes("'unsafe-eval'")) {
|
|
111
|
+
weaknesses.push("unsafe-eval allows code execution");
|
|
112
|
+
}
|
|
113
|
+
if (csp.includes('data:') && (csp.includes('script-src') || !csp.includes('default-src'))) {
|
|
114
|
+
weaknesses.push("data: URIs can be exploited for XSS");
|
|
115
|
+
}
|
|
116
|
+
if (csp.match(/script-src[^;]*\*/)) {
|
|
117
|
+
weaknesses.push("Wildcard in script-src");
|
|
118
|
+
}
|
|
119
|
+
if (weaknesses.length > 0) {
|
|
120
|
+
return createResult({ id: 'security-csp-xss-effective', name: 'CSP XSS Effectiveness', category: 'security', severity: 'warning' }, 'warn', `CSP may not be effective against XSS: ${weaknesses.join(', ')}`, {
|
|
121
|
+
recommendation: 'Use nonce-based CSP or strict-dynamic for better XSS protection',
|
|
122
|
+
evidence: {
|
|
123
|
+
found: weaknesses,
|
|
124
|
+
expected: 'No unsafe-inline, unsafe-eval, or wildcards',
|
|
125
|
+
impact: 'Attackers may be able to execute malicious scripts',
|
|
126
|
+
learnMore: 'https://web.dev/strict-csp/',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return createResult({ id: 'security-csp-xss-effective', name: 'CSP XSS Effectiveness', category: 'security', severity: 'warning' }, 'pass', 'CSP appears effective against XSS attacks');
|
|
62
131
|
},
|
|
63
132
|
},
|
|
64
133
|
{
|
|
65
|
-
id: 'security-
|
|
66
|
-
name: '
|
|
134
|
+
id: 'security-csp-directives',
|
|
135
|
+
name: 'CSP Required Directives',
|
|
67
136
|
category: 'security',
|
|
68
|
-
severity: '
|
|
69
|
-
description: '
|
|
137
|
+
severity: 'error',
|
|
138
|
+
description: 'CSP should have script-src and object-src directives to prevent unsafe script execution',
|
|
70
139
|
check: (ctx) => {
|
|
71
140
|
if (!ctx.responseHeaders)
|
|
72
141
|
return null;
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
return
|
|
142
|
+
const cspHeader = ctx.responseHeaders['content-security-policy'] || ctx.responseHeaders['Content-Security-Policy'];
|
|
143
|
+
if (!cspHeader)
|
|
144
|
+
return null;
|
|
145
|
+
const csp = String(cspHeader).toLowerCase();
|
|
146
|
+
const missingDirectives = [];
|
|
147
|
+
if (!csp.includes('script-src') && !csp.includes('default-src')) {
|
|
148
|
+
missingDirectives.push({
|
|
149
|
+
directive: 'script-src',
|
|
150
|
+
severity: 'High',
|
|
151
|
+
impact: 'Allows execution of unsafe scripts from any source',
|
|
152
|
+
});
|
|
76
153
|
}
|
|
77
|
-
if (!
|
|
78
|
-
|
|
154
|
+
if (!csp.includes('object-src') && !csp.includes('default-src')) {
|
|
155
|
+
missingDirectives.push({
|
|
156
|
+
directive: 'object-src',
|
|
157
|
+
severity: 'High',
|
|
158
|
+
impact: 'Allows injection of plugins that execute unsafe scripts',
|
|
159
|
+
});
|
|
79
160
|
}
|
|
80
|
-
|
|
161
|
+
if (!csp.includes('base-uri')) {
|
|
162
|
+
missingDirectives.push({
|
|
163
|
+
directive: 'base-uri',
|
|
164
|
+
severity: 'Medium',
|
|
165
|
+
impact: 'Allows attackers to change the base URL for relative links',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (missingDirectives.length > 0) {
|
|
169
|
+
const highSeverity = missingDirectives.filter((d) => d.severity === 'High');
|
|
170
|
+
if (highSeverity.length > 0) {
|
|
171
|
+
return createResult({ id: 'security-csp-directives', name: 'CSP Required Directives', category: 'security', severity: 'error' }, 'fail', `Missing critical CSP directives: ${highSeverity.map((d) => d.directive).join(', ')}`, {
|
|
172
|
+
recommendation: "Add script-src and object-src directives. Consider setting object-src to 'none' if plugins are not needed.",
|
|
173
|
+
evidence: {
|
|
174
|
+
found: missingDirectives.map((d) => `${d.directive} (${d.severity}): ${d.impact}`),
|
|
175
|
+
expected: "script-src 'self'; object-src 'none'; base-uri 'self'",
|
|
176
|
+
impact: 'Page is vulnerable to script injection attacks',
|
|
177
|
+
learnMore: 'https://web.dev/csp/',
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return createResult({ id: 'security-csp-directives', name: 'CSP Required Directives', category: 'security', severity: 'error' }, 'warn', `Missing recommended CSP directives: ${missingDirectives.map((d) => d.directive).join(', ')}`, {
|
|
182
|
+
recommendation: 'Add base-uri directive to prevent base tag hijacking',
|
|
183
|
+
evidence: {
|
|
184
|
+
found: missingDirectives.map((d) => `${d.directive}: ${d.impact}`),
|
|
185
|
+
expected: "base-uri 'self'",
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return createResult({ id: 'security-csp-directives', name: 'CSP Required Directives', category: 'security', severity: 'error' }, 'pass', 'CSP has required script-src and object-src directives');
|
|
81
190
|
},
|
|
82
191
|
},
|
|
83
192
|
{
|
|
@@ -91,11 +200,215 @@ export const securityRules = [
|
|
|
91
200
|
return null;
|
|
92
201
|
const hstsHeader = ctx.responseHeaders['strict-transport-security'] || ctx.responseHeaders['Strict-Transport-Security'];
|
|
93
202
|
if (!hstsHeader) {
|
|
94
|
-
return createResult({ id: 'security-hsts-exists', name: 'HSTS Header', category: 'security', severity: 'warning' }, 'warn', 'Strict-Transport-Security header is missing', {
|
|
203
|
+
return createResult({ id: 'security-hsts-exists', name: 'HSTS Header', category: 'security', severity: 'warning' }, 'warn', 'Strict-Transport-Security header is missing', {
|
|
204
|
+
recommendation: 'Implement HSTS to force secure connections',
|
|
205
|
+
evidence: {
|
|
206
|
+
expected: 'Strict-Transport-Security header',
|
|
207
|
+
impact: 'Users may connect over insecure HTTP on first visit',
|
|
208
|
+
learnMore: 'https://web.dev/security-headers/#hsts',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
95
211
|
}
|
|
96
212
|
return createResult({ id: 'security-hsts-exists', name: 'HSTS Header', category: 'security', severity: 'warning' }, 'pass', `Strict-Transport-Security header is present: ${hstsHeader}`);
|
|
97
213
|
},
|
|
98
214
|
},
|
|
215
|
+
{
|
|
216
|
+
id: 'security-hsts-strong',
|
|
217
|
+
name: 'Strong HSTS Policy',
|
|
218
|
+
category: 'security',
|
|
219
|
+
severity: 'info',
|
|
220
|
+
description: 'HSTS should have a strong policy with long max-age and includeSubDomains',
|
|
221
|
+
check: (ctx) => {
|
|
222
|
+
if (!ctx.responseHeaders)
|
|
223
|
+
return null;
|
|
224
|
+
const hstsHeader = ctx.responseHeaders['strict-transport-security'] || ctx.responseHeaders['Strict-Transport-Security'];
|
|
225
|
+
if (!hstsHeader)
|
|
226
|
+
return null;
|
|
227
|
+
const hsts = String(hstsHeader).toLowerCase();
|
|
228
|
+
const weaknesses = [];
|
|
229
|
+
const maxAgeMatch = hsts.match(/max-age\s*=\s*(\d+)/);
|
|
230
|
+
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : 0;
|
|
231
|
+
const oneYear = 31536000;
|
|
232
|
+
if (maxAge < oneYear) {
|
|
233
|
+
weaknesses.push(`max-age is ${maxAge}s (recommended: ${oneYear}s / 1 year)`);
|
|
234
|
+
}
|
|
235
|
+
if (!hsts.includes('includesubdomains')) {
|
|
236
|
+
weaknesses.push('Missing includeSubDomains');
|
|
237
|
+
}
|
|
238
|
+
if (!hsts.includes('preload')) {
|
|
239
|
+
weaknesses.push('Missing preload (optional but recommended)');
|
|
240
|
+
}
|
|
241
|
+
if (weaknesses.length > 0) {
|
|
242
|
+
return createResult({ id: 'security-hsts-strong', name: 'Strong HSTS Policy', category: 'security', severity: 'info' }, 'info', `HSTS policy could be stronger: ${weaknesses.join('; ')}`, {
|
|
243
|
+
recommendation: 'Use max-age=31536000; includeSubDomains; preload',
|
|
244
|
+
evidence: {
|
|
245
|
+
found: hstsHeader,
|
|
246
|
+
expected: 'max-age=31536000; includeSubDomains; preload',
|
|
247
|
+
learnMore: 'https://hstspreload.org/',
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return createResult({ id: 'security-hsts-strong', name: 'Strong HSTS Policy', category: 'security', severity: 'info' }, 'pass', 'Strong HSTS policy with long max-age and includeSubDomains');
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'security-coop',
|
|
256
|
+
name: 'Cross-Origin-Opener-Policy (COOP)',
|
|
257
|
+
category: 'security',
|
|
258
|
+
severity: 'info',
|
|
259
|
+
description: 'COOP ensures proper origin isolation for security',
|
|
260
|
+
check: (ctx) => {
|
|
261
|
+
if (!ctx.responseHeaders)
|
|
262
|
+
return null;
|
|
263
|
+
const coopHeader = ctx.responseHeaders['cross-origin-opener-policy'] || ctx.responseHeaders['Cross-Origin-Opener-Policy'];
|
|
264
|
+
if (!coopHeader) {
|
|
265
|
+
return createResult({ id: 'security-coop', name: 'Cross-Origin-Opener-Policy', category: 'security', severity: 'info' }, 'info', 'Cross-Origin-Opener-Policy header is missing', {
|
|
266
|
+
recommendation: 'Add COOP header to isolate your origin from attackers',
|
|
267
|
+
evidence: {
|
|
268
|
+
expected: 'Cross-Origin-Opener-Policy: same-origin',
|
|
269
|
+
impact: 'Lack of origin isolation may enable cross-origin attacks',
|
|
270
|
+
learnMore: 'https://web.dev/coop-coep/',
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const coop = String(coopHeader).toLowerCase();
|
|
275
|
+
if (coop === 'same-origin') {
|
|
276
|
+
return createResult({ id: 'security-coop', name: 'Cross-Origin-Opener-Policy', category: 'security', severity: 'info' }, 'pass', 'COOP: same-origin (full isolation)');
|
|
277
|
+
}
|
|
278
|
+
if (coop === 'same-origin-allow-popups') {
|
|
279
|
+
return createResult({ id: 'security-coop', name: 'Cross-Origin-Opener-Policy', category: 'security', severity: 'info' }, 'pass', 'COOP: same-origin-allow-popups');
|
|
280
|
+
}
|
|
281
|
+
const coopValue = Array.isArray(coopHeader) ? coopHeader.join(', ') : String(coopHeader);
|
|
282
|
+
return createResult({ id: 'security-coop', name: 'Cross-Origin-Opener-Policy', category: 'security', severity: 'info' }, 'info', `COOP: ${coopValue}`, { value: coopValue });
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: 'security-coep',
|
|
287
|
+
name: 'Cross-Origin-Embedder-Policy (COEP)',
|
|
288
|
+
category: 'security',
|
|
289
|
+
severity: 'info',
|
|
290
|
+
description: 'COEP prevents loading cross-origin resources without explicit permission',
|
|
291
|
+
check: (ctx) => {
|
|
292
|
+
if (!ctx.responseHeaders)
|
|
293
|
+
return null;
|
|
294
|
+
const coepHeader = ctx.responseHeaders['cross-origin-embedder-policy'] || ctx.responseHeaders['Cross-Origin-Embedder-Policy'];
|
|
295
|
+
if (!coepHeader) {
|
|
296
|
+
return createResult({ id: 'security-coep', name: 'Cross-Origin-Embedder-Policy', category: 'security', severity: 'info' }, 'info', 'Cross-Origin-Embedder-Policy header is missing', {
|
|
297
|
+
recommendation: 'Add COEP header for cross-origin isolation',
|
|
298
|
+
evidence: {
|
|
299
|
+
expected: 'Cross-Origin-Embedder-Policy: require-corp',
|
|
300
|
+
impact: 'Required for SharedArrayBuffer and high-resolution timers',
|
|
301
|
+
learnMore: 'https://web.dev/coop-coep/',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return createResult({ id: 'security-coep', name: 'Cross-Origin-Embedder-Policy', category: 'security', severity: 'info' }, 'pass', `COEP: ${coepHeader}`);
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: 'security-trusted-types',
|
|
310
|
+
name: 'Trusted Types',
|
|
311
|
+
category: 'security',
|
|
312
|
+
severity: 'info',
|
|
313
|
+
description: 'Trusted Types help prevent DOM-based XSS attacks',
|
|
314
|
+
check: (ctx) => {
|
|
315
|
+
if (!ctx.responseHeaders)
|
|
316
|
+
return null;
|
|
317
|
+
const cspHeader = ctx.responseHeaders['content-security-policy'] || ctx.responseHeaders['Content-Security-Policy'];
|
|
318
|
+
if (!cspHeader)
|
|
319
|
+
return null;
|
|
320
|
+
const csp = String(cspHeader).toLowerCase();
|
|
321
|
+
if (csp.includes('require-trusted-types-for')) {
|
|
322
|
+
return createResult({ id: 'security-trusted-types', name: 'Trusted Types', category: 'security', severity: 'info' }, 'pass', 'Trusted Types policy enabled via CSP');
|
|
323
|
+
}
|
|
324
|
+
return createResult({ id: 'security-trusted-types', name: 'Trusted Types', category: 'security', severity: 'info' }, 'info', 'Trusted Types not enabled', {
|
|
325
|
+
recommendation: 'Add require-trusted-types-for to CSP for DOM XSS protection',
|
|
326
|
+
evidence: {
|
|
327
|
+
expected: "Content-Security-Policy: require-trusted-types-for 'script'",
|
|
328
|
+
impact: 'DOM manipulation may be vulnerable to XSS attacks',
|
|
329
|
+
learnMore: 'https://web.dev/trusted-types/',
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: 'security-xfo-exists',
|
|
336
|
+
name: 'X-Frame-Options',
|
|
337
|
+
category: 'security',
|
|
338
|
+
severity: 'warning',
|
|
339
|
+
description: 'X-Frame-Options header should be present to prevent clickjacking.',
|
|
340
|
+
check: (ctx) => {
|
|
341
|
+
if (!ctx.responseHeaders)
|
|
342
|
+
return null;
|
|
343
|
+
const xfoHeader = ctx.responseHeaders['x-frame-options'] || ctx.responseHeaders['X-Frame-Options'];
|
|
344
|
+
if (!xfoHeader) {
|
|
345
|
+
return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Frame-Options header is missing', {
|
|
346
|
+
recommendation: 'Implement X-Frame-Options to prevent clickjacking attacks',
|
|
347
|
+
evidence: {
|
|
348
|
+
expected: 'X-Frame-Options: DENY or SAMEORIGIN',
|
|
349
|
+
impact: 'Page can be embedded in malicious iframes for clickjacking',
|
|
350
|
+
learnMore: 'https://web.dev/security-headers/#xfo',
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'pass', `X-Frame-Options header is present: ${xfoHeader}`);
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: 'security-frame-ancestors',
|
|
359
|
+
name: 'CSP frame-ancestors',
|
|
360
|
+
category: 'security',
|
|
361
|
+
severity: 'info',
|
|
362
|
+
description: 'CSP frame-ancestors is the modern replacement for X-Frame-Options',
|
|
363
|
+
check: (ctx) => {
|
|
364
|
+
if (!ctx.responseHeaders)
|
|
365
|
+
return null;
|
|
366
|
+
const cspHeader = ctx.responseHeaders['content-security-policy'] || ctx.responseHeaders['Content-Security-Policy'];
|
|
367
|
+
const xfoHeader = ctx.responseHeaders['x-frame-options'] || ctx.responseHeaders['X-Frame-Options'];
|
|
368
|
+
if (!cspHeader && !xfoHeader)
|
|
369
|
+
return null;
|
|
370
|
+
if (cspHeader && String(cspHeader).toLowerCase().includes('frame-ancestors')) {
|
|
371
|
+
return createResult({ id: 'security-frame-ancestors', name: 'CSP frame-ancestors', category: 'security', severity: 'info' }, 'pass', 'CSP frame-ancestors directive present (modern clickjacking protection)');
|
|
372
|
+
}
|
|
373
|
+
if (xfoHeader && !cspHeader) {
|
|
374
|
+
return createResult({ id: 'security-frame-ancestors', name: 'CSP frame-ancestors', category: 'security', severity: 'info' }, 'info', 'Using X-Frame-Options; consider migrating to CSP frame-ancestors', {
|
|
375
|
+
recommendation: 'Use CSP frame-ancestors for better control over framing',
|
|
376
|
+
evidence: {
|
|
377
|
+
found: `X-Frame-Options: ${xfoHeader}`,
|
|
378
|
+
expected: "Content-Security-Policy: frame-ancestors 'self'",
|
|
379
|
+
learnMore: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors',
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: 'security-cors-config',
|
|
388
|
+
name: 'CORS Configuration',
|
|
389
|
+
category: 'security',
|
|
390
|
+
severity: 'warning',
|
|
391
|
+
description: 'Review Access-Control-Allow-Origin header for proper CORS configuration.',
|
|
392
|
+
check: (ctx) => {
|
|
393
|
+
if (!ctx.responseHeaders)
|
|
394
|
+
return null;
|
|
395
|
+
const acaoHeader = ctx.responseHeaders['access-control-allow-origin'] || ctx.responseHeaders['Access-Control-Allow-Origin'];
|
|
396
|
+
if (acaoHeader === '*') {
|
|
397
|
+
return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'warn', 'Access-Control-Allow-Origin is set to "*"', {
|
|
398
|
+
recommendation: 'Avoid wildcard (*) in Access-Control-Allow-Origin for sensitive content',
|
|
399
|
+
evidence: {
|
|
400
|
+
found: '*',
|
|
401
|
+
expected: 'Specific origins only',
|
|
402
|
+
impact: 'Any website can make requests to your API',
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (!acaoHeader) {
|
|
407
|
+
return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'info', 'Access-Control-Allow-Origin header is missing', { recommendation: 'Configure CORS if resources are consumed cross-origin' });
|
|
408
|
+
}
|
|
409
|
+
return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'pass', `Access-Control-Allow-Origin: ${acaoHeader}`);
|
|
410
|
+
},
|
|
411
|
+
},
|
|
99
412
|
{
|
|
100
413
|
id: 'security-xcto-exists',
|
|
101
414
|
name: 'X-Content-Type-Options',
|
|
@@ -107,9 +420,15 @@ export const securityRules = [
|
|
|
107
420
|
return null;
|
|
108
421
|
const xctoHeader = ctx.responseHeaders['x-content-type-options'] || ctx.responseHeaders['X-Content-Type-Options'];
|
|
109
422
|
if (!xctoHeader) {
|
|
110
|
-
return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Content-Type-Options header is missing', {
|
|
423
|
+
return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Content-Type-Options header is missing', {
|
|
424
|
+
recommendation: 'Implement X-Content-Type-Options: nosniff',
|
|
425
|
+
evidence: {
|
|
426
|
+
expected: 'X-Content-Type-Options: nosniff',
|
|
427
|
+
impact: 'Browser may interpret files incorrectly, leading to security issues',
|
|
428
|
+
},
|
|
429
|
+
});
|
|
111
430
|
}
|
|
112
|
-
return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'pass', `X-Content-Type-Options
|
|
431
|
+
return createResult({ id: 'security-xcto-exists', name: 'X-Content-Type-Options', category: 'security', severity: 'warning' }, 'pass', `X-Content-Type-Options: ${xctoHeader}`);
|
|
113
432
|
},
|
|
114
433
|
},
|
|
115
434
|
{
|
|
@@ -123,9 +442,15 @@ export const securityRules = [
|
|
|
123
442
|
return null;
|
|
124
443
|
const rpHeader = ctx.responseHeaders['referrer-policy'] || ctx.responseHeaders['Referrer-Policy'];
|
|
125
444
|
if (!rpHeader) {
|
|
126
|
-
return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'info', 'Referrer-Policy header is missing', {
|
|
445
|
+
return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'info', 'Referrer-Policy header is missing', {
|
|
446
|
+
recommendation: 'Implement Referrer-Policy for privacy (e.g., strict-origin-when-cross-origin)',
|
|
447
|
+
evidence: {
|
|
448
|
+
expected: 'Referrer-Policy: strict-origin-when-cross-origin',
|
|
449
|
+
impact: 'Full URLs may leak in referrer headers',
|
|
450
|
+
},
|
|
451
|
+
});
|
|
127
452
|
}
|
|
128
|
-
return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'pass', `Referrer-Policy
|
|
453
|
+
return createResult({ id: 'security-rp-exists', name: 'Referrer-Policy', category: 'security', severity: 'info' }, 'pass', `Referrer-Policy: ${rpHeader}`);
|
|
129
454
|
},
|
|
130
455
|
},
|
|
131
456
|
{
|
|
@@ -133,15 +458,68 @@ export const securityRules = [
|
|
|
133
458
|
name: 'Permissions-Policy',
|
|
134
459
|
category: 'security',
|
|
135
460
|
severity: 'info',
|
|
136
|
-
description: 'Permissions-Policy controls browser features available to the page
|
|
461
|
+
description: 'Permissions-Policy controls browser features available to the page.',
|
|
137
462
|
check: (ctx) => {
|
|
138
463
|
if (!ctx.responseHeaders)
|
|
139
464
|
return null;
|
|
140
465
|
const ppHeader = ctx.responseHeaders['permissions-policy'] || ctx.responseHeaders['Permissions-Policy'];
|
|
141
466
|
if (!ppHeader) {
|
|
142
|
-
return createResult({ id: 'security-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'info', 'Permissions-Policy header is missing', {
|
|
467
|
+
return createResult({ id: 'security-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'info', 'Permissions-Policy header is missing', {
|
|
468
|
+
recommendation: 'Disable unused browser features (e.g., camera=(), microphone=())',
|
|
469
|
+
evidence: {
|
|
470
|
+
expected: 'Permissions-Policy header',
|
|
471
|
+
impact: 'Third-party code may access browser features unnecessarily',
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return createResult({ id: 'security-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'pass', 'Permissions-Policy header is present');
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
id: 'security-xxss',
|
|
480
|
+
name: 'X-XSS-Protection',
|
|
481
|
+
category: 'security',
|
|
482
|
+
severity: 'info',
|
|
483
|
+
description: 'X-XSS-Protection is deprecated but may still provide protection in older browsers',
|
|
484
|
+
check: (ctx) => {
|
|
485
|
+
if (!ctx.responseHeaders)
|
|
486
|
+
return null;
|
|
487
|
+
const xxssHeader = ctx.responseHeaders['x-xss-protection'] || ctx.responseHeaders['X-XSS-Protection'];
|
|
488
|
+
if (xxssHeader && String(xxssHeader).includes('1')) {
|
|
489
|
+
return createResult({ id: 'security-xxss', name: 'X-XSS-Protection', category: 'security', severity: 'info' }, 'info', 'X-XSS-Protection is enabled but deprecated', {
|
|
490
|
+
recommendation: 'Use Content-Security-Policy instead; X-XSS-Protection can be disabled',
|
|
491
|
+
evidence: {
|
|
492
|
+
found: xxssHeader,
|
|
493
|
+
expected: 'CSP for XSS protection',
|
|
494
|
+
impact: 'X-XSS-Protection can introduce vulnerabilities in some cases',
|
|
495
|
+
learnMore: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection',
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: 'security-corp',
|
|
504
|
+
name: 'Cross-Origin-Resource-Policy (CORP)',
|
|
505
|
+
category: 'security',
|
|
506
|
+
severity: 'info',
|
|
507
|
+
description: 'CORP restricts which origins can load your resources',
|
|
508
|
+
check: (ctx) => {
|
|
509
|
+
if (!ctx.responseHeaders)
|
|
510
|
+
return null;
|
|
511
|
+
const corpHeader = ctx.responseHeaders['cross-origin-resource-policy'] || ctx.responseHeaders['Cross-Origin-Resource-Policy'];
|
|
512
|
+
if (!corpHeader) {
|
|
513
|
+
return createResult({ id: 'security-corp', name: 'Cross-Origin-Resource-Policy', category: 'security', severity: 'info' }, 'info', 'Cross-Origin-Resource-Policy header is missing', {
|
|
514
|
+
recommendation: 'Consider adding CORP to control resource loading',
|
|
515
|
+
evidence: {
|
|
516
|
+
expected: 'Cross-Origin-Resource-Policy: same-origin or same-site',
|
|
517
|
+
impact: 'Resources may be loaded by any origin',
|
|
518
|
+
learnMore: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy',
|
|
519
|
+
},
|
|
520
|
+
});
|
|
143
521
|
}
|
|
144
|
-
return createResult({ id: 'security-
|
|
522
|
+
return createResult({ id: 'security-corp', name: 'Cross-Origin-Resource-Policy', category: 'security', severity: 'info' }, 'pass', `CORP: ${corpHeader}`);
|
|
145
523
|
},
|
|
146
524
|
},
|
|
147
525
|
];
|