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.
Files changed (38) hide show
  1. package/dist/cli/tui/shell.d.ts +1 -0
  2. package/dist/cli/tui/shell.js +339 -5
  3. package/dist/scrape/index.d.ts +2 -0
  4. package/dist/scrape/index.js +1 -0
  5. package/dist/scrape/spider.d.ts +61 -0
  6. package/dist/scrape/spider.js +250 -0
  7. package/dist/seo/analyzer.js +27 -0
  8. package/dist/seo/index.d.ts +3 -1
  9. package/dist/seo/index.js +1 -0
  10. package/dist/seo/rules/accessibility.js +620 -54
  11. package/dist/seo/rules/best-practices.d.ts +2 -0
  12. package/dist/seo/rules/best-practices.js +188 -0
  13. package/dist/seo/rules/crawl.d.ts +2 -0
  14. package/dist/seo/rules/crawl.js +307 -0
  15. package/dist/seo/rules/cwv.d.ts +2 -0
  16. package/dist/seo/rules/cwv.js +337 -0
  17. package/dist/seo/rules/ecommerce.d.ts +2 -0
  18. package/dist/seo/rules/ecommerce.js +252 -0
  19. package/dist/seo/rules/i18n.d.ts +2 -0
  20. package/dist/seo/rules/i18n.js +222 -0
  21. package/dist/seo/rules/index.d.ts +32 -0
  22. package/dist/seo/rules/index.js +71 -0
  23. package/dist/seo/rules/internal-linking.d.ts +2 -0
  24. package/dist/seo/rules/internal-linking.js +375 -0
  25. package/dist/seo/rules/local.d.ts +2 -0
  26. package/dist/seo/rules/local.js +265 -0
  27. package/dist/seo/rules/pwa.d.ts +2 -0
  28. package/dist/seo/rules/pwa.js +302 -0
  29. package/dist/seo/rules/readability.d.ts +2 -0
  30. package/dist/seo/rules/readability.js +255 -0
  31. package/dist/seo/rules/security.js +406 -28
  32. package/dist/seo/rules/social.d.ts +2 -0
  33. package/dist/seo/rules/social.js +373 -0
  34. package/dist/seo/rules/types.d.ts +155 -0
  35. package/dist/seo/seo-spider.d.ts +47 -0
  36. package/dist/seo/seo-spider.js +362 -0
  37. package/dist/seo/types.d.ts +24 -0
  38. 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', { recommendation: 'Enable HTTPS for all pages' });
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)', { recommendation: 'Update all resources to use HTTPS' });
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', { recommendation: 'Implement a strong Content-Security-Policy to prevent XSS attacks.' });
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-xfo-exists',
50
- name: 'X-Frame-Options',
94
+ id: 'security-csp-xss-effective',
95
+ name: 'CSP XSS Effectiveness',
51
96
  category: 'security',
52
97
  severity: 'warning',
53
- description: 'X-Frame-Options header should be present to prevent clickjacking.',
98
+ description: 'CSP should be effective against XSS attacks',
54
99
  check: (ctx) => {
55
100
  if (!ctx.responseHeaders)
56
101
  return null;
57
- const xfoHeader = ctx.responseHeaders['x-frame-options'] || ctx.responseHeaders['X-Frame-Options'];
58
- if (!xfoHeader) {
59
- return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'warn', 'X-Frame-Options header is missing', { recommendation: 'Implement X-Frame-Options to prevent clickjacking attacks.' });
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
- return createResult({ id: 'security-xfo-exists', name: 'X-Frame-Options', category: 'security', severity: 'warning' }, 'pass', `X-Frame-Options header is present: ${xfoHeader}`);
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-cors-config',
66
- name: 'CORS Configuration',
134
+ id: 'security-csp-directives',
135
+ name: 'CSP Required Directives',
67
136
  category: 'security',
68
- severity: 'warning',
69
- description: 'Review Access-Control-Allow-Origin header for proper CORS configuration.',
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 acaoHeader = ctx.responseHeaders['access-control-allow-origin'] || ctx.responseHeaders['Access-Control-Allow-Origin'];
74
- if (acaoHeader === '*') {
75
- return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'warn', 'Access-Control-Allow-Origin is set to "*"', { recommendation: 'Avoid wildcard (*) in Access-Control-Allow-Origin for sensitive content. Specify allowed origins.' });
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 (!acaoHeader) {
78
- return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'info', 'Access-Control-Allow-Origin header is missing', { recommendation: 'Consider explicit CORS configuration if resources are consumed cross-origin.' });
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
- return createResult({ id: 'security-cors-config', name: 'CORS Configuration', category: 'security', severity: 'warning' }, 'pass', `Access-Control-Allow-Origin: ${acaoHeader}`);
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', { recommendation: 'Implement HSTS to force secure connections and benefit SEO.' });
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', { recommendation: 'Implement X-Content-Type-Options: nosniff to prevent MIME sniffing.' });
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 header is present: ${xctoHeader}`);
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', { recommendation: 'Consider implementing a Referrer-Policy for better privacy and control (e.g., strict-origin-when-cross-origin).' });
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 header is present: ${rpHeader}`);
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 and iframes.',
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', { recommendation: 'Consider implementing a Permissions-Policy to disable unused browser features and enhance security (e.g., camera=(), microphone=()).' });
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-pp-exists', name: 'Permissions-Policy', category: 'security', severity: 'info' }, 'pass', `Permissions-Policy header is present: ${ppHeader}`);
522
+ return createResult({ id: 'security-corp', name: 'Cross-Origin-Resource-Policy', category: 'security', severity: 'info' }, 'pass', `CORP: ${corpHeader}`);
145
523
  },
146
524
  },
147
525
  ];
@@ -0,0 +1,2 @@
1
+ import { SeoRule } from './types.js';
2
+ export declare const socialRules: SeoRule[];