@eightmedia/flightcheck 0.0.0-stage → 1.0.0
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/CHANGELOG.md +16 -0
- package/LICENSE +691 -0
- package/README.md +152 -2
- package/dist/analyze/fixManifest.d.ts +65 -0
- package/dist/analyze/fixManifest.d.ts.map +1 -0
- package/dist/analyze/fixManifest.js +588 -0
- package/dist/analyze/report.d.ts +283 -0
- package/dist/analyze/report.d.ts.map +1 -0
- package/dist/analyze/report.js +726 -0
- package/dist/analyze/reportTier1.d.ts +46 -0
- package/dist/analyze/reportTier1.d.ts.map +1 -0
- package/dist/analyze/reportTier1.js +251 -0
- package/dist/analyze/stack.d.ts +33 -0
- package/dist/analyze/stack.d.ts.map +1 -0
- package/dist/analyze/stack.js +188 -0
- package/dist/analyze/structuredReport.d.ts +62 -0
- package/dist/analyze/structuredReport.d.ts.map +1 -0
- package/dist/analyze/structuredReport.js +524 -0
- package/dist/analyze.d.ts +3 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +40 -0
- package/dist/checks/agent-ready.d.ts +63 -0
- package/dist/checks/agent-ready.d.ts.map +1 -0
- package/dist/checks/agent-ready.js +219 -0
- package/dist/checks/analyze.d.ts +15 -0
- package/dist/checks/analyze.d.ts.map +1 -0
- package/dist/checks/analyze.js +63 -0
- package/dist/checks/crawl.d.ts +28 -0
- package/dist/checks/crawl.d.ts.map +1 -0
- package/dist/checks/crawl.js +86 -0
- package/dist/checks/dns.d.ts +25 -0
- package/dist/checks/dns.d.ts.map +1 -0
- package/dist/checks/dns.js +80 -0
- package/dist/checks/index.d.ts +24 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +12 -0
- package/dist/checks/issues.d.ts +8 -0
- package/dist/checks/issues.d.ts.map +1 -0
- package/dist/checks/issues.js +3 -0
- package/dist/checks/redirect.d.ts +38 -0
- package/dist/checks/redirect.d.ts.map +1 -0
- package/dist/checks/redirect.js +209 -0
- package/dist/checks/registry.d.ts +8 -0
- package/dist/checks/registry.d.ts.map +1 -0
- package/dist/checks/registry.js +275 -0
- package/dist/checks/robots-txt.d.ts +16 -0
- package/dist/checks/robots-txt.d.ts.map +1 -0
- package/dist/checks/robots-txt.js +99 -0
- package/dist/checks/runner.d.ts +9 -0
- package/dist/checks/runner.d.ts.map +1 -0
- package/dist/checks/runner.js +110 -0
- package/dist/checks/security-headers.d.ts +12 -0
- package/dist/checks/security-headers.d.ts.map +1 -0
- package/dist/checks/security-headers.js +53 -0
- package/dist/checks/sitemap.d.ts +36 -0
- package/dist/checks/sitemap.d.ts.map +1 -0
- package/dist/checks/sitemap.js +140 -0
- package/dist/checks/types.d.ts +72 -0
- package/dist/checks/types.d.ts.map +1 -0
- package/dist/checks/types.js +20 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +125 -0
- package/dist/crawler/checkGroups.d.ts +7 -0
- package/dist/crawler/checkGroups.d.ts.map +1 -0
- package/dist/crawler/checkGroups.js +29 -0
- package/dist/crawler/crawlState.d.ts +7 -0
- package/dist/crawler/crawlState.d.ts.map +1 -0
- package/dist/crawler/crawlState.js +6 -0
- package/dist/crawler/defaults.d.ts +11 -0
- package/dist/crawler/defaults.d.ts.map +1 -0
- package/dist/crawler/defaults.js +10 -0
- package/dist/crawler/extractors.d.ts +5 -0
- package/dist/crawler/extractors.d.ts.map +1 -0
- package/dist/crawler/extractors.js +169 -0
- package/dist/crawler/jsAssets.d.ts +2 -0
- package/dist/crawler/jsAssets.d.ts.map +1 -0
- package/dist/crawler/jsAssets.js +22 -0
- package/dist/crawler/outputPaths.d.ts +14 -0
- package/dist/crawler/outputPaths.d.ts.map +1 -0
- package/dist/crawler/outputPaths.js +51 -0
- package/dist/crawler/reporter.d.ts +3 -0
- package/dist/crawler/reporter.d.ts.map +1 -0
- package/dist/crawler/reporter.js +12 -0
- package/dist/crawler/robots.d.ts +5 -0
- package/dist/crawler/robots.d.ts.map +1 -0
- package/dist/crawler/robots.js +29 -0
- package/dist/crawler/runCrawler.d.ts +22 -0
- package/dist/crawler/runCrawler.d.ts.map +1 -0
- package/dist/crawler/runCrawler.js +172 -0
- package/dist/crawler/schema.d.ts +87 -0
- package/dist/crawler/schema.d.ts.map +1 -0
- package/dist/crawler/schema.js +44 -0
- package/dist/crawler/secrets.d.ts +4 -0
- package/dist/crawler/secrets.d.ts.map +1 -0
- package/dist/crawler/secrets.js +70 -0
- package/dist/crawler/sitemap.d.ts +8 -0
- package/dist/crawler/sitemap.d.ts.map +1 -0
- package/dist/crawler/sitemap.js +105 -0
- package/dist/crawler/stack.d.ts +3 -0
- package/dist/crawler/stack.d.ts.map +1 -0
- package/dist/crawler/stack.js +1 -0
- package/dist/crawler/stackDetect.d.ts +24 -0
- package/dist/crawler/stackDetect.d.ts.map +1 -0
- package/dist/crawler/stackDetect.js +604 -0
- package/dist/crawler/tier1Extractors.d.ts +5 -0
- package/dist/crawler/tier1Extractors.d.ts.map +1 -0
- package/dist/crawler/tier1Extractors.js +255 -0
- package/dist/crawler/tracking.d.ts +8 -0
- package/dist/crawler/tracking.d.ts.map +1 -0
- package/dist/crawler/tracking.js +50 -0
- package/dist/crawler/urlUtils.d.ts +12 -0
- package/dist/crawler/urlUtils.d.ts.map +1 -0
- package/dist/crawler/urlUtils.js +53 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/docs/agent-fixes.md +17 -0
- package/docs/agent-ready.md +44 -0
- package/docs/analyze.md +103 -0
- package/docs/check-api.md +142 -0
- package/docs/checks.md +116 -0
- package/docs/crawler.md +158 -0
- package/docs/index.md +24 -0
- package/docs/qc-checks-plan.md +796 -0
- package/docs/stack.md +99 -0
- package/package.json +86 -4
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { CHECK_DEFINITIONS, getCheckDefinition, listChecks, listImplementedChecks, runRegisteredCheck } from "./registry.js";
|
|
2
|
+
import { analyzeRedirect, checksSkippedByRedirect, resolveChecksForRedirect, runRedirectCheck, } from "./redirect.js";
|
|
3
|
+
import { DEFAULT_CHECK_IDS } from "./types.js";
|
|
4
|
+
export function planCheckWaves(requestedChecks) {
|
|
5
|
+
const requested = new Set(requestedChecks);
|
|
6
|
+
const waves = [];
|
|
7
|
+
const independent = ["crawl", "lighthouse", "security-headers", "dns", "robots-txt", "agent-ready"].filter((id) => requested.has(id));
|
|
8
|
+
if (independent.length > 0) {
|
|
9
|
+
waves.push(independent);
|
|
10
|
+
}
|
|
11
|
+
const afterCrawl = ["sitemap"].filter((id) => requested.has(id));
|
|
12
|
+
if (afterCrawl.length > 0) {
|
|
13
|
+
waves.push(afterCrawl);
|
|
14
|
+
}
|
|
15
|
+
if (requested.has("analyze")) {
|
|
16
|
+
waves.push(["analyze"]);
|
|
17
|
+
}
|
|
18
|
+
return waves;
|
|
19
|
+
}
|
|
20
|
+
function canRunAnalyze(context) {
|
|
21
|
+
return Boolean(context.outputs.redirect?.redirectOnly ||
|
|
22
|
+
context.outputs.crawl ||
|
|
23
|
+
context.options.analyze?.records ||
|
|
24
|
+
context.options.analyze?.inputPath);
|
|
25
|
+
}
|
|
26
|
+
function skippedResult(id, message) {
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
status: "skipped",
|
|
30
|
+
error: message,
|
|
31
|
+
durationMs: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function executeCheck(context, id) {
|
|
35
|
+
const definition = getCheckDefinition(id);
|
|
36
|
+
if (!definition.implemented) {
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
status: "unsupported",
|
|
40
|
+
error: `${definition.name} check is not implemented yet.`,
|
|
41
|
+
durationMs: 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (id === "analyze" && !canRunAnalyze(context)) {
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
status: "skipped",
|
|
48
|
+
error: "Analyze check requires crawl output, redirect output, or analyze.inputPath / analyze.records.",
|
|
49
|
+
durationMs: 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (id === "sitemap" && !context.outputs.crawl) {
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
status: "skipped",
|
|
56
|
+
error: "Sitemap check requires crawl output.",
|
|
57
|
+
durationMs: 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return runRegisteredCheck(context, id);
|
|
61
|
+
}
|
|
62
|
+
export async function runCheck(id, options) {
|
|
63
|
+
const context = {
|
|
64
|
+
startUrl: options.startUrl,
|
|
65
|
+
outputs: {},
|
|
66
|
+
options,
|
|
67
|
+
};
|
|
68
|
+
return executeCheck(context, id);
|
|
69
|
+
}
|
|
70
|
+
export async function probeStartUrlRedirect(startUrl) {
|
|
71
|
+
return runRedirectCheck(startUrl);
|
|
72
|
+
}
|
|
73
|
+
export async function runChecks(options) {
|
|
74
|
+
const requestedChecks = options.checks ?? DEFAULT_CHECK_IDS;
|
|
75
|
+
const context = {
|
|
76
|
+
startUrl: options.startUrl,
|
|
77
|
+
outputs: {},
|
|
78
|
+
options,
|
|
79
|
+
};
|
|
80
|
+
const results = [];
|
|
81
|
+
const redirect = await runRedirectCheck(options.startUrl);
|
|
82
|
+
context.outputs.redirect = redirect;
|
|
83
|
+
const effectiveChecks = resolveChecksForRedirect(requestedChecks, redirect);
|
|
84
|
+
const skippedChecks = checksSkippedByRedirect(requestedChecks, effectiveChecks);
|
|
85
|
+
const skipMessage = "Skipped because the start URL is a domain redirect (use --checks crawl to force a crawl).";
|
|
86
|
+
for (const id of skippedChecks) {
|
|
87
|
+
results.push(skippedResult(id, skipMessage));
|
|
88
|
+
}
|
|
89
|
+
const waves = planCheckWaves(effectiveChecks);
|
|
90
|
+
for (const wave of waves) {
|
|
91
|
+
const waveResults = options.parallel && wave.length > 1
|
|
92
|
+
? await Promise.all(wave.map((id) => executeCheck(context, id)))
|
|
93
|
+
: await wave.reduce(async (accPromise, id) => {
|
|
94
|
+
const acc = await accPromise;
|
|
95
|
+
acc.push(await executeCheck(context, id));
|
|
96
|
+
return acc;
|
|
97
|
+
}, Promise.resolve([]));
|
|
98
|
+
results.push(...waveResults);
|
|
99
|
+
const failedRequired = waveResults.find((result) => result.status === "failure" && result.id === "crawl" && effectiveChecks.includes("analyze"));
|
|
100
|
+
if (failedRequired) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
results,
|
|
106
|
+
outputs: context.outputs,
|
|
107
|
+
redirectOnly: redirect.redirectOnly,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export { CHECK_DEFINITIONS, getCheckDefinition, listChecks, listImplementedChecks, analyzeRedirect, resolveChecksForRedirect };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CheckIssue } from "./issues.js";
|
|
2
|
+
export type AnalyzeSecurityHeadersOptions = {
|
|
3
|
+
isHttps?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function analyzeSecurityHeaders(headers: Headers, options?: AnalyzeSecurityHeadersOptions): CheckIssue[];
|
|
6
|
+
export type SecurityHeadersCheckResult = {
|
|
7
|
+
url: string;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
issues: CheckIssue[];
|
|
10
|
+
};
|
|
11
|
+
export declare function runSecurityHeadersCheck(startUrl: string): Promise<SecurityHeadersCheckResult>;
|
|
12
|
+
//# sourceMappingURL=security-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../../src/checks/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAWF,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,6BAAkC,GAC1C,UAAU,EAAE,CA0Cd;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB,CAAC;AAEF,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAgBnG"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { issue } from "./issues.js";
|
|
2
|
+
function headerValue(headers, name) {
|
|
3
|
+
return headers.get(name) ?? undefined;
|
|
4
|
+
}
|
|
5
|
+
function hasFrameAncestorsCsp(csp) {
|
|
6
|
+
if (!csp)
|
|
7
|
+
return false;
|
|
8
|
+
return /frame-ancestors/i.test(csp);
|
|
9
|
+
}
|
|
10
|
+
export function analyzeSecurityHeaders(headers, options = {}) {
|
|
11
|
+
const isHttps = options.isHttps ?? true;
|
|
12
|
+
const issues = [];
|
|
13
|
+
const hsts = headerValue(headers, "strict-transport-security");
|
|
14
|
+
const csp = headerValue(headers, "content-security-policy");
|
|
15
|
+
const xFrameOptions = headerValue(headers, "x-frame-options");
|
|
16
|
+
const nosniff = headerValue(headers, "x-content-type-options");
|
|
17
|
+
const referrerPolicy = headerValue(headers, "referrer-policy");
|
|
18
|
+
const permissionsPolicy = headerValue(headers, "permissions-policy");
|
|
19
|
+
if (isHttps && !hsts) {
|
|
20
|
+
issues.push(issue("missing-hsts", "error", "Strict-Transport-Security header is missing on HTTPS."));
|
|
21
|
+
}
|
|
22
|
+
if (!csp) {
|
|
23
|
+
issues.push(issue("missing-csp", "warning", "Content-Security-Policy header is missing."));
|
|
24
|
+
}
|
|
25
|
+
if (!xFrameOptions && !hasFrameAncestorsCsp(csp)) {
|
|
26
|
+
issues.push(issue("missing-clickjacking-protection", "warning", "Neither X-Frame-Options nor CSP frame-ancestors is set."));
|
|
27
|
+
}
|
|
28
|
+
if (!nosniff || nosniff.toLowerCase() !== "nosniff") {
|
|
29
|
+
issues.push(issue("missing-nosniff", "warning", "X-Content-Type-Options: nosniff is missing."));
|
|
30
|
+
}
|
|
31
|
+
if (!referrerPolicy) {
|
|
32
|
+
issues.push(issue("missing-referrer-policy", "review", "Referrer-Policy header is missing."));
|
|
33
|
+
}
|
|
34
|
+
if (!permissionsPolicy) {
|
|
35
|
+
issues.push(issue("missing-permissions-policy", "review", "Permissions-Policy header is missing."));
|
|
36
|
+
}
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
export async function runSecurityHeadersCheck(startUrl) {
|
|
40
|
+
const url = new URL(startUrl);
|
|
41
|
+
const response = await fetch(url.origin, { redirect: "follow" });
|
|
42
|
+
const headers = {};
|
|
43
|
+
response.headers.forEach((value, key) => {
|
|
44
|
+
headers[key.toLowerCase()] = value;
|
|
45
|
+
});
|
|
46
|
+
const headerBag = new Headers(headers);
|
|
47
|
+
const issues = analyzeSecurityHeaders(headerBag, { isHttps: url.protocol === "https:" });
|
|
48
|
+
return {
|
|
49
|
+
url: url.origin,
|
|
50
|
+
headers,
|
|
51
|
+
issues,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type CheckIssue } from "./issues.js";
|
|
2
|
+
export type SitemapSourceResult = {
|
|
3
|
+
url: string;
|
|
4
|
+
status: number | null;
|
|
5
|
+
pageUrls: string[];
|
|
6
|
+
};
|
|
7
|
+
export type SitemapCheckResult = {
|
|
8
|
+
sources: SitemapSourceResult[];
|
|
9
|
+
sitemapPageUrls: string[];
|
|
10
|
+
crawledUrls: string[];
|
|
11
|
+
inSitemapNotCrawled: string[];
|
|
12
|
+
crawledNotInSitemap: string[];
|
|
13
|
+
mismatchPercent: number;
|
|
14
|
+
issues: CheckIssue[];
|
|
15
|
+
};
|
|
16
|
+
export type AnalyzeSitemapCompareInput = {
|
|
17
|
+
sources: SitemapSourceResult[];
|
|
18
|
+
crawledUrls: string[];
|
|
19
|
+
mismatchThresholdPercent?: number;
|
|
20
|
+
excludePatterns?: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare function analyzeSitemapCompare(input: AnalyzeSitemapCompareInput): SitemapCheckResult;
|
|
23
|
+
export declare function discoverSitemapCandidates(origin: string, robotsSitemapUrls?: string[]): string[];
|
|
24
|
+
export declare function loadSitemapSources(candidateUrls: string[], origin: string, timeoutMs: number): Promise<SitemapSourceResult[]>;
|
|
25
|
+
export type RunSitemapCheckOptions = {
|
|
26
|
+
startUrl: string;
|
|
27
|
+
crawledUrls: string[];
|
|
28
|
+
robotsSitemapUrls?: string[];
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
mismatchThresholdPercent?: number;
|
|
31
|
+
excludePatterns?: string[];
|
|
32
|
+
};
|
|
33
|
+
export type SitemapCheckOptions = Pick<RunSitemapCheckOptions, "timeoutMs" | "mismatchThresholdPercent" | "excludePatterns">;
|
|
34
|
+
export declare function runSitemapCheck(options: RunSitemapCheckOptions): Promise<SitemapCheckResult>;
|
|
35
|
+
export declare function collectSitemapPageUrls(startUrl: string, timeoutMs: number, robotsSitemapUrls?: string[], customSitemapUrl?: string): Promise<string[]>;
|
|
36
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/checks/sitemap.ts"],"names":[],"mappings":"AAGA,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAMrD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B,CAAC;AAQF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,kBAAkB,CA6G3F;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAIpG;AAkBD,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,MAAM,EAAE,EACvB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAchC;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,sBAAsB,EACtB,WAAW,GAAG,0BAA0B,GAAG,iBAAiB,CAC7D,CAAC;AAEF,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAYlG;AAED,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,iBAAiB,GAAE,MAAM,EAAO,EAChC,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBnB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { DEFAULT_SITEMAP_URL_SUFFIX, DEFAULT_TIMEOUT_MS, DEFAULT_USER_AGENT } from "../crawler/defaults.js";
|
|
2
|
+
import { collectUrlsFromSitemap } from "../crawler/sitemap.js";
|
|
3
|
+
import { matchesExcludePattern, normalizeUrl } from "../crawler/urlUtils.js";
|
|
4
|
+
import { issue } from "./issues.js";
|
|
5
|
+
const SITEMAP_URL_NOT_CRAWLED_TIP = "Probable cause: sitemap URLs must match crawled URLs exactly, including hostname (apex vs www). " +
|
|
6
|
+
"These URLs were not crawled — check page limits, robots blocks, stale sitemap entries, or a failing sitemap regeneration job (cron).";
|
|
7
|
+
function filterComparableUrls(urls, excludePatterns) {
|
|
8
|
+
const normalized = [...new Set(urls.map((url) => normalizeUrl(url)))];
|
|
9
|
+
if (excludePatterns.length === 0)
|
|
10
|
+
return normalized;
|
|
11
|
+
return normalized.filter((url) => !matchesExcludePattern(url, excludePatterns));
|
|
12
|
+
}
|
|
13
|
+
export function analyzeSitemapCompare(input) {
|
|
14
|
+
const threshold = input.mismatchThresholdPercent ?? 10;
|
|
15
|
+
const excludePatterns = input.excludePatterns ?? [];
|
|
16
|
+
const issues = [];
|
|
17
|
+
for (const source of input.sources) {
|
|
18
|
+
if (source.status === null || source.status >= 400) {
|
|
19
|
+
issues.push(issue("sitemap-unreachable", "error", `Sitemap not reachable: ${source.url} (${source.status ?? "unreachable"}).`));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const successfulSources = input.sources.filter((source) => source.status !== null && source.status >= 200 && source.status < 300);
|
|
23
|
+
if (successfulSources.length === 0) {
|
|
24
|
+
issues.push(issue("sitemap-missing", "error", "No sitemap found."));
|
|
25
|
+
return {
|
|
26
|
+
sources: input.sources,
|
|
27
|
+
sitemapPageUrls: [],
|
|
28
|
+
crawledUrls: filterComparableUrls(input.crawledUrls, excludePatterns),
|
|
29
|
+
inSitemapNotCrawled: [],
|
|
30
|
+
crawledNotInSitemap: [],
|
|
31
|
+
mismatchPercent: 0,
|
|
32
|
+
issues,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const sitemapPageUrls = filterComparableUrls(successfulSources.flatMap((source) => source.pageUrls), excludePatterns);
|
|
36
|
+
const crawledUrls = filterComparableUrls(input.crawledUrls, excludePatterns);
|
|
37
|
+
if (sitemapPageUrls.length === 0) {
|
|
38
|
+
issues.push(issue("sitemap-empty", "error", "Sitemap was reachable but contained no page URLs (check sitemap index nesting and host aliases such as apex vs www)."));
|
|
39
|
+
return {
|
|
40
|
+
sources: input.sources,
|
|
41
|
+
sitemapPageUrls,
|
|
42
|
+
crawledUrls,
|
|
43
|
+
inSitemapNotCrawled: [],
|
|
44
|
+
crawledNotInSitemap: crawledUrls,
|
|
45
|
+
mismatchPercent: crawledUrls.length === 0 ? 0 : 100,
|
|
46
|
+
issues,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const sitemapSet = new Set(sitemapPageUrls);
|
|
50
|
+
const crawledSet = new Set(crawledUrls);
|
|
51
|
+
const inSitemapNotCrawled = sitemapPageUrls.filter((url) => !crawledSet.has(url));
|
|
52
|
+
const crawledNotInSitemap = crawledUrls.filter((url) => !sitemapSet.has(url));
|
|
53
|
+
if (inSitemapNotCrawled.length > 0) {
|
|
54
|
+
issues.push(issue("sitemap-url-not-crawled", "critical", `${inSitemapNotCrawled.length} sitemap URL(s) were not crawled. ${SITEMAP_URL_NOT_CRAWLED_TIP}`));
|
|
55
|
+
}
|
|
56
|
+
if (crawledNotInSitemap.length > 0) {
|
|
57
|
+
issues.push(issue("sitemap-missing-crawled-url", "critical", `${crawledNotInSitemap.length} crawled URL(s) are missing from the sitemap.`));
|
|
58
|
+
}
|
|
59
|
+
const total = new Set([...sitemapPageUrls, ...crawledUrls]).size;
|
|
60
|
+
const mismatchCount = new Set([...inSitemapNotCrawled, ...crawledNotInSitemap]).size;
|
|
61
|
+
const mismatchPercent = total === 0 ? 0 : (mismatchCount / total) * 100;
|
|
62
|
+
if (mismatchPercent > threshold) {
|
|
63
|
+
issues.push(issue("sitemap-mismatch-threshold", "critical", `Sitemap mismatch is ${mismatchPercent.toFixed(1)}% (threshold ${threshold}%). ` +
|
|
64
|
+
"Sitemap URLs must match crawled URLs exactly, including hostname."));
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
sources: input.sources,
|
|
68
|
+
sitemapPageUrls,
|
|
69
|
+
crawledUrls,
|
|
70
|
+
inSitemapNotCrawled,
|
|
71
|
+
crawledNotInSitemap,
|
|
72
|
+
mismatchPercent,
|
|
73
|
+
issues,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function discoverSitemapCandidates(origin, robotsSitemapUrls = []) {
|
|
77
|
+
const defaultSitemap = normalizeUrl(`${origin}${DEFAULT_SITEMAP_URL_SUFFIX}`);
|
|
78
|
+
const candidates = [...robotsSitemapUrls.map((url) => normalizeUrl(url)), defaultSitemap];
|
|
79
|
+
return [...new Set(candidates)];
|
|
80
|
+
}
|
|
81
|
+
async function probeSitemapStatus(url, timeoutMs) {
|
|
82
|
+
try {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
redirect: "follow",
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
headers: { "user-agent": DEFAULT_USER_AGENT },
|
|
89
|
+
});
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
return response.status;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function loadSitemapSources(candidateUrls, origin, timeoutMs) {
|
|
98
|
+
const sources = [];
|
|
99
|
+
for (const url of candidateUrls) {
|
|
100
|
+
const status = await probeSitemapStatus(url, timeoutMs);
|
|
101
|
+
if (status !== null && status >= 200 && status < 300) {
|
|
102
|
+
const pageUrls = Array.from(await collectUrlsFromSitemap(url, origin, timeoutMs));
|
|
103
|
+
sources.push({ url, status, pageUrls });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
sources.push({ url, status, pageUrls: [] });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return sources;
|
|
110
|
+
}
|
|
111
|
+
export async function runSitemapCheck(options) {
|
|
112
|
+
const origin = new URL(normalizeUrl(options.startUrl)).origin;
|
|
113
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
114
|
+
const candidates = discoverSitemapCandidates(origin, options.robotsSitemapUrls);
|
|
115
|
+
const sources = await loadSitemapSources(candidates, origin, timeoutMs);
|
|
116
|
+
return analyzeSitemapCompare({
|
|
117
|
+
sources,
|
|
118
|
+
crawledUrls: options.crawledUrls,
|
|
119
|
+
mismatchThresholdPercent: options.mismatchThresholdPercent,
|
|
120
|
+
excludePatterns: options.excludePatterns,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export async function collectSitemapPageUrls(startUrl, timeoutMs, robotsSitemapUrls = [], customSitemapUrl) {
|
|
124
|
+
const origin = new URL(normalizeUrl(startUrl)).origin;
|
|
125
|
+
const candidates = discoverSitemapCandidates(origin, robotsSitemapUrls);
|
|
126
|
+
if (customSitemapUrl) {
|
|
127
|
+
const normalized = normalizeUrl(customSitemapUrl);
|
|
128
|
+
if (!candidates.includes(normalized)) {
|
|
129
|
+
candidates.unshift(normalized);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const pageUrls = new Set();
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
const urls = await collectUrlsFromSitemap(candidate, origin, timeoutMs);
|
|
135
|
+
for (const url of urls) {
|
|
136
|
+
pageUrls.add(url);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return [...pageUrls];
|
|
140
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { VerificationRecord } from "../crawler/schema.js";
|
|
2
|
+
import type { AgentReadyCheckResult } from "./agent-ready.js";
|
|
3
|
+
import type { DnsCheckResult } from "./dns.js";
|
|
4
|
+
import type { RedirectCheckResult } from "./redirect.js";
|
|
5
|
+
import type { RobotsTxtCheckResult } from "./robots-txt.js";
|
|
6
|
+
import type { SecurityHeadersCheckResult } from "./security-headers.js";
|
|
7
|
+
import type { SitemapCheckResult } from "./sitemap.js";
|
|
8
|
+
export declare const CHECK_IDS: readonly ["crawl", "analyze", "lighthouse", "security-headers", "dns", "robots-txt", "sitemap", "agent-ready"];
|
|
9
|
+
export type CheckId = (typeof CHECK_IDS)[number];
|
|
10
|
+
export type ImplementedCheckId = "crawl" | "analyze" | "security-headers" | "dns" | "robots-txt" | "sitemap" | "agent-ready";
|
|
11
|
+
export type PlannedCheckId = Exclude<CheckId, ImplementedCheckId>;
|
|
12
|
+
/** Default pipeline: all implemented checks (Tier 1 infra + crawl + analyze). */
|
|
13
|
+
export declare const DEFAULT_CHECK_IDS: CheckId[];
|
|
14
|
+
export type CheckStatus = "success" | "failure" | "skipped" | "unsupported";
|
|
15
|
+
export type CheckExecutionResult<TData = unknown> = {
|
|
16
|
+
id: CheckId;
|
|
17
|
+
status: CheckStatus;
|
|
18
|
+
data?: TData;
|
|
19
|
+
error?: string;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
};
|
|
22
|
+
export type CrawlCheckResult = {
|
|
23
|
+
records: VerificationRecord[];
|
|
24
|
+
outputPath: string;
|
|
25
|
+
interrupted: boolean;
|
|
26
|
+
secretsFound: number;
|
|
27
|
+
};
|
|
28
|
+
export type AnalyzeCheckResult = {
|
|
29
|
+
inputPath: string;
|
|
30
|
+
reportPath: string;
|
|
31
|
+
reportJsonPath: string;
|
|
32
|
+
markdown: string;
|
|
33
|
+
report: import("../analyze/structuredReport.js").FlightcheckReport;
|
|
34
|
+
pagesAnalyzed: number;
|
|
35
|
+
};
|
|
36
|
+
export type CheckOutputs = {
|
|
37
|
+
crawl?: CrawlCheckResult;
|
|
38
|
+
analyze?: AnalyzeCheckResult;
|
|
39
|
+
redirect?: RedirectCheckResult;
|
|
40
|
+
securityHeaders?: SecurityHeadersCheckResult;
|
|
41
|
+
dns?: DnsCheckResult;
|
|
42
|
+
robotsTxt?: RobotsTxtCheckResult;
|
|
43
|
+
sitemap?: SitemapCheckResult;
|
|
44
|
+
agentReady?: AgentReadyCheckResult;
|
|
45
|
+
};
|
|
46
|
+
export type CheckDefinition<TData = unknown> = {
|
|
47
|
+
id: CheckId;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
dependencies: CheckId[];
|
|
51
|
+
implemented: boolean;
|
|
52
|
+
run: (context: CheckRunContext) => Promise<CheckExecutionResult<TData>>;
|
|
53
|
+
};
|
|
54
|
+
export type CheckRunContext = {
|
|
55
|
+
startUrl: string;
|
|
56
|
+
outputs: CheckOutputs;
|
|
57
|
+
options: RunChecksOptions;
|
|
58
|
+
};
|
|
59
|
+
export type RunChecksOptions = {
|
|
60
|
+
startUrl: string;
|
|
61
|
+
checks?: CheckId[];
|
|
62
|
+
parallel?: boolean;
|
|
63
|
+
crawl?: import("./crawl.js").CrawlCheckOptions;
|
|
64
|
+
analyze?: import("./analyze.js").AnalyzeCheckOptions;
|
|
65
|
+
sitemap?: import("./sitemap.js").SitemapCheckOptions;
|
|
66
|
+
};
|
|
67
|
+
export type RunChecksResult = {
|
|
68
|
+
results: CheckExecutionResult[];
|
|
69
|
+
outputs: CheckOutputs;
|
|
70
|
+
redirectOnly: boolean;
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/checks/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,SAAS,gHASZ,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD,MAAM,MAAM,kBAAkB,GAC1B,OAAO,GACP,SAAS,GACT,kBAAkB,GAClB,KAAK,GACL,YAAY,GACZ,SAAS,GACT,aAAa,CAAC;AAElB,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;AAElE,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,EAAE,OAAO,EAQtC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAE5E,MAAM,MAAM,oBAAoB,CAAC,KAAK,GAAG,OAAO,IAAI;IAClD,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,gCAAgC,EAAE,iBAAiB,CAAC;IACnE,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,eAAe,CAAC,EAAE,0BAA0B,CAAC;IAC7C,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,KAAK,GAAG,OAAO,IAAI;IAC7C,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,EAAE,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,GAAG,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,YAAY,EAAE,iBAAiB,CAAC;IAC/C,OAAO,CAAC,EAAE,OAAO,cAAc,EAAE,mBAAmB,CAAC;IACrD,OAAO,CAAC,EAAE,OAAO,cAAc,EAAE,mBAAmB,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,OAAO,EAAE,YAAY,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const CHECK_IDS = [
|
|
2
|
+
"crawl",
|
|
3
|
+
"analyze",
|
|
4
|
+
"lighthouse",
|
|
5
|
+
"security-headers",
|
|
6
|
+
"dns",
|
|
7
|
+
"robots-txt",
|
|
8
|
+
"sitemap",
|
|
9
|
+
"agent-ready",
|
|
10
|
+
];
|
|
11
|
+
/** Default pipeline: all implemented checks (Tier 1 infra + crawl + analyze). */
|
|
12
|
+
export const DEFAULT_CHECK_IDS = [
|
|
13
|
+
"crawl",
|
|
14
|
+
"security-headers",
|
|
15
|
+
"dns",
|
|
16
|
+
"robots-txt",
|
|
17
|
+
"sitemap",
|
|
18
|
+
"agent-ready",
|
|
19
|
+
"analyze",
|
|
20
|
+
];
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { runChecks } from "./checks/runner.js";
|
|
4
|
+
import { DEFAULT_CHECK_IDS } from "./checks/types.js";
|
|
5
|
+
import { DEFAULT_BURST_DELAY_MS, DEFAULT_CONCURRENCY, DEFAULT_DELAY_MS, DEFAULT_MAX_PAGES, DEFAULT_TIMEOUT_MS, } from "./crawler/defaults.js";
|
|
6
|
+
import { buildDefaultOutputPaths } from "./crawler/outputPaths.js";
|
|
7
|
+
import { isHttpUrl, normalizeUrl, parseExcludePatterns } from "./crawler/urlUtils.js";
|
|
8
|
+
async function main() {
|
|
9
|
+
const sanitizedArgv = [...process.argv];
|
|
10
|
+
const separatorIndex = sanitizedArgv.indexOf("--");
|
|
11
|
+
if (separatorIndex >= 2) {
|
|
12
|
+
sanitizedArgv.splice(separatorIndex, 1);
|
|
13
|
+
}
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name("flightcheck")
|
|
17
|
+
.description("Crawl a website and generate crawl JSON plus structured report JSON/Markdown.")
|
|
18
|
+
.argument("<startUrl>", "Start URL for the crawl")
|
|
19
|
+
.option("--output <file>", "Output crawl JSON file path")
|
|
20
|
+
.option("--report <file>", "Output Markdown report path")
|
|
21
|
+
.option("--json-only", "Skip analyze report generation", false)
|
|
22
|
+
.option("--errors-only", "Include only critical/error findings in the Markdown report", false)
|
|
23
|
+
.option("--parallel", "Run independent checks in parallel", false)
|
|
24
|
+
.option("--checks <ids>", "Comma-separated checks to run (crawl,analyze,lighthouse,security-headers,dns,robots-txt,sitemap,agent-ready)")
|
|
25
|
+
.option("--max-pages <n>", "Maximum number of pages", String(DEFAULT_MAX_PAGES))
|
|
26
|
+
.option("--delay-ms <n>", "Delay between requests in ms", String(DEFAULT_DELAY_MS))
|
|
27
|
+
.option("--timeout-ms <n>", "Request timeout in ms", String(DEFAULT_TIMEOUT_MS))
|
|
28
|
+
.option("--concurrency <n>", "Maximum concurrent requests", String(DEFAULT_CONCURRENCY))
|
|
29
|
+
.option("--burst-delay-ms <n>", "Pause after each burst in ms", String(DEFAULT_BURST_DELAY_MS))
|
|
30
|
+
.option("--sitemap-start", "Seed crawl with sitemap URLs", false)
|
|
31
|
+
.option("--sitemap-url <url>", "Custom sitemap URL")
|
|
32
|
+
.option("--respect-robots-txt", "Respect robots.txt disallow rules", false)
|
|
33
|
+
.option("--quiet", "Suppress Crawlee info/statistics logs (only warnings and errors)", false)
|
|
34
|
+
.option("--exclude <patterns>", "Exclude patterns (comma-separated, repeatable)", (value, acc) => {
|
|
35
|
+
acc.push(value);
|
|
36
|
+
return acc;
|
|
37
|
+
}, [])
|
|
38
|
+
.option("--only <groups>", "Run only these check groups (comma-separated): meta,icons,social,tracking,stack,security,images,a11y-static")
|
|
39
|
+
.option("--skip <groups>", "Skip these check groups (comma-separated)")
|
|
40
|
+
.option("--reveal-secrets", "Show full secret values instead of masked ones", false)
|
|
41
|
+
.action(async (startUrl, opts) => {
|
|
42
|
+
if (!isHttpUrl(startUrl)) {
|
|
43
|
+
throw new Error("<start-url> must be a valid http(s) URL.");
|
|
44
|
+
}
|
|
45
|
+
const normalizedStartUrl = normalizeUrl(startUrl);
|
|
46
|
+
const defaultPaths = buildDefaultOutputPaths(normalizedStartUrl);
|
|
47
|
+
const outputPath = opts.output;
|
|
48
|
+
const reportPath = opts.report;
|
|
49
|
+
const jsonOnly = Boolean(opts.jsonOnly);
|
|
50
|
+
const errorsOnly = Boolean(opts.errorsOnly);
|
|
51
|
+
const checks = opts.checks
|
|
52
|
+
? opts.checks.split(",").map((value) => value.trim()).filter(Boolean)
|
|
53
|
+
: jsonOnly
|
|
54
|
+
? ["crawl"]
|
|
55
|
+
: DEFAULT_CHECK_IDS;
|
|
56
|
+
const { outputs, results } = await runChecks({
|
|
57
|
+
startUrl: normalizedStartUrl,
|
|
58
|
+
checks,
|
|
59
|
+
parallel: Boolean(opts.parallel),
|
|
60
|
+
crawl: {
|
|
61
|
+
startUrl: normalizedStartUrl,
|
|
62
|
+
outputPath: outputPath ?? defaultPaths.rawJson,
|
|
63
|
+
maxPages: Number(opts.maxPages),
|
|
64
|
+
delayMs: Number(opts.delayMs),
|
|
65
|
+
timeoutMs: Number(opts.timeoutMs),
|
|
66
|
+
concurrency: Number(opts.concurrency),
|
|
67
|
+
burstDelayMs: Number(opts.burstDelayMs),
|
|
68
|
+
sitemapStart: opts.sitemapStart,
|
|
69
|
+
sitemapUrl: opts.sitemapUrl,
|
|
70
|
+
excludePatterns: parseExcludePatterns(opts.exclude),
|
|
71
|
+
respectRobotsTxt: opts.respectRobotsTxt,
|
|
72
|
+
only: opts.only,
|
|
73
|
+
skip: opts.skip,
|
|
74
|
+
revealSecrets: opts.revealSecrets,
|
|
75
|
+
quiet: opts.quiet,
|
|
76
|
+
},
|
|
77
|
+
analyze: {
|
|
78
|
+
reportPath: reportPath ?? defaultPaths.reportMd,
|
|
79
|
+
reportJsonPath: defaultPaths.reportJson,
|
|
80
|
+
errorsOnly,
|
|
81
|
+
siteUrl: normalizedStartUrl,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const crawlResult = outputs.crawl;
|
|
85
|
+
const analyzeResult = outputs.analyze;
|
|
86
|
+
const crawlFailure = results.find((result) => result.id === "crawl" && result.status === "failure");
|
|
87
|
+
if (crawlFailure) {
|
|
88
|
+
throw new Error(crawlFailure.error ?? "Crawl check failed.");
|
|
89
|
+
}
|
|
90
|
+
const summary = {
|
|
91
|
+
message: crawlResult?.interrupted ? "Flightcheck interrupted, partial reports written" : "Flightcheck reports written",
|
|
92
|
+
redirectOnly: outputs.redirect?.redirectOnly ?? false,
|
|
93
|
+
checks: results.map((result) => ({ id: result.id, status: result.status, durationMs: result.durationMs })),
|
|
94
|
+
};
|
|
95
|
+
if (outputs.redirect?.redirectOnly) {
|
|
96
|
+
summary.redirect = {
|
|
97
|
+
url: outputs.redirect.url,
|
|
98
|
+
status: outputs.redirect.status,
|
|
99
|
+
location: outputs.redirect.location,
|
|
100
|
+
resolvedTarget: outputs.redirect.resolvedTarget,
|
|
101
|
+
issues: outputs.redirect.issues.length,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (crawlResult) {
|
|
105
|
+
summary.output = crawlResult.outputPath;
|
|
106
|
+
summary.pages = crawlResult.records.length;
|
|
107
|
+
summary.secretsFound = crawlResult.secretsFound;
|
|
108
|
+
summary.interrupted = crawlResult.interrupted;
|
|
109
|
+
if (crawlResult.secretsFound > 0 && !opts.revealSecrets) {
|
|
110
|
+
summary.hint = `${crawlResult.secretsFound} secret(s) found (masked). Re-run with --reveal-secrets to show full values.`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (analyzeResult) {
|
|
114
|
+
summary.report = analyzeResult.reportPath;
|
|
115
|
+
summary.reportJson = analyzeResult.reportJsonPath;
|
|
116
|
+
summary.findings = analyzeResult.report.findings.length;
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
119
|
+
});
|
|
120
|
+
await program.parseAsync(sanitizedArgv);
|
|
121
|
+
}
|
|
122
|
+
main().catch((error) => {
|
|
123
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const CHECK_GROUPS: readonly ["meta", "icons", "social", "tracking", "stack", "security", "images", "a11y-static"];
|
|
2
|
+
export type CheckGroup = (typeof CHECK_GROUPS)[number];
|
|
3
|
+
export declare function resolveActiveGroups(opts: {
|
|
4
|
+
only?: string;
|
|
5
|
+
skip?: string;
|
|
6
|
+
}): Set<CheckGroup>;
|
|
7
|
+
//# sourceMappingURL=checkGroups.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkGroups.d.ts","sourceRoot":"","sources":["../../src/crawler/checkGroups.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,gGAAiG,CAAC;AAE3H,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAmBvD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAAC,UAAU,CAAC,CAY3F"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const CHECK_GROUPS = ["meta", "icons", "social", "tracking", "stack", "security", "images", "a11y-static"];
|
|
2
|
+
function parseList(value) {
|
|
3
|
+
return value
|
|
4
|
+
.split(",")
|
|
5
|
+
.map((part) => part.trim())
|
|
6
|
+
.filter((part) => part.length > 0);
|
|
7
|
+
}
|
|
8
|
+
function assertValid(names) {
|
|
9
|
+
const valid = new Set(CHECK_GROUPS);
|
|
10
|
+
for (const name of names) {
|
|
11
|
+
if (!valid.has(name)) {
|
|
12
|
+
throw new Error(`Unknown check group "${name}". Valid groups: ${CHECK_GROUPS.join(", ")}.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return names;
|
|
16
|
+
}
|
|
17
|
+
export function resolveActiveGroups(opts) {
|
|
18
|
+
if (opts.only && opts.skip) {
|
|
19
|
+
throw new Error("--only and --skip are mutually exclusive. Use one or the other.");
|
|
20
|
+
}
|
|
21
|
+
if (opts.only) {
|
|
22
|
+
return new Set(assertValid(parseList(opts.only)));
|
|
23
|
+
}
|
|
24
|
+
if (opts.skip) {
|
|
25
|
+
const skip = new Set(assertValid(parseList(opts.skip)));
|
|
26
|
+
return new Set(CHECK_GROUPS.filter((group) => !skip.has(group)));
|
|
27
|
+
}
|
|
28
|
+
return new Set(CHECK_GROUPS);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crawlState.d.ts","sourceRoot":"","sources":["../../src/crawler/crawlState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,wBAAgB,gBAAgB,IAAI,UAAU,CAK7C"}
|