@akotliar/sitemap-qa 1.0.0-alpha.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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/index.cjs +484 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +484 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/analyze.ts","../src/config/config-loader.ts","../src/types/config.ts","../src/errors/network-errors.ts","../src/utils/http-client.ts","../src/core/discovery.ts","../src/core/parser.ts","../src/core/extractor.ts","../src/core/consolidator.ts","../src/core/patterns/risk-patterns.ts","../src/core/patterns/domain-patterns.ts","../src/core/patterns/admin-patterns.ts","../src/utils/sanitizer.ts","../src/core/risk-grouper.ts","../src/core/risk-detector.ts","../src/summarizer.ts","../src/reporters/json-reporter.ts","../src/reporters/html-reporter.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport 'dotenv/config';\r\nimport { Command } from 'commander';\r\nimport { analyzeCommand } from '@/commands/analyze';\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\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 { promises as fs } from 'fs';\r\nimport ora from 'ora';\r\nimport chalk from 'chalk';\r\nimport { loadConfig } from '@/config/config-loader';\r\nimport { discoverSitemaps } from '@/core/discovery';\r\nimport { extractAllUrls } from '@/core/extractor';\r\nimport { consolidateUrls } from '@/core/consolidator';\r\nimport { detectRisks } from '@/core/risk-detector';\r\nimport { groupRiskFindings } from '@/core/risk-grouper';\r\nimport { summarizeRisks } from '@/summarizer';\r\nimport { generateJsonReport } from '@/reporters/json-reporter';\r\nimport { writeHtmlReport } from '@/reporters/html-reporter';\r\nimport type { Config } from '@/types/config';\r\nimport type { DiscoveryResult } from '@/core/discovery';\r\nimport type { RiskSummary } from '@/summarizer';\r\nimport type { RiskGroup } from '@/core/risk-grouper';\r\n\r\ninterface AnalyzeOptions {\r\n timeout: string;\r\n progress: boolean;\r\n output: 'json' | 'html';\r\n outputDir?: string;\r\n outputFile?: string;\r\n color: boolean;\r\n verbose: boolean;\r\n acceptedPatterns?: string;\r\n}\r\n\r\ninterface AnalysisPipelineResult {\r\n discoveryResult: DiscoveryResult;\r\n totalUrls: number;\r\n riskGroups: RiskGroup[];\r\n summary: RiskSummary;\r\n errors: Error[];\r\n executionTime: number;\r\n}\r\n\r\nexport const analyzeCommand = new Command('analyze')\r\n .description('Analyze sitemap for QA issues')\r\n .argument('<url>', 'Base URL to analyze')\r\n .option('--timeout <seconds>', 'HTTP timeout in seconds', '30')\r\n .option('--no-progress', 'Disable progress bar')\r\n .option('--output <format>', 'Output format: html or json', 'html')\r\n .option('--output-dir <path>', 'Output directory for reports')\r\n .option('--output-file <path>', 'Custom output filename')\r\n .option('--accepted-patterns <patterns>', 'Comma-separated regex patterns to exclude from risk detection')\r\n .option('--no-color', 'Disable ANSI color codes in CLI output')\r\n .option('--verbose', 'Enable verbose logging', false)\r\n .action(async (url: string, options: AnalyzeOptions) => {\r\n let config: Config | undefined;\r\n \r\n try {\r\n // Validate options\r\n validateAnalyzeOptions(options);\r\n \r\n // Load configuration with hierarchy\r\n const loadedConfig = await loadConfig({\r\n ...options,\r\n baseUrl: url,\r\n outputFormat: options.output,\r\n });\r\n config = loadedConfig;\r\n \r\n console.log(`\\nš Analyzing ${url}...\\n`);\r\n \r\n // Run analysis pipeline\r\n const result = await runAnalysisPipeline(url, config);\r\n \r\n // Show simple CLI summary\r\n showCliSummary(result);\r\n \r\n // Create output directory\r\n await fs.mkdir(config.outputDir, { recursive: true });\r\n \r\n // Always generate HTML report\r\n const htmlFileName = options.outputFile || `sitemap-qa-report-${Date.now()}.html`;\r\n const htmlFilePath = `${config.outputDir}/${htmlFileName}`;\r\n await writeHtmlReport(\r\n result.summary,\r\n result.discoveryResult,\r\n result.totalUrls,\r\n config,\r\n htmlFilePath,\r\n result.errors,\r\n { maxUrlsPerGroup: 10 }\r\n );\r\n console.log(`\\nš Full report saved to: ${chalk.cyan(htmlFilePath)}`);\r\n \r\n // Generate JSON if requested\r\n if (options.output === 'json') {\r\n const jsonFileName = htmlFileName.replace(/\\.html$/, '.json');\r\n const jsonFilePath = `${config.outputDir}/${jsonFileName}`;\r\n const jsonReport = generateJsonReport(\r\n result.summary,\r\n result.discoveryResult,\r\n { totalCount: result.totalUrls, uniqueUrls: [], errors: [] },\r\n result.riskGroups,\r\n config,\r\n result.executionTime,\r\n { pretty: true, indent: 2 }\r\n );\r\n await fs.writeFile(jsonFilePath, jsonReport, 'utf-8');\r\n console.log(`š JSON report saved to: ${chalk.cyan(jsonFilePath)}`);\r\n }\r\n \r\n // Exit with appropriate code\r\n const exitCode = determineExitCode(result);\r\n process.exit(exitCode);\r\n \r\n } catch (error) {\r\n handleAnalysisError(error, config!);\r\n process.exit(2);\r\n }\r\n });\r\n\r\n/**\r\n * Validate analyze command options\r\n */\r\nfunction validateAnalyzeOptions(options: AnalyzeOptions): void {\r\n // Validate output format\r\n const validFormats = ['json', 'html'];\r\n if (!validFormats.includes(options.output)) {\r\n throw new Error(\r\n `Invalid output format: ${options.output}. Must be one of: ${validFormats.join(', ')}`\r\n );\r\n }\r\n \r\n // Validate timeout\r\n const timeout = parseInt(options.timeout);\r\n if (isNaN(timeout) || timeout <= 0) {\r\n throw new Error(`Invalid timeout: ${options.timeout}. Must be a positive number.`);\r\n }\r\n}\r\n\r\n/**\r\n * Show simple CLI summary\r\n */\r\nfunction showCliSummary(result: AnalysisPipelineResult): void {\r\n console.log('');\r\n const riskyUrlCount = result.summary.categoryInsights.reduce((sum, g) => sum + g.count, 0);\r\n \r\n if (riskyUrlCount === 0) {\r\n console.log(chalk.green('ā
No issues found - sitemap looks clean!'));\r\n } else {\r\n console.log(chalk.yellow(`ā ļø Found ${riskyUrlCount} potentially risky URL(s)`));\r\n console.log('');\r\n \r\n // Show breakdown by severity\r\n const { high, medium, low } = result.summary.severityBreakdown;\r\n if (high > 0) {\r\n console.log(chalk.red(` šØ High severity: ${high} URLs`));\r\n }\r\n if (medium > 0) {\r\n console.log(chalk.yellow(` ā ļø Medium severity: ${medium} URLs`));\r\n }\r\n if (low > 0) {\r\n console.log(chalk.blue(` ā¹ļø Low severity: ${low} URLs`));\r\n }\r\n }\r\n console.log('');\r\n}\r\n\r\n/**\r\n * Run complete analysis pipeline (5 phases)\r\n */\r\nasync function runAnalysisPipeline(\r\n url: string,\r\n config: Config\r\n): Promise<AnalysisPipelineResult> {\r\n const startTime = Date.now();\r\n const errors: Error[] = [];\r\n \r\n // Phase 1: Discovery\r\n const discoverySpinner = ora('Discovering sitemaps...').start();\r\n const discoveryResult = await discoverSitemaps(url, config);\r\n discoverySpinner.succeed(`Found ${discoveryResult.sitemaps.length} sitemap(s)`);\r\n \r\n // Check for access issues\r\n if (discoveryResult.accessIssues.length > 0) {\r\n console.warn(`ā ļø Warning: ${discoveryResult.accessIssues.length} sitemap(s) are access-blocked`);\r\n for (const issue of discoveryResult.accessIssues) {\r\n errors.push(new Error(`Access blocked: ${issue.url} (${issue.statusCode})`));\r\n }\r\n }\r\n \r\n if (discoveryResult.sitemaps.length === 0) {\r\n throw new Error(`No sitemaps found at ${url}. Tried: /sitemap.xml, /sitemap_index.xml, /robots.txt`);\r\n }\r\n \r\n // Phase 2: Parsing & Extraction\r\n const extractionSpinner = ora('Parsing sitemaps...').start();\r\n const extractionResult = await extractAllUrls(discoveryResult.sitemaps, config);\r\n extractionSpinner.succeed(`Extracted ${extractionResult.allUrls.length.toLocaleString()} URLs`);\r\n \r\n // Collect errors without displaying them (they'll be in the report)\r\n if (extractionResult.errors.length > 0) {\r\n for (const err of extractionResult.errors) {\r\n if (typeof err === 'string') {\r\n errors.push(new Error(err));\r\n } else {\r\n errors.push(err);\r\n }\r\n }\r\n }\r\n \r\n if (extractionResult.allUrls.length === 0) {\r\n throw new Error('No URLs extracted from sitemaps');\r\n }\r\n \r\n // Phase 3: Consolidation & Deduplication\r\n const consolidationSpinner = ora('Removing duplicates...').start();\r\n const consolidatedResult = consolidateUrls(extractionResult.allUrls);\r\n const duplicatesRemoved = extractionResult.allUrls.length - consolidatedResult.uniqueUrls.length;\r\n if (duplicatesRemoved > 0) {\r\n consolidationSpinner.succeed(`${consolidatedResult.uniqueUrls.length.toLocaleString()} unique URLs (removed ${duplicatesRemoved.toLocaleString()} duplicates)`);\r\n } else {\r\n consolidationSpinner.succeed(`${consolidatedResult.uniqueUrls.length.toLocaleString()} unique URLs`);\r\n }\r\n \r\n // Phase 4: Risk Detection\r\n const riskSpinner = ora('Analyzing for risks...').start();\r\n const riskResult = await detectRisks(consolidatedResult.uniqueUrls, url, config);\r\n const riskGroups = groupRiskFindings(riskResult.findings);\r\n \r\n const totalRiskyUrls = riskGroups.groups.reduce((sum, g) => sum + g.count, 0);\r\n if (totalRiskyUrls > 0) {\r\n riskSpinner.warn(`Found ${totalRiskyUrls} risky URL(s)`);\r\n } else {\r\n riskSpinner.succeed('No risks detected');\r\n }\r\n \r\n \r\n // Phase 5: Generate Summary\r\n const executionTime = Date.now() - startTime;\r\n const summarySpinner = ora('Generating report...').start();\r\n const summary = summarizeRisks({\r\n riskGroups: riskGroups.groups,\r\n totalUrls: consolidatedResult.uniqueUrls.length,\r\n sitemapUrl: url,\r\n processingTime: executionTime,\r\n });\r\n summarySpinner.succeed('Analysis complete');\r\n \r\n return {\r\n discoveryResult,\r\n totalUrls: consolidatedResult.uniqueUrls.length,\r\n riskGroups: riskGroups.groups,\r\n summary,\r\n errors,\r\n executionTime,\r\n };\r\n}\r\n\r\n/**\r\n * Determine exit code based on analysis results\r\n */\r\nfunction determineExitCode(result: AnalysisPipelineResult): number {\r\n // Exit code 0: Success with no high-severity issues\r\n // Exit code 1: High-severity issues found\r\n // Exit code 2: Analysis failed with errors (handled in catch block)\r\n \r\n const highSeverityCount = result.summary.severityBreakdown.high;\r\n \r\n if (highSeverityCount > 0) {\r\n return 1; // High-severity issues found\r\n }\r\n \r\n return 0; // Success\r\n}\r\n\r\n/**\r\n * Handle analysis errors with user-friendly messages\r\n */\r\nfunction handleAnalysisError(error: unknown, config?: Config): void {\r\n console.error('\\nā Analysis failed\\n');\r\n \r\n if (error instanceof Error) {\r\n console.error(`Error: ${error.message}`);\r\n \r\n if (config?.verbose && error.stack) {\r\n console.error('\\nStack trace:');\r\n console.error(error.stack);\r\n }\r\n \r\n // Provide helpful suggestions based on error message\r\n if (error.message.includes('No sitemaps found')) {\r\n console.error('\\nSuggestions:');\r\n console.error(' ⢠Verify the base URL is correct');\r\n console.error(' ⢠Check if the site has a sitemap');\r\n console.error(' ⢠Ensure the sitemap is publicly accessible');\r\n } else if (error.message.includes('Network') || error.message.includes('timeout')) {\r\n console.error('\\nSuggestions:');\r\n console.error(' ⢠Check your internet connection');\r\n console.error(' ⢠Verify the URL is accessible');\r\n console.error(' ⢠Try increasing the timeout with --timeout option');\r\n }\r\n } else {\r\n console.error('Unknown error occurred');\r\n console.error(String(error));\r\n }\r\n}\r\n","import { readFile } from 'fs/promises';\r\nimport { existsSync } from 'fs';\r\nimport { join } from 'path';\r\nimport { homedir } from 'os';\r\nimport { DEFAULT_CONFIG, type Config } from '@/types/config';\r\n\r\nexport async function loadConfig(cliOptions: Record<string, any>): Promise<Config> {\r\n // Start with defaults\r\n let config: Partial<Config> = { ...DEFAULT_CONFIG };\r\n \r\n // Layer 4: Global config (~/.sitemap-qa/config.json)\r\n const globalConfigPath = join(homedir(), '.sitemap-qa', 'config.json');\r\n if (existsSync(globalConfigPath)) {\r\n try {\r\n const globalConfig = JSON.parse(await readFile(globalConfigPath, 'utf-8'));\r\n config = { ...config, ...globalConfig };\r\n } catch (error) {\r\n console.warn(`Warning: Failed to load global config: ${error}`);\r\n }\r\n }\r\n \r\n // Layer 3: Project config (.sitemap-qa.config.json)\r\n const projectConfigPath = join(process.cwd(), '.sitemap-qa.config.json');\r\n if (existsSync(projectConfigPath)) {\r\n try {\r\n const projectConfig = JSON.parse(await readFile(projectConfigPath, 'utf-8'));\r\n config = { ...config, ...projectConfig };\r\n } catch (error) {\r\n console.warn(`Warning: Failed to load project config: ${error}`);\r\n }\r\n }\r\n \r\n // Layer 2: Environment variables\r\n const envConfig = loadFromEnv();\r\n config = { ...config, ...envConfig };\r\n \r\n // Layer 1: CLI options (highest priority)\r\n config = mergeCliOptions(config, cliOptions);\r\n \r\n // Add baseUrl from cliOptions\r\n if (cliOptions.baseUrl) {\r\n config.baseUrl = cliOptions.baseUrl;\r\n }\r\n \r\n // Validate final config\r\n validateConfig(config as Config);\r\n \r\n return config as Config;\r\n}\r\n\r\nfunction loadFromEnv(): Partial<Config> {\r\n const env: Partial<Config> = {};\r\n \r\n if (process.env.SITEMAP_VERIFY_TIMEOUT) {\r\n env.timeout = parseInt(process.env.SITEMAP_VERIFY_TIMEOUT, 10);\r\n }\r\n \r\n return env;\r\n}\r\n\r\nfunction mergeCliOptions(config: Partial<Config>, cliOptions: Record<string, any>): Partial<Config> {\r\n const merged = { ...config };\r\n \r\n // Only override if explicitly set (not default string values)\r\n if (cliOptions.timeout && cliOptions.timeout !== '30') {\r\n merged.timeout = parseInt(cliOptions.timeout, 10);\r\n }\r\n \r\n if (cliOptions.output) {\r\n merged.outputFormat = cliOptions.output as 'json' | 'html';\r\n }\r\n \r\n if (cliOptions.outputDir) {\r\n merged.outputDir = cliOptions.outputDir;\r\n }\r\n \r\n if (cliOptions.verbose === true) {\r\n merged.verbose = true;\r\n }\r\n \r\n if (cliOptions.acceptedPatterns) {\r\n // Parse comma-separated patterns\r\n merged.acceptedPatterns = cliOptions.acceptedPatterns.split(',').map((p: string) => p.trim()).filter(Boolean);\r\n }\r\n \r\n return merged;\r\n}\r\n\r\nfunction validateConfig(config: Config): void {\r\n if (config.timeout < 1 || config.timeout > 300) {\r\n throw new Error('Timeout must be between 1 and 300 seconds');\r\n }\r\n \r\n if (!['json', 'html'].includes(config.outputFormat)) {\r\n throw new Error('Output format must be json or html');\r\n }\r\n}\r\n","export interface Config {\r\n // Network settings\r\n timeout: number; // HTTP timeout in seconds\r\n concurrency: number; // Concurrent sitemap fetches\r\n \r\n // Output settings\r\n outputFormat: 'json' | 'html';\r\n outputDir: string; // Output directory for reports\r\n verbose: boolean; // Verbose logging\r\n \r\n // Base URL for analysis\r\n baseUrl: string;\r\n \r\n // Risk detection settings\r\n acceptedPatterns?: string[]; // URL patterns to exclude from risk detection (regex strings)\r\n}\r\n\r\nexport const DEFAULT_CONFIG: Config = {\r\n timeout: 30,\r\n concurrency: 10,\r\n outputFormat: 'html',\r\n outputDir: './sitemap-qa/report',\r\n verbose: false,\r\n baseUrl: 'https://example.com', // Default for tests\r\n acceptedPatterns: [],\r\n};\r\n","export class NetworkError extends Error {\r\n readonly code = 'NETWORK_ERROR';\r\n \r\n constructor(\r\n public readonly url: string,\r\n public readonly originalError: Error\r\n ) {\r\n super(`Network request failed for ${url}: ${originalError.message}`);\r\n this.name = 'NetworkError';\r\n }\r\n}\r\n\r\nexport class HttpError extends Error {\r\n readonly code = 'HTTP_ERROR';\r\n \r\n constructor(\r\n public readonly url: string,\r\n public readonly statusCode: number,\r\n public readonly statusText?: string\r\n ) {\r\n let message = `HTTP ${statusCode} error for ${url}`;\r\n \r\n // Add helpful context for common blocking scenarios\r\n if (statusCode === 403) {\r\n message += '\\n Note: 403 Forbidden often indicates bot protection (Cloudflare, etc.) or access restrictions';\r\n }\r\n \r\n super(message);\r\n this.name = 'HttpError';\r\n }\r\n}\r\n","import { NetworkError, HttpError } from '@/errors/network-errors';\r\nimport { chromium } from 'playwright';\r\n\r\n\r\nexport interface FetchOptions {\r\n timeout?: number; // Timeout in seconds\r\n maxRetries?: number; // Maximum retry attempts (default: 3)\r\n retryDelay?: number; // Initial retry delay in ms (default: 1000)\r\n useBrowser?: boolean; // Force use of headless browser (default: auto-detect on 403)\r\n}\r\n\r\nexport interface FetchResult {\r\n content: string; // Response body as text\r\n statusCode: number; // HTTP status code\r\n url: string; // Final URL after redirects\r\n}\r\n\r\n/**\r\n * Fetch URL using headless browser (Playwright) to bypass bot protection\r\n */\r\nasync function fetchUrlWithBrowser(\r\n url: string,\r\n timeout: number\r\n): Promise<FetchResult> {\r\n let browser;\r\n try {\r\n browser = await chromium.launch({ \r\n headless: true,\r\n args: [\r\n '--disable-blink-features=AutomationControlled', // Hide automation flags\r\n '--disable-dev-shm-usage',\r\n '--no-sandbox'\r\n ]\r\n });\r\n \r\n const context = await browser.newContext({\r\n userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\r\n viewport: { width: 1920, height: 1080 },\r\n locale: 'en-US',\r\n timezoneId: 'America/New_York',\r\n extraHTTPHeaders: {\r\n 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',\r\n 'Accept-Language': 'en-US,en;q=0.9',\r\n 'Accept-Encoding': 'gzip, deflate, br',\r\n 'DNT': '1',\r\n 'Connection': 'keep-alive',\r\n 'Upgrade-Insecure-Requests': '1'\r\n }\r\n });\r\n \r\n const page = await context.newPage();\r\n \r\n // Add script to mask automation\r\n await page.addInitScript(() => {\r\n // Override the navigator.webdriver property\r\n Object.defineProperty(navigator, 'webdriver', {\r\n get: () => false,\r\n });\r\n \r\n // Mock Chrome runtime\r\n (window as any).chrome = {\r\n runtime: {},\r\n };\r\n \r\n // Mock permissions\r\n const originalQuery = window.navigator.permissions.query;\r\n window.navigator.permissions.query = (parameters: any) =>\r\n parameters.name === 'notifications'\r\n ? Promise.resolve({ state: Notification.permission } as PermissionStatus)\r\n : originalQuery(parameters);\r\n });\r\n \r\n // Set timeout for page navigation\r\n page.setDefaultTimeout(timeout * 1000);\r\n \r\n // Navigate and wait for content to load\r\n const response = await page.goto(url, { \r\n waitUntil: 'domcontentloaded', // Changed from networkidle - faster for simple XML\r\n timeout: timeout * 1000 \r\n });\r\n \r\n if (!response) {\r\n throw new Error('No response received from page');\r\n }\r\n \r\n const statusCode = response.status();\r\n \r\n // For XML sitemaps, get the raw content\r\n const content = await page.content();\r\n const finalUrl = page.url();\r\n \r\n await browser.close();\r\n \r\n if (statusCode >= 200 && statusCode < 300) {\r\n return {\r\n content,\r\n statusCode,\r\n url: finalUrl\r\n };\r\n }\r\n \r\n throw new HttpError(finalUrl, statusCode);\r\n \r\n } catch (error: any) {\r\n if (browser) {\r\n await browser.close();\r\n }\r\n \r\n if (error.code === 'HTTP_ERROR') {\r\n throw error;\r\n }\r\n \r\n throw new NetworkError(url, error);\r\n }\r\n}\r\n\r\nexport async function fetchUrl(\r\n url: string,\r\n options: FetchOptions = {}\r\n): Promise<FetchResult> {\r\n const {\r\n timeout = 30,\r\n maxRetries = 3,\r\n retryDelay = 1000,\r\n useBrowser = false\r\n } = options;\r\n\r\n // Validate URL before starting retry loop\r\n new URL(url); // Throws TypeError if invalid\r\n\r\n const retryableStatuses = [408, 429, 500, 502, 503, 504];\r\n \r\n let lastError: Error | null = null;\r\n let attemptedBrowser = false;\r\n \r\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\r\n try {\r\n // If forced to use browser or if we previously got 403, use browser\r\n if (useBrowser || attemptedBrowser) {\r\n return await fetchUrlWithBrowser(url, timeout);\r\n }\r\n \r\n // Use fetch with automatic redirect following\r\n const controller = new AbortController();\r\n const timeoutId = setTimeout(() => controller.abort(), timeout * 1000);\r\n \r\n const response = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n 'User-Agent': 'sitemap-qa/1.0.0',\r\n 'Accept': 'text/xml,application/xml,text/plain,*/*'\r\n },\r\n signal: controller.signal,\r\n redirect: 'follow' // Follow redirects automatically (default behavior)\r\n });\r\n \r\n clearTimeout(timeoutId);\r\n \r\n const statusCode = response.status;\r\n const body = await response.text();\r\n \r\n // Success - return result\r\n if (statusCode >= 200 && statusCode < 300) {\r\n return {\r\n content: body,\r\n statusCode: statusCode,\r\n url: response.url // Final URL after redirects\r\n };\r\n }\r\n \r\n // If we get 403, try with browser on next attempt\r\n if (statusCode === 403 && !attemptedBrowser) {\r\n attemptedBrowser = true;\r\n // Silently retry with headless browser\r\n continue; // Retry immediately with browser\r\n }\r\n \r\n // Non-retryable error - throw immediately\r\n if (!retryableStatuses.includes(statusCode)) {\r\n throw new HttpError(response.url, statusCode);\r\n }\r\n \r\n // Retryable error - continue to next attempt\r\n lastError = new HttpError(response.url, statusCode);\r\n \r\n } catch (error: any) {\r\n // Already formatted HttpError - rethrow if non-retryable\r\n if (error.code === 'HTTP_ERROR') {\r\n const httpError = error as HttpError;\r\n if (!retryableStatuses.includes(httpError.statusCode)) {\r\n throw error;\r\n }\r\n lastError = error;\r\n } else {\r\n // Network error (connection failed, timeout, DNS error, etc.)\r\n lastError = new NetworkError(url, error);\r\n }\r\n \r\n // Don't retry on last attempt\r\n if (attempt === maxRetries) break;\r\n }\r\n \r\n // Wait before retry (exponential backoff)\r\n if (attempt < maxRetries) {\r\n const delay = retryDelay * Math.pow(2, attempt);\r\n await new Promise(resolve => setTimeout(resolve, delay));\r\n }\r\n }\r\n \r\n // All retries exhausted - throw last error\r\n throw lastError!;\r\n}\r\n","import { Config } from '@/types/config';\r\nimport { fetchUrl } from '@/utils/http-client';\r\nimport { HttpError, NetworkError } from '@/errors/network-errors';\r\n\r\nexport interface SitemapAccessIssue {\r\n url: string;\r\n statusCode: number;\r\n error: string;\r\n}\r\n\r\nexport interface DiscoveryResult {\r\n sitemaps: string[];\r\n source: 'standard-path' | 'robots-txt' | 'none';\r\n accessIssues: SitemapAccessIssue[]; // Track sitemaps that exist but are inaccessible\r\n canonicalDomain?: string; \r\n}\r\n\r\nfunction normalizeBaseUrl(url: string): string {\r\n const parsed = new URL(url);\r\n return parsed.origin;\r\n}\r\n\r\n/**\r\n * Detect the canonical domain by checking which version (www vs non-www) is final.\r\n * Makes a HEAD request and follows redirects to determine the canonical version.\r\n */\r\nasync function detectCanonicalDomain(baseUrl: string, config: Config): Promise<string> {\r\n const urlObj = new URL(baseUrl);\r\n const hasWww = urlObj.hostname.startsWith('www.');\r\n \r\n // Try the opposite version to see which one works\r\n const alternateHostname = hasWww \r\n ? urlObj.hostname.substring(4) // Remove 'www.'\r\n : `www.${urlObj.hostname}`; // Add 'www.'\r\n \r\n const alternateUrl = `${urlObj.protocol}//${alternateHostname}/robots.txt`;\r\n \r\n try {\r\n // Try to fetch robots.txt from the alternate version\r\n const result = await fetchUrl(alternateUrl, {\r\n timeout: config.timeout,\r\n maxRetries: 1\r\n });\r\n \r\n // If alternate version succeeds (200 or 404), it's accessible - use it as canonical\r\n if (result.statusCode === 200 || result.statusCode === 404) {\r\n return alternateHostname;\r\n }\r\n \r\n // Otherwise, current version is probably canonical\r\n return urlObj.hostname;\r\n \r\n } catch (error) {\r\n if (error instanceof HttpError && error.statusCode === 301) {\r\n // Alternate redirects back, current version is canonical\r\n return urlObj.hostname;\r\n }\r\n \r\n // If alternate fails, current version is canonical\r\n return urlObj.hostname;\r\n }\r\n}\r\n\r\nasync function tryStandardPaths(\r\n baseUrl: string,\r\n config: Config\r\n): Promise<{ sitemaps: string[]; issues: SitemapAccessIssue[]; redirectedToCanonical?: string }> {\r\n const baseDomain = new URL(baseUrl).origin;\r\n const accessIssues: SitemapAccessIssue[] = [];\r\n \r\n const standardPaths = [\r\n '/sitemap.xml',\r\n '/sitemap_index.xml',\r\n '/sitemap-index.xml'\r\n ];\r\n \r\n // Try all standard paths concurrently\r\n const results = await Promise.allSettled(\r\n standardPaths.map(async (path) => {\r\n const sitemapUrl = `${baseDomain}${path}`;\r\n \r\n try {\r\n const result = await fetchUrl(sitemapUrl, { \r\n timeout: config.timeout,\r\n maxRetries: 1\r\n });\r\n \r\n if (result.statusCode === 200) {\r\n if (config.verbose) {\r\n console.log(`ā Found sitemap at: ${sitemapUrl}`);\r\n }\r\n return { found: true, url: sitemapUrl };\r\n }\r\n return { found: false };\r\n } catch (error) {\r\n if (error instanceof HttpError) {\r\n // Track 401/403 as access issues\r\n if (error.statusCode === 401 || error.statusCode === 403) {\r\n accessIssues.push({\r\n url: sitemapUrl,\r\n statusCode: error.statusCode,\r\n error: error.statusCode === 401 ? 'Unauthorized' : 'Access Denied'\r\n });\r\n \r\n if (config.verbose) {\r\n console.log(`ā Access denied: ${sitemapUrl} (${error.statusCode})`);\r\n }\r\n } else if (config.verbose) {\r\n console.log(`ā Not found: ${sitemapUrl} (${error.statusCode})`);\r\n }\r\n } else if (config.verbose) {\r\n console.log(`ā Not found: ${sitemapUrl}`);\r\n }\r\n return { found: false };\r\n }\r\n })\r\n );\r\n \r\n // Find the first successful result\r\n for (const result of results) {\r\n if (result.status === 'fulfilled' && result.value.found) {\r\n return { sitemaps: [result.value.url!], issues: accessIssues };\r\n }\r\n }\r\n \r\n if (config.verbose) {\r\n console.log('No sitemap found at standard paths');\r\n }\r\n \r\n return { sitemaps: [], issues: accessIssues };\r\n}\r\n\r\nasync function parseRobotsTxt(\r\n baseUrl: string,\r\n config: Config\r\n): Promise<string[]> {\r\n const robotsUrl = `${new URL(baseUrl).origin}/robots.txt`;\r\n \r\n try {\r\n const result = await fetchUrl(robotsUrl, {\r\n timeout: config.timeout,\r\n maxRetries: 1\r\n });\r\n \r\n const lines = result.content.split('\\n');\r\n const sitemaps: string[] = [];\r\n \r\n for (const line of lines) {\r\n const match = line.match(/^Sitemap:\\s*(.+)$/i);\r\n if (match) {\r\n const sitemapUrl = match[1].trim();\r\n \r\n try {\r\n new URL(sitemapUrl);\r\n sitemaps.push(sitemapUrl);\r\n } catch {\r\n if (config.verbose) {\r\n console.warn(`Invalid sitemap URL in robots.txt: ${sitemapUrl}`);\r\n }\r\n }\r\n }\r\n }\r\n \r\n if (config.verbose && sitemaps.length > 0) {\r\n console.log(`Found ${sitemaps.length} sitemap(s) in robots.txt`);\r\n }\r\n \r\n return sitemaps;\r\n \r\n } catch (error) {\r\n if (config.verbose) {\r\n console.log(`No robots.txt found at ${robotsUrl}`);\r\n }\r\n return [];\r\n }\r\n}\r\n\r\nfunction isSitemapIndex(xmlContent: string): boolean {\r\n // Check for proper sitemapindex format\r\n if (xmlContent.includes('<sitemapindex')) {\r\n return true;\r\n }\r\n \r\n // Check for malformed format: urlset containing sitemap URLs\r\n // This is a heuristic - if we see multiple URLs ending in .xml or containing 'sitemap'\r\n // within the first few URL entries, it's likely a malformed sitemap index\r\n if (xmlContent.includes('<urlset')) {\r\n const urlBlockRegex = /<url[^>]*>.*?<loc>([^<]+)<\\/loc>.*?<\\/url>/gs;\r\n const matches = Array.from(xmlContent.matchAll(urlBlockRegex));\r\n \r\n // Check first 5 URLs (or all if less than 5)\r\n const samplesToCheck = Math.min(5, matches.length);\r\n let sitemapLikeCount = 0;\r\n \r\n for (let i = 0; i < samplesToCheck; i++) {\r\n const url = matches[i][1].trim().toLowerCase();\r\n if (url.includes('sitemap') || url.endsWith('.xml')) {\r\n sitemapLikeCount++;\r\n }\r\n }\r\n \r\n // If majority of sampled URLs look like sitemaps, treat as malformed index\r\n return sitemapLikeCount > samplesToCheck / 2;\r\n }\r\n \r\n return false;\r\n}\r\n\r\n/**\r\n * Extracts sitemap URLs from a sitemap index file.\r\n * Handles both:\r\n * 1. Proper format: <sitemapindex><sitemap><loc>...</loc></sitemap></sitemapindex>\r\n * 2. Malformed format: <urlset><url><loc>sitemap.xml</loc></url></urlset>\r\n */\r\nfunction extractSitemapIndexUrls(xmlContent: string): string[] {\r\n const urls: string[] = [];\r\n \r\n // For proper sitemapindex format: extract from <sitemap> blocks only\r\n if (xmlContent.includes('<sitemapindex')) {\r\n const sitemapBlockRegex = /<sitemap[^>]*>(.*?)<\\/sitemap>/gs;\r\n let sitemapMatch;\r\n \r\n while ((sitemapMatch = sitemapBlockRegex.exec(xmlContent)) !== null) {\r\n const locMatch = /<loc>([^<]+)<\\/loc>/i.exec(sitemapMatch[1]);\r\n if (locMatch) {\r\n const url = locMatch[1].trim();\r\n try {\r\n new URL(url);\r\n urls.push(url);\r\n } catch {\r\n // Invalid URL - skip\r\n }\r\n }\r\n }\r\n } else {\r\n // For malformed sitemap (urlset format but contains sitemap URLs)\r\n // Extract all <loc> tags from <url> blocks and check if they look like sitemaps\r\n const urlBlockRegex = /<url[^>]*>(.*?)<\\/url>/gs;\r\n let urlMatch;\r\n \r\n while ((urlMatch = urlBlockRegex.exec(xmlContent)) !== null) {\r\n const locMatch = /<loc>([^<]+)<\\/loc>/i.exec(urlMatch[1]);\r\n if (locMatch) {\r\n const url = locMatch[1].trim();\r\n \r\n // Check if this URL looks like a sitemap (ends with .xml or contains 'sitemap')\r\n if (url.toLowerCase().includes('sitemap') || url.toLowerCase().endsWith('.xml')) {\r\n try {\r\n new URL(url);\r\n urls.push(url);\r\n } catch {\r\n // Invalid URL - skip\r\n }\r\n }\r\n }\r\n }\r\n }\r\n \r\n return urls;\r\n}\r\n\r\nasync function discoverAllSitemaps(\r\n initialSitemaps: string[],\r\n config: Config,\r\n baseUrl: string,\r\n canonicalDomain?: string, // Optional - will be detected on first 301 if needed\r\n _maxDepth: number = 10\r\n): Promise<{ sitemaps: string[]; canonicalDomain?: string }> {\r\n const finalSitemaps: string[] = [];\r\n const toProcess = [...initialSitemaps];\r\n const processed = new Set<string>();\r\n const failed = new Set<string>();\r\n const redirected = new Set<string>();\r\n let detectedCanonical = canonicalDomain;\r\n const BATCH_SIZE = 5; // Process 5 sitemaps concurrently\r\n \r\n while (toProcess.length > 0) {\r\n // Take a batch of URLs to process concurrently\r\n const batch = toProcess.splice(0, Math.min(BATCH_SIZE, toProcess.length));\r\n \r\n await Promise.all(batch.map(async (sitemapUrl) => {\r\n if (processed.has(sitemapUrl)) {\r\n if (config.verbose) {\r\n console.warn(`Skipping duplicate sitemap: ${sitemapUrl}`);\r\n }\r\n return;\r\n }\r\n \r\n processed.add(sitemapUrl);\r\n \r\n try {\r\n const result = await fetchUrl(sitemapUrl, {\r\n timeout: config.timeout,\r\n maxRetries: 2\r\n });\r\n \r\n if (isSitemapIndex(result.content)) {\r\n if (config.verbose) {\r\n console.log(`Found sitemap index: ${sitemapUrl}`);\r\n }\r\n \r\n const childUrls = extractSitemapIndexUrls(result.content);\r\n toProcess.push(...childUrls);\r\n \r\n if (config.verbose) {\r\n console.log(` āā Contains ${childUrls.length} child sitemap(s)`);\r\n }\r\n } else {\r\n finalSitemaps.push(sitemapUrl);\r\n \r\n if (config.verbose) {\r\n console.log(`ā Discovered sitemap: ${sitemapUrl}`);\r\n }\r\n }\r\n \r\n } catch (error) {\r\n // Track failures\r\n if (error instanceof HttpError && error.statusCode === 301) {\r\n redirected.add(sitemapUrl);\r\n } else {\r\n failed.add(sitemapUrl);\r\n }\r\n \r\n if (config.verbose) {\r\n const message = error instanceof Error ? error.message : String(error);\r\n \r\n if (error instanceof HttpError && error.statusCode === 301) {\r\n if (!detectedCanonical) {\r\n detectedCanonical = await detectCanonicalDomain(baseUrl, config);\r\n if (config.verbose) {\r\n console.log(`Canonical domain detected: ${detectedCanonical}`);\r\n }\r\n }\r\n \r\n try {\r\n const sitemapUrlObj = new URL(sitemapUrl);\r\n \r\n if (sitemapUrlObj.hostname !== detectedCanonical) {\r\n console.warn(`ā ļø Sitemap URL redirects (301): ${sitemapUrl}`);\r\n console.warn(` Problem: The sitemap index contains a URL that redirects.`);\r\n console.warn(` Likely issue: Domain mismatch - expected \"${detectedCanonical}\" but got \"${sitemapUrlObj.hostname}\"`);\r\n console.warn(` Fix: Update sitemap index to use \"https://${detectedCanonical}${sitemapUrlObj.pathname}\"`);\r\n } else {\r\n console.warn(`ā ļø Sitemap URL redirects (301): ${sitemapUrl}`);\r\n console.warn(` Fix: Update the sitemap index to reference the final URL after redirect.`);\r\n }\r\n } catch {\r\n console.warn(`Failed to fetch sitemap ${sitemapUrl}: ${message}`);\r\n }\r\n } else {\r\n console.warn(`Failed to fetch sitemap ${sitemapUrl}: ${message}`);\r\n }\r\n }\r\n }\r\n }));\r\n \r\n // Safety check\r\n if (processed.size > 1000) {\r\n console.warn(`ā ļø Processed over 1000 sitemap URLs. Stopping to prevent excessive requests.`);\r\n break;\r\n }\r\n }\r\n \r\n // Provide helpful feedback about sitemap discovery results\r\n const totalProcessed = processed.size;\r\n const totalFailed = failed.size;\r\n const totalRedirected = redirected.size;\r\n const sitemapIndexCount = totalProcessed - finalSitemaps.length - totalFailed - totalRedirected;\r\n \r\n if (finalSitemaps.length === 0 && totalProcessed > 0) {\r\n console.warn(`\\nā ļø SITEMAP DISCOVERY ISSUE`);\r\n \r\n if (sitemapIndexCount > 0 && (totalFailed > 0 || totalRedirected > 0)) {\r\n console.warn(`Found ${sitemapIndexCount} sitemap index(es) containing ${totalFailed + totalRedirected} child sitemap(s):`);\r\n if (totalRedirected > 0) {\r\n console.warn(` - ${totalRedirected} sitemap(s) return 301 redirects (content not accessible without following redirect)`);\r\n }\r\n if (totalFailed > 0) {\r\n console.warn(` - ${totalFailed} sitemap(s) returned errors (404, 403, 500, or network issues)`);\r\n }\r\n } else if (totalRedirected > 0) {\r\n console.warn(`All ${totalRedirected} sitemap(s) return 301 redirects.`);\r\n } else if (totalFailed > 0) {\r\n console.warn(`All ${totalFailed} sitemap(s) returned errors.`);\r\n console.warn(`\\nCommon causes:`);\r\n console.warn(` - 403 Forbidden: Bot protection (Cloudflare, etc.) or IP blocking`);\r\n console.warn(` - 404 Not Found: Sitemaps don't exist at these URLs`);\r\n console.warn(` - 500/502/503: Server errors or maintenance`);\r\n console.warn(`\\nIf sitemaps work in your browser but not here, the site likely has bot protection.`);\r\n console.warn(`Try: Check if sitemaps load without JavaScript, or contact site administrator.`);\r\n } else {\r\n console.warn(`Processed ${totalProcessed} URL(s) but found no accessible sitemaps.`);\r\n }\r\n \r\n console.warn(`\\nNote: This tool does not follow redirects for sitemap URLs.`);\r\n if (totalRedirected > 0) {\r\n console.warn(`\\nPossible causes of redirects:`);\r\n console.warn(` - Sitemap index uses non-canonical domain (e.g., missing 'www' or vice versa)`);\r\n console.warn(` - Sitemap URLs redirect from HTTP to HTTPS`);\r\n console.warn(` - Intentional redirects in your site configuration`);\r\n console.warn(`\\nRecommendation: Update sitemap index URLs to match the final destination (no redirects).`);\r\n }\r\n console.warn(``);\r\n }\r\n \r\n return { sitemaps: finalSitemaps, canonicalDomain: detectedCanonical };\r\n}\r\n\r\nexport async function discoverSitemaps(\r\n baseUrl: string,\r\n config: Config\r\n): Promise<DiscoveryResult> {\r\n const normalizedUrl = normalizeBaseUrl(baseUrl);\r\n let allAccessIssues: SitemapAccessIssue[] = [];\r\n \r\n // Canonical domain will be detected lazily if we encounter redirects\r\n let canonicalDomain: string | undefined;\r\n \r\n // Strategy 1: Try robots.txt first\r\n if (config.verbose) {\r\n console.log('Strategy 1: Checking robots.txt for sitemap directives...');\r\n }\r\n \r\n const robotsSitemaps = await parseRobotsTxt(normalizedUrl, config);\r\n if (robotsSitemaps.length > 0) {\r\n const { sitemaps: allSitemaps, canonicalDomain: detected } = await discoverAllSitemaps(robotsSitemaps, config, normalizedUrl, canonicalDomain);\r\n canonicalDomain = detected; // Update if it was detected during traversal\r\n \r\n // If we successfully found sitemaps via robots.txt, don't report\r\n // standard path access issues as critical (they're just alternatives)\r\n return {\r\n sitemaps: allSitemaps,\r\n source: 'robots-txt',\r\n accessIssues: [], // Clear access issues since we found working sitemaps\r\n canonicalDomain\r\n };\r\n }\r\n \r\n // Strategy 2: Try standard paths as fallback\r\n if (config.verbose) {\r\n console.log('Strategy 2: Trying standard sitemap paths...');\r\n }\r\n \r\n const { sitemaps: standardSitemaps, issues, redirectedToCanonical } = await tryStandardPaths(normalizedUrl, config);\r\n allAccessIssues = issues;\r\n \r\n if (standardSitemaps.length > 0) {\r\n const { sitemaps: allSitemaps, canonicalDomain: detected } = await discoverAllSitemaps(standardSitemaps, config, normalizedUrl, canonicalDomain);\r\n canonicalDomain = detected; // Update if it was detected during traversal\r\n return {\r\n sitemaps: allSitemaps,\r\n source: 'standard-path',\r\n accessIssues: [], // Clear access issues since we found working sitemaps\r\n canonicalDomain\r\n };\r\n }\r\n \r\n // Strategy 3: If all requests redirected, try the canonical domain\r\n if (redirectedToCanonical) {\r\n const canonicalUrl = `https://${redirectedToCanonical}`;\r\n console.log(`\\nš” All requests redirected. Retrying with canonical domain: ${redirectedToCanonical}\\n`);\r\n \r\n // Try robots.txt on canonical domain\r\n const canonicalRobotsSitemaps = await parseRobotsTxt(canonicalUrl, config);\r\n if (canonicalRobotsSitemaps.length > 0) {\r\n const { sitemaps: allSitemaps, canonicalDomain: detected } = await discoverAllSitemaps(canonicalRobotsSitemaps, config, canonicalUrl, redirectedToCanonical);\r\n return {\r\n sitemaps: allSitemaps,\r\n source: 'robots-txt',\r\n accessIssues: [],\r\n canonicalDomain: detected || redirectedToCanonical\r\n };\r\n }\r\n \r\n // Try standard paths on canonical domain\r\n const { sitemaps: canonicalStandardSitemaps } = await tryStandardPaths(canonicalUrl, config);\r\n if (canonicalStandardSitemaps.length > 0) {\r\n const { sitemaps: allSitemaps, canonicalDomain: detected } = await discoverAllSitemaps(canonicalStandardSitemaps, config, canonicalUrl, redirectedToCanonical);\r\n return {\r\n sitemaps: allSitemaps,\r\n source: 'standard-path',\r\n accessIssues: [],\r\n canonicalDomain: detected || redirectedToCanonical\r\n };\r\n }\r\n }\r\n \r\n // Strategy 4: No sitemaps found - NOW report access issues as critical\r\n // because they prevented us from finding any accessible sitemap\r\n return {\r\n sitemaps: [],\r\n source: 'none',\r\n accessIssues: allAccessIssues,\r\n canonicalDomain\r\n };\r\n}\r\n","import { XMLParser, XMLValidator } from 'fast-xml-parser';\r\n\r\nexport interface UrlEntry {\r\n loc: string; // Required: URL location\r\n lastmod?: string; // Optional: Last modification date\r\n changefreq?: string; // Optional: Change frequency\r\n priority?: number; // Optional: Priority (0.0-1.0)\r\n source: string; // Which sitemap this came from\r\n extractedAt?: string; // ISO timestamp of extraction\r\n}\r\n\r\nexport interface ParseResult {\r\n urls: UrlEntry[]; // Successfully parsed URLs\r\n errors: string[]; // Parsing errors/warnings\r\n totalCount: number; // Total URLs parsed\r\n sitemapUrl: string; // Source sitemap URL\r\n}\r\n\r\nconst parser = new XMLParser({\r\n ignoreAttributes: false,\r\n attributeNamePrefix: '@_',\r\n textNodeName: '_text',\r\n parseAttributeValue: true,\r\n trimValues: true,\r\n allowBooleanAttributes: true,\r\n parseTagValue: false, // Keep values as strings for validation\r\n});\r\n\r\nfunction extractUrls(parsedXml: any, sitemapUrl: string): UrlEntry[] {\r\n const urls: UrlEntry[] = [];\r\n\r\n // Handle urlset format\r\n if (parsedXml.urlset) {\r\n // Normalize to array (single <url> vs multiple)\r\n const urlNodes = Array.isArray(parsedXml.urlset.url)\r\n ? parsedXml.urlset.url\r\n : [parsedXml.urlset.url];\r\n\r\n for (const node of urlNodes) {\r\n // Skip entries without loc field\r\n if (!node || !node.loc) {\r\n continue;\r\n }\r\n\r\n urls.push({\r\n loc: node.loc,\r\n lastmod: node.lastmod,\r\n changefreq: node.changefreq,\r\n priority: node.priority ? parseFloat(node.priority) : undefined,\r\n source: sitemapUrl,\r\n });\r\n }\r\n }\r\n\r\n return urls;\r\n}\r\n\r\nexport async function parseSitemap(\r\n xml: string,\r\n sitemapUrl: string\r\n): Promise<ParseResult> {\r\n const errors: string[] = [];\r\n\r\n try {\r\n // Validate XML first\r\n const validationResult = XMLValidator.validate(xml);\r\n if (validationResult !== true) {\r\n const validationError = typeof validationResult === 'object'\r\n ? validationResult.err.msg\r\n : 'Invalid XML';\r\n return {\r\n urls: [],\r\n errors: [\r\n `[${sitemapUrl}] XML parsing failed: ${validationError}`,\r\n ],\r\n totalCount: 0,\r\n sitemapUrl,\r\n };\r\n }\r\n\r\n // Parse XML\r\n const parsed = parser.parse(xml);\r\n\r\n // Extract URLs\r\n const urls = extractUrls(parsed, sitemapUrl);\r\n\r\n // Validate extracted URLs\r\n const validUrls: UrlEntry[] = [];\r\n for (const entry of urls) {\r\n try {\r\n // Validate URL format\r\n new URL(entry.loc);\r\n\r\n // Validate priority range\r\n if (entry.priority !== undefined) {\r\n if (entry.priority < 0 || entry.priority > 1) {\r\n errors.push(\r\n `Invalid priority ${entry.priority} for ${entry.loc} - clamping to 0-1`\r\n );\r\n entry.priority = Math.max(0, Math.min(1, entry.priority));\r\n }\r\n }\r\n\r\n // Validate changefreq\r\n if (entry.changefreq) {\r\n const validFreqs = [\r\n 'always',\r\n 'hourly',\r\n 'daily',\r\n 'weekly',\r\n 'monthly',\r\n 'yearly',\r\n 'never',\r\n ];\r\n if (!validFreqs.includes(entry.changefreq.toLowerCase())) {\r\n errors.push(\r\n `Invalid changefreq \"${entry.changefreq}\" for ${entry.loc}`\r\n );\r\n entry.changefreq = undefined;\r\n }\r\n }\r\n\r\n validUrls.push(entry);\r\n } catch (urlError) {\r\n errors.push(`Invalid URL format: ${entry.loc}`);\r\n }\r\n }\r\n\r\n return {\r\n urls: validUrls,\r\n errors,\r\n totalCount: validUrls.length,\r\n sitemapUrl,\r\n };\r\n } catch (parseError) {\r\n // XML parsing failed completely\r\n const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);\r\n return {\r\n urls: [],\r\n errors: [\r\n `[${sitemapUrl}] XML parsing failed: ${errorMsg}`,\r\n ],\r\n totalCount: 0,\r\n sitemapUrl,\r\n };\r\n }\r\n}\r\n","import { Config } from '@/types/config';\r\nimport { UrlEntry, parseSitemap } from '@/core/parser';\r\nimport { fetchUrl } from '@/utils/http-client';\r\n\r\nexport interface ExtractionResult {\r\n allUrls: UrlEntry[]; // All URLs from all sitemaps\r\n sitemapsProcessed: number; // Number of sitemaps successfully parsed\r\n sitemapsFailed: number; // Number of sitemaps that failed\r\n totalUrls: number; // Total URLs extracted\r\n errors: string[]; // All errors collected\r\n}\r\n\r\nexport async function extractAllUrls(\r\n sitemapUrls: string[],\r\n config: Config\r\n): Promise<ExtractionResult> {\r\n const allUrls: UrlEntry[] = [];\r\n const allErrors: string[] = [];\r\n let sitemapsProcessed = 0;\r\n let sitemapsFailed = 0;\r\n\r\n if (config.verbose) {\r\n console.log(`\\nExtracting URLs from ${sitemapUrls.length} sitemap(s)...`);\r\n }\r\n\r\n // Process sitemaps in parallel with concurrency limit\r\n const CONCURRENCY = 10; // Process 10 sitemaps at a time\r\n const results = await processInBatches(sitemapUrls, CONCURRENCY, async (sitemapUrl) => {\r\n try {\r\n if (config.verbose) {\r\n console.log(`Extracting URLs from: ${sitemapUrl}`);\r\n }\r\n\r\n // Fetch sitemap content\r\n const response = await fetchUrl(sitemapUrl, {\r\n timeout: config.timeout,\r\n maxRetries: 2\r\n });\r\n\r\n // Parse sitemap XML\r\n const parseResult = await parseSitemap(response.content, sitemapUrl);\r\n\r\n // Add extraction timestamp to each URL\r\n const urlsWithTimestamp = parseResult.urls.map((url) => ({\r\n ...url,\r\n extractedAt: new Date().toISOString(),\r\n }));\r\n\r\n if (config.verbose) {\r\n console.log(` ā Extracted ${parseResult.urls.length} URLs from ${sitemapUrl}`);\r\n }\r\n\r\n return {\r\n success: true,\r\n urls: urlsWithTimestamp,\r\n errors: parseResult.errors,\r\n };\r\n } catch (error) {\r\n const errorMsg = `Failed to process ${sitemapUrl}: ${\r\n error instanceof Error ? error.message : String(error)\r\n }`;\r\n\r\n if (config.verbose) {\r\n console.error(` ā ${errorMsg}`);\r\n }\r\n\r\n return {\r\n success: false,\r\n urls: [],\r\n errors: [errorMsg],\r\n };\r\n }\r\n });\r\n\r\n // Aggregate results\r\n for (const result of results) {\r\n if (result.success) {\r\n sitemapsProcessed++;\r\n allUrls.push(...result.urls);\r\n } else {\r\n sitemapsFailed++;\r\n }\r\n allErrors.push(...result.errors);\r\n }\r\n\r\n if (config.verbose) {\r\n console.log(`\\nExtraction complete:`);\r\n console.log(` - Sitemaps processed: ${sitemapsProcessed}`);\r\n console.log(` - Sitemaps failed: ${sitemapsFailed}`);\r\n console.log(` - Total URLs: ${allUrls.length}`);\r\n console.log(` - Errors: ${allErrors.length}`);\r\n }\r\n\r\n return {\r\n allUrls,\r\n sitemapsProcessed,\r\n sitemapsFailed,\r\n totalUrls: allUrls.length,\r\n errors: allErrors,\r\n };\r\n}\r\n\r\n/**\r\n * Process items in batches with controlled concurrency\r\n */\r\nasync function processInBatches<T, R>(\r\n items: T[],\r\n concurrency: number,\r\n processor: (item: T) => Promise<R>\r\n): Promise<R[]> {\r\n const results: R[] = [];\r\n \r\n for (let i = 0; i < items.length; i += concurrency) {\r\n const batch = items.slice(i, i + concurrency);\r\n const batchResults = await Promise.all(batch.map(processor));\r\n results.push(...batchResults);\r\n }\r\n \r\n return results;\r\n}\r\n","import { UrlEntry } from '@/core/parser';\r\n\r\nexport interface ConsolidatedResult {\r\n uniqueUrls: UrlEntry[]; // Deduplicated URLs\r\n totalInputUrls: number; // Original count before deduplication\r\n duplicatesRemoved: number; // Number of duplicates removed\r\n duplicateGroups?: DuplicateGroup[]; // Optional: groups of duplicates for debugging\r\n}\r\n\r\nexport interface DuplicateGroup {\r\n url: string; // The canonical URL\r\n count: number; // How many times it appeared\r\n sources: string[]; // Which sitemaps contained it\r\n}\r\n\r\nexport function normalizeUrl(url: string): string {\r\n try {\r\n const parsed = new URL(url);\r\n\r\n // Remove trailing slash\r\n let pathname = parsed.pathname;\r\n if (pathname.endsWith('/') && pathname !== '/') {\r\n pathname = pathname.slice(0, -1);\r\n }\r\n\r\n // Sort query parameters alphabetically\r\n const params = Array.from(parsed.searchParams.entries()).sort(([a], [b]) =>\r\n a.localeCompare(b)\r\n );\r\n const sortedParams = new URLSearchParams(params);\r\n\r\n // Reconstruct URL\r\n return `${parsed.protocol}//${parsed.host}${pathname}${\r\n sortedParams.toString() ? '?' + sortedParams.toString() : ''\r\n }${parsed.hash}`;\r\n } catch {\r\n // If URL parsing fails, use original\r\n return url;\r\n }\r\n}\r\n\r\nfunction mergeUrlEntries(entries: UrlEntry[]): UrlEntry {\r\n if (entries.length === 1) return entries[0];\r\n\r\n // Use the first entry as base\r\n const merged: UrlEntry = { ...entries[0] };\r\n\r\n // Merge sources\r\n const sources = entries.map((e) => e.source);\r\n merged.source = sources.join(', ');\r\n\r\n // Use most recent lastmod\r\n const lastmods = entries\r\n .map((e) => e.lastmod)\r\n .filter((lm): lm is string => !!lm)\r\n .map((lm) => new Date(lm).getTime())\r\n .sort((a, b) => b - a);\r\n\r\n if (lastmods.length > 0) {\r\n merged.lastmod = new Date(lastmods[0]).toISOString();\r\n }\r\n\r\n // Use highest priority\r\n const priorities = entries\r\n .map((e) => e.priority)\r\n .filter((p): p is number => p !== undefined);\r\n\r\n if (priorities.length > 0) {\r\n merged.priority = Math.max(...priorities);\r\n }\r\n\r\n // Use most frequent changefreq (or first if tie)\r\n const changefreqs = entries\r\n .map((e) => e.changefreq)\r\n .filter((cf): cf is string => !!cf);\r\n\r\n if (changefreqs.length > 0) {\r\n const counts = new Map<string, number>();\r\n for (const cf of changefreqs) {\r\n counts.set(cf, (counts.get(cf) || 0) + 1);\r\n }\r\n const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);\r\n merged.changefreq = sorted[0][0];\r\n }\r\n\r\n // Use most recent extractedAt\r\n const extractedAts = entries\r\n .map((e) => e.extractedAt)\r\n .filter((ea): ea is string => !!ea)\r\n .map((ea) => new Date(ea).getTime())\r\n .sort((a, b) => b - a);\r\n\r\n if (extractedAts.length > 0) {\r\n merged.extractedAt = new Date(extractedAts[0]).toISOString();\r\n }\r\n\r\n return merged;\r\n}\r\n\r\nexport function consolidateUrls(\r\n urls: UrlEntry[],\r\n verbose: boolean = false\r\n): ConsolidatedResult {\r\n const totalInputUrls = urls.length;\r\n\r\n if (verbose) {\r\n console.log(`\\nConsolidating ${urls.length} URL(s)...`);\r\n }\r\n\r\n // Group by normalized URL\r\n const urlMap = new Map<string, UrlEntry[]>();\r\n\r\n for (const entry of urls) {\r\n const normalized = normalizeUrl(entry.loc);\r\n if (!urlMap.has(normalized)) {\r\n urlMap.set(normalized, []);\r\n }\r\n urlMap.get(normalized)!.push(entry);\r\n }\r\n\r\n // Merge duplicates\r\n const uniqueUrls: UrlEntry[] = [];\r\n const duplicateGroups: DuplicateGroup[] = [];\r\n\r\n for (const [normalized, entries] of urlMap.entries()) {\r\n const merged = mergeUrlEntries(entries);\r\n uniqueUrls.push(merged);\r\n\r\n if (entries.length > 1) {\r\n duplicateGroups.push({\r\n url: normalized,\r\n count: entries.length,\r\n sources: entries.map((e) => e.source),\r\n });\r\n }\r\n }\r\n\r\n if (verbose) {\r\n console.log(`Consolidation complete:`);\r\n console.log(` - Input URLs: ${totalInputUrls}`);\r\n console.log(` - Unique URLs: ${uniqueUrls.length}`);\r\n console.log(` - Duplicates removed: ${totalInputUrls - uniqueUrls.length}`);\r\n\r\n if (duplicateGroups.length > 0) {\r\n console.log(`\\nTop duplicates:`);\r\n const top5 = duplicateGroups\r\n .sort((a, b) => b.count - a.count)\r\n .slice(0, 5);\r\n\r\n for (const group of top5) {\r\n console.log(` - ${group.url} (${group.count} times)`);\r\n }\r\n }\r\n }\r\n\r\n return {\r\n uniqueUrls,\r\n totalInputUrls,\r\n duplicatesRemoved: totalInputUrls - uniqueUrls.length,\r\n duplicateGroups,\r\n };\r\n}\r\n","import { RiskPattern } from '@/core/risk-detector';\r\n\r\nexport const RISK_PATTERNS: RiskPattern[] = [\r\n // Note: Environment leakage patterns moved to domain-patterns.ts\r\n // Note: Admin path patterns moved to admin-patterns.ts\r\n // to avoid duplication and improve maintainability\r\n \r\n // Sensitive Parameter Patterns (HIGH)\r\n {\r\n name: 'Authentication Parameter',\r\n category: 'sensitive_params',\r\n severity: 'high',\r\n regex: /[?&](token|auth|key|password|secret|apikey|session|credentials)=/i,\r\n description: 'Query parameter may contain sensitive authentication data'\r\n },\r\n {\r\n name: 'Debug Parameter',\r\n category: 'sensitive_params',\r\n severity: 'medium',\r\n regex: /[?&](debug|trace|verbose|test_mode)=/i,\r\n description: 'Query parameter may contain debug or diagnostic flag'\r\n },\r\n \r\n // Protocol Inconsistency Patterns (MEDIUM)\r\n {\r\n name: 'HTTP in HTTPS Site',\r\n category: 'protocol_inconsistency',\r\n severity: 'medium',\r\n regex: /^http:\\/\\//,\r\n description: 'HTTP URL in HTTPS sitemap (potential mixed content)'\r\n },\r\n \r\n // Test/Unfinished Content Patterns (MEDIUM)\r\n // Focuses on obvious test/placeholder patterns, avoiding false positives with legitimate content\r\n {\r\n name: 'Test Content Path',\r\n category: 'test_content',\r\n severity: 'medium',\r\n regex: /\\/(?:test-|demo-|sample-|temp-|temporary-|placeholder-)|\\/(test|demo|sample|temp|temporary|placeholder)(?:\\/|$)/i,\r\n description: 'URL path suggests test, demo, or unfinished content that may not be intended for indexing'\r\n }\r\n];\r\n","import { RiskPattern } from '@/core/risk-detector';\r\n\r\nexport interface DomainValidationOptions {\r\n allowedSubdomains?: string[]; // e.g., ['www', 'blog', 'shop']\r\n}\r\n\r\nfunction escapeRegex(str: string): string {\r\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\r\n}\r\n\r\nexport function extractRootDomain(hostname: string): string {\r\n const parts = hostname.split('.');\r\n if (parts.length >= 2) {\r\n return parts.slice(-2).join('.');\r\n }\r\n return hostname;\r\n}\r\n\r\nexport function createDomainMismatchPattern(\r\n baseUrl: string,\r\n options?: DomainValidationOptions\r\n): RiskPattern {\r\n const baseDomain = new URL(baseUrl).hostname;\r\n const rootDomain = extractRootDomain(baseDomain);\r\n \r\n // If allowed subdomains specified, create more lenient pattern\r\n if (options?.allowedSubdomains && options.allowedSubdomains.length > 0) {\r\n const escapedRoot = escapeRegex(rootDomain);\r\n const escapedSubdomains = options.allowedSubdomains.map(escapeRegex).join('|');\r\n \r\n // Match URLs that DON'T start with: (allowed-subdomain OR no-subdomain) + root domain\r\n // Pattern: NOT (www.example.com OR blog.example.com OR example.com)\r\n const pattern = `^https?://(?!(?:(?:${escapedSubdomains})\\\\.)?${escapedRoot}(?:/|$))`;\r\n \r\n return {\r\n name: 'Domain Mismatch',\r\n category: 'domain_mismatch',\r\n severity: 'high',\r\n regex: new RegExp(pattern),\r\n description: `URL does not match expected domain or allowed subdomains`\r\n };\r\n }\r\n \r\n // Default: Allow both www and non-www variants of the same root domain\r\n const escapedRoot = escapeRegex(rootDomain);\r\n \r\n // Match URLs that DON'T belong to the root domain (with or without www)\r\n // Pattern: NOT (example.com OR www.example.com)\r\n const pattern = `^https?://(?!(?:www\\\\.)?${escapedRoot}(?:/|$))`;\r\n \r\n return {\r\n name: 'Domain Mismatch',\r\n category: 'domain_mismatch',\r\n severity: 'high',\r\n regex: new RegExp(pattern),\r\n description: `URL does not match expected domain: ${rootDomain} (including www variant)`\r\n };\r\n}\r\n\r\nexport const ENVIRONMENT_PATTERNS: RiskPattern[] = [\r\n {\r\n name: 'Staging Subdomain',\r\n category: 'environment_leakage',\r\n severity: 'high',\r\n regex: /^https?:\\/\\/(staging|stg)\\./i,\r\n description: 'URL uses staging subdomain'\r\n },\r\n {\r\n name: 'Development Subdomain',\r\n category: 'environment_leakage',\r\n severity: 'high',\r\n regex: /^https?:\\/\\/(dev|development)\\./i,\r\n description: 'URL uses development subdomain'\r\n },\r\n {\r\n name: 'QA/Test Subdomain',\r\n category: 'environment_leakage',\r\n severity: 'high',\r\n regex: /^https?:\\/\\/(qa|test|uat|preprod)\\./i,\r\n description: 'URL uses test environment subdomain'\r\n },\r\n {\r\n name: 'Localhost URL',\r\n category: 'environment_leakage',\r\n severity: 'high',\r\n regex: /^https?:\\/\\/(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0)/,\r\n description: 'URL points to localhost (development environment)'\r\n },\r\n {\r\n name: 'Environment in Path',\r\n category: 'environment_leakage',\r\n severity: 'high',\r\n regex: /^https?:\\/\\/[^/]+\\/(staging|dev|qa|uat|preprod)\\//i,\r\n description: 'URL path contains environment identifier at root level'\r\n }\r\n];\r\n","import { RiskPattern } from '@/core/risk-detector';\r\n\r\nexport const ADMIN_PATH_PATTERNS: RiskPattern[] = [\r\n {\r\n name: 'Admin Path',\r\n category: 'admin_paths',\r\n severity: 'high',\r\n regex: /\\/(admin|administrator)(?:\\/|$|\\?)/i,\r\n description: 'URL contains /admin or /administrator as a path segment'\r\n },\r\n {\r\n name: 'Dashboard Path',\r\n category: 'admin_paths',\r\n severity: 'high',\r\n regex: /\\/dashboard(?:\\/|$|\\?)/i,\r\n description: 'URL contains /dashboard as a path segment'\r\n },\r\n {\r\n name: 'Config Path',\r\n category: 'admin_paths',\r\n severity: 'high',\r\n regex: /\\/(config|configuration)(?:\\/|$|\\?)/i,\r\n description: 'URL contains /config or /configuration as a path segment'\r\n },\r\n {\r\n name: 'Console Path',\r\n category: 'admin_paths',\r\n severity: 'high',\r\n regex: /\\/console(?:\\/|$|\\?)/i,\r\n description: 'URL contains /console as a path segment'\r\n },\r\n {\r\n name: 'Control Panel Path',\r\n category: 'admin_paths',\r\n severity: 'high',\r\n regex: /\\/(cpanel|control-panel)(?:\\/|$|\\?)/i,\r\n description: 'URL contains control panel as a path segment'\r\n }\r\n];\r\n\r\n// Internal content patterns - lower severity as these may be legitimate public-facing content\r\n// that happens to use \"internal\" in the naming (e.g., internal ticket requests, internal forms)\r\nexport const INTERNAL_CONTENT_PATTERNS: RiskPattern[] = [\r\n {\r\n name: 'Internal Content Path',\r\n category: 'internal_content',\r\n severity: 'medium',\r\n regex: /\\/internal\\b/i,\r\n description: 'URL contains /internal path segment - may be internal-only content not intended for public indexing'\r\n }\r\n];\r\n\r\nexport const SENSITIVE_PARAM_PATTERNS: RiskPattern[] = [\r\n {\r\n name: 'Authentication Token Parameter',\r\n category: 'sensitive_params',\r\n severity: 'high',\r\n regex: /[?&](token|auth_token|access_token|api_token)=/i,\r\n description: 'Query parameter may contain authentication token'\r\n },\r\n {\r\n name: 'API Key Parameter',\r\n category: 'sensitive_params',\r\n severity: 'high',\r\n regex: /[?&](apikey|api_key|key)=/i,\r\n description: 'Query parameter may contain API key'\r\n },\r\n {\r\n name: 'Password Parameter',\r\n category: 'sensitive_params',\r\n severity: 'high',\r\n regex: /[?&](password|passwd|pwd)=/i,\r\n description: 'Query parameter may contain password'\r\n },\r\n {\r\n name: 'Secret Parameter',\r\n category: 'sensitive_params',\r\n severity: 'high',\r\n regex: /[?&](secret|client_secret)=/i,\r\n description: 'Query parameter may contain secret value'\r\n },\r\n {\r\n name: 'Session Parameter',\r\n category: 'sensitive_params',\r\n severity: 'high',\r\n regex: /[?&](session|sessionid|sid)=/i,\r\n description: 'Query parameter may contain session identifier'\r\n },\r\n {\r\n name: 'Credentials Parameter',\r\n category: 'sensitive_params',\r\n severity: 'high',\r\n regex: /[?&]credentials=/i,\r\n description: 'Query parameter may contain credentials'\r\n },\r\n {\r\n name: 'Debug Parameter',\r\n category: 'sensitive_params',\r\n severity: 'medium',\r\n regex: /[?&](debug|trace|verbose)=/i,\r\n description: 'Query parameter contains debug or diagnostic flag'\r\n },\r\n {\r\n name: 'Test Mode Parameter',\r\n category: 'sensitive_params',\r\n severity: 'medium',\r\n regex: /[?&](test_mode|test|testing)=/i,\r\n description: 'Query parameter indicates test mode'\r\n }\r\n];\r\n","export function sanitizeUrl(url: string): string {\r\n try {\r\n const parsed = new URL(url);\r\n const sensitiveParams = [\r\n 'token', 'auth', 'auth_token', 'access_token', 'api_token',\r\n 'apikey', 'api_key', 'key',\r\n 'password', 'passwd', 'pwd',\r\n 'secret', 'client_secret',\r\n 'session', 'sessionid', 'sid',\r\n 'credentials'\r\n ];\r\n \r\n for (const param of sensitiveParams) {\r\n if (parsed.searchParams.has(param)) {\r\n parsed.searchParams.set(param, '[REDACTED]');\r\n }\r\n }\r\n \r\n return parsed.toString();\r\n } catch {\r\n // Invalid URL - return as-is\r\n return url;\r\n }\r\n}\r\n\r\nexport function sanitizeUrls(urls: string[]): string[] {\r\n return urls.map(url => sanitizeUrl(url));\r\n}\r\n","import { RiskFinding, RiskCategory, Severity } from '@/core/risk-detector';\r\n\r\nexport interface RiskGroup {\r\n category: RiskCategory;\r\n severity: Severity;\r\n count: number;\r\n rationale: string;\r\n sampleUrls: string[];\r\n recommendedAction: string;\r\n allUrls?: string[]; // Optional: full list for detailed analysis\r\n}\r\n\r\nexport interface RiskGroupingResult {\r\n groups: RiskGroup[];\r\n totalRiskUrls: number;\r\n highSeverityCount: number;\r\n mediumSeverityCount: number;\r\n lowSeverityCount: number;\r\n}\r\n\r\nfunction generateRecommendation(\r\n category: RiskCategory,\r\n _severity: Severity,\r\n count: number\r\n): { rationale: string; recommendedAction: string } {\r\n switch (category) {\r\n case 'environment_leakage':\r\n return {\r\n rationale: `Production sitemap contains ${count} URL(s) from non-production environments (staging, dev, QA, test). This indicates configuration errors or environment leakage.`,\r\n recommendedAction: 'Verify sitemap generation excludes non-production environments. Review deployment configuration and environment filtering rules.'\r\n };\r\n \r\n case 'admin_paths':\r\n return {\r\n rationale: `${count} administrative path(s) detected in public sitemap (admin, dashboard, config). These paths may expose privileged access points.`,\r\n recommendedAction: 'Confirm if admin paths should be publicly indexed. Consider excluding via robots.txt or removing from sitemap. Verify access controls.'\r\n };\r\n \r\n case 'internal_content':\r\n return {\r\n rationale: `${count} URL(s) contain \"internal\" in the path. These may be internal-facing content not intended for public indexing.`,\r\n recommendedAction: 'Review URLs to determine if they should be publicly accessible. Consider excluding internal content from sitemap or adding noindex meta tags.'\r\n };\r\n \r\n case 'test_content':\r\n return {\r\n rationale: `${count} URL(s) contain test/demo/sample identifiers. These may be placeholder or unfinished content not intended for indexing.`,\r\n recommendedAction: 'Review and remove test content from production sitemaps. Verify content is production-ready before including in sitemap.'\r\n };\r\n \r\n case 'sensitive_params':\r\n return {\r\n rationale: `${count} URL(s) contain sensitive query parameters (token, auth, key, password, session). This may expose authentication credentials or debugging flags.`,\r\n recommendedAction: 'Review why sensitive parameters are in sitemap URLs. Remove authentication tokens from URLs. Consider POST requests for sensitive data.'\r\n };\r\n \r\n case 'protocol_inconsistency':\r\n return {\r\n rationale: `${count} URL(s) use HTTP protocol in HTTPS sitemap. This creates mixed content warnings and potential security issues.`,\r\n recommendedAction: 'Update URLs to use HTTPS consistently. Verify SSL certificate coverage. Check for hardcoded HTTP URLs in content.'\r\n };\r\n \r\n case 'domain_mismatch':\r\n return {\r\n rationale: `${count} URL(s) do not match expected base domain. This may indicate external links, CDN URLs, or configuration errors.`,\r\n recommendedAction: 'Verify if external domains are intentional. Review sitemap generation logic. Confirm CDN or subdomain configuration is correct.'\r\n };\r\n \r\n default:\r\n return {\r\n rationale: `${count} URL(s) flagged in category: ${category}`,\r\n recommendedAction: 'Review flagged URLs and determine appropriate action.'\r\n };\r\n }\r\n}\r\n\r\nexport function groupRiskFindings(\r\n findings: RiskFinding[],\r\n maxSampleUrls: number = 5\r\n): RiskGroupingResult {\r\n // Group by category\r\n const categoryMap = new Map<RiskCategory, RiskFinding[]>();\r\n \r\n for (const finding of findings) {\r\n if (!categoryMap.has(finding.category)) {\r\n categoryMap.set(finding.category, []);\r\n }\r\n categoryMap.get(finding.category)!.push(finding);\r\n }\r\n \r\n // Create groups\r\n const groups: RiskGroup[] = [];\r\n \r\n for (const [category, categoryFindings] of categoryMap.entries()) {\r\n // Get unique URLs for this category\r\n const uniqueUrls = Array.from(new Set(categoryFindings.map(f => f.url)));\r\n \r\n // Determine severity (highest severity in category)\r\n const severity = categoryFindings.reduce((highest, finding) => {\r\n const severityOrder: Severity[] = ['low', 'medium', 'high'];\r\n return severityOrder.indexOf(finding.severity) > severityOrder.indexOf(highest)\r\n ? finding.severity\r\n : highest;\r\n }, 'low' as Severity);\r\n \r\n // Select sample URLs\r\n const sampleUrls = uniqueUrls.slice(0, maxSampleUrls);\r\n \r\n // Generate rationale and recommendation\r\n const { rationale, recommendedAction } = generateRecommendation(category, severity, uniqueUrls.length);\r\n \r\n groups.push({\r\n category,\r\n severity,\r\n count: uniqueUrls.length,\r\n rationale,\r\n sampleUrls,\r\n recommendedAction,\r\n allUrls: uniqueUrls\r\n });\r\n }\r\n \r\n // Sort groups by severity (HIGH ā MEDIUM ā LOW)\r\n groups.sort((a, b) => {\r\n const severityOrder: Severity[] = ['high', 'medium', 'low'];\r\n return severityOrder.indexOf(a.severity) - severityOrder.indexOf(b.severity);\r\n });\r\n \r\n // Calculate summary counts\r\n const totalRiskUrls = new Set(findings.map(f => f.url)).size;\r\n const highSeverityCount = groups.filter(g => g.severity === 'high').reduce((sum, g) => sum + g.count, 0);\r\n const mediumSeverityCount = groups.filter(g => g.severity === 'medium').reduce((sum, g) => sum + g.count, 0);\r\n const lowSeverityCount = groups.filter(g => g.severity === 'low').reduce((sum, g) => sum + g.count, 0);\r\n \r\n return {\r\n groups,\r\n totalRiskUrls,\r\n highSeverityCount,\r\n mediumSeverityCount,\r\n lowSeverityCount\r\n };\r\n}\r\n","import { UrlEntry } from '@/core/parser';\r\nimport { Config } from '@/types/config';\r\nimport { RISK_PATTERNS } from '@/core/patterns/risk-patterns';\r\nimport { ENVIRONMENT_PATTERNS, createDomainMismatchPattern } from '@/core/patterns/domain-patterns';\r\nimport { ADMIN_PATH_PATTERNS, SENSITIVE_PARAM_PATTERNS, INTERNAL_CONTENT_PATTERNS } from '@/core/patterns/admin-patterns';\r\nimport { sanitizeUrl } from '@/utils/sanitizer';\r\nimport { groupRiskFindings, RiskGroup } from '@/core/risk-grouper';\r\n\r\nexport type RiskCategory = \r\n | 'environment_leakage'\r\n | 'admin_paths'\r\n | 'sensitive_params'\r\n | 'protocol_inconsistency'\r\n | 'domain_mismatch'\r\n | 'test_content'\r\n | 'internal_content';\r\n\r\nexport type Severity = 'high' | 'medium' | 'low';\r\n\r\nexport interface RiskPattern {\r\n name: string;\r\n category: RiskCategory;\r\n severity: Severity;\r\n regex: RegExp;\r\n description: string;\r\n}\r\n\r\nexport interface RiskFinding {\r\n url: string;\r\n category: RiskCategory;\r\n severity: Severity;\r\n pattern: string;\r\n rationale: string;\r\n matchedValue?: string; // Optional: the specific text that matched\r\n}\r\n\r\nexport interface RiskDetectionResult {\r\n findings: RiskFinding[];\r\n groups: RiskGroup[];\r\n totalUrlsAnalyzed: number;\r\n riskUrlCount: number;\r\n cleanUrlCount: number;\r\n highSeverityCount: number;\r\n mediumSeverityCount: number;\r\n lowSeverityCount: number;\r\n processingTimeMs: number;\r\n}\r\n\r\nexport async function detectRisks(\r\n urls: UrlEntry[],\r\n baseUrl: string,\r\n config: Config\r\n): Promise<RiskDetectionResult> {\r\n const startTime = Date.now();\r\n const findings: RiskFinding[] = [];\r\n \r\n // Add dynamic domain mismatch pattern and all other patterns\r\n const domainPattern = createDomainMismatchPattern(baseUrl);\r\n const allPatterns = [\r\n ...RISK_PATTERNS,\r\n ...ENVIRONMENT_PATTERNS,\r\n ...ADMIN_PATH_PATTERNS,\r\n ...SENSITIVE_PARAM_PATTERNS,\r\n ...INTERNAL_CONTENT_PATTERNS,\r\n domainPattern\r\n ];\r\n \r\n // Compile accepted patterns\r\n const acceptedPatterns: RegExp[] = [];\r\n if (config.acceptedPatterns && config.acceptedPatterns.length > 0) {\r\n for (const pattern of config.acceptedPatterns) {\r\n try {\r\n // Convert user-friendly pattern to regex:\r\n // 1. Escape special regex chars except * \r\n // 2. Convert * to .* for wildcard matching\r\n // 3. Ensure pattern matches complete words/segments (not substrings)\r\n let regexPattern = pattern\r\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&') // Escape special chars except *\r\n .replace(/\\*/g, '[^/]*'); // Convert * to [^/]* (anything except /)\r\n \r\n // Add word boundary at the end to ensure it matches complete path segments\r\n if (!regexPattern.endsWith('$') && !regexPattern.includes('(?:')) {\r\n regexPattern = regexPattern + '(?:/|$|\\\\?|#)';\r\n }\r\n \r\n acceptedPatterns.push(new RegExp(regexPattern, 'i'));\r\n } catch (error) {\r\n if (config.verbose) {\r\n console.warn(`Invalid accepted pattern: ${pattern}`);\r\n }\r\n }\r\n }\r\n }\r\n \r\n if (config.verbose) {\r\n console.log(`\\nAnalyzing ${urls.length} URLs for risk patterns...`);\r\n try {\r\n console.log(`Base domain: ${new URL(baseUrl).hostname}`);\r\n } catch (error) {\r\n console.log(`Base URL: ${baseUrl}`);\r\n }\r\n if (acceptedPatterns.length > 0) {\r\n console.log(`Accepted patterns: ${acceptedPatterns.length}`);\r\n }\r\n }\r\n \r\n // Determine expected protocol from base URL\r\n let expectedProtocol: string;\r\n try {\r\n expectedProtocol = new URL(baseUrl).protocol;\r\n } catch (error) {\r\n if (config.verbose) {\r\n console.warn(`Invalid base URL: ${baseUrl}, defaulting to https:`);\r\n }\r\n expectedProtocol = 'https:';\r\n }\r\n \r\n let processed = 0;\r\n \r\n for (const urlEntry of urls) {\r\n const url = urlEntry.loc;\r\n processed++;\r\n \r\n // Progress tracking every 10k URLs with in-place update\r\n if (processed % 10000 === 0 || processed === urls.length) {\r\n process.stdout.write(`\\r\\x1b[K Analyzing: ${processed.toLocaleString()}/${urls.length.toLocaleString()} URLs...`);\r\n }\r\n \r\n // Check if URL matches accepted patterns\r\n let isAccepted = false;\r\n for (const acceptedPattern of acceptedPatterns) {\r\n if (acceptedPattern.test(url)) {\r\n isAccepted = true;\r\n break;\r\n }\r\n }\r\n \r\n if (isAccepted) {\r\n continue; // Skip accepted URLs\r\n }\r\n \r\n // Test each pattern against the URL\r\n for (const pattern of allPatterns) {\r\n // Special handling for protocol inconsistency\r\n if (pattern.category === 'protocol_inconsistency') {\r\n try {\r\n const urlProtocol = new URL(url).protocol;\r\n if (expectedProtocol === 'https:' && urlProtocol === 'http:') {\r\n findings.push({\r\n url,\r\n category: pattern.category,\r\n severity: pattern.severity,\r\n pattern: pattern.name,\r\n rationale: pattern.description,\r\n matchedValue: 'http://'\r\n });\r\n }\r\n } catch (error) {\r\n // Invalid URL - skip\r\n if (config.verbose) {\r\n console.warn(`Skipping invalid URL: ${url}`);\r\n }\r\n continue;\r\n }\r\n } else {\r\n // Standard regex matching\r\n try {\r\n const match = url.match(pattern.regex);\r\n if (match) {\r\n findings.push({\r\n url: pattern.category === 'sensitive_params' ? sanitizeUrl(url) : url,\r\n category: pattern.category,\r\n severity: pattern.severity,\r\n pattern: pattern.name,\r\n rationale: pattern.description,\r\n matchedValue: match[0]\r\n });\r\n }\r\n } catch (error) {\r\n if (config.verbose) {\r\n console.error(`Pattern matching failed for ${pattern.name}: ${error instanceof Error ? error.message : String(error)}`);\r\n }\r\n continue;\r\n }\r\n }\r\n }\r\n }\r\n \r\n // Clear progress line\r\n if (urls.length >= 10000) {\r\n process.stdout.write('\\r\\x1b[K');\r\n }\r\n \r\n // Group findings\r\n const groupingResult = groupRiskFindings(findings);\r\n \r\n const processingTimeMs = Date.now() - startTime;\r\n \r\n if (config.verbose) {\r\n console.log(`\\nRisk Summary:`);\r\n console.log(` - Total URLs analyzed: ${urls.length}`);\r\n console.log(` - Risk URLs found: ${groupingResult.totalRiskUrls}`);\r\n console.log(` - HIGH severity: ${groupingResult.highSeverityCount}`);\r\n console.log(` - MEDIUM severity: ${groupingResult.mediumSeverityCount}`);\r\n console.log(` - LOW severity: ${groupingResult.lowSeverityCount}`);\r\n console.log(` - Processing time: ${processingTimeMs}ms`);\r\n \r\n if (groupingResult.groups.length > 0) {\r\n console.log(`\\nRisk Categories Found:`);\r\n for (const group of groupingResult.groups) {\r\n console.log(` - ${group.category}: ${group.count} URLs (${group.severity.toUpperCase()})`);\r\n }\r\n }\r\n }\r\n \r\n return {\r\n findings,\r\n groups: groupingResult.groups,\r\n totalUrlsAnalyzed: urls.length,\r\n riskUrlCount: groupingResult.totalRiskUrls,\r\n cleanUrlCount: urls.length - groupingResult.totalRiskUrls,\r\n highSeverityCount: groupingResult.highSeverityCount,\r\n mediumSeverityCount: groupingResult.mediumSeverityCount,\r\n lowSeverityCount: groupingResult.lowSeverityCount,\r\n processingTimeMs\r\n };\r\n}\r\n","import type { RiskGroup } from '@/core/risk-grouper';\r\n\r\nexport interface RiskSummaryRequest {\r\n riskGroups: RiskGroup[];\r\n totalUrls: number;\r\n sitemapUrl: string;\r\n processingTime?: number;\r\n}\r\n\r\nexport interface RiskSummary {\r\n overview: string;\r\n keyFindings: string[];\r\n categoryInsights: CategoryInsight[];\r\n severityBreakdown: {\r\n high: number;\r\n medium: number;\r\n low: number;\r\n };\r\n recommendations: string[];\r\n generatedBy: string;\r\n metadata: {\r\n tokensUsed: number;\r\n processingTime: number;\r\n model: string;\r\n };\r\n}\r\n\r\nexport interface CategoryInsight {\r\n category: string;\r\n count: number;\r\n severity: 'high' | 'medium' | 'low';\r\n summary: string;\r\n examples: string[];\r\n allUrls: string[]; // Full list of all URLs in this category\r\n}\r\n\r\nexport function summarizeRisks(request: RiskSummaryRequest): RiskSummary {\r\n const severityBreakdown = {\r\n high: 0,\r\n medium: 0,\r\n low: 0\r\n };\r\n \r\n const categoryInsights: CategoryInsight[] = request.riskGroups.map(group => {\r\n severityBreakdown[group.severity] += group.count;\r\n \r\n const urls = group.allUrls || group.sampleUrls;\r\n \r\n return {\r\n category: group.category,\r\n count: group.count,\r\n severity: group.severity,\r\n summary: group.rationale,\r\n examples: urls.slice(0, 3),\r\n allUrls: urls // Include all URLs for download functionality\r\n };\r\n });\r\n \r\n const totalRisks = request.riskGroups.reduce((sum, g) => sum + g.count, 0);\r\n const overview = totalRisks > 0\r\n ? `Found ${totalRisks} potentially risky URLs across ${request.riskGroups.length} categories in ${request.totalUrls} total URLs.`\r\n : `Analyzed ${request.totalUrls} URLs. No suspicious patterns detected.`;\r\n \r\n const keyFindings: string[] = [];\r\n if (severityBreakdown.high > 0) {\r\n keyFindings.push(`${severityBreakdown.high} high-severity issues require immediate attention`);\r\n }\r\n if (severityBreakdown.medium > 0) {\r\n keyFindings.push(`${severityBreakdown.medium} medium-severity issues should be reviewed`);\r\n }\r\n if (severityBreakdown.low > 0) {\r\n keyFindings.push(`${severityBreakdown.low} low-severity items flagged for awareness`);\r\n }\r\n \r\n return {\r\n overview,\r\n keyFindings,\r\n categoryInsights,\r\n severityBreakdown,\r\n recommendations: [],\r\n generatedBy: 'rule-based analysis',\r\n metadata: {\r\n tokensUsed: 0,\r\n processingTime: request.processingTime || 0,\r\n model: 'pattern-matching'\r\n }\r\n };\r\n}\r\n","import { promises as fs } from 'fs';\r\nimport type { RiskSummary } from '@/summarizer';\r\nimport type { DiscoveryResult } from '@/core/discovery';\r\nimport type { RiskGroup } from '@/core/risk-grouper';\r\nimport type { Config } from '@/types/config';\r\n\r\n// Version is injected at build time by tsup\r\ndeclare const __PACKAGE_VERSION__: string;\r\nconst TOOL_VERSION = __PACKAGE_VERSION__;\r\n\r\nexport interface JsonReporterOptions {\r\n pretty?: boolean; // Pretty-print with indentation (default: true)\r\n indent?: number; // Indentation spaces (default: 2)\r\n includeMetadata?: boolean; // Include generation metadata (default: true)\r\n}\r\n\r\nexport interface ParseResult {\r\n totalCount: number;\r\n uniqueUrls: string[];\r\n errors: Error[];\r\n}\r\n\r\ninterface AnalysisMetadata {\r\n baseUrl: string;\r\n analysisTimestamp: string;\r\n toolVersion: string;\r\n executionTimeMs: number;\r\n analysisType: string;\r\n}\r\n\r\ninterface SuspiciousGroup {\r\n category: string;\r\n severity: string;\r\n count: number;\r\n pattern: string;\r\n rationale: string;\r\n sampleUrls: string[];\r\n recommendedAction: string;\r\n}\r\n\r\ninterface SummaryStats {\r\n highSeverityCount: number;\r\n mediumSeverityCount: number;\r\n lowSeverityCount: number;\r\n totalRiskyUrls: number;\r\n overallStatus: 'clean' | 'issues_found' | 'errors';\r\n}\r\n\r\ninterface RiskSummaryData {\r\n overview: string;\r\n keyFindings: string[];\r\n recommendations: string[];\r\n}\r\n\r\ninterface ErrorDetail {\r\n code: string;\r\n message: string;\r\n context?: Record<string, unknown>;\r\n}\r\n\r\ninterface AnalysisResult {\r\n analysisMetadata: AnalysisMetadata;\r\n sitemapsDiscovered: string[];\r\n totalUrlCount: number;\r\n urlsAnalyzed: number;\r\n suspiciousGroups: SuspiciousGroup[];\r\n riskSummary: RiskSummaryData;\r\n summary: SummaryStats;\r\n errors: ErrorDetail[];\r\n}\r\n\r\n/**\r\n * Generate JSON report from analysis results\r\n */\r\nexport function generateJsonReport(\r\n summary: RiskSummary,\r\n discoveryResult: DiscoveryResult,\r\n parseResult: ParseResult,\r\n riskGroups: RiskGroup[],\r\n config: Config,\r\n startTime: number,\r\n options: JsonReporterOptions = {}\r\n): string {\r\n const {\r\n pretty = true,\r\n indent = 2,\r\n } = options;\r\n \r\n const result = buildAnalysisResult(\r\n summary,\r\n discoveryResult,\r\n parseResult,\r\n riskGroups,\r\n config,\r\n startTime\r\n );\r\n \r\n const jsonOutput = transformToJsonOutput(result);\r\n \r\n if (pretty) {\r\n return JSON.stringify(jsonOutput, null, indent);\r\n } else {\r\n return JSON.stringify(jsonOutput);\r\n }\r\n}\r\n\r\n/**\r\n * Write JSON report to file\r\n */\r\nexport async function writeJsonReport(\r\n summary: RiskSummary,\r\n discoveryResult: DiscoveryResult,\r\n parseResult: ParseResult,\r\n riskGroups: RiskGroup[],\r\n config: Config,\r\n startTime: number,\r\n outputPath: string\r\n): Promise<void> {\r\n const jsonContent = generateJsonReport(\r\n summary,\r\n discoveryResult,\r\n parseResult,\r\n riskGroups,\r\n config,\r\n startTime\r\n );\r\n \r\n await fs.writeFile(outputPath, jsonContent, 'utf-8');\r\n}\r\n\r\n/**\r\n * Build internal analysis result structure\r\n */\r\nfunction buildAnalysisResult(\r\n summary: RiskSummary,\r\n discoveryResult: DiscoveryResult,\r\n parseResult: ParseResult,\r\n riskGroups: RiskGroup[],\r\n config: Config,\r\n startTime: number\r\n): AnalysisResult {\r\n const metadata = buildAnalysisMetadata(\r\n config.baseUrl || 'unknown',\r\n startTime,\r\n summary\r\n );\r\n \r\n const suspiciousGroups = riskGroups.map(group => ({\r\n category: group.category,\r\n severity: group.severity,\r\n count: group.count,\r\n pattern: group.category, // Use category as pattern identifier\r\n rationale: group.rationale,\r\n sampleUrls: group.sampleUrls.slice(0, 5), // Limit to 5 samples\r\n recommendedAction: group.recommendedAction\r\n }));\r\n \r\n const summaryStats: SummaryStats = {\r\n highSeverityCount: summary.severityBreakdown.high,\r\n mediumSeverityCount: summary.severityBreakdown.medium,\r\n lowSeverityCount: summary.severityBreakdown.low,\r\n totalRiskyUrls: riskGroups.reduce((sum, g) => sum + g.count, 0),\r\n overallStatus: determineOverallStatus(\r\n summary.severityBreakdown,\r\n parseResult.errors\r\n )\r\n };\r\n \r\n const riskSummary: RiskSummaryData = {\r\n overview: summary.overview,\r\n keyFindings: summary.keyFindings,\r\n recommendations: summary.recommendations\r\n };\r\n \r\n const errors = parseResult.errors.map(transformError);\r\n \r\n return {\r\n analysisMetadata: metadata,\r\n sitemapsDiscovered: discoveryResult.sitemaps,\r\n totalUrlCount: parseResult.totalCount,\r\n urlsAnalyzed: parseResult.totalCount,\r\n suspiciousGroups,\r\n riskSummary,\r\n summary: summaryStats,\r\n errors\r\n };\r\n}\r\n\r\n/**\r\n * Build analysis metadata\r\n */\r\nfunction buildAnalysisMetadata(\r\n baseUrl: string,\r\n startTime: number,\r\n summary: RiskSummary\r\n): AnalysisMetadata {\r\n return {\r\n baseUrl,\r\n analysisTimestamp: new Date().toISOString(),\r\n toolVersion: TOOL_VERSION,\r\n executionTimeMs: Date.now() - startTime,\r\n analysisType: summary.generatedBy\r\n };\r\n}\r\n\r\n/**\r\n * Determine overall status based on severity and errors\r\n */\r\nfunction determineOverallStatus(\r\n severityBreakdown: { high: number; medium: number; low: number },\r\n errors: Error[]\r\n): 'clean' | 'issues_found' | 'errors' {\r\n if (errors.length > 0) {\r\n return 'errors';\r\n }\r\n \r\n const totalIssues = severityBreakdown.high + severityBreakdown.medium + severityBreakdown.low;\r\n \r\n return totalIssues > 0 ? 'issues_found' : 'clean';\r\n}\r\n\r\n/**\r\n * Transform internal camelCase structure to external snake_case JSON\r\n */\r\nfunction transformToJsonOutput(result: AnalysisResult): object {\r\n return {\r\n analysis_metadata: transformMetadata(result.analysisMetadata),\r\n sitemaps_discovered: result.sitemapsDiscovered,\r\n total_url_count: result.totalUrlCount,\r\n urls_analyzed: result.urlsAnalyzed,\r\n suspicious_groups: result.suspiciousGroups.map(transformGroup),\r\n risk_summary: transformRiskSummary(result.riskSummary),\r\n summary: transformSummary(result.summary),\r\n errors: result.errors\r\n };\r\n}\r\n\r\n/**\r\n * Transform metadata to snake_case\r\n */\r\nfunction transformMetadata(meta: AnalysisMetadata): object {\r\n return {\r\n base_url: meta.baseUrl,\r\n analysis_timestamp: meta.analysisTimestamp,\r\n tool_version: meta.toolVersion,\r\n execution_time_ms: meta.executionTimeMs,\r\n analysis_type: meta.analysisType\r\n };\r\n}\r\n\r\n/**\r\n * Transform suspicious group to snake_case\r\n */\r\nfunction transformGroup(group: SuspiciousGroup): object {\r\n return {\r\n category: group.category,\r\n severity: group.severity,\r\n count: group.count,\r\n pattern: group.pattern,\r\n rationale: group.rationale,\r\n sample_urls: group.sampleUrls,\r\n recommended_action: group.recommendedAction\r\n };\r\n}\r\n\r\n/**\r\n * Transform risk summary to snake_case\r\n */\r\nfunction transformRiskSummary(summary: RiskSummaryData): object {\r\n return {\r\n overview: summary.overview,\r\n key_findings: summary.keyFindings,\r\n recommendations: summary.recommendations\r\n };\r\n}\r\n\r\n/**\r\n * Transform summary stats to snake_case\r\n */\r\nfunction transformSummary(summary: SummaryStats): object {\r\n return {\r\n high_severity_count: summary.highSeverityCount,\r\n medium_severity_count: summary.mediumSeverityCount,\r\n low_severity_count: summary.lowSeverityCount,\r\n total_risky_urls: summary.totalRiskyUrls,\r\n overall_status: summary.overallStatus\r\n };\r\n}\r\n\r\n/**\r\n * Transform error to structured format\r\n */\r\nfunction transformError(error: Error): ErrorDetail {\r\n // Handle custom error types\r\n if ('code' in error) {\r\n const customError = error as any;\r\n const errorDetail: ErrorDetail = {\r\n code: customError.code || 'UNKNOWN_ERROR',\r\n message: error.message\r\n };\r\n \r\n // Add context for specific error types\r\n if ('attemptedPaths' in customError) {\r\n errorDetail.context = {\r\n attempted_paths: customError.attemptedPaths\r\n };\r\n } else if ('sitemapUrl' in customError && 'lineNumber' in customError) {\r\n errorDetail.context = {\r\n sitemap_url: customError.sitemapUrl,\r\n line_number: customError.lineNumber\r\n };\r\n } else if ('url' in customError) {\r\n errorDetail.context = {\r\n url: customError.url\r\n };\r\n }\r\n \r\n return errorDetail;\r\n }\r\n \r\n // Generic error\r\n return {\r\n code: 'UNKNOWN_ERROR',\r\n message: error.message\r\n };\r\n}\r\n","import { promises as fs } from 'fs';\r\nimport type { RiskSummary, CategoryInsight } from '@/summarizer';\r\nimport type { DiscoveryResult } from '@/core/discovery';\r\nimport type { Config } from '@/types/config';\r\n\r\n// Version is injected at build time by tsup\r\ndeclare const __PACKAGE_VERSION__: string;\r\nconst TOOL_VERSION = __PACKAGE_VERSION__;\r\n\r\nexport interface HtmlReporterOptions {\r\n maxUrlsPerGroup?: number; // Max sample URLs to display (default: 10)\r\n}\r\n\r\n/**\r\n * Generate HTML report from analysis results\r\n */\r\nexport function generateHtmlReport(\r\n summary: RiskSummary,\r\n discoveryResult: DiscoveryResult,\r\n totalUrls: number,\r\n config: Config,\r\n errors: Error[],\r\n options: HtmlReporterOptions = {}\r\n): string {\r\n const maxUrls = options.maxUrlsPerGroup ?? 10;\r\n const timestamp = new Date().toISOString();\r\n const riskyUrlCount = summary.categoryInsights.reduce((sum, g) => sum + g.count, 0);\r\n\r\n // Group by severity\r\n const highSeverity = summary.categoryInsights.filter((g) => g.severity === 'high');\r\n const mediumSeverity = summary.categoryInsights.filter((g) => g.severity === 'medium');\r\n const lowSeverity = summary.categoryInsights.filter((g) => g.severity === 'low');\r\n\r\n const html = `<!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 QA Report - ${config.baseUrl}</title>\r\n <style>\r\n * { margin: 0; padding: 0; box-sizing: border-box; }\r\n body {\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\r\n line-height: 1.6;\r\n color: #1f2937;\r\n background: #ffffff;\r\n padding: 24px;\r\n }\r\n .container {\r\n max-width: 1400px;\r\n margin: 0 auto;\r\n background: white;\r\n box-shadow: 0 1px 3px rgba(0,0,0,0.05);\r\n border-radius: 12px;\r\n overflow: hidden;\r\n border: 1px solid #e5e7eb;\r\n }\r\n .header {\r\n background: #0f172a;\r\n color: white;\r\n padding: 48px 40px;\r\n border-bottom: 3px solid #3b82f6;\r\n }\r\n .header h1 { \r\n font-size: 1.875rem; \r\n font-weight: 700;\r\n margin-bottom: 12px;\r\n letter-spacing: -0.025em;\r\n }\r\n .header .meta { \r\n opacity: 0.75; \r\n font-size: 0.875rem;\r\n font-weight: 400;\r\n }\r\n .summary {\r\n display: grid;\r\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\r\n gap: 1px;\r\n background: #e5e7eb;\r\n border-bottom: 1px solid #e5e7eb;\r\n }\r\n .summary-card {\r\n background: white;\r\n padding: 28px 32px;\r\n text-align: center;\r\n }\r\n .summary-card .label { \r\n font-size: 0.75rem; \r\n color: #6b7280;\r\n text-transform: uppercase; \r\n letter-spacing: 0.05em;\r\n font-weight: 600;\r\n margin-bottom: 8px;\r\n }\r\n .summary-card .value { \r\n font-size: 2.25rem; \r\n font-weight: 700;\r\n color: #0f172a;\r\n font-variant-numeric: tabular-nums;\r\n }\r\n .content { padding: 40px; }\r\n .status-clean {\r\n text-align: center;\r\n padding: 80px 32px;\r\n background: #f0fdf4;\r\n border-radius: 8px;\r\n border: 1px solid #86efac;\r\n }\r\n .status-clean h2 { \r\n font-size: 1.875rem;\r\n margin-bottom: 12px;\r\n color: #166534;\r\n font-weight: 700;\r\n }\r\n .status-clean p { \r\n font-size: 1rem;\r\n color: #65a30d;\r\n }\r\n .severity-section { margin-bottom: 32px; }\r\n .severity-section h2 {\r\n font-size: 1.125rem;\r\n font-weight: 600;\r\n padding: 16px 20px;\r\n margin-bottom: 16px;\r\n border-radius: 8px;\r\n display: flex;\r\n align-items: center;\r\n gap: 12px;\r\n cursor: pointer;\r\n user-select: none;\r\n transition: all 0.2s;\r\n }\r\n .severity-section h2:hover {\r\n opacity: 0.85;\r\n transform: translateY(-1px);\r\n }\r\n .severity-section h2::after {\r\n content: 'ā¼';\r\n margin-left: auto;\r\n font-size: 0.8em;\r\n transition: transform 0.3s ease;\r\n opacity: 0.7;\r\n }\r\n .severity-section h2.collapsed::after {\r\n transform: rotate(-90deg);\r\n }\r\n .severity-section h2.collapsed {\r\n margin-bottom: 0;\r\n }\r\n .severity-content {\r\n max-height: none;\r\n overflow: visible;\r\n transition: max-height 0.4s ease-out, opacity 0.3s ease-out;\r\n opacity: 1;\r\n }\r\n .severity-content.collapsed {\r\n max-height: 0;\r\n overflow: hidden;\r\n opacity: 0;\r\n }\r\n .severity-high { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }\r\n .severity-medium { background: #fffbeb; color: #d97706; border: 1px solid #fde68a; }\r\n .severity-low { background: #eff6ff; color: #2563eb; border: 1px solid #dbeafe; }\r\n .risk-group {\r\n background: white;\r\n border: 1px solid #e5e7eb;\r\n border-radius: 8px;\r\n padding: 24px;\r\n margin-bottom: 16px;\r\n }\r\n .risk-group h3 {\r\n font-size: 1rem;\r\n margin-bottom: 12px;\r\n color: #0f172a;\r\n font-weight: 600;\r\n }\r\n .risk-group .count {\r\n display: inline-block;\r\n background: #3b82f6;\r\n color: white;\r\n padding: 2px 10px;\r\n border-radius: 9999px;\r\n font-size: 0.75rem;\r\n font-weight: 600;\r\n margin-left: 8px;\r\n }\r\n .risk-group .impact {\r\n color: #64748b;\r\n margin-bottom: 16px;\r\n font-size: 0.875rem;\r\n line-height: 1.6;\r\n }\r\n .risk-group .urls {\r\n background: #f8fafc;\r\n border: 1px solid #e2e8f0;\r\n border-radius: 6px;\r\n padding: 16px;\r\n }\r\n .risk-group .urls h4 {\r\n font-size: 0.75rem;\r\n color: #64748b;\r\n margin-bottom: 12px;\r\n text-transform: uppercase;\r\n letter-spacing: 0.05em;\r\n font-weight: 600;\r\n }\r\n .risk-group .urls ul { list-style: none; }\r\n .risk-group .urls li {\r\n padding: 10px 12px;\r\n border-bottom: 1px solid #e2e8f0;\r\n font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Consolas', monospace;\r\n font-size: 0.8125rem;\r\n color: #334155;\r\n background: white;\r\n margin-bottom: 4px;\r\n border-radius: 4px;\r\n word-break: break-all;\r\n line-height: 1.6;\r\n }\r\n .risk-group .urls li:last-child { border-bottom: none; margin-bottom: 0; }\r\n .risk-group .more {\r\n color: #3b82f6;\r\n font-style: italic;\r\n margin-top: 8px;\r\n font-size: 0.8125rem;\r\n }\r\n .download-btn {\r\n display: inline-block;\r\n background: #3b82f6;\r\n color: white;\r\n padding: 8px 16px;\r\n border-radius: 6px;\r\n text-decoration: none;\r\n font-size: 0.8125rem;\r\n font-weight: 500;\r\n margin-top: 12px;\r\n cursor: pointer;\r\n border: none;\r\n transition: all 0.15s;\r\n }\r\n .download-btn:hover {\r\n background: #2563eb;\r\n transform: translateY(-1px);\r\n box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);\r\n }\r\n .footer {\r\n background: #f8fafc;\r\n padding: 24px 40px;\r\n border-top: 1px solid #e5e7eb;\r\n text-align: center;\r\n color: #64748b;\r\n font-size: 0.8125rem;\r\n }\r\n .sitemaps {\r\n background: white;\r\n border: 1px solid #e5e7eb;\r\n border-radius: 8px;\r\n margin-bottom: 24px;\r\n overflow: hidden;\r\n }\r\n .sitemaps h3 {\r\n font-size: 1.125rem;\r\n font-weight: 600;\r\n padding: 16px 20px;\r\n margin: 0;\r\n color: #0f172a;\r\n background: #f8fafc;\r\n cursor: pointer;\r\n user-select: none;\r\n transition: all 0.15s;\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n }\r\n .sitemaps h3:hover {\r\n background: #f1f5f9;\r\n }\r\n .sitemaps h3::after {\r\n content: 'ā¼';\r\n margin-left: auto;\r\n font-size: 0.8em;\r\n transition: transform 0.3s ease;\r\n opacity: 0.7;\r\n }\r\n .sitemaps h3.collapsed::after {\r\n transform: rotate(-90deg);\r\n }\r\n .sitemaps-content {\r\n max-height: none;\r\n overflow: visible;\r\n transition: max-height 0.4s ease-out, opacity 0.3s ease-out;\r\n opacity: 1;\r\n padding: 20px;\r\n }\r\n .sitemaps-content.collapsed {\r\n max-height: 0;\r\n overflow: hidden;\r\n opacity: 0;\r\n padding: 0 20px;\r\n }\r\n .sitemaps ul { list-style: none; }\r\n .sitemaps li {\r\n padding: 10px 12px;\r\n font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Consolas', monospace;\r\n font-size: 0.8125rem;\r\n color: #475569;\r\n word-break: break-all;\r\n line-height: 1.6;\r\n background: #f8fafc;\r\n margin-bottom: 4px;\r\n border-radius: 4px;\r\n }\r\n .sitemaps li:last-child { margin-bottom: 0; }\r\n .errors-section {\r\n background: #fffbeb;\r\n border-left: 4px solid #f59e0b;\r\n padding: 20px;\r\n margin-bottom: 24px;\r\n border-radius: 8px;\r\n border: 1px solid #fde68a;\r\n }\r\n .errors-section h3 {\r\n color: #92400e;\r\n margin-bottom: 16px;\r\n font-size: 1.125rem;\r\n font-weight: 600;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n .errors-section ul {\r\n list-style: none;\r\n padding: 0;\r\n }\r\n .errors-section li {\r\n padding: 12px;\r\n background: white;\r\n margin-bottom: 8px;\r\n border-radius: 6px;\r\n font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Consolas', monospace;\r\n font-size: 0.8125rem;\r\n color: #78350f;\r\n word-break: break-all;\r\n line-height: 1.6;\r\n border: 1px solid #fde68a;\r\n }\r\n .errors-section li:last-child {\r\n margin-bottom: 0;\r\n }\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"container\">\r\n <div class=\"header\">\r\n <h1>Sitemap Analysis</h1>\r\n <div class=\"meta\">\r\n <div>${config.baseUrl}</div>\r\n <div>${new Date(timestamp).toLocaleString()}</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"summary\">\r\n <div class=\"summary-card\">\r\n <div class=\"label\">Sitemaps</div>\r\n <div class=\"value\">${discoveryResult.sitemaps.length}</div>\r\n </div>\r\n <div class=\"summary-card\">\r\n <div class=\"label\">URLs Analyzed</div>\r\n <div class=\"value\">${totalUrls.toLocaleString()}</div>\r\n </div>\r\n <div class=\"summary-card\">\r\n <div class=\"label\">Issues Found</div>\r\n <div class=\"value\" style=\"color: ${riskyUrlCount > 0 ? '#dc2626' : '#059669'}\">${riskyUrlCount}</div>\r\n </div>\r\n <div class=\"summary-card\">\r\n <div class=\"label\">Scan Time</div>\r\n <div class=\"value\">${(summary.metadata.processingTime / 1000).toFixed(1)}s</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"content\">\r\n ${errors.length > 0 ? `\r\n <div class=\"errors-section\">\r\n <h3>Parsing Errors & Warnings (${errors.length})</h3>\r\n <ul>\r\n ${errors.map(err => `<li>${err.message}</li>`).join('\\n ')}\r\n </ul>\r\n </div>\r\n ` : ''}\r\n\r\n ${discoveryResult.sitemaps.length > 0 ? `\r\n <div class=\"sitemaps\">\r\n <h3 class=\"collapsed\" onclick=\"toggleSection(this)\">Sitemaps Discovered (${discoveryResult.sitemaps.length})</h3>\r\n <div class=\"sitemaps-content collapsed\">\r\n <ul>\r\n ${discoveryResult.sitemaps.map(s => `<li>⢠${s}</li>`).join('\\n ')}\r\n </ul>\r\n </div>\r\n </div>\r\n ` : ''}\r\n\r\n ${riskyUrlCount === 0 ? `\r\n <div class=\"status-clean\">\r\n <h2>No Issues Found</h2>\r\n <p>All URLs in the sitemap passed validation checks.</p>\r\n </div>\r\n ` : ''}\r\n\r\n ${highSeverity.length > 0 ? `\r\n <div class=\"severity-section\">\r\n <h2 class=\"severity-high\" onclick=\"toggleSection(this)\">High Severity (${highSeverity.reduce((sum, g) => sum + g.count, 0)} URLs)</h2>\r\n <div class=\"severity-content\">\r\n ${highSeverity.map(group => renderRiskGroup(group, maxUrls)).join('\\n ')}\r\n </div>\r\n </div>\r\n ` : ''}\r\n\r\n ${mediumSeverity.length > 0 ? `\r\n <div class=\"severity-section\">\r\n <h2 class=\"severity-medium\" onclick=\"toggleSection(this)\">Medium Severity (${mediumSeverity.reduce((sum, g) => sum + g.count, 0)} URLs)</h2>\r\n <div class=\"severity-content\">\r\n ${mediumSeverity.map(group => renderRiskGroup(group, maxUrls)).join('\\n ')}\r\n </div>\r\n </div>\r\n ` : ''}\r\n\r\n ${lowSeverity.length > 0 ? `\r\n <div class=\"severity-section\">\r\n <h2 class=\"severity-low\" onclick=\"toggleSection(this)\">Low Severity (${lowSeverity.reduce((sum, g) => sum + g.count, 0)} URLs)</h2>\r\n <div class=\"severity-content\">\r\n ${lowSeverity.map(group => renderRiskGroup(group, maxUrls)).join('\\n ')}\r\n </div>\r\n </div>\r\n ` : ''}\r\n </div>\r\n\r\n <div class=\"footer\">\r\n Generated by <strong>sitemap-qa</strong> v${TOOL_VERSION}\r\n </div>\r\n </div>\r\n \r\n <script>\r\n function toggleSection(header) {\r\n header.classList.toggle('collapsed');\r\n const content = header.nextElementSibling;\r\n content.classList.toggle('collapsed');\r\n }\r\n \r\n function downloadUrls(categorySlug, encodedUrls) {\r\n // Decode HTML entities and parse JSON\r\n const textarea = document.createElement('textarea');\r\n textarea.innerHTML = encodedUrls;\r\n const urls = JSON.parse(textarea.value);\r\n \r\n // Create text content (one URL per line)\r\n const textContent = urls.join('\\\\n');\r\n \r\n // Create blob and download\r\n const blob = new Blob([textContent], { type: 'text/plain' });\r\n const url = URL.createObjectURL(blob);\r\n const a = document.createElement('a');\r\n a.href = url;\r\n a.download = categorySlug + '_urls.txt';\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n URL.revokeObjectURL(url);\r\n }\r\n </script>\r\n</body>\r\n</html>`;\r\n\r\n return html;\r\n}\r\n\r\nfunction renderRiskGroup(group: CategoryInsight, maxUrls: number): string {\r\n const categoryTitle = group.category\r\n .split('_')\r\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\r\n .join(' ');\r\n\r\n const urlsToShow = group.examples.slice(0, maxUrls);\r\n const remaining = group.count - urlsToShow.length;\r\n \r\n // Create sanitized filename for download\r\n const categorySlug = group.category.toLowerCase();\r\n \r\n // Encode all URLs as data for download functionality\r\n const allUrlsJson = JSON.stringify(group.allUrls);\r\n const encodedUrls = escapeHtml(allUrlsJson);\r\n\r\n return `<div class=\"risk-group\">\r\n <h3>${categoryTitle} <span class=\"count\">${group.count} URLs</span></h3>\r\n <div class=\"impact\">${group.summary}</div>\r\n <div class=\"urls\">\r\n <h4>Sample URLs</h4>\r\n <ul>\r\n ${urlsToShow.map(url => `<li>${escapeHtml(url)}</li>`).join('\\n ')}\r\n </ul>\r\n ${remaining > 0 ? `<div class=\"more\">... and ${remaining} more</div>` : ''}\r\n <button class=\"download-btn\" onclick=\"downloadUrls('${categorySlug}', '${encodedUrls}')\">š„ Download All ${group.count} URLs</button>\r\n </div>\r\n </div>`;\r\n}\r\n\r\nfunction escapeHtml(text: string): string {\r\n return text\r\n .replace(/&/g, '&')\r\n .replace(/</g, '<')\r\n .replace(/>/g, '>')\r\n .replace(/\"/g, '"')\r\n .replace(/'/g, ''');\r\n}\r\n\r\n/**\r\n * Write HTML report to file\r\n */\r\nexport async function writeHtmlReport(\r\n summary: RiskSummary,\r\n discoveryResult: DiscoveryResult,\r\n totalUrls: number,\r\n config: Config,\r\n outputPath: string,\r\n errors: Error[],\r\n options: HtmlReporterOptions = {}\r\n): Promise<void> {\r\n const htmlContent = generateHtmlReport(summary, discoveryResult, totalUrls, config, errors, options);\r\n await fs.writeFile(outputPath, htmlContent, 'utf-8');\r\n}\r\n"],"mappings":"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C,EAAqC,CAChD,CACE,KAAM,aACN,SAAU,cACV,SAAU,OACV,MAAO,sCACP,YAAa,yDACf,EACA,CACE,KAAM,iBACN,SAAU,cACV,SAAU,OACV,MAAO,0BACP,YAAa,2CACf,EACA,CACE,KAAM,cACN,SAAU,cACV,SAAU,OACV,MAAO,uCACP,YAAa,0DACf,EACA,CACE,KAAM,eACN,SAAU,cACV,SAAU,OACV,MAAO,wBACP,YAAa,yCACf,EACA,CACE,KAAM,qBACN,SAAU,cACV,SAAU,OACV,MAAO,uCACP,YAAa,8CACf,CACF,EAIaC,EAA2C,CACtD,CACE,KAAM,wBACN,SAAU,mBACV,SAAU,SACV,MAAO,gBACP,YAAa,qGACf,CACF,EAEaC,GAA0C,CACrD,CACE,KAAM,iCACN,SAAU,mBACV,SAAU,OACV,MAAO,kDACP,YAAa,kDACf,EACA,CACE,KAAM,oBACN,SAAU,mBACV,SAAU,OACV,MAAO,6BACP,YAAa,qCACf,EACA,CACE,KAAM,qBACN,SAAU,mBACV,SAAU,OACV,MAAO,8BACP,YAAa,sCACf,EACA,CACE,KAAM,mBACN,SAAU,mBACV,SAAU,OACV,MAAO,+BACP,YAAa,0CACf,EACA,CACE,KAAM,oBACN,SAAU,mBACV,SAAU,OACV,MAAO,gCACP,YAAa,gDACf,EACA,CACE,KAAM,wBACN,SAAU,mBACV,SAAU,OACV,MAAO,oBACP,YAAa,yCACf,EACA,CACE,KAAM,kBACN,SAAU,mBACV,SAAU,SACV,MAAO,8BACP,YAAa,mDACf,EACA,CACE,KAAM,sBACN,SAAU,mBACV,SAAU,SACV,MAAO,iCACP,YAAa,qCACf,CACF,EC7GO,SAASC,GAAYC,EAAqB,CAC/C,GAAI,CACF,IAAMC,EAAS,IAAI,IAAID,CAAG,EACpBE,EAAkB,CACtB,QAAS,OAAQ,aAAc,eAAgB,YAC/C,SAAU,UAAW,MACrB,WAAY,SAAU,MACtB,SAAU,gBACV,UAAW,YAAa,MACxB,aACF,EAEA,QAAWC,KAASD,EACdD,EAAO,aAAa,IAAIE,CAAK,GAC/BF,EAAO,aAAa,IAAIE,EAAO,YAAY,EAI/C,OAAOF,EAAO,SAAS,CACzB,MAAQ,CAEN,OAAOD,CACT,CACF,CCHA,SAASI,GACPC,EACAC,EACAC,EACkD,CAClD,OAAQF,EAAU,CAChB,IAAK,sBACH,MAAO,CACL,UAAW,+BAA+BE,CAAK,iIAC/C,kBAAmB,kIACrB,EAEF,IAAK,cACH,MAAO,CACL,UAAW,GAAGA,CAAK,kIACnB,kBAAmB,wIACrB,EAEF,IAAK,mBACH,MAAO,CACL,UAAW,GAAGA,CAAK,iHACnB,kBAAmB,+IACrB,EAEF,IAAK,eACH,MAAO,CACL,UAAW,GAAGA,CAAK,0HACnB,kBAAmB,0HACrB,EAEF,IAAK,mBACH,MAAO,CACL,UAAW,GAAGA,CAAK,mJACnB,kBAAmB,yIACrB,EAEF,IAAK,yBACH,MAAO,CACL,UAAW,GAAGA,CAAK,iHACnB,kBAAmB,mHACrB,EAEF,IAAK,kBACH,MAAO,CACL,UAAW,GAAGA,CAAK,kHACnB,kBAAmB,iIACrB,EAEF,QACE,MAAO,CACL,UAAW,GAAGA,CAAK,gCAAgCF,CAAQ,GAC3D,kBAAmB,uDACrB,CACJ,CACF,CAEO,SAASG,EACdC,EACAC,EAAwB,EACJ,CAEpB,IAAMC,EAAc,IAAI,IAExB,QAAWC,KAAWH,EACfE,EAAY,IAAIC,EAAQ,QAAQ,GACnCD,EAAY,IAAIC,EAAQ,SAAU,CAAC,CAAC,EAEtCD,EAAY,IAAIC,EAAQ,QAAQ,EAAG,KAAKA,CAAO,EAIjD,IAAMC,EAAsB,CAAC,EAE7B,OAAW,CAACR,EAAUS,CAAgB,IAAKH,EAAY,QAAQ,EAAG,CAEhE,IAAMI,EAAa,MAAM,KAAK,IAAI,IAAID,EAAiB,IAAI,GAAK,EAAE,GAAG,CAAC,CAAC,EAGjEE,EAAWF,EAAiB,OAAO,CAACG,EAASL,IAAY,CAC7D,IAAMM,EAA4B,CAAC,MAAO,SAAU,MAAM,EAC1D,OAAOA,EAAc,QAAQN,EAAQ,QAAQ,EAAIM,EAAc,QAAQD,CAAO,EAC1EL,EAAQ,SACRK,CACN,EAAG,KAAiB,EAGdE,EAAaJ,EAAW,MAAM,EAAGL,CAAa,EAG9C,CAAE,UAAAU,EAAW,kBAAAC,CAAkB,EAAIjB,GAAuBC,EAAUW,EAAUD,EAAW,MAAM,EAErGF,EAAO,KAAK,CACV,SAAAR,EACA,SAAAW,EACA,MAAOD,EAAW,OAClB,UAAAK,EACA,WAAAD,EACA,kBAAAE,EACA,QAASN,CACX,CAAC,CACH,CAGAF,EAAO,KAAK,CAACS,EAAGC,IAAM,CACpB,IAAML,EAA4B,CAAC,OAAQ,SAAU,KAAK,EAC1D,OAAOA,EAAc,QAAQI,EAAE,QAAQ,EAAIJ,EAAc,QAAQK,EAAE,QAAQ,CAC7E,CAAC,EAGD,IAAMC,EAAgB,IAAI,IAAIf,EAAS,IAAIgB,GAAKA,EAAE,GAAG,CAAC,EAAE,KAClDC,EAAoBb,EAAO,OAAOc,GAAKA,EAAE,WAAa,MAAM,EAAE,OAAO,CAACC,EAAKD,IAAMC,EAAMD,EAAE,MAAO,CAAC,EACjGE,EAAsBhB,EAAO,OAAOc,GAAKA,EAAE,WAAa,QAAQ,EAAE,OAAO,CAACC,EAAKD,IAAMC,EAAMD,EAAE,MAAO,CAAC,EACrGG,EAAmBjB,EAAO,OAAOc,GAAKA,EAAE,WAAa,KAAK,EAAE,OAAO,CAACC,EAAKD,IAAMC,EAAMD,EAAE,MAAO,CAAC,EAErG,MAAO,CACL,OAAAd,EACA,cAAAW,EACA,kBAAAE,EACA,oBAAAG,EACA,iBAAAC,CACF,CACF,CC7FA,eAAsBC,GACpBC,EACAC,EACAC,EAC8B,CAC9B,IAAMC,EAAY,KAAK,IAAI,EACrBC,EAA0B,CAAC,EAG3BC,EAAgBC,EAA4BL,CAAO,EACnDM,EAAc,CAClB,GAAGC,EACH,GAAGC,EACH,GAAGC,EACH,GAAGC,GACH,GAAGC,EACHP,CACF,EAGMQ,EAA6B,CAAC,EACpC,GAAIX,EAAO,kBAAoBA,EAAO,iBAAiB,OAAS,EAC9D,QAAWY,KAAWZ,EAAO,iBAC3B,GAAI,CAKF,IAAIa,EAAeD,EAChB,QAAQ,qBAAsB,MAAM,EACpC,QAAQ,MAAO,OAAO,EAGrB,CAACC,EAAa,SAAS,GAAG,GAAK,CAACA,EAAa,SAAS,KAAK,IAC7DA,EAAeA,EAAe,iBAGhCF,EAAiB,KAAK,IAAI,OAAOE,EAAc,GAAG,CAAC,CACrD,MAAgB,CACVb,EAAO,SACT,QAAQ,KAAK,6BAA6BY,CAAO,EAAE,CAEvD,CAIJ,GAAIZ,EAAO,QAAS,CAClB,QAAQ,IAAI;AAAA,YAAeF,EAAK,MAAM,4BAA4B,EAClE,GAAI,CACF,QAAQ,IAAI,gBAAgB,IAAI,IAAIC,CAAO,EAAE,QAAQ,EAAE,CACzD,MAAgB,CACd,QAAQ,IAAI,aAAaA,CAAO,EAAE,CACpC,CACIY,EAAiB,OAAS,GAC5B,QAAQ,IAAI,sBAAsBA,EAAiB,MAAM,EAAE,CAE/D,CAGA,IAAIG,EACJ,GAAI,CACFA,EAAmB,IAAI,IAAIf,CAAO,EAAE,QACtC,MAAgB,CACVC,EAAO,SACT,QAAQ,KAAK,qBAAqBD,CAAO,wBAAwB,EAEnEe,EAAmB,QACrB,CAEA,IAAIC,EAAY,EAEhB,QAAWC,KAAYlB,EAAM,CAC3B,IAAMmB,EAAMD,EAAS,IACrBD,KAGIA,EAAY,MAAU,GAAKA,IAAcjB,EAAK,SAChD,QAAQ,OAAO,MAAM,wBAAwBiB,EAAU,eAAe,CAAC,IAAIjB,EAAK,OAAO,eAAe,CAAC,UAAU,EAInH,IAAIoB,EAAa,GACjB,QAAWC,KAAmBR,EAC5B,GAAIQ,EAAgB,KAAKF,CAAG,EAAG,CAC7BC,EAAa,GACb,KACF,CAGF,GAAI,CAAAA,EAKJ,QAAWN,KAAWP,EAEpB,GAAIO,EAAQ,WAAa,yBACvB,GAAI,CACF,IAAMQ,EAAc,IAAI,IAAIH,CAAG,EAAE,SAC7BH,IAAqB,UAAYM,IAAgB,SACnDlB,EAAS,KAAK,CACZ,IAAAe,EACA,SAAUL,EAAQ,SAClB,SAAUA,EAAQ,SAClB,QAASA,EAAQ,KACjB,UAAWA,EAAQ,YACnB,aAAc,SAChB,CAAC,CAEL,MAAgB,CAEVZ,EAAO,SACT,QAAQ,KAAK,yBAAyBiB,CAAG,EAAE,EAE7C,QACF,KAGA,IAAI,CACF,IAAMI,EAAQJ,EAAI,MAAML,EAAQ,KAAK,EACjCS,GACFnB,EAAS,KAAK,CACZ,IAAKU,EAAQ,WAAa,mBAAqBU,GAAYL,CAAG,EAAIA,EAClE,SAAUL,EAAQ,SAClB,SAAUA,EAAQ,SAClB,QAASA,EAAQ,KACjB,UAAWA,EAAQ,YACnB,aAAcS,EAAM,CAAC,CACvB,CAAC,CAEL,OAASE,EAAO,CACVvB,EAAO,SACT,QAAQ,MAAM,+BAA+BY,EAAQ,IAAI,KAAKW,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,EAExH,QACF,CAGN,CAGIzB,EAAK,QAAU,KACjB,QAAQ,OAAO,MAAM,UAAU,EAIjC,IAAM0B,EAAiBC,EAAkBvB,CAAQ,EAE3CwB,EAAmB,KAAK,IAAI,EAAIzB,EAEtC,GAAID,EAAO,UACT,QAAQ,IAAI;AAAA,cAAiB,EAC7B,QAAQ,IAAI,4BAA4BF,EAAK,MAAM,EAAE,EACrD,QAAQ,IAAI,wBAAwB0B,EAAe,aAAa,EAAE,EAClE,QAAQ,IAAI,sBAAsBA,EAAe,iBAAiB,EAAE,EACpE,QAAQ,IAAI,wBAAwBA,EAAe,mBAAmB,EAAE,EACxE,QAAQ,IAAI,qBAAqBA,EAAe,gBAAgB,EAAE,EAClE,QAAQ,IAAI,wBAAwBE,CAAgB,IAAI,EAEpDF,EAAe,OAAO,OAAS,GAAG,CACpC,QAAQ,IAAI;AAAA,uBAA0B,EACtC,QAAWG,KAASH,EAAe,OACjC,QAAQ,IAAI,OAAOG,EAAM,QAAQ,KAAKA,EAAM,KAAK,UAAUA,EAAM,SAAS,YAAY,CAAC,GAAG,CAE9F,CAGF,MAAO,CACL,SAAAzB,EACA,OAAQsB,EAAe,OACvB,kBAAmB1B,EAAK,OACxB,aAAc0B,EAAe,cAC7B,cAAe1B,EAAK,OAAS0B,EAAe,cAC5C,kBAAmBA,EAAe,kBAClC,oBAAqBA,EAAe,oBACpC,iBAAkBA,EAAe,iBACjC,iBAAAE,CACF,CACF,CC9LO,SAASE,GAAeC,EAA0C,CACvE,IAAMC,EAAoB,CACxB,KAAM,EACN,OAAQ,EACR,IAAK,CACP,EAEMC,EAAsCF,EAAQ,WAAW,IAAIG,GAAS,CAC1EF,EAAkBE,EAAM,QAAQ,GAAKA,EAAM,MAE3C,IAAMC,EAAOD,EAAM,SAAWA,EAAM,WAEpC,MAAO,CACL,SAAUA,EAAM,SAChB,MAAOA,EAAM,MACb,SAAUA,EAAM,SAChB,QAASA,EAAM,UACf,SAAUC,EAAK,MAAM,EAAG,CAAC,EACzB,QAASA,CACX,CACF,CAAC,EAEKC,EAAaL,EAAQ,WAAW,OAAO,CAACM,EAAKC,IAAMD,EAAMC,EAAE,MAAO,CAAC,EACnEC,EAAWH,EAAa,EAC1B,SAASA,CAAU,kCAAkCL,EAAQ,WAAW,MAAM,kBAAkBA,EAAQ,SAAS,eACjH,YAAYA,EAAQ,SAAS,0CAE3BS,EAAwB,CAAC,EAC/B,OAAIR,EAAkB,KAAO,GAC3BQ,EAAY,KAAK,GAAGR,EAAkB,IAAI,mDAAmD,EAE3FA,EAAkB,OAAS,GAC7BQ,EAAY,KAAK,GAAGR,EAAkB,MAAM,4CAA4C,EAEtFA,EAAkB,IAAM,GAC1BQ,EAAY,KAAK,GAAGR,EAAkB,GAAG,2CAA2C,EAG/E,CACL,SAAAO,EACA,YAAAC,EACA,iBAAAP,EACA,kBAAAD,EACA,gBAAiB,CAAC,EAClB,YAAa,sBACb,SAAU,CACR,WAAY,EACZ,eAAgBD,EAAQ,gBAAkB,EAC1C,MAAO,kBACT,CACF,CACF,CC/EA,IAAMU,GAAe,gBAkEd,SAASC,GACdC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EAA+B,CAAC,EACxB,CACR,GAAM,CACJ,OAAAC,EAAS,GACT,OAAAC,EAAS,CACX,EAAIF,EAEEG,EAASC,GACbV,EACAC,EACAC,EACAC,EACAC,EACAC,CACF,EAEMM,EAAaC,GAAsBH,CAAM,EAE/C,OAAIF,EACK,KAAK,UAAUI,EAAY,KAAMH,CAAM,EAEvC,KAAK,UAAUG,CAAU,CAEpC,CA6BA,SAASE,GACPC,EACAC,EACAC,EACAC,EACAC,EACAC,EACgB,CAChB,IAAMC,EAAWC,GACfH,EAAO,SAAW,UAClBC,EACAL,CACF,EAEMQ,EAAmBL,EAAW,IAAIM,IAAU,CAChD,SAAUA,EAAM,SAChB,SAAUA,EAAM,SAChB,MAAOA,EAAM,MACb,QAASA,EAAM,SACf,UAAWA,EAAM,UACjB,WAAYA,EAAM,WAAW,MAAM,EAAG,CAAC,EACvC,kBAAmBA,EAAM,iBAC3B,EAAE,EAEIC,EAA6B,CACjC,kBAAmBV,EAAQ,kBAAkB,KAC7C,oBAAqBA,EAAQ,kBAAkB,OAC/C,iBAAkBA,EAAQ,kBAAkB,IAC5C,eAAgBG,EAAW,OAAO,CAACQ,EAAKC,IAAMD,EAAMC,EAAE,MAAO,CAAC,EAC9D,cAAeC,GACbb,EAAQ,kBACRE,EAAY,MACd,CACF,EAEMY,EAA+B,CACnC,SAAUd,EAAQ,SAClB,YAAaA,EAAQ,YACrB,gBAAiBA,EAAQ,eAC3B,EAEMe,EAASb,EAAY,OAAO,IAAIc,EAAc,EAEpD,MAAO,CACL,iBAAkBV,EAClB,mBAAoBL,EAAgB,SACpC,cAAeC,EAAY,WAC3B,aAAcA,EAAY,WAC1B,iBAAAM,EACA,YAAAM,EACA,QAASJ,EACT,OAAAK,CACF,CACF,CAKA,SAASR,GACPU,EACAZ,EACAL,EACkB,CAClB,MAAO,CACL,QAAAiB,EACA,kBAAmB,IAAI,KAAK,EAAE,YAAY,EAC1C,YAAaC,GACb,gBAAiB,KAAK,IAAI,EAAIb,EAC9B,aAAcL,EAAQ,WACxB,CACF,CAKA,SAASa,GACPM,EACAJ,EACqC,CACrC,OAAIA,EAAO,OAAS,EACX,SAGWI,EAAkB,KAAOA,EAAkB,OAASA,EAAkB,IAErE,EAAI,eAAiB,OAC5C,CAKA,SAASC,GAAsBC,EAAgC,CAC7D,MAAO,CACL,kBAAmBC,GAAkBD,EAAO,gBAAgB,EAC5D,oBAAqBA,EAAO,mBAC5B,gBAAiBA,EAAO,cACxB,cAAeA,EAAO,aACtB,kBAAmBA,EAAO,iBAAiB,IAAIE,EAAc,EAC7D,aAAcC,GAAqBH,EAAO,WAAW,EACrD,QAASI,GAAiBJ,EAAO,OAAO,EACxC,OAAQA,EAAO,MACjB,CACF,CAKA,SAASC,GAAkBI,EAAgC,CACzD,MAAO,CACL,SAAUA,EAAK,QACf,mBAAoBA,EAAK,kBACzB,aAAcA,EAAK,YACnB,kBAAmBA,EAAK,gBACxB,cAAeA,EAAK,YACtB,CACF,CAKA,SAASH,GAAed,EAAgC,CACtD,MAAO,CACL,SAAUA,EAAM,SAChB,SAAUA,EAAM,SAChB,MAAOA,EAAM,MACb,QAASA,EAAM,QACf,UAAWA,EAAM,UACjB,YAAaA,EAAM,WACnB,mBAAoBA,EAAM,iBAC5B,CACF,CAKA,SAASe,GAAqBxB,EAAkC,CAC9D,MAAO,CACL,SAAUA,EAAQ,SAClB,aAAcA,EAAQ,YACtB,gBAAiBA,EAAQ,eAC3B,CACF,CAKA,SAASyB,GAAiBzB,EAA+B,CACvD,MAAO,CACL,oBAAqBA,EAAQ,kBAC7B,sBAAuBA,EAAQ,oBAC/B,mBAAoBA,EAAQ,iBAC5B,iBAAkBA,EAAQ,eAC1B,eAAgBA,EAAQ,aAC1B,CACF,CAKA,SAASgB,GAAeW,EAA2B,CAEjD,GAAI,SAAUA,EAAO,CACnB,IAAMC,EAAcD,EACdE,EAA2B,CAC/B,KAAMD,EAAY,MAAQ,gBAC1B,QAASD,EAAM,OACjB,EAGA,MAAI,mBAAoBC,EACtBC,EAAY,QAAU,CACpB,gBAAiBD,EAAY,cAC/B,EACS,eAAgBA,GAAe,eAAgBA,EACxDC,EAAY,QAAU,CACpB,YAAaD,EAAY,WACzB,YAAaA,EAAY,UAC3B,EACS,QAASA,IAClBC,EAAY,QAAU,CACpB,IAAKD,EAAY,GACnB,GAGKC,CACT,CAGA,MAAO,CACL,KAAM,gBACN,QAASF,EAAM,OACjB,CACF,CCrUA,IAAAG,GAA+B,cAOzBC,GAAe,gBASd,SAASC,GACdC,EACAC,EACAC,EACAC,EACAC,EACAC,EAA+B,CAAC,EACxB,CACR,IAAMC,EAAUD,EAAQ,iBAAmB,GACrCE,EAAY,IAAI,KAAK,EAAE,YAAY,EACnCC,EAAgBR,EAAQ,iBAAiB,OAAO,CAACS,EAAK,IAAMA,EAAM,EAAE,MAAO,CAAC,EAG5EC,EAAeV,EAAQ,iBAAiB,OAAQW,GAAMA,EAAE,WAAa,MAAM,EAC3EC,EAAiBZ,EAAQ,iBAAiB,OAAQW,GAAMA,EAAE,WAAa,QAAQ,EAC/EE,EAAcb,EAAQ,iBAAiB,OAAQW,GAAMA,EAAE,WAAa,KAAK,EAyb/E,MAvba;AAAA;AAAA;AAAA;AAAA;AAAA,+BAKgBR,EAAO,OAAO;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;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;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;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;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["import_config","import_commander","import_commander","import_fs","import_ora","import_chalk","import_promises","import_fs","import_path","import_os","DEFAULT_CONFIG","loadConfig","cliOptions","config","DEFAULT_CONFIG","globalConfigPath","globalConfig","error","projectConfigPath","projectConfig","envConfig","loadFromEnv","mergeCliOptions","validateConfig","env","merged","p","NetworkError","url","originalError","HttpError","statusCode","statusText","message","import_playwright","fetchUrlWithBrowser","url","timeout","browser","page","originalQuery","parameters","response","statusCode","content","finalUrl","HttpError","error","NetworkError","fetchUrl","options","maxRetries","retryDelay","useBrowser","retryableStatuses","lastError","attemptedBrowser","attempt","controller","timeoutId","body","httpError","delay","resolve","normalizeBaseUrl","url","detectCanonicalDomain","baseUrl","config","urlObj","alternateHostname","alternateUrl","result","fetchUrl","error","HttpError","tryStandardPaths","baseDomain","accessIssues","standardPaths","results","path","sitemapUrl","parseRobotsTxt","robotsUrl","lines","sitemaps","line","match","isSitemapIndex","xmlContent","urlBlockRegex","matches","samplesToCheck","sitemapLikeCount","i","extractSitemapIndexUrls","urls","sitemapBlockRegex","sitemapMatch","locMatch","urlMatch","discoverAllSitemaps","initialSitemaps","canonicalDomain","_maxDepth","finalSitemaps","toProcess","processed","failed","redirected","detectedCanonical","BATCH_SIZE","batch","childUrls","message","sitemapUrlObj","totalProcessed","totalFailed","totalRedirected","sitemapIndexCount","discoverSitemaps","normalizedUrl","allAccessIssues","robotsSitemaps","allSitemaps","detected","standardSitemaps","issues","redirectedToCanonical","canonicalUrl","canonicalRobotsSitemaps","canonicalStandardSitemaps","import_fast_xml_parser","parser","extractUrls","parsedXml","sitemapUrl","urls","urlNodes","node","parseSitemap","xml","errors","validationResult","validationError","parsed","validUrls","entry","parseError","errorMsg","extractAllUrls","sitemapUrls","config","allUrls","allErrors","sitemapsProcessed","sitemapsFailed","results","processInBatches","sitemapUrl","response","fetchUrl","parseResult","parseSitemap","urlsWithTimestamp","url","error","errorMsg","result","items","concurrency","processor","i","batch","batchResults","normalizeUrl","url","parsed","pathname","params","b","sortedParams","mergeUrlEntries","entries","merged","sources","e","lastmods","lm","a","priorities","p","changefreqs","cf","counts","sorted","extractedAts","ea","consolidateUrls","urls","verbose","totalInputUrls","urlMap","entry","normalized","uniqueUrls","duplicateGroups","top5","group","RISK_PATTERNS","escapeRegex","str","extractRootDomain","hostname","parts","createDomainMismatchPattern","baseUrl","options","baseDomain","rootDomain","escapedRoot","pattern","ENVIRONMENT_PATTERNS","ADMIN_PATH_PATTERNS","INTERNAL_CONTENT_PATTERNS","SENSITIVE_PARAM_PATTERNS","sanitizeUrl","url","parsed","sensitiveParams","param","generateRecommendation","category","_severity","count","groupRiskFindings","findings","maxSampleUrls","categoryMap","finding","groups","categoryFindings","uniqueUrls","severity","highest","severityOrder","sampleUrls","rationale","recommendedAction","a","b","totalRiskUrls","f","highSeverityCount","g","sum","mediumSeverityCount","lowSeverityCount","detectRisks","urls","baseUrl","config","startTime","findings","domainPattern","createDomainMismatchPattern","allPatterns","RISK_PATTERNS","ENVIRONMENT_PATTERNS","ADMIN_PATH_PATTERNS","SENSITIVE_PARAM_PATTERNS","INTERNAL_CONTENT_PATTERNS","acceptedPatterns","pattern","regexPattern","expectedProtocol","processed","urlEntry","url","isAccepted","acceptedPattern","urlProtocol","match","sanitizeUrl","error","groupingResult","groupRiskFindings","processingTimeMs","group","summarizeRisks","request","severityBreakdown","categoryInsights","group","urls","totalRisks","sum","g","overview","keyFindings","TOOL_VERSION","generateJsonReport","summary","discoveryResult","parseResult","riskGroups","config","startTime","options","pretty","indent","result","buildAnalysisResult","jsonOutput","transformToJsonOutput","buildAnalysisResult","summary","discoveryResult","parseResult","riskGroups","config","startTime","metadata","buildAnalysisMetadata","suspiciousGroups","group","summaryStats","sum","g","determineOverallStatus","riskSummary","errors","transformError","baseUrl","TOOL_VERSION","severityBreakdown","transformToJsonOutput","result","transformMetadata","transformGroup","transformRiskSummary","transformSummary","meta","error","customError","errorDetail","import_fs","TOOL_VERSION","generateHtmlReport","summary","discoveryResult","totalUrls","config","errors","options","maxUrls","timestamp","riskyUrlCount","sum","highSeverity","g","mediumSeverity","lowSeverity","err","s","group","renderRiskGroup","categoryTitle","word","urlsToShow","remaining","categorySlug","allUrlsJson","encodedUrls","escapeHtml","url","text","writeHtmlReport","outputPath","htmlContent","fs","analyzeCommand","url","options","config","validateAnalyzeOptions","loadConfig","result","runAnalysisPipeline","showCliSummary","fs","htmlFileName","htmlFilePath","writeHtmlReport","chalk","jsonFileName","jsonFilePath","jsonReport","generateJsonReport","exitCode","determineExitCode","error","handleAnalysisError","validFormats","timeout","riskyUrlCount","sum","g","high","medium","low","startTime","errors","discoverySpinner","ora","discoveryResult","discoverSitemaps","issue","extractionSpinner","extractionResult","extractAllUrls","err","consolidationSpinner","consolidatedResult","consolidateUrls","duplicatesRemoved","riskSpinner","riskResult","detectRisks","riskGroups","groupRiskFindings","totalRiskyUrls","executionTime","summarySpinner","summary","summarizeRisks","program","analyzeCommand","reason","promise"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|