UNPKG

dnsweeper

Version:

Advanced CLI tool for DNS record risk analysis and cleanup. Features CSV import for Cloudflare/Route53, automated risk assessment, and parallel DNS validation.

195 lines 7.61 kB
import { promises as dns } from 'node:dns'; import { DnsResolutionError } from '../lib/errors.js'; /** * CNAMEチェーンを追跡し、ループや無限再帰を検知する */ export async function traceCnameChain(domain, options = {}) { const { maxDepth = 10, timeout = 5000, followToEnd = true } = options; const startTime = Date.now(); const chain = []; const visited = new Set(); let currentDomain = domain.toLowerCase(); let hasLoop = false; let maxDepthReached = false; let finalTarget = null; try { while (chain.length < maxDepth) { // ループ検知 if (visited.has(currentDomain)) { hasLoop = true; break; } visited.add(currentDomain); chain.push(currentDomain); try { // タイムアウト付きでCNAME解決 const cnames = await Promise.race([ dns.resolveCname(currentDomain), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout)), ]); if (cnames.length === 0) { // CNAMEレコードがない場合、これが最終ターゲット finalTarget = currentDomain; break; } // 複数のCNAMEがある場合は最初のものを使用 const firstCname = cnames[0]; if (!firstCname) { break; } currentDomain = firstCname.toLowerCase(); if (!followToEnd) { // 一段階のみ追跡する場合 finalTarget = currentDomain; break; } } catch (error) { const nodeError = error; if (nodeError.code === 'ENODATA' || nodeError.code === 'ENOTFOUND') { // CNAMEレコードがない場合、これが最終ターゲット finalTarget = currentDomain; break; } if (error instanceof Error && error.message === 'Timeout') { throw new DnsResolutionError(`CNAME resolution timeout for ${currentDomain}`, { domain: currentDomain, timeout, }); } throw new DnsResolutionError(`Failed to resolve CNAME for ${currentDomain}: ${nodeError.code || 'Unknown error'}`, { domain: currentDomain, code: nodeError.code }); } } if (chain.length >= maxDepth && !finalTarget && !hasLoop) { maxDepthReached = true; finalTarget = currentDomain; } } catch (error) { // エラーが発生した場合でも、これまでの追跡結果を返す console.warn(`CNAME chain tracing error: ${error instanceof Error ? error.message : 'Unknown error'}`); } const resolutionTime = Date.now() - startTime; return { chain, finalTarget, hasLoop, maxDepthReached, resolutionTime, }; } /** * CNAMEチェーンを検証する */ export function validateCnameChain(result) { const issues = []; const recommendations = []; // ループの検知 if (result.hasLoop) { issues.push('CNAMEチェーンにループが検出されました'); recommendations.push('DNSレコードの設定を確認し、循環参照を修正してください'); } // 最大深度の到達 if (result.maxDepthReached) { issues.push('CNAMEチェーンが最大深度に達しました'); recommendations.push('不必要に長いCNAMEチェーンを短縮することを検討してください'); } // 長いチェーンの警告 if (result.chain.length > 5) { issues.push(`CNAMEチェーンが長すぎます(${result.chain.length}段階)`); recommendations.push('パフォーマンスのため、CNAMEチェーンを5段階以下に短縮することをお勧めします'); } // 解決時間の警告 if (result.resolutionTime > 2000) { issues.push(`CNAME解決に時間がかかりすぎています(${result.resolutionTime}ms)`); recommendations.push('DNSサーバーの応答性能を確認してください'); } // 最終ターゲットの確認 if (!result.finalTarget && !result.hasLoop) { issues.push('CNAMEチェーンの最終ターゲットが解決できませんでした'); recommendations.push('DNSレコードの設定を確認してください'); } return { isValid: issues.length === 0, issues, recommendations, }; } /** * CNAMEチェーンの統計情報を生成 */ export function getCnameChainStats(results) { if (results.length === 0) { return { totalChains: 0, averageDepth: 0, maxDepth: 0, loopCount: 0, averageResolutionTime: 0, healthScore: 100, }; } const totalDepth = results.reduce((sum, result) => sum + result.chain.length, 0); const maxDepth = Math.max(...results.map((result) => result.chain.length)); const loopCount = results.filter((result) => result.hasLoop).length; const totalResolutionTime = results.reduce((sum, result) => sum + result.resolutionTime, 0); // ヘルススコア計算(0-100) let healthScore = 100; // ループの影響 healthScore -= (loopCount / results.length) * 50; // 長いチェーンの影響 const longChains = results.filter((result) => result.chain.length > 5).length; healthScore -= (longChains / results.length) * 20; // 最大深度到達の影響 const maxDepthReached = results.filter((result) => result.maxDepthReached).length; healthScore -= (maxDepthReached / results.length) * 30; return { totalChains: results.length, averageDepth: totalDepth / results.length, maxDepth, loopCount, averageResolutionTime: totalResolutionTime / results.length, healthScore: Math.max(0, Math.round(healthScore)), }; } /** * 複数ドメインのCNAMEチェーンを並列で追跡 */ export async function traceMultipleCnameChains(domains, options = {}) { const { concurrency = 5, ...chainOptions } = options; const results = new Map(); // 並列実行のためのワーカー関数 const traceDomain = async (domain) => { try { const result = await traceCnameChain(domain, chainOptions); results.set(domain, result); } catch (error) { // エラーが発生した場合でも結果を記録 results.set(domain, { chain: [domain], finalTarget: null, hasLoop: false, maxDepthReached: false, resolutionTime: 0, }); } }; // 並列実行(指定された並列数で制限) const workers = []; let index = 0; const createWorker = async () => { while (index < domains.length) { const domain = domains[index++]; if (domain) { await traceDomain(domain); } } }; for (let i = 0; i < Math.min(concurrency, domains.length); i++) { workers.push(createWorker()); } await Promise.all(workers); return results; } //# sourceMappingURL=cname-chain.js.map