@rankcli/agent-runtime 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +242 -0
  2. package/dist/analyzer-2CSWIQGD.mjs +6 -0
  3. package/dist/chunk-YNZYHEYM.mjs +774 -0
  4. package/dist/index.d.mts +4012 -0
  5. package/dist/index.d.ts +4012 -0
  6. package/dist/index.js +29672 -0
  7. package/dist/index.mjs +28602 -0
  8. package/package.json +53 -0
  9. package/scripts/build-deno.ts +134 -0
  10. package/src/audit/ai/analyzer.ts +347 -0
  11. package/src/audit/ai/index.ts +29 -0
  12. package/src/audit/ai/prompts/content-analysis.ts +271 -0
  13. package/src/audit/ai/types.ts +179 -0
  14. package/src/audit/checks/additional-checks.ts +439 -0
  15. package/src/audit/checks/ai-citation-worthiness.ts +399 -0
  16. package/src/audit/checks/ai-content-structure.ts +325 -0
  17. package/src/audit/checks/ai-readiness.ts +339 -0
  18. package/src/audit/checks/anchor-text.ts +179 -0
  19. package/src/audit/checks/answer-conciseness.ts +322 -0
  20. package/src/audit/checks/asset-minification.ts +270 -0
  21. package/src/audit/checks/bing-optimization.ts +206 -0
  22. package/src/audit/checks/brand-mention-optimization.ts +349 -0
  23. package/src/audit/checks/caching-headers.ts +305 -0
  24. package/src/audit/checks/canonical-advanced.ts +150 -0
  25. package/src/audit/checks/canonical-domain.ts +196 -0
  26. package/src/audit/checks/citation-quality.ts +358 -0
  27. package/src/audit/checks/client-rendering.ts +542 -0
  28. package/src/audit/checks/color-contrast.ts +342 -0
  29. package/src/audit/checks/content-freshness.ts +170 -0
  30. package/src/audit/checks/content-science.ts +589 -0
  31. package/src/audit/checks/conversion-elements.ts +526 -0
  32. package/src/audit/checks/crawlability.ts +220 -0
  33. package/src/audit/checks/directory-listing.ts +172 -0
  34. package/src/audit/checks/dom-analysis.ts +191 -0
  35. package/src/audit/checks/dom-size.ts +246 -0
  36. package/src/audit/checks/duplicate-content.ts +194 -0
  37. package/src/audit/checks/eeat-signals.ts +990 -0
  38. package/src/audit/checks/entity-seo.ts +396 -0
  39. package/src/audit/checks/featured-snippet.ts +473 -0
  40. package/src/audit/checks/freshness-signals.ts +443 -0
  41. package/src/audit/checks/funnel-intent.ts +463 -0
  42. package/src/audit/checks/hreflang.ts +174 -0
  43. package/src/audit/checks/html-compliance.ts +302 -0
  44. package/src/audit/checks/image-dimensions.ts +167 -0
  45. package/src/audit/checks/images.ts +160 -0
  46. package/src/audit/checks/indexnow.ts +275 -0
  47. package/src/audit/checks/interactive-tools.ts +475 -0
  48. package/src/audit/checks/internal-link-graph.ts +436 -0
  49. package/src/audit/checks/keyword-analysis.ts +239 -0
  50. package/src/audit/checks/keyword-cannibalization.ts +385 -0
  51. package/src/audit/checks/keyword-placement.ts +471 -0
  52. package/src/audit/checks/links.ts +203 -0
  53. package/src/audit/checks/llms-txt.ts +224 -0
  54. package/src/audit/checks/local-seo.ts +296 -0
  55. package/src/audit/checks/mobile.ts +167 -0
  56. package/src/audit/checks/modern-images.ts +226 -0
  57. package/src/audit/checks/navboost-signals.ts +395 -0
  58. package/src/audit/checks/on-page.ts +209 -0
  59. package/src/audit/checks/page-resources.ts +285 -0
  60. package/src/audit/checks/pagination.ts +180 -0
  61. package/src/audit/checks/performance.ts +153 -0
  62. package/src/audit/checks/platform-presence.ts +580 -0
  63. package/src/audit/checks/redirect-analysis.ts +153 -0
  64. package/src/audit/checks/redirect-chain.ts +389 -0
  65. package/src/audit/checks/resource-hints.ts +420 -0
  66. package/src/audit/checks/responsive-css.ts +247 -0
  67. package/src/audit/checks/responsive-images.ts +396 -0
  68. package/src/audit/checks/review-ecosystem.ts +415 -0
  69. package/src/audit/checks/robots-validation.ts +373 -0
  70. package/src/audit/checks/security-headers.ts +172 -0
  71. package/src/audit/checks/security.ts +144 -0
  72. package/src/audit/checks/serp-preview.ts +251 -0
  73. package/src/audit/checks/site-maturity.ts +444 -0
  74. package/src/audit/checks/social-meta.test.ts +275 -0
  75. package/src/audit/checks/social-meta.ts +134 -0
  76. package/src/audit/checks/soft-404.ts +151 -0
  77. package/src/audit/checks/structured-data.ts +238 -0
  78. package/src/audit/checks/tech-detection.ts +496 -0
  79. package/src/audit/checks/topical-clusters.ts +435 -0
  80. package/src/audit/checks/tracker-bloat.ts +462 -0
  81. package/src/audit/checks/tracking-verification.test.ts +371 -0
  82. package/src/audit/checks/tracking-verification.ts +636 -0
  83. package/src/audit/checks/url-safety.ts +682 -0
  84. package/src/audit/deno-entry.ts +66 -0
  85. package/src/audit/discovery/index.ts +15 -0
  86. package/src/audit/discovery/link-crawler.ts +232 -0
  87. package/src/audit/discovery/repo-routes.ts +347 -0
  88. package/src/audit/engine.ts +620 -0
  89. package/src/audit/fixes/index.ts +209 -0
  90. package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
  91. package/src/audit/fixes/social-meta-fixes.ts +463 -0
  92. package/src/audit/index.ts +74 -0
  93. package/src/audit/runner.test.ts +299 -0
  94. package/src/audit/runner.ts +130 -0
  95. package/src/audit/types.ts +1953 -0
  96. package/src/content/featured-snippet.ts +367 -0
  97. package/src/content/generator.test.ts +534 -0
  98. package/src/content/generator.ts +501 -0
  99. package/src/content/headline.ts +317 -0
  100. package/src/content/index.ts +62 -0
  101. package/src/content/intent.ts +258 -0
  102. package/src/content/keyword-density.ts +349 -0
  103. package/src/content/readability.ts +262 -0
  104. package/src/executor.ts +336 -0
  105. package/src/fixer.ts +416 -0
  106. package/src/frameworks/detector.test.ts +248 -0
  107. package/src/frameworks/detector.ts +371 -0
  108. package/src/frameworks/index.ts +68 -0
  109. package/src/frameworks/recipes/angular.yaml +171 -0
  110. package/src/frameworks/recipes/astro.yaml +206 -0
  111. package/src/frameworks/recipes/django.yaml +180 -0
  112. package/src/frameworks/recipes/laravel.yaml +137 -0
  113. package/src/frameworks/recipes/nextjs.yaml +268 -0
  114. package/src/frameworks/recipes/nuxt.yaml +175 -0
  115. package/src/frameworks/recipes/rails.yaml +188 -0
  116. package/src/frameworks/recipes/react.yaml +202 -0
  117. package/src/frameworks/recipes/sveltekit.yaml +154 -0
  118. package/src/frameworks/recipes/vue.yaml +137 -0
  119. package/src/frameworks/recipes/wordpress.yaml +209 -0
  120. package/src/frameworks/suggestion-engine.ts +320 -0
  121. package/src/geo/geo-content.test.ts +305 -0
  122. package/src/geo/geo-content.ts +266 -0
  123. package/src/geo/geo-history.test.ts +473 -0
  124. package/src/geo/geo-history.ts +433 -0
  125. package/src/geo/geo-tracker.test.ts +359 -0
  126. package/src/geo/geo-tracker.ts +411 -0
  127. package/src/geo/index.ts +10 -0
  128. package/src/git/commit-helper.test.ts +261 -0
  129. package/src/git/commit-helper.ts +329 -0
  130. package/src/git/index.ts +12 -0
  131. package/src/git/pr-helper.test.ts +284 -0
  132. package/src/git/pr-helper.ts +307 -0
  133. package/src/index.ts +66 -0
  134. package/src/keywords/ai-keyword-engine.ts +1062 -0
  135. package/src/keywords/ai-summarizer.ts +387 -0
  136. package/src/keywords/ci-mode.ts +555 -0
  137. package/src/keywords/engine.ts +359 -0
  138. package/src/keywords/index.ts +151 -0
  139. package/src/keywords/llm-judge.ts +357 -0
  140. package/src/keywords/nlp-analysis.ts +706 -0
  141. package/src/keywords/prioritizer.ts +295 -0
  142. package/src/keywords/site-crawler.ts +342 -0
  143. package/src/keywords/sources/autocomplete.ts +139 -0
  144. package/src/keywords/sources/competitive-search.ts +450 -0
  145. package/src/keywords/sources/competitor-analysis.ts +374 -0
  146. package/src/keywords/sources/dataforseo.ts +206 -0
  147. package/src/keywords/sources/free-sources.ts +294 -0
  148. package/src/keywords/sources/gsc.ts +123 -0
  149. package/src/keywords/topic-grouping.ts +327 -0
  150. package/src/keywords/types.ts +144 -0
  151. package/src/keywords/wizard.ts +457 -0
  152. package/src/loader.ts +40 -0
  153. package/src/reports/index.ts +7 -0
  154. package/src/reports/report-generator.test.ts +293 -0
  155. package/src/reports/report-generator.ts +713 -0
  156. package/src/scheduler/alerts.test.ts +458 -0
  157. package/src/scheduler/alerts.ts +328 -0
  158. package/src/scheduler/index.ts +8 -0
  159. package/src/scheduler/scheduled-audit.test.ts +377 -0
  160. package/src/scheduler/scheduled-audit.ts +149 -0
  161. package/src/test/integration-test.ts +325 -0
  162. package/src/tools/analyzer.ts +373 -0
  163. package/src/tools/crawl.ts +293 -0
  164. package/src/tools/files.ts +301 -0
  165. package/src/tools/h1-fixer.ts +249 -0
  166. package/src/tools/index.ts +67 -0
  167. package/src/tracking/github-action.ts +326 -0
  168. package/src/tracking/google-analytics.ts +265 -0
  169. package/src/tracking/index.ts +45 -0
  170. package/src/tracking/report-generator.ts +386 -0
  171. package/src/tracking/search-console.ts +335 -0
  172. package/src/types.ts +134 -0
  173. package/src/utils/http.ts +302 -0
  174. package/src/wasm-adapter.ts +297 -0
  175. package/src/wasm-entry.ts +14 -0
  176. package/tsconfig.json +17 -0
  177. package/tsup.wasm.config.ts +26 -0
  178. package/vitest.config.ts +15 -0
