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.
240 lines (208 loc) • 9.12 kB
text/typescript
import fs from 'fs';
import path from 'path';
import { Command } from 'commander';
import { CSVProcessor } from '../lib/csv-processor.js';
import { DNSResolver } from '../lib/dns-resolver.js';
import { Logger } from '../lib/logger.js';
import { RiskCalculator } from '../lib/risk-calculator.js';
import type { IAnalyzeOptions, IDNSRecord } from '../types/index.js';
export function createAnalyzeCommand(): Command {
const analyzeCmd = new Command('analyze')
.alias('scan')
.description('Analyze DNS records for risks and generate report')
.argument('<file>', 'CSV file to analyze')
.option('-f, --format <format>', 'CSV format (cloudflare, route53, generic, auto)', 'auto')
.option(
'-l, --level <level>',
'Minimum risk level to report (low, medium, high, critical)',
'medium',
)
.option('-c, --check-dns', 'Check current DNS status for each record')
.option('-o, --output <file>', 'Save report to file')
.option('-v, --verbose', 'Show detailed output')
.option('-j, --json', 'Output as JSON')
.option('-q, --quiet', 'Suppress non-error output')
.action(async (file: string, options: IAnalyzeOptions) => {
const logger = new Logger({ verbose: options.verbose, quiet: options.quiet });
try {
// Validate file exists
const filePath = path.resolve(file);
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const processor = new CSVProcessor();
const calculator = new RiskCalculator();
logger.startSpinner(`Analyzing DNS records from ${path.basename(filePath)}...`);
// Parse CSV file
let parseResult;
switch (options.format) {
case 'cloudflare':
parseResult = await processor.parseCloudflare(filePath);
break;
case 'route53':
parseResult = await processor.parseRoute53(filePath);
break;
case 'generic':
parseResult = await processor.parseGeneric(filePath);
break;
default:
parseResult = await processor.parseAuto(filePath);
}
logger.stopSpinner(true, `Loaded ${parseResult.records.length} DNS records`);
// Check DNS status if requested
const lastSeenDates = new Map<string, Date>();
if (options.checkDns) {
logger.startSpinner('Checking current DNS status...');
const resolver = new DNSResolver();
let checkedCount = 0;
for (const record of parseResult.records) {
try {
const result = await resolver.resolve(record.domain, record.type);
if (result.status === 'success') {
lastSeenDates.set(record.domain, new Date());
}
checkedCount++;
if (!options.quiet && checkedCount % 10 === 0) {
logger.info(`Checked ${checkedCount}/${parseResult.records.length} records...`);
}
} catch (error) {
// DNS check failed, record might not exist
}
}
logger.stopSpinner(true, `DNS status check complete`);
}
// Calculate risk scores
logger.startSpinner('Calculating risk scores...');
// Convert CSV records to DNS records format
const dnsRecords: IDNSRecord[] = parseResult.records.map((csvRecord, index) => {
const record: IDNSRecord = {
id: `record-${index}`,
name: csvRecord.domain,
type: csvRecord.type,
value: csvRecord.value,
ttl: csvRecord.ttl || 300,
created: new Date(),
updated: new Date(),
};
// オプショナルフィールドは値が存在する場合のみ設定
if (csvRecord.priority !== undefined) record.priority = csvRecord.priority;
if (csvRecord.weight !== undefined) record.weight = csvRecord.weight;
if (csvRecord.port !== undefined) record.port = csvRecord.port;
return record;
});
const riskScores = calculator.calculateBatchRisk(dnsRecords, lastSeenDates);
const summary = calculator.getRiskSummary(dnsRecords, lastSeenDates);
logger.stopSpinner(true, 'Risk analysis complete');
// Filter by minimum risk level
const minLevel = options.level || 'medium';
const riskyRecords = calculator.filterByRiskLevel(dnsRecords, minLevel);
// Generate report
const report = {
summary: {
totalRecords: parseResult.records.length,
analyzedRecords: parseResult.records.length,
riskyRecords: riskyRecords.length,
riskBreakdown: summary.byLevel,
averageRiskScore: Math.round(summary.averageScore),
totalRecommendations: summary.recommendations,
},
records: riskyRecords
.map((record) => {
const risk = riskScores.get(record.id)!;
return {
domain: record.name,
type: record.type,
value: record.value,
ttl: record.ttl,
risk: {
score: risk.total,
level: risk.level,
factors: risk.factors,
recommendations: risk.recommendations,
},
};
})
.sort((a, b) => b.risk.score - a.risk.score), // Sort by risk score descending
};
// Display or save report
if (options.json) {
if (options.output) {
fs.writeFileSync(options.output, JSON.stringify(report, null, 2));
logger.success(`Report saved to ${options.output}`);
} else {
logger.json(report);
}
} else {
// Display summary
logger.info('\n📊 Risk Analysis Summary');
logger.info('========================');
logger.info(`Total records analyzed: ${report.summary.totalRecords}`);
logger.info(`Records at risk (${minLevel}+): ${report.summary.riskyRecords}`);
logger.info(`Average risk score: ${report.summary.averageRiskScore}/100`);
logger.info('\nRisk Distribution:');
logger.info(` 🟢 Low: ${report.summary.riskBreakdown.low}`);
logger.info(` 🟡 Medium: ${report.summary.riskBreakdown.medium}`);
logger.info(` 🟠 High: ${report.summary.riskBreakdown.high}`);
logger.info(` 🔴 Critical: ${report.summary.riskBreakdown.critical}`);
// Display top risky records
if (report.records.length > 0) {
logger.info('\n🚨 Top Risk Records:');
logger.info('===================');
const topRecords = report.records.slice(0, 10);
for (const record of topRecords) {
const icon =
record.risk.level === 'critical'
? '🔴'
: record.risk.level === 'high'
? '🟠'
: record.risk.level === 'medium'
? '🟡'
: '🟢';
logger.info(`\n${icon} ${record.domain} (${record.type})`);
logger.info(
` Risk Score: ${record.risk.score}/100 [${record.risk.level.toUpperCase()}]`,
);
logger.info(` TTL: ${record.ttl}s | Value: ${record.value}`);
if (record.risk.recommendations.length > 0) {
logger.info(' Recommendations:');
for (const rec of record.risk.recommendations) {
logger.info(` - ${rec}`);
}
}
}
if (report.records.length > 10) {
logger.info(`\n... and ${report.records.length - 10} more records`);
}
}
// Save report to file if requested
if (options.output) {
const reportText = JSON.stringify(report, null, 2);
fs.writeFileSync(options.output, reportText);
logger.success(`\nDetailed report saved to ${options.output}`);
}
}
// Summary message
if (!options.json && !options.quiet) {
logger.success('\n✅ Analysis complete!');
if (report.summary.riskyRecords > 0) {
logger.warn(`Found ${report.summary.riskyRecords} records that need attention.`);
} else {
logger.success('No significant risks detected.');
}
}
} catch (error) {
logger.stopSpinner(false, 'Analysis failed');
logger.error(error instanceof Error ? error.message : 'Unknown error occurred');
if (options.json) {
logger.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
process.exit(1);
}
});
return analyzeCmd;
}
// テスト用にanalyzeCommandをエクスポート
export const analyzeCommand = createAnalyzeCommand();