@akotliar/sitemap-qa 1.0.0-alpha.4 → 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/commands/analyze.ts","../src/config/loader.ts","../src/config/schema.ts","../src/config/defaults.ts","../src/core/discovery.ts","../src/core/parser.ts","../src/core/extractor.ts","../src/core/matcher.ts","../src/reporters/console-reporter.ts","../src/reporters/json-reporter.ts","../src/reporters/html-reporter.ts","../src/commands/init.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { Command } from 'commander';\r\nimport { analyzeCommand } from '@/commands/analyze';\r\nimport { initCommand } from '@/commands/init';\r\n\r\nconst program = new Command();\r\n\r\nprogram\r\n .name('sitemap-qa')\r\n .version('1.0.0')\r\n .description('sitemap analysis for QA teams');\r\n\r\nprogram.addCommand(analyzeCommand);\r\nprogram.addCommand(initCommand);\r\n\r\n// Global error handler\r\nprocess.on('unhandledRejection', (reason, promise) => {\r\n console.error('Unhandled Rejection at:', promise, 'reason:', reason);\r\n process.exit(1);\r\n});\r\n\r\n// Graceful shutdown handlers\r\nprocess.on('SIGINT', () => {\r\n console.log('\\nGracefully shutting down...');\r\n process.exit(0);\r\n});\r\n\r\nprocess.on('SIGTERM', () => {\r\n console.log('\\nGracefully shutting down...');\r\n process.exit(0);\r\n});\r\n\r\nprogram.parse();\r\n","import { Command } from 'commander';\r\nimport chalk from 'chalk';\r\nimport path from 'node:path';\r\nimport fs from 'node:fs/promises';\r\nimport { ConfigLoader } from '../config/loader';\r\nimport { ExtractorService } from '../core/extractor';\r\nimport { MatcherService } from '../core/matcher';\r\nimport { ConsoleReporter } from '../reporters/console-reporter';\r\nimport { JsonReporter } from '../reporters/json-reporter';\r\nimport { HtmlReporter } from '../reporters/html-reporter';\r\nimport { ReportData, Reporter } from '../reporters/base';\r\nimport { SitemapUrl } from '../types/sitemap';\r\n\r\nexport const analyzeCommand = new Command('analyze')\r\n .description('Analyze a sitemap for potential risks')\r\n .argument('<url>', 'Root sitemap URL')\r\n .option('-c, --config <path>', 'Path to sitemap-qa.yaml')\r\n .option('-o, --output <format>', 'Output format (json, html, all)')\r\n .option('-d, --out-dir <path>', 'Directory to save reports')\r\n .action(async (url: string, options: { config?: string; output?: string; outDir?: string }) => {\r\n const startTime = new Date();\r\n \r\n // 1. Load Config\r\n const config = ConfigLoader.load(options.config);\r\n const outDir = options.outDir || config.outDir || '.';\r\n const outputFormat = options.output || config.outputFormat || 'all';\r\n \r\n // 2. Initialize Services\r\n const extractor = new ExtractorService();\r\n const matcher = new MatcherService(config);\r\n \r\n const urlsWithRisks: SitemapUrl[] = [];\r\n let totalUrls = 0;\r\n let totalRisks = 0;\r\n\r\n console.log(chalk.blue(`\\n��� Starting analysis of ${url}...`));\r\n\r\n try {\r\n // 3. Pipeline: Extract -> Match\r\n for await (const urlObj of extractor.extract(url)) {\r\n totalUrls++;\r\n const risks = matcher.match(urlObj);\r\n \r\n if (risks.length > 0) {\r\n urlObj.risks = risks;\r\n urlsWithRisks.push(urlObj);\r\n totalRisks += risks.length;\r\n }\r\n\r\n if (totalUrls % 100 === 0) {\r\n process.stdout.write(chalk.gray(`\\rProcessed ${totalUrls} URLs...`));\r\n }\r\n }\r\n process.stdout.write('\\n');\r\n\r\n const endTime = new Date();\r\n const reportData: ReportData = {\r\n rootUrl: url,\r\n discoveredSitemaps: extractor.getDiscoveredSitemaps(),\r\n totalUrls,\r\n totalRisks,\r\n urlsWithRisks,\r\n startTime,\r\n endTime,\r\n };\r\n\r\n // 4. Reporting\r\n const reporters: Reporter[] = [new ConsoleReporter()];\r\n \r\n await fs.mkdir(outDir, { recursive: true });\r\n\r\n if (outputFormat === 'json' || outputFormat === 'all') {\r\n const jsonPath = path.join(outDir, 'sitemap-qa-report.json');\r\n reporters.push(new JsonReporter(jsonPath));\r\n }\r\n if (outputFormat === 'html' || outputFormat === 'all') {\r\n const htmlPath = path.join(outDir, 'sitemap-qa-report.html');\r\n reporters.push(new HtmlReporter(htmlPath));\r\n }\r\n\r\n for (const reporter of reporters) {\r\n await reporter.generate(reportData);\r\n }\r\n\r\n // 5. Exit Code\r\n if (totalRisks > 0) {\r\n process.exit(1);\r\n } else {\r\n process.exit(0);\r\n }\r\n\r\n } catch (error) {\r\n console.error(chalk.red('\\nAnalysis failed:'), error);\r\n process.exit(1);\r\n }\r\n });\r\n","import fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport yaml from 'js-yaml';\r\nimport { ConfigSchema, type Config } from './schema';\r\nimport { DEFAULT_POLICIES } from './defaults';\r\nimport chalk from 'chalk';\r\n\r\nexport class ConfigLoader {\r\n private static readonly DEFAULT_CONFIG_PATH = 'sitemap-qa.yaml';\r\n\r\n static load(configPath?: string): Config {\r\n const targetPath = configPath || path.join(process.cwd(), this.DEFAULT_CONFIG_PATH);\r\n let userConfig: Config = { policies: [] };\r\n\r\n // Load YAML config\r\n if (fs.existsSync(targetPath)) {\r\n try {\r\n const fileContent = fs.readFileSync(targetPath, 'utf8');\r\n const parsedYaml = yaml.load(fileContent);\r\n \r\n const result = ConfigSchema.safeParse(parsedYaml);\r\n \r\n if (!result.success) {\r\n console.error(chalk.red('Configuration Validation Error:'));\r\n result.error.issues.forEach((issue) => {\r\n console.error(chalk.yellow(` - ${issue.path.join('.')}: ${issue.message}`));\r\n });\r\n process.exit(2);\r\n }\r\n\r\n userConfig = result.data;\r\n } catch (error) {\r\n console.error(chalk.red('Failed to load configuration:'), error);\r\n process.exit(2);\r\n }\r\n } else if (configPath) {\r\n console.error(chalk.red(`Error: Configuration file not found at ${targetPath}`));\r\n process.exit(2);\r\n }\r\n\r\n return this.mergeConfigs(DEFAULT_POLICIES, userConfig);\r\n }\r\n\r\n private static mergeConfigs(defaults: Config, user: Config): Config {\r\n const mergedPolicies = [...defaults.policies];\r\n\r\n user.policies.forEach((userPolicy) => {\r\n const index = mergedPolicies.findIndex(p => p.category === userPolicy.category);\r\n if (index !== -1) {\r\n // Replace default category with user category (precedence)\r\n mergedPolicies[index] = userPolicy;\r\n } else {\r\n // Add new user category\r\n mergedPolicies.push(userPolicy);\r\n }\r\n });\r\n\r\n // Start from defaults, then apply merged policies and any user-specified top-level options\r\n const merged: Config = {\r\n ...defaults,\r\n policies: mergedPolicies,\r\n };\r\n\r\n if (user.outDir !== undefined) {\r\n merged.outDir = user.outDir;\r\n }\r\n\r\n if (user.outputFormat !== undefined) {\r\n merged.outputFormat = user.outputFormat;\r\n }\r\n\r\n return merged;\r\n }\r\n}\r\n","import { z } from 'zod';\r\n\r\nexport const PatternTypeSchema = z.enum(['literal', 'glob', 'regex']);\r\n\r\nexport const PatternSchema = z.object({\r\n type: PatternTypeSchema,\r\n value: z.string().min(1, \"Pattern value cannot be empty\"),\r\n reason: z.string().min(1, \"Reason is mandatory for each pattern\"),\r\n});\r\n\r\nexport const PolicySchema = z.object({\r\n category: z.string().min(1, \"Category name is mandatory\"),\r\n patterns: z.array(PatternSchema).min(1, \"At least one pattern is required per category\"),\r\n});\r\n\r\nexport const ConfigSchema = z.object({\r\n policies: z.array(PolicySchema).default([]),\r\n outDir: z.string().optional(),\r\n outputFormat: z.enum(['json', 'html', 'all']).default('all'),\r\n});\r\n\r\nexport type Config = z.infer<typeof ConfigSchema>;\r\nexport type Policy = z.infer<typeof PolicySchema>;\r\nexport type Pattern = z.infer<typeof PatternSchema>;\r\nexport type PatternType = z.infer<typeof PatternTypeSchema>;\r\n","import { type Config } from './schema';\r\n\r\nexport const DEFAULT_POLICIES: Config = {\r\n policies: [\r\n {\r\n category: \"Security & Admin\",\r\n patterns: [\r\n {\r\n type: \"glob\",\r\n value: \"**/admin/**\",\r\n reason: \"Administrative interfaces should not be publicly indexed.\"\r\n },\r\n {\r\n type: \"glob\",\r\n value: \"**/.env*\",\r\n reason: \"Environment files contain sensitive secrets.\"\r\n },\r\n {\r\n type: \"literal\",\r\n value: \"/wp-admin\",\r\n reason: \"WordPress admin paths are common attack vectors.\"\r\n }\r\n ]\r\n },\r\n {\r\n category: \"Environment Leakage\",\r\n patterns: [\r\n {\r\n type: \"glob\",\r\n value: \"**/staging.**\",\r\n reason: \"Staging environments should be restricted.\"\r\n },\r\n {\r\n type: \"glob\",\r\n value: \"**/dev.**\",\r\n reason: \"Development subdomains detected in production sitemap.\"\r\n }\r\n ]\r\n },\r\n {\r\n category: \"Sensitive Files\",\r\n patterns: [\r\n {\r\n type: \"glob\",\r\n value: \"**/*.{sql,bak,zip,tar.gz}\",\r\n reason: \"Archive or database backup files exposed.\"\r\n }\r\n ]\r\n }\r\n ]\r\n};\r\n","import { fetch } from 'undici';\r\nimport { XMLParser } from 'fast-xml-parser';\r\n\r\nexport class DiscoveryService {\r\n private readonly parser: XMLParser;\r\n private readonly visited = new Set<string>();\r\n private readonly STANDARD_PATHS = [\r\n '/sitemap.xml',\r\n '/sitemap_index.xml',\r\n '/sitemap-index.xml',\r\n '/sitemap.php',\r\n '/sitemap.xml.gz'\r\n ];\r\n\r\n constructor() {\r\n this.parser = new XMLParser({\r\n ignoreAttributes: false,\r\n attributeNamePrefix: \"@_\",\r\n });\r\n }\r\n\r\n /**\r\n * Attempts to find sitemaps for a given base website URL.\r\n */\r\n async findSitemaps(baseUrl: string): Promise<string[]> {\r\n const sitemaps = new Set<string>();\r\n const url = new URL(baseUrl);\r\n const origin = url.origin;\r\n\r\n // 1. Try robots.txt\r\n try {\r\n const robotsUrl = `${origin}/robots.txt`;\r\n const response = await fetch(robotsUrl);\r\n if (response.status === 200) {\r\n const text = await response.text();\r\n const matches = text.matchAll(/^Sitemap:\\s*(.+)$/gim);\r\n for (const match of matches) {\r\n if (match[1]) sitemaps.add(match[1].trim());\r\n }\r\n }\r\n } catch (e) {\r\n // Ignore robots.txt errors\r\n }\r\n\r\n // 2. Try standard paths if none found in robots.txt\r\n if (sitemaps.size === 0) {\r\n for (const path of this.STANDARD_PATHS) {\r\n try {\r\n const sitemapUrl = `${origin}${path}`;\r\n const response = await fetch(sitemapUrl, { method: 'HEAD' });\r\n if (response.status === 200) {\r\n sitemaps.add(sitemapUrl);\r\n }\r\n } catch (e) {\r\n // Ignore path errors\r\n }\r\n }\r\n }\r\n\r\n return Array.from(sitemaps);\r\n }\r\n\r\n /**\r\n * Recursively discovers all leaf sitemaps from a root URL.\r\n */\r\n async *discover(rootUrl: string): AsyncGenerator<string> {\r\n const queue: string[] = [rootUrl];\r\n\r\n while (queue.length > 0) {\r\n const currentUrl = queue.shift()!;\r\n if (this.visited.has(currentUrl)) continue;\r\n this.visited.add(currentUrl);\r\n\r\n try {\r\n const response = await fetch(currentUrl);\r\n if (response.status !== 200) continue;\r\n \r\n const xmlData = await response.text();\r\n const jsonObj = this.parser.parse(xmlData);\r\n\r\n if (jsonObj.sitemapindex) {\r\n const sitemaps = Array.isArray(jsonObj.sitemapindex.sitemap)\r\n ? jsonObj.sitemapindex.sitemap\r\n : [jsonObj.sitemapindex.sitemap];\r\n\r\n for (const sitemap of sitemaps) {\r\n if (sitemap?.loc) {\r\n queue.push(sitemap.loc);\r\n }\r\n }\r\n } else if (jsonObj.urlset) {\r\n // This is a leaf sitemap\r\n yield currentUrl;\r\n }\r\n } catch (error) {\r\n console.error(`Failed to fetch or parse sitemap at ${currentUrl}:`, error);\r\n }\r\n }\r\n }\r\n}\r\n","import { XMLParser } from 'fast-xml-parser';\r\nimport { fetch } from 'undici';\r\nimport { SitemapUrl } from '../types/sitemap';\r\n\r\nexport class SitemapParser {\r\n private readonly parser: XMLParser;\r\n\r\n constructor() {\r\n this.parser = new XMLParser({\r\n ignoreAttributes: false,\r\n attributeNamePrefix: \"@_\",\r\n });\r\n }\r\n\r\n /**\r\n * Parses a leaf sitemap and yields SitemapUrl objects.\r\n * Note: For true streaming of massive files, we'd use a SAX-like approach.\r\n * fast-xml-parser's parse() is fast but loads the whole string.\r\n * Given the 50k URL requirement, we'll use a more memory-efficient approach if needed,\r\n * but let's start with a clean AsyncGenerator interface.\r\n */\r\n async *parse(sitemapUrl: string): AsyncGenerator<SitemapUrl> {\r\n try {\r\n const response = await fetch(sitemapUrl);\r\n const xmlData = await response.text();\r\n const jsonObj = this.parser.parse(xmlData);\r\n\r\n if (jsonObj.urlset && jsonObj.urlset.url) {\r\n const urls = Array.isArray(jsonObj.urlset.url)\r\n ? jsonObj.urlset.url\r\n : [jsonObj.urlset.url];\r\n\r\n for (const url of urls) {\r\n if (url.loc) {\r\n yield {\r\n loc: url.loc,\r\n source: sitemapUrl,\r\n lastmod: url.lastmod,\r\n changefreq: url.changefreq,\r\n priority: url.priority,\r\n risks: [],\r\n };\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`Failed to parse sitemap at ${sitemapUrl}:`, error);\r\n }\r\n }\r\n}\r\n","import { DiscoveryService } from './discovery';\r\nimport { SitemapParser } from './parser';\r\nimport { SitemapUrl } from '../types/sitemap';\r\n\r\nexport class ExtractorService {\r\n private readonly discovery: DiscoveryService;\r\n private readonly parser: SitemapParser;\r\n private readonly seenUrls = new Set<string>();\r\n private readonly discoveredSitemaps = new Set<string>();\r\n\r\n constructor() {\r\n this.discovery = new DiscoveryService();\r\n this.parser = new SitemapParser();\r\n }\r\n\r\n /**\r\n * Returns the list of sitemaps discovered during the extraction process.\r\n */\r\n getDiscoveredSitemaps(): string[] {\r\n return Array.from(this.discoveredSitemaps);\r\n }\r\n\r\n /**\r\n * Normalizes a URL by removing trailing slashes and converting to lowercase.\r\n */\r\n private normalizeUrl(url: string): string {\r\n try {\r\n const parsed = new URL(url);\r\n let normalized = parsed.origin + parsed.pathname.replace(/\\/$/, '');\r\n if (parsed.search) normalized += parsed.search;\r\n return normalized.toLowerCase();\r\n } catch {\r\n return url.toLowerCase().replace(/\\/$/, '');\r\n }\r\n }\r\n\r\n /**\r\n * Extracts all unique URLs from a root sitemap URL or website base URL.\r\n */\r\n async *extract(inputUrl: string): AsyncGenerator<SitemapUrl> {\r\n let startUrls = [inputUrl];\r\n\r\n // If the URL doesn't end in .xml or .gz, it might be a website root\r\n if (!inputUrl.endsWith('.xml') && !inputUrl.endsWith('.gz')) {\r\n const discovered = await this.discovery.findSitemaps(inputUrl);\r\n if (discovered.length > 0) {\r\n console.log(`✅ Discovered ${discovered.length} sitemap(s): ${discovered.join(', ')}`);\r\n startUrls = discovered;\r\n } else {\r\n console.log(`⚠️ No sitemaps discovered via robots.txt or standard paths. Proceeding with input URL.`);\r\n }\r\n }\r\n\r\n for (const startUrl of startUrls) {\r\n for await (const sitemapUrl of this.discovery.discover(startUrl)) {\r\n this.discoveredSitemaps.add(sitemapUrl);\r\n for await (const urlObj of this.parser.parse(sitemapUrl)) {\r\n const normalized = this.normalizeUrl(urlObj.loc);\r\n if (!this.seenUrls.has(normalized)) {\r\n this.seenUrls.add(normalized);\r\n yield urlObj;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n","import micromatch from 'micromatch';\r\nimport { type Config, type Pattern } from '../config/schema';\r\nimport { type SitemapUrl, type Risk } from '../types/sitemap';\r\n\r\nexport class MatcherService {\r\n private readonly config: Config;\r\n\r\n constructor(config: Config) {\r\n this.config = config;\r\n }\r\n\r\n /**\r\n * Matches a URL against all policies and returns detected risks.\r\n */\r\n match(urlObj: SitemapUrl): Risk[] {\r\n const risks: Risk[] = [];\r\n\r\n for (const policy of this.config.policies) {\r\n for (const pattern of policy.patterns) {\r\n if (this.isMatch(urlObj.loc, pattern)) {\r\n risks.push({\r\n category: policy.category,\r\n pattern: pattern.value,\r\n type: pattern.type,\r\n reason: pattern.reason,\r\n });\r\n }\r\n }\r\n }\r\n\r\n return risks;\r\n }\r\n\r\n private isMatch(url: string, pattern: Pattern): boolean {\r\n switch (pattern.type) {\r\n case 'literal':\r\n return url.includes(pattern.value);\r\n case 'glob':\r\n return micromatch.isMatch(url, pattern.value, { contains: true });\r\n case 'regex':\r\n try {\r\n const regex = new RegExp(pattern.value, 'i');\r\n return regex.test(url);\r\n } catch {\r\n return false;\r\n }\r\n default:\r\n return false;\r\n }\r\n }\r\n}\r\n","import chalk from 'chalk';\r\nimport { Reporter, ReportData } from './base';\r\n\r\nexport class ConsoleReporter implements Reporter {\r\n async generate(data: ReportData): Promise<void> {\r\n console.log('\\n' + chalk.bold.blue('=== sitemap-qa Analysis Summary ==='));\r\n console.log(`Total URLs Scanned: ${data.totalUrls}`);\r\n console.log(`Total Risks Found: ${data.totalRisks > 0 ? chalk.red(data.totalRisks) : chalk.green(0)}`);\r\n console.log(`URLs with Risks: ${data.urlsWithRisks.length}`);\r\n console.log(`Duration: ${((data.endTime.getTime() - data.startTime.getTime()) / 1000).toFixed(2)}s`);\r\n\r\n if (data.urlsWithRisks.length > 0) {\r\n console.log('\\n' + chalk.bold.yellow('Top Findings:'));\r\n data.urlsWithRisks.slice(0, 10).forEach((url) => {\r\n console.log(`\\n${chalk.cyan(url.loc)}`);\r\n url.risks.forEach((risk) => {\r\n console.log(` - [${chalk.red(risk.category)}] ${risk.reason} (${chalk.gray(risk.pattern)})`);\r\n });\r\n });\r\n\r\n if (data.urlsWithRisks.length > 10) {\r\n console.log(`\\n... and ${data.urlsWithRisks.length - 10} more. See JSON/HTML report for full details.`);\r\n }\r\n }\r\n\r\n console.log('\\n' + chalk.bold.blue('==================================='));\r\n }\r\n}\r\n","import fs from 'node:fs/promises';\r\nimport { Reporter, ReportData } from './base';\r\n\r\nexport class JsonReporter implements Reporter {\r\n private readonly outputPath: string;\r\n\r\n constructor(outputPath: string = 'sitemap-qa-report.json') {\r\n this.outputPath = outputPath;\r\n }\r\n\r\n async generate(data: ReportData): Promise<void> {\r\n const report = {\r\n metadata: {\r\n generatedAt: new Date().toISOString(),\r\n durationMs: data.endTime.getTime() - data.startTime.getTime(),\r\n },\r\n summary: {\r\n totalUrls: data.totalUrls,\r\n totalRisks: data.totalRisks,\r\n urlsWithRisksCount: data.urlsWithRisks.length,\r\n },\r\n findings: data.urlsWithRisks,\r\n };\r\n\r\n await fs.writeFile(this.outputPath, JSON.stringify(report, null, 2), 'utf8');\r\n console.log(`JSON report generated at ${this.outputPath}`);\r\n }\r\n}\r\n","import fs from 'node:fs/promises';\r\nimport { Reporter, ReportData } from './base';\r\n\r\nexport class HtmlReporter implements Reporter {\r\n private readonly outputPath: string;\r\n\r\n constructor(outputPath: string = 'sitemap-qa-report.html') {\r\n this.outputPath = outputPath;\r\n }\r\n\r\n async generate(data: ReportData): Promise<void> {\r\n const categories = this.groupRisks(data);\r\n const html = this.generateHtml(data, categories);\r\n\r\n await fs.writeFile(this.outputPath, html, 'utf8');\r\n console.log(`HTML report generated at ${this.outputPath}`);\r\n }\r\n\r\n private groupRisks(data: ReportData) {\r\n const categories: Record<string, Record<string, { reason: string, urls: string[] }>> = {};\r\n\r\n for (const urlObj of data.urlsWithRisks) {\r\n for (const risk of urlObj.risks) {\r\n if (!categories[risk.category]) {\r\n categories[risk.category] = {};\r\n }\r\n if (!categories[risk.category][risk.pattern]) {\r\n categories[risk.category][risk.pattern] = {\r\n reason: risk.reason,\r\n urls: []\r\n };\r\n }\r\n categories[risk.category][risk.pattern].urls.push(urlObj.loc);\r\n }\r\n }\r\n\r\n return categories;\r\n }\r\n\r\n private generateHtml(data: ReportData, categories: any): string {\r\n const duration = ((data.endTime.getTime() - data.startTime.getTime()) / 1000).toFixed(1);\r\n const timestamp = data.endTime.toLocaleString();\r\n\r\n return `\r\n<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n <title>Sitemap Analysis - ${data.rootUrl}</title>\r\n <style>\r\n :root {\r\n --bg-dark: #0f172a;\r\n --bg-light: #f8fafc;\r\n --text-main: #1e293b;\r\n --text-muted: #64748b;\r\n --primary: #3b82f6;\r\n --danger: #ef4444;\r\n --warning: #f59e0b;\r\n --border: #e2e8f0;\r\n }\r\n body { \r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\r\n line-height: 1.5;\r\n color: var(--text-main);\r\n background-color: #fff;\r\n margin: 0;\r\n padding: 0;\r\n }\r\n header {\r\n background-color: var(--bg-dark);\r\n color: white;\r\n padding: 40px 20px;\r\n text-align: left;\r\n }\r\n .container {\r\n max-width: 1200px;\r\n margin: 0 auto;\r\n padding: 0 20px;\r\n }\r\n header h1 { margin: 0; font-size: 24px; }\r\n header .meta { margin-top: 10px; color: #94a3b8; font-size: 14px; }\r\n \r\n .summary-grid {\r\n display: grid;\r\n grid-template-columns: repeat(4, 1fr);\r\n border-bottom: 1px solid var(--border);\r\n margin-bottom: 40px;\r\n }\r\n .summary-card {\r\n padding: 30px 20px;\r\n text-align: center;\r\n border-right: 1px solid var(--border);\r\n }\r\n .summary-card:last-child { border-right: none; }\r\n .summary-card h3 { \r\n margin: 0; \r\n font-size: 12px; \r\n text-transform: uppercase; \r\n color: var(--text-muted);\r\n letter-spacing: 0.05em;\r\n }\r\n .summary-card p { \r\n margin: 10px 0 0; \r\n font-size: 32px; \r\n font-weight: 700; \r\n color: var(--text-main);\r\n }\r\n .summary-card.highlight p { color: var(--danger); }\r\n\r\n details {\r\n margin-bottom: 20px;\r\n border: 1px solid var(--border);\r\n border-radius: 8px;\r\n overflow: hidden;\r\n }\r\n summary {\r\n padding: 15px 20px;\r\n background-color: #fff;\r\n cursor: pointer;\r\n font-weight: 600;\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n list-style: none;\r\n }\r\n summary::-webkit-details-marker { display: none; }\r\n summary::after {\r\n content: '▶';\r\n font-size: 12px;\r\n color: var(--text-muted);\r\n transition: transform 0.2s;\r\n }\r\n details[open] summary::after { transform: rotate(90deg); }\r\n \r\n .category-section {\r\n border: 1px solid var(--warning);\r\n border-radius: 8px;\r\n margin-bottom: 20px;\r\n }\r\n .category-header {\r\n padding: 15px 20px;\r\n background-color: #fffbeb;\r\n color: var(--warning);\r\n font-weight: 600;\r\n cursor: pointer;\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n }\r\n .category-content {\r\n padding: 20px;\r\n background-color: #fff;\r\n }\r\n\r\n .finding-group {\r\n border: 1px solid var(--border);\r\n border-radius: 8px;\r\n padding: 20px;\r\n margin-bottom: 20px;\r\n }\r\n .finding-header {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n margin-bottom: 10px;\r\n }\r\n .finding-header h4 { margin: 0; font-size: 16px; }\r\n .badge {\r\n background-color: var(--primary);\r\n color: white;\r\n padding: 2px 8px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n }\r\n .finding-description {\r\n color: var(--text-muted);\r\n font-size: 14px;\r\n margin-bottom: 20px;\r\n }\r\n \r\n .url-list {\r\n background-color: var(--bg-light);\r\n border-radius: 4px;\r\n padding: 15px;\r\n margin-bottom: 15px;\r\n }\r\n .url-item {\r\n font-family: monospace;\r\n font-size: 13px;\r\n padding: 8px 12px;\r\n background: white;\r\n border: 1px solid var(--border);\r\n border-radius: 4px;\r\n margin-bottom: 8px;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n .url-item:last-child { margin-bottom: 0; }\r\n \r\n .more-count {\r\n font-size: 12px;\r\n color: var(--text-muted);\r\n font-style: italic;\r\n margin-bottom: 15px;\r\n }\r\n\r\n .btn {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 8px;\r\n background-color: var(--primary);\r\n color: white;\r\n padding: 8px 16px;\r\n border-radius: 6px;\r\n text-decoration: none;\r\n font-size: 13px;\r\n font-weight: 500;\r\n }\r\n .btn:hover { opacity: 0.9; }\r\n\r\n footer {\r\n text-align: center;\r\n padding: 40px;\r\n color: var(--text-muted);\r\n font-size: 12px;\r\n border-top: 1px solid var(--border);\r\n margin-top: 40px;\r\n }\r\n </style>\r\n</head>\r\n<body>\r\n <header>\r\n <div class=\"container\">\r\n <h1>Sitemap Analysis</h1>\r\n <div class=\"meta\">\r\n <div>${data.rootUrl}</div>\r\n <div>${timestamp}</div>\r\n </div>\r\n </div>\r\n </header>\r\n\r\n <div class=\"summary-grid\">\r\n <div class=\"summary-card\">\r\n <h3>Sitemaps</h3>\r\n <p>${data.discoveredSitemaps.length}</p>\r\n </div>\r\n <div class=\"summary-card\">\r\n <h3>URLs Analyzed</h3>\r\n <p>${data.totalUrls.toLocaleString()}</p>\r\n </div>\r\n <div class=\"summary-card highlight\">\r\n <h3>Issues Found</h3>\r\n <p>${data.totalRisks}</p>\r\n </div>\r\n <div class=\"summary-card\">\r\n <h3>Scan Time</h3>\r\n <p>${duration}s</p>\r\n </div>\r\n </div>\r\n\r\n <div class=\"container\">\r\n <details>\r\n <summary>Sitemaps Discovered (${data.discoveredSitemaps.length})</summary>\r\n <div style=\"padding: 20px; background: var(--bg-light);\">\r\n ${data.discoveredSitemaps.map(s => `<div class=\"url-item\">${s}</div>`).join('')}\r\n </div>\r\n </details>\r\n\r\n ${Object.entries(categories).map(([category, findings]: [string, any]) => {\r\n const totalCategoryUrls = Object.values(findings).reduce((acc: number, f: any) => acc + f.urls.length, 0);\r\n return `\r\n <div class=\"category-section\">\r\n <div class=\"category-header\">\r\n <span>${category} (${totalCategoryUrls} URLs)</span>\r\n <span>▼</span>\r\n </div>\r\n <div class=\"category-content\">\r\n ${Object.entries(findings).map(([pattern, finding]: [string, any]) => `\r\n <div class=\"finding-group\">\r\n <div class=\"finding-header\">\r\n <h4>${pattern}</h4>\r\n <span class=\"badge\">${finding.urls.length} URLs</span>\r\n </div>\r\n <div class=\"finding-description\">\r\n ${finding.reason}\r\n </div>\r\n <div class=\"url-list\">\r\n ${finding.urls.slice(0, 3).map((url: string) => `\r\n <div class=\"url-item\">${url}</div>\r\n `).join('')}\r\n </div>\r\n ${finding.urls.length > 3 ? `\r\n <div class=\"more-count\">... and ${finding.urls.length - 3} more</div>\r\n ` : ''}\r\n <a href=\"#\" class=\"btn\" onclick=\"downloadUrls('${pattern}', ${JSON.stringify(finding.urls).replace(/\"/g, '&quot;')})\">\r\n 📥 Download All ${finding.urls.length} URLs\r\n </a>\r\n </div>\r\n `).join('')}\r\n </div>\r\n </div>\r\n `;\r\n }).join('')}\r\n </div>\r\n\r\n <footer>\r\n Generated by sitemap-qa v1.0.0\r\n </footer>\r\n\r\n <script>\r\n function downloadUrls(name, urls) {\r\n const blob = new Blob([urls.join('\\\\n')], { type: 'text/plain' });\r\n const url = window.URL.createObjectURL(blob);\r\n const a = document.createElement('a');\r\n a.href = url;\r\n a.download = \\`\\${name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_urls.txt\\`;\r\n document.body.appendChild(a);\r\n a.click();\r\n window.URL.revokeObjectURL(url);\r\n document.body.removeChild(a);\r\n }\r\n </script>\r\n</body>\r\n</html>\r\n`;\r\n }\r\n}\r\n","import { Command } from 'commander';\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport chalk from 'chalk';\r\n\r\nconst DEFAULT_CONFIG = `# sitemap-qa configuration\r\n# This file defines the risk categories and patterns to monitor.\r\n\r\n# Risk Categories\r\n# Each category contains a list of patterns to match against URLs found in sitemaps.\r\n# Patterns can be:\r\n# - literal: Exact string match\r\n# - glob: Glob pattern (e.g., **/admin/**)\r\n# - regex: Regular expression (e.g., /\\\\/v[0-9]+\\\\//)\r\n\r\npolicies:\r\n - category: \"Security & Admin\"\r\n patterns:\r\n - type: \"glob\"\r\n value: \"**/admin/**\"\r\n reason: \"Administrative interfaces should not be publicly indexed.\"\r\n - type: \"glob\"\r\n value: \"**/.env*\"\r\n reason: \"Environment files contain sensitive secrets.\"\r\n - type: \"literal\"\r\n value: \"/wp-admin\"\r\n reason: \"WordPress admin paths are common attack vectors.\"\r\n\r\n - category: \"Environment Leakage\"\r\n patterns:\r\n - type: \"glob\"\r\n value: \"**/staging.**\"\r\n reason: \"Staging environments should be restricted.\"\r\n - type: \"glob\"\r\n value: \"**/dev.**\"\r\n reason: \"Development subdomains detected in production sitemap.\"\r\n\r\n - category: \"Sensitive Files\"\r\n patterns:\r\n - type: \"glob\"\r\n value: \"**/*.{sql,bak,zip,tar.gz}\"\r\n reason: \"Archive or database backup files exposed.\"\r\n`;\r\n\r\nexport const initCommand = new Command('init')\r\n .description('Initialize a default sitemap-qa.yaml configuration file')\r\n .action(() => {\r\n const configPath = path.join(process.cwd(), 'sitemap-qa.yaml');\r\n\r\n if (fs.existsSync(configPath)) {\r\n console.error(chalk.red(`Error: ${configPath} already exists.`));\r\n process.exit(1);\r\n }\r\n\r\n try {\r\n fs.writeFileSync(configPath, DEFAULT_CONFIG, 'utf8');\r\n console.log(chalk.green(`Successfully created ${configPath}`));\r\n } catch (error) {\r\n console.error(chalk.red('Failed to create configuration file:'), error);\r\n process.exit(1);\r\n }\r\n });\r\n"],"mappings":"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","names":["Command","chalk","path","fs","path","XMLParser","fetch","chalk","fs","fs","chalk","fs","path","Command","fs","path","chalk","Command"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/analyze.ts","../src/config/loader.ts","../src/config/schema.ts","../src/config/defaults.ts","../src/core/discovery.ts","../src/core/xml-parser.ts","../src/core/parser.ts","../src/core/extractor.ts","../src/core/matcher.ts","../src/reporters/console-reporter.ts","../src/reporters/json-reporter.ts","../src/reporters/html-reporter.ts","../src/commands/init.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { Command } from 'commander';\r\nimport { analyzeCommand } from '@/commands/analyze';\r\nimport { initCommand } from '@/commands/init';\r\n\r\nconst program = new Command();\r\n\r\nprogram\r\n .name('sitemap-qa')\r\n .version('1.0.0')\r\n .description('sitemap analysis for QA teams');\r\n\r\nprogram.addCommand(analyzeCommand);\r\nprogram.addCommand(initCommand);\r\n\r\n// Global error handler\r\nprocess.on('unhandledRejection', (reason, promise) => {\r\n console.error('Unhandled Rejection at:', promise, 'reason:', reason);\r\n process.exit(1);\r\n});\r\n\r\n// Graceful shutdown handlers\r\nprocess.on('SIGINT', () => {\r\n console.log('\\nGracefully shutting down...');\r\n process.exit(0);\r\n});\r\n\r\nprocess.on('SIGTERM', () => {\r\n console.log('\\nGracefully shutting down...');\r\n process.exit(0);\r\n});\r\n\r\nprogram.parse();\r\n","import { Command } from 'commander';\r\nimport chalk from 'chalk';\r\nimport path from 'node:path';\r\nimport fs from 'node:fs/promises';\r\nimport { ConfigLoader } from '../config/loader';\r\nimport { ExtractorService } from '../core/extractor';\r\nimport { MatcherService } from '../core/matcher';\r\nimport { ConsoleReporter } from '../reporters/console-reporter';\r\nimport { JsonReporter } from '../reporters/json-reporter';\r\nimport { HtmlReporter } from '../reporters/html-reporter';\r\nimport { ReportData, Reporter } from '../reporters/base';\r\nimport { SitemapUrl } from '../types/sitemap';\r\n\r\nexport const analyzeCommand = new Command('analyze')\r\n .description('Analyze a sitemap for potential risks')\r\n .argument('<url>', 'Root sitemap URL')\r\n .option('-c, --config <path>', 'Path to sitemap-qa.yaml')\r\n .option('-o, --output <format>', 'Output format (json, html, all)')\r\n .option('-d, --out-dir <path>', 'Directory to save reports')\r\n .action(async (url: string, options: { config?: string; output?: string; outDir?: string }) => {\r\n const startTime = new Date();\r\n \r\n // 1. Load Config\r\n const config = ConfigLoader.load(options.config);\r\n const outDir = options.outDir || config.outDir || '.';\r\n const outputFormat = options.output || config.outputFormat || 'all';\r\n \r\n // 2. Initialize Services\r\n const extractor = new ExtractorService();\r\n const matcher = new MatcherService(config, url);\r\n \r\n const urlsWithRisks: SitemapUrl[] = [];\r\n const ignoredUrls: SitemapUrl[] = [];\r\n let totalUrls = 0;\r\n let totalRisks = 0;\r\n\r\n console.log(chalk.blue(`\\n🚀 Starting analysis of ${url}...`));\r\n\r\n try {\r\n // 3. Pipeline: Extract -> Match\r\n for await (const urlObj of extractor.extract(url)) {\r\n totalUrls++;\r\n const risks = matcher.match(urlObj);\r\n \r\n if (risks.length > 0) {\r\n urlObj.risks = risks;\r\n urlsWithRisks.push(urlObj);\r\n totalRisks += risks.length;\r\n } else if (urlObj.ignored) {\r\n ignoredUrls.push(urlObj);\r\n }\r\n\r\n if (totalUrls % 100 === 0) {\r\n process.stdout.write(chalk.gray(`\\rProcessed ${totalUrls} URLs...`));\r\n }\r\n }\r\n process.stdout.write('\\n');\r\n\r\n const endTime = new Date();\r\n const reportData: ReportData = {\r\n rootUrl: url,\r\n discoveredSitemaps: extractor.getDiscoveredSitemaps(),\r\n totalUrls,\r\n totalRisks,\r\n urlsWithRisks,\r\n ignoredUrls,\r\n startTime,\r\n endTime,\r\n };\r\n\r\n // 4. Reporting\r\n const reporters: Reporter[] = [new ConsoleReporter()];\r\n \r\n await fs.mkdir(outDir, { recursive: true });\r\n\r\n if (outputFormat === 'json' || outputFormat === 'all') {\r\n const jsonPath = path.join(outDir, 'sitemap-qa-report.json');\r\n reporters.push(new JsonReporter(jsonPath));\r\n }\r\n if (outputFormat === 'html' || outputFormat === 'all') {\r\n const htmlPath = path.join(outDir, 'sitemap-qa-report.html');\r\n reporters.push(new HtmlReporter(htmlPath));\r\n }\r\n\r\n for (const reporter of reporters) {\r\n await reporter.generate(reportData);\r\n }\r\n\r\n // 5. Exit Code\r\n if (totalRisks > 0) {\r\n process.exit(1);\r\n } else {\r\n process.exit(0);\r\n }\r\n\r\n } catch (error) {\r\n console.error(chalk.red('\\nAnalysis failed:'), error);\r\n process.exit(1);\r\n }\r\n });\r\n","import fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport yaml from 'js-yaml';\r\nimport { ConfigSchema, type Config } from './schema';\r\nimport { DEFAULT_POLICIES } from './defaults';\r\nimport chalk from 'chalk';\r\n\r\nexport class ConfigLoader {\r\n private static readonly DEFAULT_CONFIG_PATH = 'sitemap-qa.yaml';\r\n\r\n static load(configPath?: string): Config {\r\n const targetPath = configPath || path.join(process.cwd(), this.DEFAULT_CONFIG_PATH);\r\n let userConfig: Config = {\r\n acceptable_patterns: [],\r\n policies: [],\r\n outputFormat: 'all',\r\n enforceDomainConsistency: true,\r\n };\r\n\r\n // Load YAML config\r\n if (fs.existsSync(targetPath)) {\r\n try {\r\n const fileContent = fs.readFileSync(targetPath, 'utf8');\r\n const parsedYaml = yaml.load(fileContent);\r\n \r\n const result = ConfigSchema.safeParse(parsedYaml);\r\n \r\n if (!result.success) {\r\n console.error(chalk.red('Configuration Validation Error:'));\r\n result.error.issues.forEach((issue) => {\r\n console.error(chalk.yellow(` - ${issue.path.join('.')}: ${issue.message}`));\r\n });\r\n process.exit(2);\r\n return DEFAULT_POLICIES; // TypeScript safety: never reached in production\r\n }\r\n\r\n userConfig = result.data;\r\n } catch (error) {\r\n console.error(chalk.red('Failed to load configuration:'), error);\r\n process.exit(2);\r\n return DEFAULT_POLICIES; // TypeScript safety: never reached in production\r\n }\r\n } else if (configPath) {\r\n console.error(chalk.red(`Error: Configuration file not found at ${targetPath}`));\r\n process.exit(2);\r\n return DEFAULT_POLICIES; // TypeScript safety: never reached in production\r\n }\r\n\r\n return this.mergeConfigs(DEFAULT_POLICIES, userConfig);\r\n }\r\n\r\n private static mergeConfigs(defaults: Config, user: Config): Config {\r\n const mergedPolicies = [...defaults.policies];\r\n\r\n user.policies.forEach((userPolicy) => {\r\n const index = mergedPolicies.findIndex(p => p.category === userPolicy.category);\r\n if (index !== -1) {\r\n // Replace default category with user category (precedence)\r\n mergedPolicies[index] = userPolicy;\r\n } else {\r\n // Add new user category\r\n mergedPolicies.push(userPolicy);\r\n }\r\n });\r\n\r\n // Start from defaults, then apply merged policies and any user-specified top-level options\r\n const merged: Config = {\r\n ...defaults,\r\n acceptable_patterns: [...(defaults.acceptable_patterns || []), ...(user.acceptable_patterns || [])],\r\n policies: mergedPolicies,\r\n };\r\n\r\n if (user.outDir !== undefined) {\r\n merged.outDir = user.outDir;\r\n }\r\n\r\n if (user.outputFormat !== undefined) {\r\n merged.outputFormat = user.outputFormat;\r\n }\r\n\r\n if (user.enforceDomainConsistency !== undefined) {\r\n merged.enforceDomainConsistency = user.enforceDomainConsistency;\r\n }\r\n\r\n return merged;\r\n }\r\n}\r\n","import { z } from 'zod';\r\n\r\nexport const PatternTypeSchema = z.enum(['literal', 'glob', 'regex']);\r\n\r\nexport const PatternSchema = z.object({\r\n type: PatternTypeSchema,\r\n value: z.string().min(1, \"Pattern value cannot be empty\"),\r\n reason: z.string().min(1, \"Reason is mandatory for each pattern\"),\r\n});\r\n\r\nexport const PolicySchema = z.object({\r\n category: z.string().min(1, \"Category name is mandatory\"),\r\n patterns: z.array(PatternSchema).min(1, \"At least one pattern is required per category\"),\r\n});\r\n\r\nexport const ConfigSchema = z.object({\r\n acceptable_patterns: z.array(PatternSchema).default([]),\r\n policies: z.array(PolicySchema).default([]),\r\n outDir: z.string().optional(),\r\n outputFormat: z.enum(['json', 'html', 'all']).default('all'),\r\n enforceDomainConsistency: z.boolean().default(true),\r\n});\r\n\r\nexport type Config = z.infer<typeof ConfigSchema>;\r\nexport type Policy = z.infer<typeof PolicySchema>;\r\nexport type Pattern = z.infer<typeof PatternSchema>;\r\nexport type PatternType = z.infer<typeof PatternTypeSchema>;\r\n","import { type Config } from './schema';\r\n\r\nexport const DEFAULT_POLICIES: Config = {\r\n acceptable_patterns: [],\r\n outputFormat: \"all\",\r\n enforceDomainConsistency: true,\r\n policies: [\r\n {\r\n category: \"Security & Admin\",\r\n patterns: [\r\n {\r\n type: \"glob\",\r\n value: \"**/admin/**\",\r\n reason: \"Administrative interfaces should not be publicly indexed.\"\r\n },\r\n {\r\n type: \"glob\",\r\n value: \"**/.env*\",\r\n reason: \"Environment files contain sensitive secrets.\"\r\n },\r\n {\r\n type: \"literal\",\r\n value: \"/wp-admin\",\r\n reason: \"WordPress admin paths are common attack vectors.\"\r\n }\r\n ]\r\n },\r\n {\r\n category: \"Environment Leakage\",\r\n patterns: [\r\n {\r\n type: \"glob\",\r\n value: \"**/staging.**\",\r\n reason: \"Staging environments should be restricted.\"\r\n },\r\n {\r\n type: \"glob\",\r\n value: \"**/dev.**\",\r\n reason: \"Development subdomains detected in production sitemap.\"\r\n }\r\n ]\r\n },\r\n {\r\n category: \"Sensitive Files\",\r\n patterns: [\r\n {\r\n type: \"glob\",\r\n value: \"**/*.{sql,bak,zip,tar.gz}\",\r\n reason: \"Archive or database backup files exposed.\"\r\n }\r\n ]\r\n }\r\n ]\r\n};\r\n","import { fetch } from 'undici';\r\nimport { Readable } from 'node:stream';\r\nimport { ReadableStream } from 'node:stream/web';\r\nimport { StreamingXmlParser } from './xml-parser';\r\n\r\nexport interface DiscoveredSitemap {\r\n url: string;\r\n xmlData: string;\r\n stream?: ReadableStream | Readable;\r\n}\r\n\r\nexport class DiscoveryService {\r\n private readonly xmlParser: StreamingXmlParser;\r\n private readonly visited = new Set<string>();\r\n private readonly STANDARD_PATHS = [\r\n '/sitemap.xml',\r\n '/sitemap_index.xml',\r\n '/sitemap-index.xml',\r\n '/sitemap.php',\r\n '/sitemap.xml.gz'\r\n ];\r\n\r\n constructor() {\r\n this.xmlParser = new StreamingXmlParser();\r\n }\r\n\r\n /**\r\n * Attempts to find sitemaps for a given base website URL.\r\n */\r\n async findSitemaps(baseUrl: string): Promise<string[]> {\r\n const sitemaps = new Set<string>();\r\n const url = new URL(baseUrl);\r\n const origin = url.origin;\r\n\r\n // 1. Try robots.txt\r\n try {\r\n const robotsUrl = `${origin}/robots.txt`;\r\n const response = await fetch(robotsUrl);\r\n if (response.status === 200) {\r\n const text = await response.text();\r\n const matches = text.matchAll(/^Sitemap:\\s*(.+)$/gim);\r\n for (const match of matches) {\r\n if (match[1]) sitemaps.add(match[1].trim());\r\n }\r\n }\r\n } catch (e) {\r\n // Ignore robots.txt errors\r\n }\r\n\r\n // 2. Try standard paths if none found in robots.txt\r\n if (sitemaps.size === 0) {\r\n for (const path of this.STANDARD_PATHS) {\r\n try {\r\n const sitemapUrl = `${origin}${path}`;\r\n const response = await fetch(sitemapUrl, { method: 'HEAD' });\r\n if (response.status === 200) {\r\n sitemaps.add(sitemapUrl);\r\n }\r\n } catch (e) {\r\n // Ignore path errors\r\n }\r\n }\r\n }\r\n\r\n return Array.from(sitemaps);\r\n }\r\n\r\n /**\r\n * Recursively discovers all leaf sitemaps from a root URL.\r\n * Returns both the sitemap URL and its XML data to avoid duplicate fetches.\r\n */\r\n async *discover(rootUrl: string): AsyncGenerator<DiscoveredSitemap> {\r\n const queue: string[] = [rootUrl];\r\n\r\n while (queue.length > 0) {\r\n const currentUrl = queue.shift()!;\r\n if (this.visited.has(currentUrl)) continue;\r\n this.visited.add(currentUrl);\r\n\r\n try {\r\n const response = await fetch(currentUrl);\r\n if (response.status !== 200) continue;\r\n \r\n let isIndex = false;\r\n let isLeaf = false;\r\n const childSitemaps: string[] = [];\r\n \r\n let xmlData: string | undefined;\r\n let source: any;\r\n\r\n if (response.body) {\r\n // Convert Web Stream to Node Stream\r\n const nodeStream = Readable.fromWeb(response.body as ReadableStream);\r\n source = nodeStream;\r\n } else {\r\n // Fallback for environments/mocks where body is not available\r\n xmlData = await response.text();\r\n source = xmlData;\r\n }\r\n\r\n // Process entries as they're yielded from the parser\r\n for await (const entry of this.xmlParser.parse(source)) {\r\n if (entry.type === 'sitemap') {\r\n isIndex = true;\r\n childSitemaps.push(entry.loc);\r\n } else if (entry.type === 'url') {\r\n isLeaf = true;\r\n }\r\n }\r\n\r\n if (isIndex) {\r\n for (const loc of childSitemaps) {\r\n queue.push(loc);\r\n }\r\n }\r\n \r\n // Get xmlData for downstream consumers (parser caches it for us)\r\n if (!xmlData) {\r\n xmlData = this.xmlParser.getLastParsedXml() || '';\r\n }\r\n \r\n // If it's a leaf, or if it's neither (but has urlset), yield it.\r\n if (isLeaf || (!isIndex && xmlData.includes('<urlset'))) {\r\n yield { \r\n url: currentUrl, \r\n xmlData,\r\n };\r\n }\r\n } catch (error) {\r\n console.error(`Failed to fetch or parse sitemap at ${currentUrl}:`, error);\r\n }\r\n }\r\n }\r\n}\r\n\r\n","import { XMLParser } from 'fast-xml-parser';\r\nimport { Readable } from 'node:stream';\r\nimport { gunzipSync } from 'node:zlib';\r\n\r\nexport interface SitemapIndexEntry {\r\n type: 'sitemap';\r\n loc: string;\r\n}\r\n\r\nexport interface SitemapUrlEntry {\r\n type: 'url';\r\n loc: string;\r\n lastmod?: string;\r\n changefreq?: string;\r\n priority?: number;\r\n}\r\n\r\nexport type ParsedEntry = SitemapIndexEntry | SitemapUrlEntry;\r\n\r\n/**\r\n * A unified streaming XML parser for sitemaps.\r\n * Uses fast-xml-parser's XMLParser in a way that could be adapted for streaming if needed,\r\n * but currently focuses on providing a consistent interface for both discovery and extraction.\r\n */\r\nexport class StreamingXmlParser {\r\n private readonly parser: XMLParser;\r\n private lastParsedXml?: string;\r\n\r\n constructor() {\r\n this.parser = new XMLParser({\r\n ignoreAttributes: false,\r\n attributeNamePrefix: \"@_\",\r\n // Ensure we always get arrays for sitemap and url tags\r\n isArray: (name) => name === 'sitemap' || name === 'url',\r\n removeNSPrefix: true,\r\n });\r\n }\r\n\r\n /**\r\n * Parses an XML stream and yields typed entries as they are found.\r\n * Generator-first design allows consumers to process entries without pre-collecting.\r\n */\r\n async *parse(stream: Readable | string): AsyncGenerator<ParsedEntry> {\r\n const xmlData = typeof stream === 'string' ? stream : await this.streamToString(stream);\r\n \r\n // Store the decompressed XML for consumers who need it\r\n this.lastParsedXml = xmlData;\r\n \r\n const jsonObj = this.parser.parse(xmlData);\r\n\r\n // Yield sitemap index entries\r\n if (jsonObj.sitemapindex?.sitemap) {\r\n const sitemaps = jsonObj.sitemapindex.sitemap;\r\n for (const sitemap of sitemaps) {\r\n if (sitemap?.loc) {\r\n yield { type: 'sitemap', loc: sitemap.loc };\r\n }\r\n }\r\n }\r\n\r\n // Yield URL entries\r\n if (jsonObj.urlset?.url) {\r\n const urls = jsonObj.urlset.url;\r\n for (const url of urls) {\r\n if (url?.loc) {\r\n yield {\r\n type: 'url',\r\n loc: url.loc,\r\n lastmod: url.lastmod,\r\n changefreq: url.changefreq,\r\n priority: url.priority,\r\n };\r\n }\r\n }\r\n }\r\n }\r\n \r\n /**\r\n * Get the last parsed XML data (useful to avoid re-fetching).\r\n */\r\n getLastParsedXml(): string | undefined {\r\n return this.lastParsedXml;\r\n }\r\n\r\n private async streamToString(stream: Readable): Promise<string> {\r\n const chunks: Buffer[] = [];\r\n for await (const chunk of stream) {\r\n chunks.push(Buffer.from(chunk));\r\n }\r\n const buffer = Buffer.concat(chunks);\r\n \r\n // Check if content is gzipped (magic number: 1f 8b)\r\n if (buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b) {\r\n try {\r\n const decompressed = gunzipSync(buffer);\r\n return decompressed.toString('utf8');\r\n } catch (error) {\r\n throw new Error(`Failed to decompress gzipped content: ${error}`);\r\n }\r\n }\r\n \r\n return buffer.toString('utf8');\r\n }\r\n}\r\n","import { SitemapUrl } from '../types/sitemap';\r\nimport { Readable } from 'node:stream';\r\nimport { ReadableStream } from 'node:stream/web';\r\nimport { StreamingXmlParser } from './xml-parser';\r\nimport { fetch } from 'undici';\r\n\r\nexport class SitemapParser {\r\n private readonly xmlParser: StreamingXmlParser;\r\n\r\n constructor() {\r\n this.xmlParser = new StreamingXmlParser();\r\n }\r\n\r\n /**\r\n * Parses a leaf sitemap and yields SitemapUrl objects.\r\n * Uses the shared StreamingXmlParser for consistent and efficient parsing.\r\n * \r\n * @param sitemapUrlOrData - Accepts one of three input types:\r\n * - `string`: A URL string. The method will fetch the sitemap from this URL.\r\n * Use this when you need to fetch a sitemap from a remote location.\r\n * - `{ type: 'xmlData'; url: string; xmlData: string }`: An object with a URL and pre-fetched XML data.\r\n * Use this when you already have the XML content (e.g., from a cache or file)\r\n * and want to avoid an additional HTTP request.\r\n * - `{ type: 'stream'; url: string; stream: ReadableStream | Readable }`: An object with a URL and a stream.\r\n * Accepts either a Web ReadableStream or Node.js Readable stream.\r\n * Use this when you have a stream source (e.g., from a streaming HTTP response)\r\n * that should be consumed and parsed. Web streams are converted to Node.js Readable internally.\r\n * \r\n * @yields {SitemapUrl} Parsed sitemap URL entries containing `loc` (URL), `source` (sitemap URL),\r\n * optional metadata (`lastmod`, `changefreq`, `priority`), and a `risks` array (initialized as empty,\r\n * populated later in the processing pipeline). Other properties like `ignored`/`ignoredBy` are not\r\n * set by this method and may be added by downstream processors.\r\n */\r\n async *parse(sitemapUrlOrData: string | { type: 'xmlData'; url: string; xmlData: string } | { type: 'stream'; url: string; stream: ReadableStream | Readable }): AsyncGenerator<SitemapUrl> {\r\n // Extract URL first so it's available in catch block\r\n const sitemapUrl = typeof sitemapUrlOrData === 'string' \r\n ? sitemapUrlOrData \r\n : sitemapUrlOrData.url;\r\n \r\n try {\r\n let source: Readable | string;\r\n\r\n if (typeof sitemapUrlOrData === 'string') {\r\n const response = await fetch(sitemapUrl);\r\n if (response.status !== 200) throw new Error(`Failed to fetch sitemap at ${sitemapUrl}: HTTP ${response.status}`);\r\n \r\n if (response.body) {\r\n source = Readable.fromWeb(response.body as ReadableStream);\r\n } else {\r\n // Fallback for environments where body might be missing or mocked without body\r\n source = await response.text();\r\n }\r\n } else if (sitemapUrlOrData.type === 'stream') {\r\n // Handle both Web ReadableStream and Node.js Readable\r\n if (sitemapUrlOrData.stream instanceof Readable) {\r\n source = sitemapUrlOrData.stream;\r\n } else {\r\n source = Readable.fromWeb(sitemapUrlOrData.stream as ReadableStream);\r\n }\r\n } else {\r\n source = sitemapUrlOrData.xmlData;\r\n source = sitemapUrlOrData.xmlData;\r\n }\r\n\r\n // Yield URL entries directly from the parser generator\r\n for await (const entry of this.xmlParser.parse(source)) {\r\n if (entry.type === 'url') {\r\n yield {\r\n loc: entry.loc,\r\n source: sitemapUrl,\r\n lastmod: entry.lastmod,\r\n changefreq: entry.changefreq,\r\n priority: entry.priority,\r\n risks: [],\r\n };\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`Failed to parse sitemap at ${sitemapUrl}:`, error);\r\n }\r\n }\r\n\r\n private async streamToString(stream: Readable): Promise<string> {\r\n const chunks: Buffer[] = [];\r\n for await (const chunk of stream) {\r\n chunks.push(Buffer.from(chunk));\r\n }\r\n return Buffer.concat(chunks).toString('utf8');\r\n }\r\n}\r\n","import { DiscoveryService } from './discovery';\r\nimport { SitemapParser } from './parser';\r\nimport { SitemapUrl } from '../types/sitemap';\r\n\r\nexport class ExtractorService {\r\n private readonly discovery: DiscoveryService;\r\n private readonly parser: SitemapParser;\r\n private readonly seenUrls = new Set<string>();\r\n private readonly discoveredSitemaps = new Set<string>();\r\n\r\n constructor() {\r\n this.discovery = new DiscoveryService();\r\n this.parser = new SitemapParser();\r\n }\r\n\r\n /**\r\n * Returns the list of sitemaps discovered during the extraction process.\r\n */\r\n getDiscoveredSitemaps(): string[] {\r\n return Array.from(this.discoveredSitemaps);\r\n }\r\n\r\n /**\r\n * Normalizes a URL by removing trailing slashes and converting to lowercase.\r\n */\r\n private normalizeUrl(url: string): string {\r\n try {\r\n const parsed = new URL(url);\r\n let normalized = parsed.origin + parsed.pathname.replace(/\\/$/, '');\r\n if (parsed.search) normalized += parsed.search;\r\n return normalized.toLowerCase();\r\n } catch {\r\n return url.toLowerCase().replace(/\\/$/, '');\r\n }\r\n }\r\n\r\n /**\r\n * Extracts all unique URLs from a root sitemap URL or website base URL.\r\n */\r\n async *extract(inputUrl: string): AsyncGenerator<SitemapUrl> {\r\n let startUrls = [inputUrl];\r\n\r\n // If the URL doesn't end in .xml or .gz, it might be a website root\r\n if (!inputUrl.endsWith('.xml') && !inputUrl.endsWith('.gz')) {\r\n const discovered = await this.discovery.findSitemaps(inputUrl);\r\n if (discovered.length > 0) {\r\n console.log(`✅ Discovered ${discovered.length} sitemap(s): ${discovered.join(', ')}`);\r\n startUrls = discovered;\r\n } else {\r\n console.log(`⚠️ No sitemaps discovered via robots.txt or standard paths. Proceeding with input URL.`);\r\n }\r\n }\r\n\r\n for (const startUrl of startUrls) {\r\n for await (const discovered of this.discovery.discover(startUrl)) {\r\n this.discoveredSitemaps.add(discovered.url);\r\n // Pass the discovered sitemap to the parser with buffered xmlData\r\n const parserInput = { type: 'xmlData' as const, url: discovered.url, xmlData: discovered.xmlData };\r\n \r\n for await (const urlObj of this.parser.parse(parserInput)) {\r\n const normalized = this.normalizeUrl(urlObj.loc);\r\n if (!this.seenUrls.has(normalized)) {\r\n this.seenUrls.add(normalized);\r\n yield urlObj;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n","import micromatch from 'micromatch';\r\nimport { type Config, type Pattern } from '../config/schema';\r\nimport { type SitemapUrl, type Risk } from '../types/sitemap';\r\n\r\nexport class MatcherService {\r\n private readonly config: Config;\r\n private readonly rootDomain?: string;\r\n\r\n constructor(config: Config, rootUrl?: string) {\r\n this.config = config;\r\n if (rootUrl) {\r\n try {\r\n this.rootDomain = new URL(rootUrl).hostname.replace(/^www\\./, '');\r\n } catch {\r\n // Invalid URL, ignore\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Matches a URL against all policies and returns detected risks.\r\n */\r\n match(urlObj: SitemapUrl): Risk[] {\r\n const risks: Risk[] = [];\r\n\r\n // 1. Domain Consistency Check (Highest Priority)\r\n // This check always runs and its risks are never suppressed by acceptable patterns.\r\n if (this.config.enforceDomainConsistency && this.rootDomain) {\r\n try {\r\n const currentDomain = new URL(urlObj.loc).hostname.replace(/^www\\./, '');\r\n if (currentDomain !== this.rootDomain) {\r\n risks.push({\r\n category: 'Domain Consistency',\r\n pattern: this.rootDomain,\r\n type: 'literal',\r\n reason: `URL domain mismatch: expected ${this.rootDomain} (or www.${this.rootDomain}), but found ${currentDomain}.`,\r\n });\r\n }\r\n } catch {\r\n // Invalid URL in sitemap\r\n }\r\n }\r\n\r\n // 2. Check acceptable patterns (Allowlist)\r\n // If a URL matches an acceptable pattern, it is marked as ignored.\r\n // We return early, but we MUST still return any domain consistency risks.\r\n for (const pattern of this.config.acceptable_patterns) {\r\n if (this.isMatch(urlObj.loc, pattern)) {\r\n urlObj.ignored = true;\r\n urlObj.ignoredBy = pattern.reason;\r\n return risks; // Return any existing risks (e.g. Domain Consistency)\r\n }\r\n }\r\n\r\n // 3. Policy Checks\r\n for (const policy of this.config.policies) {\r\n for (const pattern of policy.patterns) {\r\n if (this.isMatch(urlObj.loc, pattern)) {\r\n risks.push({\r\n category: policy.category,\r\n pattern: pattern.value,\r\n type: pattern.type,\r\n reason: pattern.reason,\r\n });\r\n }\r\n }\r\n }\r\n\r\n return risks;\r\n }\r\n\r\n private isMatch(url: string, pattern: Pattern): boolean {\r\n switch (pattern.type) {\r\n case 'literal':\r\n return url.includes(pattern.value);\r\n case 'glob':\r\n return micromatch.isMatch(url, pattern.value, { contains: true });\r\n case 'regex':\r\n try {\r\n const regex = new RegExp(pattern.value, 'i');\r\n return regex.test(url);\r\n } catch {\r\n return false;\r\n }\r\n default:\r\n return false;\r\n }\r\n }\r\n}\r\n","import chalk from 'chalk';\r\nimport { Reporter, ReportData } from './base';\r\n\r\nexport class ConsoleReporter implements Reporter {\r\n async generate(data: ReportData): Promise<void> {\r\n console.log('\\n' + chalk.bold.blue('=== sitemap-qa Analysis Summary ==='));\r\n console.log(`Total URLs Scanned: ${data.totalUrls}`);\r\n console.log(`Total Risks Found: ${data.totalRisks > 0 ? chalk.red(data.totalRisks) : chalk.green(0)}`);\r\n console.log(`URLs with Risks: ${data.urlsWithRisks.length}`);\r\n console.log(`URLs Ignored: ${data.ignoredUrls.length > 0 ? chalk.yellow(data.ignoredUrls.length) : 0}`);\r\n console.log(`Duration: ${((data.endTime.getTime() - data.startTime.getTime()) / 1000).toFixed(2)}s`);\r\n\r\n if (data.urlsWithRisks.length > 0) {\r\n console.log('\\n' + chalk.bold.yellow('Top Findings:'));\r\n data.urlsWithRisks.slice(0, 10).forEach((url) => {\r\n console.log(`\\n${chalk.cyan(url.loc)}`);\r\n url.risks.forEach((risk) => {\r\n console.log(` - [${chalk.red(risk.category)}] ${risk.reason} (${chalk.gray(risk.pattern)})`);\r\n });\r\n });\r\n\r\n if (data.urlsWithRisks.length > 10) {\r\n console.log(`\\n... and ${data.urlsWithRisks.length - 10} more. See JSON/HTML report for full details.`);\r\n }\r\n }\r\n\r\n console.log('\\n' + chalk.bold.blue('==================================='));\r\n }\r\n}\r\n","import fs from 'node:fs/promises';\r\nimport { Reporter, ReportData } from './base';\r\n\r\nexport class JsonReporter implements Reporter {\r\n private readonly outputPath: string;\r\n\r\n constructor(outputPath: string = 'sitemap-qa-report.json') {\r\n this.outputPath = outputPath;\r\n }\r\n\r\n async generate(data: ReportData): Promise<void> {\r\n const report = {\r\n metadata: {\r\n generatedAt: new Date().toISOString(),\r\n durationMs: data.endTime.getTime() - data.startTime.getTime(),\r\n },\r\n summary: {\r\n totalUrls: data.totalUrls,\r\n totalRisks: data.totalRisks,\r\n urlsWithRisksCount: data.urlsWithRisks.length,\r\n ignoredUrlsCount: data.ignoredUrls.length,\r\n },\r\n findings: data.urlsWithRisks,\r\n ignored: data.ignoredUrls,\r\n };\r\n\r\n await fs.writeFile(this.outputPath, JSON.stringify(report, null, 2), 'utf8');\r\n console.log(`JSON report generated at ${this.outputPath}`);\r\n }\r\n}\r\n","import fs from 'node:fs/promises';\r\nimport path from 'node:path';\r\nimport { fileURLToPath } from 'node:url';\r\nimport Handlebars from 'handlebars';\r\nimport { Reporter, ReportData } from './base';\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\nexport class HtmlReporter implements Reporter {\r\n private readonly outputPath: string;\r\n\r\n constructor(outputPath: string = 'sitemap-qa-report.html') {\r\n this.outputPath = outputPath;\r\n\r\n // Register helpers\r\n Handlebars.registerHelper('json', (context) => {\r\n return JSON.stringify(context);\r\n });\r\n }\r\n\r\n async generate(data: ReportData): Promise<void> {\r\n // Register partials\r\n const partialsDir = path.join(__dirname, 'templates', 'partials');\r\n try {\r\n const partialFiles = await fs.readdir(partialsDir);\r\n for (const file of partialFiles) {\r\n if (file.endsWith('.hbs')) {\r\n const partialName = path.basename(file, '.hbs');\r\n const partialSource = await fs.readFile(path.join(partialsDir, file), 'utf8');\r\n Handlebars.registerPartial(partialName, partialSource);\r\n }\r\n }\r\n } catch (error) {\r\n console.warn('Could not load partials:', error);\r\n }\r\n\r\n const templatePath = path.join(__dirname, 'templates', 'report.hbs');\r\n const templateSource = await fs.readFile(templatePath, 'utf8');\r\n const template = Handlebars.compile(templateSource);\r\n\r\n const templateData = this.prepareTemplateData(data);\r\n const html = template(templateData);\r\n\r\n await fs.writeFile(this.outputPath, html, 'utf8');\r\n console.log(`HTML report generated at ${this.outputPath}`);\r\n }\r\n\r\n private prepareTemplateData(data: ReportData) {\r\n const duration = ((data.endTime.getTime() - data.startTime.getTime()) / 1000).toFixed(1);\r\n const timestamp = data.endTime.toLocaleString();\r\n\r\n const categoriesMap: Record<string, Record<string, { reason: string, urls: string[] }>> = {};\r\n\r\n for (const urlObj of data.urlsWithRisks) {\r\n for (const risk of urlObj.risks) {\r\n if (!categoriesMap[risk.category]) {\r\n categoriesMap[risk.category] = {};\r\n }\r\n if (!categoriesMap[risk.category][risk.pattern]) {\r\n categoriesMap[risk.category][risk.pattern] = {\r\n reason: risk.reason,\r\n urls: []\r\n };\r\n }\r\n categoriesMap[risk.category][risk.pattern].urls.push(urlObj.loc);\r\n }\r\n }\r\n\r\n const categories = Object.entries(categoriesMap).map(([name, findingsMap]) => {\r\n const findings = Object.entries(findingsMap).map(([pattern, finding]) => ({\r\n pattern,\r\n urls: finding.urls,\r\n reason: finding.reason,\r\n displayUrls: finding.urls.slice(0, 3),\r\n moreCount: finding.urls.length > 3 ? finding.urls.length - 3 : 0\r\n }));\r\n\r\n const totalUrls = findings.reduce((acc, f) => acc + f.urls.length, 0);\r\n\r\n return {\r\n name,\r\n totalUrls,\r\n findings\r\n };\r\n });\r\n\r\n const ignoredUrls = data.ignoredUrls.map(u => {\r\n const suppressedCategories = u.risks.length > 0\r\n ? [...new Set(u.risks.map(r => r.category))].join(', ')\r\n : undefined;\r\n\r\n return {\r\n loc: u.loc,\r\n ignoredBy: u.ignoredBy ?? 'Unknown',\r\n suppressedCategories\r\n };\r\n });\r\n\r\n return {\r\n rootUrl: data.rootUrl,\r\n timestamp,\r\n discoveredSitemaps: data.discoveredSitemaps,\r\n totalUrls: data.totalUrls.toLocaleString(),\r\n totalRisks: data.totalRisks,\r\n ignoredUrls,\r\n duration,\r\n categories\r\n };\r\n }\r\n}\r\n","import { Command } from 'commander';\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport chalk from 'chalk';\r\n\r\nconst DEFAULT_CONFIG = `# sitemap-qa configuration\r\n# This file defines the risk categories and patterns to monitor.\r\n\r\n# Tool Settings\r\noutDir: \"./sitemap-qa/report\"\r\noutputFormat: \"all\" # Options: json, html, all\r\nenforceDomainConsistency: true\r\n\r\n# Risk Categories\r\n# Each category contains a list of patterns to match against URLs found in sitemaps.\r\n# Patterns can be:\r\n# - literal: Exact string match\r\n# - glob: Glob pattern (e.g., **/admin/**)\r\n# - regex: Regular expression (e.g., /\\\\/v[0-9]+\\\\//)\r\n\r\n# Acceptable Patterns\r\n# URLs matching these patterns will be ignored and not flagged as risks.\r\nacceptable_patterns:\r\n - type: \"literal\"\r\n value: \"/acceptable-path\"\r\n reason: \"Example of an acceptable path that should not be flagged.\"\r\n - type: \"glob\"\r\n value: \"**/public-docs/**\"\r\n reason: \"Public documentation is always acceptable.\"\r\n\r\npolicies:\r\n - category: \"Security & Admin\"\r\n patterns:\r\n - type: \"glob\"\r\n value: \"**/admin/**\"\r\n reason: \"Administrative interfaces should not be publicly indexed.\"\r\n - type: \"glob\"\r\n value: \"**/.env*\"\r\n reason: \"Environment files contain sensitive secrets.\"\r\n - type: \"literal\"\r\n value: \"/wp-admin\"\r\n reason: \"WordPress admin paths are common attack vectors.\"\r\n\r\n - category: \"Environment Leakage\"\r\n patterns:\r\n - type: \"glob\"\r\n value: \"**/staging.**\"\r\n reason: \"Staging environments should be restricted.\"\r\n - type: \"glob\"\r\n value: \"**/dev.**\"\r\n reason: \"Development subdomains detected in production sitemap.\"\r\n\r\n - category: \"Sensitive Files\"\r\n patterns:\r\n - type: \"glob\"\r\n value: \"**/*.{sql,bak,zip,tar.gz}\"\r\n reason: \"Archive or database backup files exposed.\"\r\n`;\r\n\r\nexport const initCommand = new Command('init')\r\n .description('Initialize a default sitemap-qa.yaml configuration file')\r\n .action(() => {\r\n const configPath = path.join(process.cwd(), 'sitemap-qa.yaml');\r\n\r\n if (fs.existsSync(configPath)) {\r\n console.error(chalk.red(`Error: ${configPath} already exists.`));\r\n process.exit(1);\r\n }\r\n\r\n try {\r\n fs.writeFileSync(configPath, DEFAULT_CONFIG, 'utf8');\r\n console.log(chalk.green(`Successfully created ${configPath}`));\r\n } catch (error) {\r\n console.error(chalk.red('Failed to create configuration file:'), error);\r\n process.exit(1);\r\n }\r\n });\r\n"],"mappings":";;;AACA,SAAS,WAAAA,gBAAe;;;ACDxB,SAAS,eAAe;AACxB,OAAOC,YAAW;AAClB,OAAOC,WAAU;AACjB,OAAOC,SAAQ;;;ACHf,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,UAAU;;;ACFjB,SAAS,SAAS;AAEX,IAAM,oBAAoB,EAAE,KAAK,CAAC,WAAW,QAAQ,OAAO,CAAC;AAE7D,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,MAAM;AAAA,EACN,OAAO,EAAE,OAAO,EAAE,IAAI,GAAG,+BAA+B;AAAA,EACxD,QAAQ,EAAE,OAAO,EAAE,IAAI,GAAG,sCAAsC;AAClE,CAAC;AAEM,IAAM,eAAe,EAAE,OAAO;AAAA,EACnC,UAAU,EAAE,OAAO,EAAE,IAAI,GAAG,4BAA4B;AAAA,EACxD,UAAU,EAAE,MAAM,aAAa,EAAE,IAAI,GAAG,+CAA+C;AACzF,CAAC;AAEM,IAAM,eAAe,EAAE,OAAO;AAAA,EACnC,qBAAqB,EAAE,MAAM,aAAa,EAAE,QAAQ,CAAC,CAAC;AAAA,EACtD,UAAU,EAAE,MAAM,YAAY,EAAE,QAAQ,CAAC,CAAC;AAAA,EAC1C,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,cAAc,EAAE,KAAK,CAAC,QAAQ,QAAQ,KAAK,CAAC,EAAE,QAAQ,KAAK;AAAA,EAC3D,0BAA0B,EAAE,QAAQ,EAAE,QAAQ,IAAI;AACpD,CAAC;;;ACnBM,IAAM,mBAA2B;AAAA,EACtC,qBAAqB,CAAC;AAAA,EACtB,cAAc;AAAA,EACd,0BAA0B;AAAA,EAC1B,UAAU;AAAA,IACR;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,QACR;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,QACR;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,UAAU;AAAA,QACR;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AFhDA,OAAO,WAAW;AAEX,IAAM,eAAN,MAAmB;AAAA,EACxB,OAAwB,sBAAsB;AAAA,EAE9C,OAAO,KAAK,YAA6B;AACvC,UAAM,aAAa,cAAc,KAAK,KAAK,QAAQ,IAAI,GAAG,KAAK,mBAAmB;AAClF,QAAI,aAAqB;AAAA,MACvB,qBAAqB,CAAC;AAAA,MACtB,UAAU,CAAC;AAAA,MACX,cAAc;AAAA,MACd,0BAA0B;AAAA,IAC5B;AAGA,QAAI,GAAG,WAAW,UAAU,GAAG;AAC7B,UAAI;AACF,cAAM,cAAc,GAAG,aAAa,YAAY,MAAM;AACtD,cAAM,aAAa,KAAK,KAAK,WAAW;AAExC,cAAM,SAAS,aAAa,UAAU,UAAU;AAEhD,YAAI,CAAC,OAAO,SAAS;AACnB,kBAAQ,MAAM,MAAM,IAAI,iCAAiC,CAAC;AAC1D,iBAAO,MAAM,OAAO,QAAQ,CAAC,UAAU;AACrC,oBAAQ,MAAM,MAAM,OAAO,OAAO,MAAM,KAAK,KAAK,GAAG,CAAC,KAAK,MAAM,OAAO,EAAE,CAAC;AAAA,UAC7E,CAAC;AACD,kBAAQ,KAAK,CAAC;AACd,iBAAO;AAAA,QACT;AAEA,qBAAa,OAAO;AAAA,MACtB,SAAS,OAAO;AACd,gBAAQ,MAAM,MAAM,IAAI,+BAA+B,GAAG,KAAK;AAC/D,gBAAQ,KAAK,CAAC;AACd,eAAO;AAAA,MACT;AAAA,IACF,WAAW,YAAY;AACrB,cAAQ,MAAM,MAAM,IAAI,0CAA0C,UAAU,EAAE,CAAC;AAC/E,cAAQ,KAAK,CAAC;AACd,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,aAAa,kBAAkB,UAAU;AAAA,EACvD;AAAA,EAEA,OAAe,aAAa,UAAkB,MAAsB;AAClE,UAAM,iBAAiB,CAAC,GAAG,SAAS,QAAQ;AAE5C,SAAK,SAAS,QAAQ,CAAC,eAAe;AACpC,YAAM,QAAQ,eAAe,UAAU,OAAK,EAAE,aAAa,WAAW,QAAQ;AAC9E,UAAI,UAAU,IAAI;AAEhB,uBAAe,KAAK,IAAI;AAAA,MAC1B,OAAO;AAEL,uBAAe,KAAK,UAAU;AAAA,MAChC;AAAA,IACF,CAAC;AAGD,UAAM,SAAiB;AAAA,MACrB,GAAG;AAAA,MACH,qBAAqB,CAAC,GAAI,SAAS,uBAAuB,CAAC,GAAI,GAAI,KAAK,uBAAuB,CAAC,CAAE;AAAA,MAClG,UAAU;AAAA,IACZ;AAEA,QAAI,KAAK,WAAW,QAAW;AAC7B,aAAO,SAAS,KAAK;AAAA,IACvB;AAEA,QAAI,KAAK,iBAAiB,QAAW;AACnC,aAAO,eAAe,KAAK;AAAA,IAC7B;AAEA,QAAI,KAAK,6BAA6B,QAAW;AAC/C,aAAO,2BAA2B,KAAK;AAAA,IACzC;AAEA,WAAO;AAAA,EACT;AACF;;;AGtFA,SAAS,aAAa;AACtB,SAAS,gBAAgB;;;ACDzB,SAAS,iBAAiB;AAE1B,SAAS,kBAAkB;AAsBpB,IAAM,qBAAN,MAAyB;AAAA,EACb;AAAA,EACT;AAAA,EAER,cAAc;AACZ,SAAK,SAAS,IAAI,UAAU;AAAA,MAC1B,kBAAkB;AAAA,MAClB,qBAAqB;AAAA;AAAA,MAErB,SAAS,CAAC,SAAS,SAAS,aAAa,SAAS;AAAA,MAClD,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,MAAM,QAAwD;AACnE,UAAM,UAAU,OAAO,WAAW,WAAW,SAAS,MAAM,KAAK,eAAe,MAAM;AAGtF,SAAK,gBAAgB;AAErB,UAAM,UAAU,KAAK,OAAO,MAAM,OAAO;AAGzC,QAAI,QAAQ,cAAc,SAAS;AACjC,YAAM,WAAW,QAAQ,aAAa;AACtC,iBAAW,WAAW,UAAU;AAC9B,YAAI,SAAS,KAAK;AAChB,gBAAM,EAAE,MAAM,WAAW,KAAK,QAAQ,IAAI;AAAA,QAC5C;AAAA,MACF;AAAA,IACF;AAGA,QAAI,QAAQ,QAAQ,KAAK;AACvB,YAAM,OAAO,QAAQ,OAAO;AAC5B,iBAAW,OAAO,MAAM;AACtB,YAAI,KAAK,KAAK;AACZ,gBAAM;AAAA,YACJ,MAAM;AAAA,YACN,KAAK,IAAI;AAAA,YACT,SAAS,IAAI;AAAA,YACb,YAAY,IAAI;AAAA,YAChB,UAAU,IAAI;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAuC;AACrC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,eAAe,QAAmC;AAC9D,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ;AAChC,aAAO,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,IAChC;AACA,UAAM,SAAS,OAAO,OAAO,MAAM;AAGnC,QAAI,OAAO,UAAU,KAAK,OAAO,CAAC,MAAM,MAAQ,OAAO,CAAC,MAAM,KAAM;AAClE,UAAI;AACF,cAAM,eAAe,WAAW,MAAM;AACtC,eAAO,aAAa,SAAS,MAAM;AAAA,MACrC,SAAS,OAAO;AACd,cAAM,IAAI,MAAM,yCAAyC,KAAK,EAAE;AAAA,MAClE;AAAA,IACF;AAEA,WAAO,OAAO,SAAS,MAAM;AAAA,EAC/B;AACF;;;AD5FO,IAAM,mBAAN,MAAuB;AAAA,EACX;AAAA,EACA,UAAU,oBAAI,IAAY;AAAA,EAC1B,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,SAAK,YAAY,IAAI,mBAAmB;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,SAAoC;AACrD,UAAM,WAAW,oBAAI,IAAY;AACjC,UAAM,MAAM,IAAI,IAAI,OAAO;AAC3B,UAAM,SAAS,IAAI;AAGnB,QAAI;AACF,YAAM,YAAY,GAAG,MAAM;AAC3B,YAAM,WAAW,MAAM,MAAM,SAAS;AACtC,UAAI,SAAS,WAAW,KAAK;AAC3B,cAAM,OAAO,MAAM,SAAS,KAAK;AACjC,cAAM,UAAU,KAAK,SAAS,sBAAsB;AACpD,mBAAW,SAAS,SAAS;AAC3B,cAAI,MAAM,CAAC,EAAG,UAAS,IAAI,MAAM,CAAC,EAAE,KAAK,CAAC;AAAA,QAC5C;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AAAA,IAEZ;AAGA,QAAI,SAAS,SAAS,GAAG;AACvB,iBAAWC,SAAQ,KAAK,gBAAgB;AACtC,YAAI;AACF,gBAAM,aAAa,GAAG,MAAM,GAAGA,KAAI;AACnC,gBAAM,WAAW,MAAM,MAAM,YAAY,EAAE,QAAQ,OAAO,CAAC;AAC3D,cAAI,SAAS,WAAW,KAAK;AAC3B,qBAAS,IAAI,UAAU;AAAA,UACzB;AAAA,QACF,SAAS,GAAG;AAAA,QAEZ;AAAA,MACF;AAAA,IACF;AAEA,WAAO,MAAM,KAAK,QAAQ;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,SAAS,SAAoD;AAClE,UAAM,QAAkB,CAAC,OAAO;AAEhC,WAAO,MAAM,SAAS,GAAG;AACvB,YAAM,aAAa,MAAM,MAAM;AAC/B,UAAI,KAAK,QAAQ,IAAI,UAAU,EAAG;AAClC,WAAK,QAAQ,IAAI,UAAU;AAE3B,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,UAAU;AACvC,YAAI,SAAS,WAAW,IAAK;AAE7B,YAAI,UAAU;AACd,YAAI,SAAS;AACb,cAAM,gBAA0B,CAAC;AAEjC,YAAI;AACJ,YAAI;AAEJ,YAAI,SAAS,MAAM;AAEjB,gBAAM,aAAa,SAAS,QAAQ,SAAS,IAAsB;AACnE,mBAAS;AAAA,QACX,OAAO;AAEL,oBAAU,MAAM,SAAS,KAAK;AAC9B,mBAAS;AAAA,QACX;AAGA,yBAAiB,SAAS,KAAK,UAAU,MAAM,MAAM,GAAG;AACtD,cAAI,MAAM,SAAS,WAAW;AAC5B,sBAAU;AACV,0BAAc,KAAK,MAAM,GAAG;AAAA,UAC9B,WAAW,MAAM,SAAS,OAAO;AAC/B,qBAAS;AAAA,UACX;AAAA,QACF;AAEA,YAAI,SAAS;AACX,qBAAW,OAAO,eAAe;AAC/B,kBAAM,KAAK,GAAG;AAAA,UAChB;AAAA,QACF;AAGA,YAAI,CAAC,SAAS;AACZ,oBAAU,KAAK,UAAU,iBAAiB,KAAK;AAAA,QACjD;AAGA,YAAI,UAAW,CAAC,WAAW,QAAQ,SAAS,SAAS,GAAI;AACvD,gBAAM;AAAA,YACJ,KAAK;AAAA,YACL;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,MAAM,uCAAuC,UAAU,KAAK,KAAK;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AACF;;;AEpIA,SAAS,YAAAC,iBAAgB;AAGzB,SAAS,SAAAC,cAAa;AAEf,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EAEjB,cAAc;AACZ,SAAK,YAAY,IAAI,mBAAmB;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBA,OAAO,MAAM,kBAA+K;AAE1L,UAAM,aAAa,OAAO,qBAAqB,WAC3C,mBACA,iBAAiB;AAErB,QAAI;AACF,UAAI;AAEJ,UAAI,OAAO,qBAAqB,UAAU;AACxC,cAAM,WAAW,MAAMA,OAAM,UAAU;AACvC,YAAI,SAAS,WAAW,IAAK,OAAM,IAAI,MAAM,8BAA8B,UAAU,UAAU,SAAS,MAAM,EAAE;AAEhH,YAAI,SAAS,MAAM;AACjB,mBAASC,UAAS,QAAQ,SAAS,IAAsB;AAAA,QAC3D,OAAO;AAEL,mBAAS,MAAM,SAAS,KAAK;AAAA,QAC/B;AAAA,MACF,WAAW,iBAAiB,SAAS,UAAU;AAE7C,YAAI,iBAAiB,kBAAkBA,WAAU;AAC/C,mBAAS,iBAAiB;AAAA,QAC5B,OAAO;AACL,mBAASA,UAAS,QAAQ,iBAAiB,MAAwB;AAAA,QACrE;AAAA,MACF,OAAO;AACL,iBAAS,iBAAiB;AAC1B,iBAAS,iBAAiB;AAAA,MAC5B;AAGA,uBAAiB,SAAS,KAAK,UAAU,MAAM,MAAM,GAAG;AACtD,YAAI,MAAM,SAAS,OAAO;AACxB,gBAAM;AAAA,YACJ,KAAK,MAAM;AAAA,YACX,QAAQ;AAAA,YACR,SAAS,MAAM;AAAA,YACf,YAAY,MAAM;AAAA,YAClB,UAAU,MAAM;AAAA,YAChB,OAAO,CAAC;AAAA,UACV;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,8BAA8B,UAAU,KAAK,KAAK;AAAA,IAClE;AAAA,EACF;AAAA,EAEA,MAAc,eAAe,QAAmC;AAC9D,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ;AAChC,aAAO,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,IAChC;AACA,WAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AAAA,EAC9C;AACF;;;ACrFO,IAAM,mBAAN,MAAuB;AAAA,EACX;AAAA,EACA;AAAA,EACA,WAAW,oBAAI,IAAY;AAAA,EAC3B,qBAAqB,oBAAI,IAAY;AAAA,EAEtD,cAAc;AACZ,SAAK,YAAY,IAAI,iBAAiB;AACtC,SAAK,SAAS,IAAI,cAAc;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,wBAAkC;AAChC,WAAO,MAAM,KAAK,KAAK,kBAAkB;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAa,KAAqB;AACxC,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,aAAa,OAAO,SAAS,OAAO,SAAS,QAAQ,OAAO,EAAE;AAClE,UAAI,OAAO,OAAQ,eAAc,OAAO;AACxC,aAAO,WAAW,YAAY;AAAA,IAChC,QAAQ;AACN,aAAO,IAAI,YAAY,EAAE,QAAQ,OAAO,EAAE;AAAA,IAC5C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,QAAQ,UAA8C;AAC3D,QAAI,YAAY,CAAC,QAAQ;AAGzB,QAAI,CAAC,SAAS,SAAS,MAAM,KAAK,CAAC,SAAS,SAAS,KAAK,GAAG;AAC3D,YAAM,aAAa,MAAM,KAAK,UAAU,aAAa,QAAQ;AAC7D,UAAI,WAAW,SAAS,GAAG;AACzB,gBAAQ,IAAI,qBAAgB,WAAW,MAAM,gBAAgB,WAAW,KAAK,IAAI,CAAC,EAAE;AACpF,oBAAY;AAAA,MACd,OAAO;AACL,gBAAQ,IAAI,kGAAwF;AAAA,MACtG;AAAA,IACF;AAEA,eAAW,YAAY,WAAW;AAChC,uBAAiB,cAAc,KAAK,UAAU,SAAS,QAAQ,GAAG;AAChE,aAAK,mBAAmB,IAAI,WAAW,GAAG;AAE1C,cAAM,cAAc,EAAE,MAAM,WAAoB,KAAK,WAAW,KAAK,SAAS,WAAW,QAAQ;AAEjG,yBAAiB,UAAU,KAAK,OAAO,MAAM,WAAW,GAAG;AACzD,gBAAM,aAAa,KAAK,aAAa,OAAO,GAAG;AAC/C,cAAI,CAAC,KAAK,SAAS,IAAI,UAAU,GAAG;AAClC,iBAAK,SAAS,IAAI,UAAU;AAC5B,kBAAM;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACrEA,OAAO,gBAAgB;AAIhB,IAAM,iBAAN,MAAqB;AAAA,EACT;AAAA,EACA;AAAA,EAEjB,YAAY,QAAgB,SAAkB;AAC5C,SAAK,SAAS;AACd,QAAI,SAAS;AACX,UAAI;AACF,aAAK,aAAa,IAAI,IAAI,OAAO,EAAE,SAAS,QAAQ,UAAU,EAAE;AAAA,MAClE,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAA4B;AAChC,UAAM,QAAgB,CAAC;AAIvB,QAAI,KAAK,OAAO,4BAA4B,KAAK,YAAY;AAC3D,UAAI;AACD,cAAM,gBAAgB,IAAI,IAAI,OAAO,GAAG,EAAE,SAAS,QAAQ,UAAU,EAAE;AACxE,YAAI,kBAAkB,KAAK,YAAY;AACrC,gBAAM,KAAK;AAAA,YACT,UAAU;AAAA,YACV,SAAS,KAAK;AAAA,YACd,MAAM;AAAA,YACN,QAAQ,iCAAiC,KAAK,UAAU,YAAY,KAAK,UAAU,gBAAgB,aAAa;AAAA,UAClH,CAAC;AAAA,QACH;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAKA,eAAW,WAAW,KAAK,OAAO,qBAAqB;AACrD,UAAI,KAAK,QAAQ,OAAO,KAAK,OAAO,GAAG;AACrC,eAAO,UAAU;AACjB,eAAO,YAAY,QAAQ;AAC3B,eAAO;AAAA,MACT;AAAA,IACF;AAGA,eAAW,UAAU,KAAK,OAAO,UAAU;AACzC,iBAAW,WAAW,OAAO,UAAU;AACrC,YAAI,KAAK,QAAQ,OAAO,KAAK,OAAO,GAAG;AACrC,gBAAM,KAAK;AAAA,YACT,UAAU,OAAO;AAAA,YACjB,SAAS,QAAQ;AAAA,YACjB,MAAM,QAAQ;AAAA,YACd,QAAQ,QAAQ;AAAA,UAClB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,QAAQ,KAAa,SAA2B;AACtD,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,eAAO,IAAI,SAAS,QAAQ,KAAK;AAAA,MACnC,KAAK;AACH,eAAO,WAAW,QAAQ,KAAK,QAAQ,OAAO,EAAE,UAAU,KAAK,CAAC;AAAA,MAClE,KAAK;AACH,YAAI;AACF,gBAAM,QAAQ,IAAI,OAAO,QAAQ,OAAO,GAAG;AAC3C,iBAAO,MAAM,KAAK,GAAG;AAAA,QACvB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AACE,eAAO;AAAA,IACX;AAAA,EACF;AACF;;;ACxFA,OAAOC,YAAW;AAGX,IAAM,kBAAN,MAA0C;AAAA,EAC/C,MAAM,SAAS,MAAiC;AAC9C,YAAQ,IAAI,OAAOA,OAAM,KAAK,KAAK,qCAAqC,CAAC;AACzE,YAAQ,IAAI,uBAAuB,KAAK,SAAS,EAAE;AACnD,YAAQ,IAAI,uBAAuB,KAAK,aAAa,IAAIA,OAAM,IAAI,KAAK,UAAU,IAAIA,OAAM,MAAM,CAAC,CAAC,EAAE;AACtG,YAAQ,IAAI,uBAAuB,KAAK,cAAc,MAAM,EAAE;AAC9D,YAAQ,IAAI,uBAAuB,KAAK,YAAY,SAAS,IAAIA,OAAM,OAAO,KAAK,YAAY,MAAM,IAAI,CAAC,EAAE;AAC5G,YAAQ,IAAI,yBAAyB,KAAK,QAAQ,QAAQ,IAAI,KAAK,UAAU,QAAQ,KAAK,KAAM,QAAQ,CAAC,CAAC,GAAG;AAE7G,QAAI,KAAK,cAAc,SAAS,GAAG;AACjC,cAAQ,IAAI,OAAOA,OAAM,KAAK,OAAO,eAAe,CAAC;AACrD,WAAK,cAAc,MAAM,GAAG,EAAE,EAAE,QAAQ,CAAC,QAAQ;AAC/C,gBAAQ,IAAI;AAAA,EAAKA,OAAM,KAAK,IAAI,GAAG,CAAC,EAAE;AACtC,YAAI,MAAM,QAAQ,CAAC,SAAS;AAC1B,kBAAQ,IAAI,QAAQA,OAAM,IAAI,KAAK,QAAQ,CAAC,KAAK,KAAK,MAAM,KAAKA,OAAM,KAAK,KAAK,OAAO,CAAC,GAAG;AAAA,QAC9F,CAAC;AAAA,MACH,CAAC;AAED,UAAI,KAAK,cAAc,SAAS,IAAI;AAClC,gBAAQ,IAAI;AAAA,UAAa,KAAK,cAAc,SAAS,EAAE,+CAA+C;AAAA,MACxG;AAAA,IACF;AAEA,YAAQ,IAAI,OAAOA,OAAM,KAAK,KAAK,qCAAqC,CAAC;AAAA,EAC3E;AACF;;;AC5BA,OAAOC,SAAQ;AAGR,IAAM,eAAN,MAAuC;AAAA,EAC3B;AAAA,EAEjB,YAAY,aAAqB,0BAA0B;AACzD,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,MAAM,SAAS,MAAiC;AAC9C,UAAM,SAAS;AAAA,MACb,UAAU;AAAA,QACR,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,YAAY,KAAK,QAAQ,QAAQ,IAAI,KAAK,UAAU,QAAQ;AAAA,MAC9D;AAAA,MACA,SAAS;AAAA,QACP,WAAW,KAAK;AAAA,QAChB,YAAY,KAAK;AAAA,QACjB,oBAAoB,KAAK,cAAc;AAAA,QACvC,kBAAkB,KAAK,YAAY;AAAA,MACrC;AAAA,MACA,UAAU,KAAK;AAAA,MACf,SAAS,KAAK;AAAA,IAChB;AAEA,UAAMA,IAAG,UAAU,KAAK,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,MAAM;AAC3E,YAAQ,IAAI,4BAA4B,KAAK,UAAU,EAAE;AAAA,EAC3D;AACF;;;AC7BA,OAAOC,SAAQ;AACf,OAAOC,WAAU;AACjB,SAAS,qBAAqB;AAC9B,OAAO,gBAAgB;AAGvB,IAAMC,cAAa,cAAc,YAAY,GAAG;AAChD,IAAMC,aAAYF,MAAK,QAAQC,WAAU;AAElC,IAAM,eAAN,MAAuC;AAAA,EAC3B;AAAA,EAEjB,YAAY,aAAqB,0BAA0B;AACzD,SAAK,aAAa;AAGlB,eAAW,eAAe,QAAQ,CAAC,YAAY;AAC7C,aAAO,KAAK,UAAU,OAAO;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,SAAS,MAAiC;AAE9C,UAAM,cAAcD,MAAK,KAAKE,YAAW,aAAa,UAAU;AAChE,QAAI;AACF,YAAM,eAAe,MAAMH,IAAG,QAAQ,WAAW;AACjD,iBAAW,QAAQ,cAAc;AAC/B,YAAI,KAAK,SAAS,MAAM,GAAG;AACzB,gBAAM,cAAcC,MAAK,SAAS,MAAM,MAAM;AAC9C,gBAAM,gBAAgB,MAAMD,IAAG,SAASC,MAAK,KAAK,aAAa,IAAI,GAAG,MAAM;AAC5E,qBAAW,gBAAgB,aAAa,aAAa;AAAA,QACvD;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,KAAK,4BAA4B,KAAK;AAAA,IAChD;AAEA,UAAM,eAAeA,MAAK,KAAKE,YAAW,aAAa,YAAY;AACnE,UAAM,iBAAiB,MAAMH,IAAG,SAAS,cAAc,MAAM;AAC7D,UAAM,WAAW,WAAW,QAAQ,cAAc;AAElD,UAAM,eAAe,KAAK,oBAAoB,IAAI;AAClD,UAAM,OAAO,SAAS,YAAY;AAElC,UAAMA,IAAG,UAAU,KAAK,YAAY,MAAM,MAAM;AAChD,YAAQ,IAAI,4BAA4B,KAAK,UAAU,EAAE;AAAA,EAC3D;AAAA,EAEQ,oBAAoB,MAAkB;AAC5C,UAAM,aAAa,KAAK,QAAQ,QAAQ,IAAI,KAAK,UAAU,QAAQ,KAAK,KAAM,QAAQ,CAAC;AACvF,UAAM,YAAY,KAAK,QAAQ,eAAe;AAE9C,UAAM,gBAAoF,CAAC;AAE3F,eAAW,UAAU,KAAK,eAAe;AACvC,iBAAW,QAAQ,OAAO,OAAO;AAC/B,YAAI,CAAC,cAAc,KAAK,QAAQ,GAAG;AACjC,wBAAc,KAAK,QAAQ,IAAI,CAAC;AAAA,QAClC;AACA,YAAI,CAAC,cAAc,KAAK,QAAQ,EAAE,KAAK,OAAO,GAAG;AAC/C,wBAAc,KAAK,QAAQ,EAAE,KAAK,OAAO,IAAI;AAAA,YAC3C,QAAQ,KAAK;AAAA,YACb,MAAM,CAAC;AAAA,UACT;AAAA,QACF;AACA,sBAAc,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAE,KAAK,KAAK,OAAO,GAAG;AAAA,MACjE;AAAA,IACF;AAEA,UAAM,aAAa,OAAO,QAAQ,aAAa,EAAE,IAAI,CAAC,CAAC,MAAM,WAAW,MAAM;AAC5E,YAAM,WAAW,OAAO,QAAQ,WAAW,EAAE,IAAI,CAAC,CAAC,SAAS,OAAO,OAAO;AAAA,QACxE;AAAA,QACA,MAAM,QAAQ;AAAA,QACd,QAAQ,QAAQ;AAAA,QAChB,aAAa,QAAQ,KAAK,MAAM,GAAG,CAAC;AAAA,QACpC,WAAW,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,IAAI;AAAA,MACjE,EAAE;AAEF,YAAM,YAAY,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,QAAQ,CAAC;AAEpE,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAED,UAAM,cAAc,KAAK,YAAY,IAAI,OAAK;AAC5C,YAAM,uBAAuB,EAAE,MAAM,SAAS,IAC1C,CAAC,GAAG,IAAI,IAAI,EAAE,MAAM,IAAI,OAAK,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI,IACpD;AAEJ,aAAO;AAAA,QACL,KAAK,EAAE;AAAA,QACP,WAAW,EAAE,aAAa;AAAA,QAC1B;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd;AAAA,MACA,oBAAoB,KAAK;AAAA,MACzB,WAAW,KAAK,UAAU,eAAe;AAAA,MACzC,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;;;AXjGO,IAAM,iBAAiB,IAAI,QAAQ,SAAS,EAChD,YAAY,uCAAuC,EACnD,SAAS,SAAS,kBAAkB,EACpC,OAAO,uBAAuB,yBAAyB,EACvD,OAAO,yBAAyB,iCAAiC,EACjE,OAAO,wBAAwB,2BAA2B,EAC1D,OAAO,OAAO,KAAa,YAAmE;AAC7F,QAAM,YAAY,oBAAI,KAAK;AAG3B,QAAM,SAAS,aAAa,KAAK,QAAQ,MAAM;AAC/C,QAAM,SAAS,QAAQ,UAAU,OAAO,UAAU;AAClD,QAAM,eAAe,QAAQ,UAAU,OAAO,gBAAgB;AAG9D,QAAM,YAAY,IAAI,iBAAiB;AACvC,QAAM,UAAU,IAAI,eAAe,QAAQ,GAAG;AAE9C,QAAM,gBAA8B,CAAC;AACrC,QAAM,cAA4B,CAAC;AACnC,MAAI,YAAY;AAChB,MAAI,aAAa;AAEjB,UAAQ,IAAII,OAAM,KAAK;AAAA,iCAA6B,GAAG,KAAK,CAAC;AAE7D,MAAI;AAEF,qBAAiB,UAAU,UAAU,QAAQ,GAAG,GAAG;AACjD;AACA,YAAM,QAAQ,QAAQ,MAAM,MAAM;AAElC,UAAI,MAAM,SAAS,GAAG;AACpB,eAAO,QAAQ;AACf,sBAAc,KAAK,MAAM;AACzB,sBAAc,MAAM;AAAA,MACtB,WAAW,OAAO,SAAS;AACzB,oBAAY,KAAK,MAAM;AAAA,MACzB;AAEA,UAAI,YAAY,QAAQ,GAAG;AACzB,gBAAQ,OAAO,MAAMA,OAAM,KAAK,eAAe,SAAS,UAAU,CAAC;AAAA,MACrE;AAAA,IACF;AACA,YAAQ,OAAO,MAAM,IAAI;AAEzB,UAAM,UAAU,oBAAI,KAAK;AACzB,UAAM,aAAyB;AAAA,MAC7B,SAAS;AAAA,MACT,oBAAoB,UAAU,sBAAsB;AAAA,MACpD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,UAAM,YAAwB,CAAC,IAAI,gBAAgB,CAAC;AAEpD,UAAMC,IAAG,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AAE1C,QAAI,iBAAiB,UAAU,iBAAiB,OAAO;AACrD,YAAM,WAAWC,MAAK,KAAK,QAAQ,wBAAwB;AAC3D,gBAAU,KAAK,IAAI,aAAa,QAAQ,CAAC;AAAA,IAC3C;AACA,QAAI,iBAAiB,UAAU,iBAAiB,OAAO;AACrD,YAAM,WAAWA,MAAK,KAAK,QAAQ,wBAAwB;AAC3D,gBAAU,KAAK,IAAI,aAAa,QAAQ,CAAC;AAAA,IAC3C;AAEA,eAAW,YAAY,WAAW;AAChC,YAAM,SAAS,SAAS,UAAU;AAAA,IACpC;AAGA,QAAI,aAAa,GAAG;AAClB,cAAQ,KAAK,CAAC;AAAA,IAChB,OAAO;AACL,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EAEF,SAAS,OAAO;AACd,YAAQ,MAAMF,OAAM,IAAI,oBAAoB,GAAG,KAAK;AACpD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;;;AYnGH,SAAS,WAAAG,gBAAe;AACxB,OAAOC,SAAQ;AACf,OAAOC,WAAU;AACjB,OAAOC,YAAW;AAElB,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsDhB,IAAM,cAAc,IAAIH,SAAQ,MAAM,EAC1C,YAAY,yDAAyD,EACrE,OAAO,MAAM;AACZ,QAAM,aAAaE,MAAK,KAAK,QAAQ,IAAI,GAAG,iBAAiB;AAE7D,MAAID,IAAG,WAAW,UAAU,GAAG;AAC7B,YAAQ,MAAME,OAAM,IAAI,UAAU,UAAU,kBAAkB,CAAC;AAC/D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI;AACF,IAAAF,IAAG,cAAc,YAAY,gBAAgB,MAAM;AACnD,YAAQ,IAAIE,OAAM,MAAM,wBAAwB,UAAU,EAAE,CAAC;AAAA,EAC/D,SAAS,OAAO;AACd,YAAQ,MAAMA,OAAM,IAAI,sCAAsC,GAAG,KAAK;AACtE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;;;AbvEH,IAAM,UAAU,IAAIC,SAAQ;AAE5B,QACG,KAAK,YAAY,EACjB,QAAQ,OAAO,EACf,YAAY,+BAA+B;AAE9C,QAAQ,WAAW,cAAc;AACjC,QAAQ,WAAW,WAAW;AAG9B,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,YAAY;AACpD,UAAQ,MAAM,2BAA2B,SAAS,WAAW,MAAM;AACnE,UAAQ,KAAK,CAAC;AAChB,CAAC;AAGD,QAAQ,GAAG,UAAU,MAAM;AACzB,UAAQ,IAAI,+BAA+B;AAC3C,UAAQ,KAAK,CAAC;AAChB,CAAC;AAED,QAAQ,GAAG,WAAW,MAAM;AAC1B,UAAQ,IAAI,+BAA+B;AAC3C,UAAQ,KAAK,CAAC;AAChB,CAAC;AAED,QAAQ,MAAM;","names":["Command","chalk","path","fs","path","Readable","fetch","Readable","chalk","fs","fs","path","__filename","__dirname","chalk","fs","path","Command","fs","path","chalk","Command"]}
@@ -0,0 +1,20 @@
1
+ <div class="finding-group">
2
+ <div class="finding-header">
3
+ <h4>{{pattern}}</h4>
4
+ <span class="badge">{{urls.length}} URLs</span>
5
+ </div>
6
+ <div class="finding-description">
7
+ {{reason}}
8
+ </div>
9
+ <div class="url-list">
10
+ {{#each displayUrls}}
11
+ <div class="url-item">{{this}}</div>
12
+ {{/each}}
13
+ </div>
14
+ {{#if moreCount}}
15
+ <div class="more-count">... and {{moreCount}} more</div>
16
+ {{/if}}
17
+ <button class="btn" onclick="downloadUrls({{json pattern}}, {{json urls}})">
18
+ 📥 Download All {{urls.length}} URLs
19
+ </button>
20
+ </div>
@@ -0,0 +1,9 @@
1
+ <header>
2
+ <div class="container">
3
+ <h1>Sitemap Analysis</h1>
4
+ <div class="meta">
5
+ <div>{{rootUrl}}</div>
6
+ <div>{{timestamp}}</div>
7
+ </div>
8
+ </div>
9
+ </header>
@@ -0,0 +1,22 @@
1
+ <div class="summary-grid">
2
+ <div class="summary-card">
3
+ <h3>Sitemaps</h3>
4
+ <p>{{discoveredSitemaps.length}}</p>
5
+ </div>
6
+ <div class="summary-card">
7
+ <h3>URLs Analyzed</h3>
8
+ <p>{{totalUrls}}</p>
9
+ </div>
10
+ <div class="summary-card highlight">
11
+ <h3>Issues Found</h3>
12
+ <p>{{totalRisks}}</p>
13
+ </div>
14
+ <div class="summary-card">
15
+ <h3>URLs Ignored</h3>
16
+ <p>{{ignoredUrls.length}}</p>
17
+ </div>
18
+ <div class="summary-card">
19
+ <h3>Scan Time</h3>
20
+ <p>{{duration}}s</p>
21
+ </div>
22
+ </div>
@@ -0,0 +1,293 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sitemap Analysis - {{rootUrl}}</title>
7
+ <style>
8
+ :root {
9
+ --bg-dark: #0f172a;
10
+ --bg-light: #f8fafc;
11
+ --text-main: #1e293b;
12
+ --text-muted: #64748b;
13
+ --primary: #3b82f6;
14
+ --danger: #ef4444;
15
+ --warning: #f59e0b;
16
+ --border: #e2e8f0;
17
+ scrollbar-gutter: stable;
18
+ }
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
21
+ line-height: 1.5;
22
+ color: var(--text-main);
23
+ background-color: #fff;
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+ header {
28
+ background-color: var(--bg-dark);
29
+ color: white;
30
+ padding: 40px 20px;
31
+ text-align: left;
32
+ }
33
+ .container {
34
+ max-width: 1200px;
35
+ margin: 0 auto;
36
+ padding: 0 20px;
37
+ }
38
+ header h1 { margin: 0; font-size: 24px; }
39
+ header .meta { margin-top: 10px; color: #94a3b8; font-size: 14px; }
40
+
41
+ .summary-grid {
42
+ display: grid;
43
+ grid-template-columns: repeat(5, 1fr);
44
+ border-bottom: 1px solid var(--border);
45
+ margin-bottom: 40px;
46
+ }
47
+ .summary-card {
48
+ padding: 30px 20px;
49
+ text-align: center;
50
+ border-right: 1px solid var(--border);
51
+ }
52
+ .summary-card:last-child { border-right: none; }
53
+ .summary-card h3 {
54
+ margin: 0;
55
+ font-size: 12px;
56
+ text-transform: uppercase;
57
+ color: var(--text-muted);
58
+ letter-spacing: 0.05em;
59
+ }
60
+ .summary-card p {
61
+ margin: 10px 0 0;
62
+ font-size: 32px;
63
+ font-weight: 700;
64
+ color: var(--text-main);
65
+ }
66
+ .summary-card.highlight p { color: var(--danger); }
67
+
68
+ details {
69
+ margin-bottom: 20px;
70
+ border: 1px solid var(--border);
71
+ border-radius: 8px;
72
+ overflow: hidden;
73
+ }
74
+ summary {
75
+ padding: 15px 20px;
76
+ background-color: #fff;
77
+ cursor: pointer;
78
+ font-weight: 600;
79
+ display: flex;
80
+ justify-content: space-between;
81
+ align-items: center;
82
+ list-style: none;
83
+ }
84
+ summary::-webkit-details-marker { display: none; }
85
+ summary::after {
86
+ content: '▶';
87
+ font-size: 12px;
88
+ color: var(--text-muted);
89
+ transition: transform 0.2s;
90
+ }
91
+ details[open] summary::after { transform: rotate(90deg); }
92
+
93
+ .category-section {
94
+ border: 1px solid var(--warning);
95
+ border-radius: 8px;
96
+ margin-bottom: 20px;
97
+ overflow: hidden;
98
+ }
99
+ .category-header {
100
+ padding: 15px 20px;
101
+ background-color: #fffbeb;
102
+ color: var(--warning);
103
+ font-weight: 600;
104
+ cursor: pointer;
105
+ display: flex;
106
+ justify-content: space-between;
107
+ align-items: center;
108
+ list-style: none;
109
+ }
110
+ .category-header::-webkit-details-marker { display: none; }
111
+ .category-header::after {
112
+ content: '▶';
113
+ font-size: 12px;
114
+ color: var(--warning);
115
+ transition: transform 0.2s;
116
+ }
117
+ .category-section[open] .category-header::after { transform: rotate(90deg); }
118
+
119
+ .category-content {
120
+ padding: 20px;
121
+ background-color: #fff;
122
+ }
123
+ .category-section.collapsed .category-content {
124
+ display: none;
125
+ }
126
+
127
+ .finding-group {
128
+ border: 1px solid var(--border);
129
+ border-radius: 8px;
130
+ padding: 20px;
131
+ margin-bottom: 20px;
132
+ }
133
+ .finding-header {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 10px;
137
+ margin-bottom: 10px;
138
+ }
139
+ .finding-header h4 { margin: 0; font-size: 16px; }
140
+ .badge {
141
+ background-color: var(--primary);
142
+ color: white;
143
+ padding: 2px 8px;
144
+ border-radius: 12px;
145
+ font-size: 12px;
146
+ }
147
+ .finding-description {
148
+ color: var(--text-muted);
149
+ font-size: 14px;
150
+ margin-bottom: 20px;
151
+ }
152
+
153
+ .url-list {
154
+ background-color: var(--bg-light);
155
+ border-radius: 4px;
156
+ padding: 15px;
157
+ margin-bottom: 15px;
158
+ }
159
+ .url-item {
160
+ font-family: monospace;
161
+ font-size: 13px;
162
+ padding: 8px 12px;
163
+ background: white;
164
+ border: 1px solid var(--border);
165
+ border-radius: 4px;
166
+ margin-bottom: 8px;
167
+ white-space: nowrap;
168
+ overflow: hidden;
169
+ text-overflow: ellipsis;
170
+ }
171
+ .url-item:last-child { margin-bottom: 0; }
172
+
173
+ .more-count {
174
+ font-size: 12px;
175
+ color: var(--text-muted);
176
+ font-style: italic;
177
+ margin-bottom: 15px;
178
+ }
179
+
180
+ .details-content {
181
+ padding: 20px;
182
+ background: var(--bg-light);
183
+ }
184
+
185
+ .meta-text {
186
+ color: var(--text-muted);
187
+ font-size: 11px;
188
+ }
189
+
190
+ .warning-text {
191
+ color: var(--danger);
192
+ font-size: 11px;
193
+ font-weight: bold;
194
+ }
195
+
196
+ .btn {
197
+ display: inline-flex;
198
+ align-items: center;
199
+ gap: 8px;
200
+ background-color: var(--primary);
201
+ color: white;
202
+ padding: 8px 16px;
203
+ border-radius: 6px;
204
+ text-decoration: none;
205
+ font-size: 13px;
206
+ font-weight: 500;
207
+ border: none;
208
+ cursor: pointer;
209
+ }
210
+ .btn:hover { opacity: 0.9; }
211
+
212
+ footer {
213
+ text-align: center;
214
+ padding: 40px;
215
+ color: var(--text-muted);
216
+ font-size: 12px;
217
+ border-top: 1px solid var(--border);
218
+ margin-top: 40px;
219
+ }
220
+ </style>
221
+ </head>
222
+ <body>
223
+ {{> header}}
224
+
225
+ {{> summary}}
226
+
227
+ <div class="container">
228
+ <details>
229
+ <summary>Sitemaps Discovered ({{discoveredSitemaps.length}})</summary>
230
+ <div class="details-content">
231
+ {{#each discoveredSitemaps}}
232
+ <div class="url-item">{{this}}</div>
233
+ {{/each}}
234
+ </div>
235
+ </details>
236
+
237
+ {{#if ignoredUrls.length}}
238
+ <details>
239
+ <summary>Ignored URLs ({{ignoredUrls.length}})</summary>
240
+ <div class="details-content">
241
+ {{#each ignoredUrls}}
242
+ <div class="url-item" title="Ignored by: {{ignoredBy}}">
243
+ {{loc}}
244
+ <span class="meta-text">(by {{ignoredBy}})</span>
245
+ {{#if suppressedCategories}}
246
+ <span class="warning-text">[Suppressed Risks: {{suppressedCategories}}]</span>
247
+ {{/if}}
248
+ </div>
249
+ {{/each}}
250
+ </div>
251
+ </details>
252
+ {{/if}}
253
+
254
+ {{#each categories}}
255
+ <details class="category-section" open>
256
+ <summary class="category-header">
257
+ <span>{{name}} ({{totalUrls}} URLs)</span>
258
+ </summary>
259
+ <div class="category-content">
260
+ {{#each findings}}
261
+ {{> finding}}
262
+ {{/each}}
263
+ </div>
264
+ </details>
265
+ {{/each}}
266
+ </div>
267
+
268
+ <footer>
269
+ Generated by sitemap-qa v1.0.0
270
+ </footer>
271
+
272
+ <script>
273
+ function downloadUrls(name, urls) {
274
+ const blob = new Blob([urls.join('\n')], { type: 'text/plain' });
275
+ const url = window.URL.createObjectURL(blob);
276
+ const a = document.createElement('a');
277
+ a.href = url;
278
+ a.download = `${name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_urls.txt`;
279
+ document.body.appendChild(a);
280
+ a.click();
281
+ window.URL.revokeObjectURL(url);
282
+ document.body.removeChild(a);
283
+ }
284
+
285
+ function toggleCategory(headerElement) {
286
+ const categorySection = headerElement.closest('.category-section');
287
+ if (categorySection) {
288
+ categorySection.classList.toggle('collapsed');
289
+ }
290
+ }
291
+ </script>
292
+ </body>
293
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akotliar/sitemap-qa",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "Detect test/qa/dev/staging URLs, admin paths, sensitive parameters, and URLs that shouldn't be publicly indexed.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,6 +49,7 @@
49
49
  "chalk": "^5.6.2",
50
50
  "commander": "^14.0.2",
51
51
  "fast-xml-parser": "^5.3.3",
52
+ "handlebars": "^4.7.8",
52
53
  "js-yaml": "^4.1.1",
53
54
  "micromatch": "^4.0.8",
54
55
  "undici": "^7.16.0",
@@ -56,9 +57,11 @@
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/cli-progress": "^3.11.6",
60
+ "@types/handlebars": "^4.0.40",
59
61
  "@types/js-yaml": "^4.0.9",
60
62
  "@types/micromatch": "^4.0.10",
61
63
  "@types/node": "^25.0.3",
64
+ "@vitest/coverage-v8": "^4.0.16",
62
65
  "tsup": "^8.5.1",
63
66
  "tsx": "^4.21.0",
64
67
  "typescript": "^5.9.3",