@@ -0,0 +1,220 @@
1
+ import { httpGet } from '../../utils/http.js';
2
+ import type { AuditIssue, PageData } from '../types.js';
3
+ import { ISSUE_DEFINITIONS } from '../types.js';
4
+
5
+ export interface RobotsTxtResult {
6
+ exists: boolean;
7
+ content?: string;
8
+ blocksAll: boolean;
9
+ hasSitemap: boolean;
10
+ sitemapUrls: string[];
11
+ }
12
+
13
+ export interface SitemapResult {
14
+ exists: boolean;
15
+ url?: string;
16
+ urlCount: number;
17
+ errors: string[];
18
+ }
19
+
20
+ export async function checkRobotsTxt(baseUrl: string): Promise<{ issues: AuditIssue[]; data: RobotsTxtResult }> {
21
+ const issues: AuditIssue[] = [];
22
+ const url = new URL('/robots.txt', baseUrl).href;
23
+
24
+ try {
25
+ const response = await httpGet<string>(url, {
26
+ timeout: 10000,
27
+ validateStatus: () => true,
28
+ });
29
+
30
+ if (response.status === 404) {
31
+ issues.push({
32
+ ...ISSUE_DEFINITIONS.ROBOTS_TXT_MISSING,
33
+ affectedUrls: [url],
34
+ });
35
+ return {
36
+ issues,
37
+ data: { exists: false, blocksAll: false, hasSitemap: false, sitemapUrls: [] }
38
+ };
39
+ }
40
+
41
+ const content = response.data as string;
42
+ const lines = content.toLowerCase().split('\n');
43
+
44
+ // Check if robots.txt blocks all crawlers
45
+ let blocksAll = false;
46
+ let currentUserAgent = '';
47
+ for (const line of lines) {
48
+ const trimmed = line.trim();
49
+ if (trimmed.startsWith('user-agent:')) {
50
+ currentUserAgent = trimmed.split(':')[1].trim();
51
+ }
52
+ if (trimmed === 'disallow: /' && currentUserAgent === '*') {
53
+ blocksAll = true;
54
+ }
55
+ }
56
+
57
+ if (blocksAll) {
58
+ issues.push({
59
+ ...ISSUE_DEFINITIONS.ROBOTS_TXT_BLOCKS_ALL,
60
+ affectedUrls: [url],
61
+ });
62
+ }
63
+
64
+ // Check for sitemap references
65
+ const sitemapUrls: string[] = [];
66
+ for (const line of content.split('\n')) {
67
+ const trimmed = line.trim().toLowerCase();
68
+ if (trimmed.startsWith('sitemap:')) {
69
+ const sitemapUrl = line.trim().substring(8).trim();
70
+ sitemapUrls.push(sitemapUrl);
71
+ }
72
+ }
73
+
74
+ return {
75
+ issues,
76
+ data: {
77
+ exists: true,
78
+ content,
79
+ blocksAll,
80
+ hasSitemap: sitemapUrls.length > 0,
81
+ sitemapUrls,
82
+ }
83
+ };
84
+ } catch (error) {
85
+ issues.push({
86
+ ...ISSUE_DEFINITIONS.ROBOTS_TXT_MISSING,
87
+ affectedUrls: [url],
88
+ details: { error: error instanceof Error ? error.message : 'Unknown error' },
89
+ });
90
+ return {
91
+ issues,
92
+ data: { exists: false, blocksAll: false, hasSitemap: false, sitemapUrls: [] }
93
+ };
94
+ }
95
+ }
96
+
97
+ export async function checkSitemap(baseUrl: string, robotsData: RobotsTxtResult): Promise<{ issues: AuditIssue[]; data: SitemapResult }> {
98
+ const issues: AuditIssue[] = [];
99
+
100
+ // Try sitemap URLs from robots.txt first, then standard locations
101
+ const sitemapUrls = robotsData.sitemapUrls.length > 0
102
+ ? robotsData.sitemapUrls
103
+ : [
104
+ new URL('/sitemap.xml', baseUrl).href,
105
+ new URL('/sitemap_index.xml', baseUrl).href,
106
+ ];
107
+
108
+ for (const sitemapUrl of sitemapUrls) {
109
+ try {
110
+ const response = await httpGet<string>(sitemapUrl, {
111
+ timeout: 10000,
112
+ validateStatus: () => true,
113
+ });
114
+
115
+ if (response.status === 200 && (response.data as string).includes('<urlset') || (response.data as string).includes('<sitemapindex')) {
116
+ // Count URLs in sitemap
117
+ const content = response.data as string;
118
+ const urlMatches = content.match(/<loc>/g);
119
+ const urlCount = urlMatches ? urlMatches.length : 0;
120
+
121
+ // Check if sitemap is referenced in robots.txt
122
+ if (!robotsData.hasSitemap) {
123
+ issues.push({
124
+ ...ISSUE_DEFINITIONS.SITEMAP_NOT_IN_ROBOTS,
125
+ affectedUrls: [sitemapUrl],
126
+ });
127
+ }
128
+
129
+ return {
130
+ issues,
131
+ data: { exists: true, url: sitemapUrl, urlCount, errors: [] }
132
+ };
133
+ }
134
+ } catch {
135
+ // Try next URL
136
+ }
137
+ }
138
+
139
+ // No sitemap found
140
+ issues.push({
141
+ ...ISSUE_DEFINITIONS.SITEMAP_MISSING,
142
+ affectedUrls: [new URL('/sitemap.xml', baseUrl).href],
143
+ });
144
+
145
+ return {
146
+ issues,
147
+ data: { exists: false, urlCount: 0, errors: ['No sitemap found'] }
148
+ };
149
+ }
150
+
151
+ export async function checkRedirects(url: string): Promise<{ issues: AuditIssue[]; chain: string[]; finalUrl: string }> {
152
+ const issues: AuditIssue[] = [];
153
+ const chain: string[] = [url];
154
+ let currentUrl = url;
155
+ let maxRedirects = 10;
156
+
157
+ while (maxRedirects > 0) {
158
+ try {
159
+ const response = await httpGet<string>(currentUrl, {
160
+ timeout: 10000,
161
+ maxRedirects: 0,
162
+ validateStatus: () => true,
163
+ });
164
+
165
+ if (response.status >= 300 && response.status < 400) {
166
+ const location = response.headers.location;
167
+ if (!location) break;
168
+
169
+ const nextUrl = new URL(location, currentUrl).href;
170
+
171
+ // Check for redirect loop
172
+ if (chain.includes(nextUrl)) {
173
+ issues.push({
174
+ ...ISSUE_DEFINITIONS.REDIRECT_LOOP,
175
+ affectedUrls: chain,
176
+ details: { chain, loopTo: nextUrl },
177
+ });
178
+ break;
179
+ }
180
+
181
+ chain.push(nextUrl);
182
+ currentUrl = nextUrl;
183
+ maxRedirects--;
184
+ } else {
185
+ break;
186
+ }
187
+ } catch {
188
+ break;
189
+ }
190
+ }
191
+
192
+ // Check for redirect chain (more than 1 redirect)
193
+ if (chain.length > 2) {
194
+ issues.push({
195
+ ...ISSUE_DEFINITIONS.REDIRECT_CHAIN,
196
+ affectedUrls: chain,
197
+ details: { chain, hops: chain.length - 1 },
198
+ });
199
+ }
200
+
201
+ return { issues, chain, finalUrl: currentUrl };
202
+ }
203
+
204
+ export async function runCrawlabilityChecks(baseUrl: string): Promise<AuditIssue[]> {
205
+ const allIssues: AuditIssue[] = [];
206
+
207
+ // Check robots.txt
208
+ const robotsResult = await checkRobotsTxt(baseUrl);
209
+ allIssues.push(...robotsResult.issues);
210
+
211
+ // Check sitemap
212
+ const sitemapResult = await checkSitemap(baseUrl, robotsResult.data);
213
+ allIssues.push(...sitemapResult.issues);
214
+
215
+ // Check for redirects on the main URL
216
+ const redirectResult = await checkRedirects(baseUrl);
217
+ allIssues.push(...redirectResult.issues);
218
+
219
+ return allIssues;
220
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Directory Listing Security Check
3
+ *
4
+ * Checks if the web server is configured to allow directory listing.
5
+ * When enabled, accessing a directory without an index file shows
6
+ * all files in that directory - a security risk.
7
+ *
8
+ * Detection methods:
9
+ * - Check common directories (/images/, /assets/, /uploads/, etc.)
10
+ * - Look for Apache/Nginx directory listing HTML patterns
11
+ * - Check response headers
12
+ */
13
+
14
+ import { httpGet } from '../../utils/http.js';
15
+ import type { AuditIssue } from '../types.js';
16
+
17
+ export interface DirectoryListingData {
18
+ directoryListingEnabled: boolean;
19
+ vulnerableDirectories: string[];
20
+ checkedDirectories: string[];
21
+ serverType?: string;
22
+ }
23
+
24
+ // Common directories to check for directory listing
25
+ const COMMON_DIRECTORIES = [
26
+ '/images/',
27
+ '/img/',
28
+ '/assets/',
29
+ '/static/',
30
+ '/uploads/',
31
+ '/files/',
32
+ '/media/',
33
+ '/css/',
34
+ '/js/',
35
+ '/scripts/',
36
+ '/fonts/',
37
+ '/backup/',
38
+ '/temp/',
39
+ '/tmp/',
40
+ ];
41
+
42
+ // Patterns that indicate directory listing is enabled
43
+ const DIRECTORY_LISTING_PATTERNS = [
44
+ // Apache
45
+ /<title>Index of/i,
46
+ /Parent Directory/i,
47
+ /<h1>Index of/i,
48
+ /\[DIR\]/i,
49
+ /\[PARENTDIR\]/i,
50
+ // Nginx
51
+ /<title>Index of.*<\/title>/i,
52
+ /autoindex/i,
53
+ // IIS
54
+ /\[To Parent Directory\]/i,
55
+ /<title>.*- \/<\/title>/i,
56
+ // Generic
57
+ /Directory listing for/i,
58
+ /<pre>.*<a href="[^"]*">[^<]*<\/a>.*<\/pre>/is,
59
+ ];
60
+
61
+ /**
62
+ * Check if HTML content indicates directory listing
63
+ */
64
+ function isDirectoryListing(html: string): boolean {
65
+ return DIRECTORY_LISTING_PATTERNS.some((pattern) => pattern.test(html));
66
+ }
67
+
68
+ /**
69
+ * Detect server type from response
70
+ */
71
+ function detectServerType(headers: Record<string, string>): string | undefined {
72
+ const server = headers.server || headers.Server;
73
+ if (server) {
74
+ if (/apache/i.test(server)) return 'Apache';
75
+ if (/nginx/i.test(server)) return 'Nginx';
76
+ if (/iis/i.test(server)) return 'IIS';
77
+ if (/cloudflare/i.test(server)) return 'Cloudflare';
78
+ return server;
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ /**
84
+ * Analyze directory listing security
85
+ */
86
+ export async function analyzeDirectoryListing(url: string): Promise<{ issues: AuditIssue[]; data: DirectoryListingData }> {
87
+ const issues: AuditIssue[] = [];
88
+ const baseUrl = new URL(url);
89
+ const baseOrigin = baseUrl.origin;
90
+
91
+ const vulnerableDirectories: string[] = [];
92
+ const checkedDirectories: string[] = [];
93
+ let serverType: string | undefined;
94
+
95
+ // Check each common directory (parallel with short timeout)
96
+ const directoriesToCheck = COMMON_DIRECTORIES.slice(0, 6); // Limit to 6 directories
97
+
98
+ // Run all directory checks in parallel with 3s timeout each
99
+ const checkPromises = directoriesToCheck.map(async (dir) => {
100
+ const dirUrl = `${baseOrigin}${dir}`;
101
+ checkedDirectories.push(dir);
102
+
103
+ try {
104
+ const response = await httpGet<string>(dirUrl, {
105
+ timeout: 3000, // Reduced timeout
106
+ validateStatus: () => true,
107
+ });
108
+
109
+ // Get server type from response
110
+ if (response.headers) {
111
+ const detectedServer = detectServerType(response.headers as Record<string, string>);
112
+ if (detectedServer && !serverType) {
113
+ serverType = detectedServer;
114
+ }
115
+ }
116
+
117
+ // Check if response indicates directory listing
118
+ if (response.status === 200 && typeof response.data === 'string') {
119
+ if (isDirectoryListing(response.data)) {
120
+ vulnerableDirectories.push(dir);
121
+ }
122
+ }
123
+ } catch {
124
+ // Skip failed requests (directory doesn't exist or blocked)
125
+ }
126
+ });
127
+
128
+ // Wait for all checks with overall timeout
129
+ await Promise.race([
130
+ Promise.all(checkPromises),
131
+ new Promise<void>((resolve) => setTimeout(resolve, 8000)), // 8s overall timeout
132
+ ]);
133
+
134
+ const directoryListingEnabled = vulnerableDirectories.length > 0;
135
+
136
+ // Generate issues
137
+ if (directoryListingEnabled) {
138
+ issues.push({
139
+ code: 'DIRECTORY_LISTING_ENABLED',
140
+ severity: 'warning',
141
+ category: 'security',
142
+ title: 'Directory listing enabled',
143
+ description: `${vulnerableDirectories.length} director${vulnerableDirectories.length === 1 ? 'y' : 'ies'} expose file listings. This reveals your site structure and potentially sensitive files.`,
144
+ impact:
145
+ 'Attackers can discover backup files, configuration files, or hidden content. This is a security vulnerability and information disclosure risk.',
146
+ howToFix:
147
+ serverType === 'Apache'
148
+ ? 'Add "Options -Indexes" to your .htaccess file or Apache configuration.'
149
+ : serverType === 'Nginx'
150
+ ? 'Remove or set "autoindex off" in your Nginx configuration.'
151
+ : 'Disable directory listing in your web server configuration. Add index files to directories.',
152
+ affectedUrls: vulnerableDirectories.map((dir) => `${baseOrigin}${dir}`),
153
+ details: {
154
+ vulnerableDirectories,
155
+ serverType,
156
+ checkedCount: checkedDirectories.length,
157
+ recommendation:
158
+ 'Ensure all directories have an index.html or index.php, or disable directory listing at the server level.',
159
+ },
160
+ });
161
+ }
162
+
163
+ return {
164
+ issues,
165
+ data: {
166
+ directoryListingEnabled,
167
+ vulnerableDirectories,
168
+ checkedDirectories,
169
+ serverType,
170
+ },
171
+ };
172
+ }
@@ -0,0 +1,191 @@
1
+ // DOM Analysis Checks
2
+ // Checks for DOM size, depth, and render-blocking resources
3
+
4
+ import * as cheerio from 'cheerio';
5
+ import type { AuditIssue } from '../types.js';
6
+ import { ISSUE_DEFINITIONS } from '../types.js';
7
+
8
+ const DOM_SIZE_WARNING = 1500;
9
+ const DOM_SIZE_ERROR = 3000;
10
+ const DOM_DEPTH_WARNING = 32;
11
+
12
+ export interface DOMAnalysisData {
13
+ nodeCount: number;
14
+ maxDepth: number;
15
+ renderBlockingResources: {
16
+ css: string[];
17
+ js: string[];
18
+ };
19
+ totalRenderBlockingSize: number;
20
+ criticalPathLength: number;
21
+ }
22
+
23
+ /**
24
+ * Count total DOM nodes
25
+ */
26
+ function countNodes($: cheerio.CheerioAPI): number {
27
+ let count = 0;
28
+
29
+ function traverse(element: unknown): void {
30
+ count++;
31
+ const el = element as { children?: unknown[] };
32
+ if (el.children) {
33
+ for (const child of el.children) {
34
+ traverse(child);
35
+ }
36
+ }
37
+ }
38
+
39
+ const root = $.root();
40
+ root.each((_, el) => traverse(el));
41
+
42
+ return count;
43
+ }
44
+
45
+ /**
46
+ * Calculate maximum DOM depth
47
+ */
48
+ function calculateMaxDepth($: cheerio.CheerioAPI): number {
49
+ let maxDepth = 0;
50
+
51
+ function traverse(element: unknown, depth: number): void {
52
+ maxDepth = Math.max(maxDepth, depth);
53
+ const el = element as { children?: unknown[] };
54
+ if (el.children) {
55
+ for (const child of el.children) {
56
+ traverse(child, depth + 1);
57
+ }
58
+ }
59
+ }
60
+
61
+ const root = $.root();
62
+ root.each((_, el) => traverse(el, 0));
63
+
64
+ return maxDepth;
65
+ }
66
+
67
+ /**
68
+ * Find render-blocking resources
69
+ */
70
+ function findRenderBlockingResources($: cheerio.CheerioAPI): { css: string[]; js: string[] } {
71
+ const css: string[] = [];
72
+ const js: string[] = [];
73
+
74
+ // Find render-blocking CSS (in head, without media="print" or disabled)
75
+ $('head link[rel="stylesheet"]').each((_, el) => {
76
+ const href = $(el).attr('href');
77
+ const media = $(el).attr('media');
78
+ const disabled = $(el).attr('disabled');
79
+
80
+ if (href && media !== 'print' && !disabled) {
81
+ css.push(href);
82
+ }
83
+ });
84
+
85
+ // Find inline style tags in head (blocking)
86
+ $('head style').each((_, el) => {
87
+ const content = $(el).html();
88
+ if (content && content.trim().length > 100) {
89
+ css.push('[inline-style]');
90
+ }
91
+ });
92
+
93
+ // Find render-blocking JS (in head, without async/defer)
94
+ $('head script[src]').each((_, el) => {
95
+ const src = $(el).attr('src');
96
+ const async = $(el).attr('async');
97
+ const defer = $(el).attr('defer');
98
+ const type = $(el).attr('type');
99
+
100
+ // Skip module scripts (they're deferred by default) and non-JS
101
+ if (type === 'module' || (type && !type.includes('javascript'))) {
102
+ return;
103
+ }
104
+
105
+ if (src && !async && !defer) {
106
+ js.push(src);
107
+ }
108
+ });
109
+
110
+ return { css, js };
111
+ }
112
+
113
+ /**
114
+ * Analyze DOM structure and render-blocking resources
115
+ */
116
+ export function analyzeDOMStructure(html: string, url: string): { issues: AuditIssue[]; data: DOMAnalysisData } {
117
+ const issues: AuditIssue[] = [];
118
+ const $ = cheerio.load(html);
119
+
120
+ // Count nodes
121
+ const nodeCount = countNodes($);
122
+
123
+ // Calculate depth
124
+ const maxDepth = calculateMaxDepth($);
125
+
126
+ // Find render-blocking resources
127
+ const renderBlockingResources = findRenderBlockingResources($);
128
+ const criticalPathLength = renderBlockingResources.css.length + renderBlockingResources.js.length;
129
+
130
+ // Generate issues
131
+ if (nodeCount > DOM_SIZE_ERROR) {
132
+ issues.push({
133
+ ...ISSUE_DEFINITIONS.DOM_SIZE_EXCESSIVE,
134
+ severity: 'error',
135
+ affectedUrls: [url],
136
+ details: {
137
+ nodeCount,
138
+ threshold: DOM_SIZE_WARNING,
139
+ recommendation: 'Reduce DOM size to under 1500 nodes',
140
+ },
141
+ });
142
+ } else if (nodeCount > DOM_SIZE_WARNING) {
143
+ issues.push({
144
+ ...ISSUE_DEFINITIONS.DOM_SIZE_EXCESSIVE,
145
+ affectedUrls: [url],
146
+ details: {
147
+ nodeCount,
148
+ threshold: DOM_SIZE_WARNING,
149
+ recommendation: 'Consider simplifying page structure',
150
+ },
151
+ });
152
+ }
153
+
154
+ if (maxDepth > DOM_DEPTH_WARNING) {
155
+ issues.push({
156
+ ...ISSUE_DEFINITIONS.DOM_DEPTH_EXCESSIVE,
157
+ affectedUrls: [url],
158
+ details: {
159
+ maxDepth,
160
+ threshold: DOM_DEPTH_WARNING,
161
+ },
162
+ });
163
+ }
164
+
165
+ if (criticalPathLength > 0) {
166
+ issues.push({
167
+ ...ISSUE_DEFINITIONS.RENDER_BLOCKING_RESOURCES,
168
+ affectedUrls: [url],
169
+ details: {
170
+ cssFiles: renderBlockingResources.css,
171
+ jsFiles: renderBlockingResources.js,
172
+ totalCount: criticalPathLength,
173
+ recommendation:
174
+ criticalPathLength > 3
175
+ ? 'Inline critical CSS and defer non-critical resources'
176
+ : 'Consider using async/defer for scripts',
177
+ },
178
+ });
179
+ }
180
+
181
+ return {
182
+ issues,
183
+ data: {
184
+ nodeCount,
185
+ maxDepth,
186
+ renderBlockingResources,
187
+ totalRenderBlockingSize: 0, // Would need to fetch resources to calculate
188
+ criticalPathLength,
189
+ },
190
+ };
191
+ }