@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.
- package/README.md +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- 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
|
+
}
|