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
JavaScript
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