summaryrefslogtreecommitdiff
path: root/DomainDigCLI.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-16 23:48:10 -0500
committerChristian Cleberg <[email protected]>2026-07-16 23:48:10 -0500
commitd8195992e519343d6d8ff7fb17aa98358800d913 (patch)
tree09ef08c1c06ac55c84299d5fd1251755fe0cac4f /DomainDigCLI.swift
parent1edf5bc83c0dcc07246908af341f37e2c871edd7 (diff)
downloaddomain-dig-d8195992e519343d6d8ff7fb17aa98358800d913.tar.gz
domain-dig-d8195992e519343d6d8ff7fb17aa98358800d913.tar.bz2
domain-dig-d8195992e519343d6d8ff7fb17aa98358800d913.zip
DomainDig v4.4.1: Consolidate Audit Mode and remove the CLI targetv4.4.1
- Make DomainDig/DomainDig/Audit* the single active Audit Mode implementation (models, views, exporter) with an Audit tab and session/export UI - Include audit sessions in backup/restore lifecycle counts, summaries, and merge behavior via DomainDataPortabilityService - Remove the DomainDigCLI target, source file, scheme, and all project references; keep the shared inspection/report pipeline for the app - Align AppVersion.current to 4.4.1 and refresh README/architecture docs - Add RELEASE_ROADMAP.md
Diffstat (limited to 'DomainDigCLI.swift')
-rw-r--r--DomainDigCLI.swift525
1 files changed, 0 insertions, 525 deletions
diff --git a/DomainDigCLI.swift b/DomainDigCLI.swift
deleted file mode 100644
index 00713fd..0000000
--- a/DomainDigCLI.swift
+++ /dev/null
@@ -1,525 +0,0 @@
-import Foundation
-
-@main
-struct DomainDigCLI {
- static func main() async {
- let arguments = Array(CommandLine.arguments.dropFirst())
- let wantsJSON = arguments.contains("--json") || arguments.contains("-j")
-
- guard let command = CommandLine.arguments.first else {
- fputs(usageText, stderr)
- Foundation.exit(1)
- }
- _ = command
-
- if arguments.first == "backup" {
- runBackupCommand(arguments: Array(arguments.dropFirst()), wantsJSON: wantsJSON)
- return
- }
-
- if arguments.first == "history" {
- runHistoryCommand(arguments: Array(arguments.dropFirst()), wantsJSON: wantsJSON)
- return
- }
-
- if arguments.first == "diff" {
- runDiffCommand(arguments: Array(arguments.dropFirst()), wantsJSON: wantsJSON)
- return
- }
-
- if arguments.first == "monitor" {
- await runMonitorCommand(wantsJSON: wantsJSON)
- return
- }
-
- let wantsOwnershipHistory = arguments.contains("--ownership-history")
- let wantsDNSHistory = arguments.contains("--dns-history")
- let wantsExtendedSubdomains = arguments.contains("--extended-subdomains")
- let wantsPricing = arguments.contains("--pricing")
- let domains = arguments.filter { !$0.hasPrefix("-") }
-
- let requestedDomains = domains
- .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
- .filter { !$0.isEmpty }
-
- guard !requestedDomains.isEmpty else {
- fputs(usageText, stderr)
- Foundation.exit(1)
- }
-
- let inspectionService = DomainInspectionService()
- let reportBuilder = DomainReportBuilder()
- var reports: [DomainReport] = []
- var seen = Set<String>()
- for domain in requestedDomains {
- let normalizedDomain = domain.lowercased()
- guard seen.insert(normalizedDomain).inserted else { continue }
- let snapshot = await inspectionService.inspectSnapshot(domain: domain)
- let enrichedSnapshot = await enrichSnapshot(
- snapshot,
- wantsOwnershipHistory: wantsOwnershipHistory,
- wantsDNSHistory: wantsDNSHistory,
- wantsExtendedSubdomains: wantsExtendedSubdomains,
- wantsPricing: wantsPricing
- )
- reports.append(reportBuilder.build(from: enrichedSnapshot))
- }
-
- do {
- let data: Data
- if reports.count == 1, let report = reports.first {
- data = try DomainReportExporter.data(
- for: report,
- format: wantsJSON ? .json : .text
- )
- } else {
- data = try DomainReportExporter.data(
- for: reports,
- format: wantsJSON ? .json : .text,
- title: "DomainDig Batch Report"
- )
- }
- FileHandle.standardOutput.write(data)
- if data.last != 0x0A {
- FileHandle.standardOutput.write(Data([0x0A]))
- }
- } catch {
- fputs("domaindig: \(error.localizedDescription)\n", stderr)
- Foundation.exit(1)
- }
- }
-
- private static func enrichSnapshot(
- _ snapshot: LookupSnapshot,
- wantsOwnershipHistory: Bool,
- wantsDNSHistory: Bool,
- wantsExtendedSubdomains: Bool,
- wantsPricing: Bool
- ) async -> LookupSnapshot {
- guard FeatureAccessService.currentTier == .proPlus else {
- return snapshot
- }
-
- let historyEntries = loadHistoryEntries()
- var ownershipHistory = snapshot.ownershipHistory
- var ownershipHistoryError = snapshot.ownershipHistoryError
- var dnsHistory = snapshot.dnsHistory
- var dnsHistoryError = snapshot.dnsHistoryError
- var extendedSubdomains = snapshot.extendedSubdomains
- var extendedSubdomainsError = snapshot.extendedSubdomainsError
- var domainPricing = snapshot.domainPricing
- var domainPricingError = snapshot.domainPricingError
-
- if wantsOwnershipHistory {
- let outcome = await ExternalDataService.shared.ownershipHistory(
- domain: snapshot.domain,
- currentOwnership: snapshot.ownership,
- historyEntries: historyEntries
- )
- switch outcome.value {
- case let .success(events):
- ownershipHistory = events
- ownershipHistoryError = nil
- case let .empty(message):
- ownershipHistoryError = message
- case let .error(message):
- ownershipHistoryError = message
- }
- }
-
- if wantsDNSHistory {
- let outcome = await ExternalDataService.shared.dnsHistory(
- domain: snapshot.domain,
- dnsSections: snapshot.dnsSections,
- historyEntries: historyEntries
- )
- switch outcome.value {
- case let .success(events):
- dnsHistory = events
- dnsHistoryError = nil
- case let .empty(message):
- dnsHistoryError = message
- case let .error(message):
- dnsHistoryError = message
- }
- }
-
- if wantsExtendedSubdomains {
- let outcome = await ExternalDataService.shared.extendedSubdomains(
- domain: snapshot.domain,
- existing: snapshot.subdomains
- )
- switch outcome.value {
- case let .success(results):
- extendedSubdomains = results
- extendedSubdomainsError = nil
- case let .empty(message):
- extendedSubdomainsError = message
- case let .error(message):
- extendedSubdomainsError = message
- }
- }
-
- if wantsPricing {
- let outcome = await ExternalDataService.shared.pricing(domain: snapshot.domain)
- switch outcome.value {
- case let .success(pricing):
- domainPricing = pricing
- domainPricingError = nil
- case let .empty(message), let .error(message):
- domainPricingError = message
- }
- }
-
- return LookupSnapshot(
- historyEntryID: snapshot.historyEntryID,
- domain: snapshot.domain,
- timestamp: snapshot.timestamp,
- trackedDomainID: snapshot.trackedDomainID,
- note: snapshot.note,
- appVersion: snapshot.appVersion,
- resolverDisplayName: snapshot.resolverDisplayName,
- resolverURLString: snapshot.resolverURLString,
- dataSources: snapshot.dataSources,
- provenanceBySection: snapshot.provenanceBySection,
- availabilityConfidence: snapshot.availabilityConfidence,
- ownershipConfidence: snapshot.ownershipConfidence,
- subdomainConfidence: snapshot.subdomainConfidence,
- emailSecurityConfidence: snapshot.emailSecurityConfidence,
- geolocationConfidence: snapshot.geolocationConfidence,
- errorDetails: snapshot.errorDetails,
- isPartialSnapshot: snapshot.isPartialSnapshot,
- validationIssues: snapshot.validationIssues,
- totalLookupDurationMs: snapshot.totalLookupDurationMs,
- snapshotIndex: snapshot.snapshotIndex,
- previousSnapshotID: snapshot.previousSnapshotID,
- changeCount: snapshot.changeCount,
- severitySummary: snapshot.severitySummary,
- dnsSections: snapshot.dnsSections,
- dnsError: snapshot.dnsError,
- availabilityResult: snapshot.availabilityResult,
- suggestions: snapshot.suggestions,
- sslInfo: snapshot.sslInfo,
- sslError: snapshot.sslError,
- hstsPreloaded: snapshot.hstsPreloaded,
- httpHeaders: snapshot.httpHeaders,
- httpSecurityGrade: snapshot.httpSecurityGrade,
- httpStatusCode: snapshot.httpStatusCode,
- httpResponseTimeMs: snapshot.httpResponseTimeMs,
- httpProtocol: snapshot.httpProtocol,
- http3Advertised: snapshot.http3Advertised,
- httpHeadersError: snapshot.httpHeadersError,
- reachabilityResults: snapshot.reachabilityResults,
- reachabilityError: snapshot.reachabilityError,
- ipGeolocation: snapshot.ipGeolocation,
- ipGeolocationError: snapshot.ipGeolocationError,
- emailSecurity: snapshot.emailSecurity,
- emailSecurityError: snapshot.emailSecurityError,
- ownership: snapshot.ownership,
- ownershipError: snapshot.ownershipError,
- ownershipHistory: ownershipHistory,
- ownershipHistoryError: ownershipHistoryError,
- inferredProvider: snapshot.inferredProvider,
- priorProviders: snapshot.priorProviders,
- domainClassification: snapshot.domainClassification,
- ownershipTransitions: snapshot.ownershipTransitions,
- hostingTransitions: snapshot.hostingTransitions,
- subdomainHistory: snapshot.subdomainHistory,
- riskSignals: snapshot.riskSignals,
- intelligenceTimeline: snapshot.intelligenceTimeline,
- ptrRecord: snapshot.ptrRecord,
- ptrError: snapshot.ptrError,
- redirectChain: snapshot.redirectChain,
- redirectChainError: snapshot.redirectChainError,
- subdomains: snapshot.subdomains,
- subdomainsError: snapshot.subdomainsError,
- extendedSubdomains: extendedSubdomains,
- extendedSubdomainsError: extendedSubdomainsError,
- dnsHistory: dnsHistory,
- dnsHistoryError: dnsHistoryError,
- domainPricing: domainPricing,
- domainPricingError: domainPricingError,
- portScanResults: snapshot.portScanResults,
- portScanError: snapshot.portScanError,
- changeSummary: snapshot.changeSummary,
- resultSource: snapshot.resultSource,
- cachedSections: snapshot.cachedSections,
- statusMessage: snapshot.statusMessage
- )
- }
-
- private static func loadHistoryEntries() -> [HistoryEntry] {
- DomainDataPortabilityService.loadHistoryEntries()
- }
-
- private static func runHistoryCommand(arguments: [String], wantsJSON: Bool) {
- guard let domain = arguments.first(where: { !$0.hasPrefix("-") }) else {
- fputs("usage: domaindig history <domain> [--json]\n", stderr)
- Foundation.exit(1)
- }
-
- let entries = loadHistoryEntries()
- .filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
- .sorted { $0.timestamp > $1.timestamp }
-
- if wantsJSON {
- let encoder = JSONEncoder()
- encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
- encoder.dateEncodingStrategy = .iso8601
- let payload = entries.map { entry in
- [
- "id": entry.id.uuidString,
- "timestamp": ISO8601DateFormatter().string(from: entry.timestamp),
- "changeSummary": entry.changeSummary?.message ?? "No change summary",
- "changeCount": "\(entry.changeCount)",
- "severity": entry.severitySummary?.title ?? "N/A"
- ]
- }
- if let data = try? JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys]) {
- FileHandle.standardOutput.write(data)
- FileHandle.standardOutput.write(Data([0x0A]))
- return
- }
- }
-
- let lines = entries.map { entry in
- [
- entry.id.uuidString,
- entry.timestamp.formatted(date: .abbreviated, time: .shortened),
- entry.changeSummary?.message ?? "No change summary"
- ].joined(separator: " | ")
- }
-
- FileHandle.standardOutput.write(Data((lines.isEmpty ? "No history found.\n" : lines.joined(separator: "\n") + "\n").utf8))
- }
-
- private static func runDiffCommand(arguments: [String], wantsJSON: Bool) {
- guard let domain = arguments.first(where: { !$0.hasPrefix("-") }) else {
- fputs("usage: domaindig diff <domain> --from <id> --to <id> [--json]\n", stderr)
- Foundation.exit(1)
- }
-
- guard let fromID = optionValue(named: "--from", in: arguments),
- let toID = optionValue(named: "--to", in: arguments),
- let fromUUID = UUID(uuidString: fromID),
- let toUUID = UUID(uuidString: toID) else {
- fputs("domaindig diff: --from and --to must be valid snapshot IDs\n", stderr)
- Foundation.exit(1)
- }
-
- let entries = loadHistoryEntries().filter { $0.domain.caseInsensitiveCompare(domain) == .orderedSame }
- guard let fromEntry = entries.first(where: { $0.id == fromUUID }),
- let toEntry = entries.first(where: { $0.id == toUUID }) else {
- fputs("domaindig diff: snapshots not found for domain\n", stderr)
- Foundation.exit(1)
- }
-
- let orderedEntries = [fromEntry, toEntry].sorted { $0.timestamp < $1.timestamp }
- let diff = DiffService.compare(from: orderedEntries[0].snapshot, to: orderedEntries[1].snapshot)
-
- if wantsJSON {
- let encoder = JSONEncoder()
- encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
- encoder.dateEncodingStrategy = .iso8601
- if let data = try? encoder.encode(diff) {
- FileHandle.standardOutput.write(data)
- FileHandle.standardOutput.write(Data([0x0A]))
- return
- }
- }
-
- var lines = [
- "DomainDig Diff",
- "Domain: \(domain)",
- "From: \(orderedEntries[0].id.uuidString)",
- "To: \(orderedEntries[1].id.uuidString)"
- ]
-
- for section in diff.sections where section.hasChanges {
- lines.append("")
- lines.append(section.title)
- for item in section.items where item.hasChanges {
- lines.append("\(item.changeType.marker) \(item.label): \(item.oldValue ?? "none") -> \(item.newValue ?? "none")")
- }
- }
-
- FileHandle.standardOutput.write(Data((lines.joined(separator: "\n") + "\n").utf8))
- }
-
- private static func optionValue(named name: String, in arguments: [String]) -> String? {
- guard let index = arguments.firstIndex(of: name), arguments.indices.contains(index + 1) else {
- return nil
- }
- return arguments[index + 1]
- }
-
- private static func runBackupCommand(arguments: [String], wantsJSON: Bool) {
- guard let subcommand = arguments.first else {
- fputs(usageText, stderr)
- Foundation.exit(1)
- }
-
- switch subcommand {
- case "export":
- do {
- let data = try DomainDataPortabilityService.backupData()
- let outputPath = arguments.dropFirst().first(where: { !$0.hasPrefix("-") })
- if let outputPath {
- try data.write(to: URL(fileURLWithPath: outputPath), options: .atomic)
- } else {
- FileHandle.standardOutput.write(data)
- if data.last != 0x0A {
- FileHandle.standardOutput.write(Data([0x0A]))
- }
- }
- } catch {
- fputs("domaindig backup export: \(error.localizedDescription)\n", stderr)
- Foundation.exit(1)
- }
- case "validate":
- guard let path = arguments.dropFirst().first(where: { !$0.hasPrefix("-") }) else {
- fputs("usage: domaindig backup validate <path>\n", stderr)
- Foundation.exit(1)
- }
-
- do {
- let data = try Data(contentsOf: URL(fileURLWithPath: path))
- let report = try DomainDataPortabilityService.validateBackup(data: data, fileName: URL(fileURLWithPath: path).lastPathComponent)
- if wantsJSON {
- let payload = [
- "warnings": report.warnings,
- "errors": report.errors
- ]
- let encoded = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys])
- FileHandle.standardOutput.write(encoded)
- } else {
- let lines = [
- "Warnings: \(report.warnings.count)",
- "Errors: \(report.errors.count)"
- ] + report.warnings.map { "warning: \($0)" } + report.errors.map { "error: \($0)" }
- FileHandle.standardOutput.write(Data(lines.joined(separator: "\n").utf8))
- }
- FileHandle.standardOutput.write(Data([0x0A]))
- if !report.errors.isEmpty {
- Foundation.exit(1)
- }
- } catch {
- fputs("domaindig backup validate: \(error.localizedDescription)\n", stderr)
- Foundation.exit(1)
- }
- case "import":
- guard let path = arguments.dropFirst().first(where: { !$0.hasPrefix("-") }) else {
- fputs("usage: domaindig backup import <path> [--replace]\n", stderr)
- Foundation.exit(1)
- }
-
- let mode: DataPortabilityImportMode = arguments.contains("--replace") ? .replace : .merge
-
- do {
- let fileURL = URL(fileURLWithPath: path)
- let data = try Data(contentsOf: fileURL)
- let preview = try DomainDataPortabilityService.prepareImport(
- data: data,
- fileName: fileURL.lastPathComponent,
- mode: mode
- )
- guard preview.kind == .backup else {
- fputs("domaindig backup import: expected a full backup file\n", stderr)
- Foundation.exit(1)
- }
- let result = try DomainDataPortabilityService.applyImport(preview, mode: mode)
- let lines = [result.summary] + result.warnings.map { "warning: \($0)" }
- FileHandle.standardOutput.write(Data(lines.joined(separator: "\n").utf8))
- FileHandle.standardOutput.write(Data([0x0A]))
- } catch {
- fputs("domaindig backup import: \(error.localizedDescription)\n", stderr)
- Foundation.exit(1)
- }
- default:
- fputs(usageText, stderr)
- Foundation.exit(1)
- }
- }
-
- private static func runMonitorCommand(wantsJSON: Bool) async {
- guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else {
- fputs("domaindig monitor: monitoring requires Pro\n", stderr)
- Foundation.exit(1)
- }
-
- let outcome = await DomainMonitoringService.shared.performMonitoring(
- trigger: .cli,
- requireEnabledSetting: false
- )
-
- guard let log = outcome.log else {
- fputs("domaindig monitor: \(outcome.message)\n", stderr)
- Foundation.exit(1)
- }
-
- do {
- let output: Data
- if wantsJSON {
- let encoder = JSONEncoder()
- encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
- output = try encoder.encode(log)
- } else {
- output = Data(monitoringTextSummary(for: log).utf8)
- }
- FileHandle.standardOutput.write(output)
- if output.last != 0x0A {
- FileHandle.standardOutput.write(Data([0x0A]))
- }
- if !outcome.success {
- Foundation.exit(1)
- }
- } catch {
- fputs("domaindig monitor: \(error.localizedDescription)\n", stderr)
- Foundation.exit(1)
- }
- }
-
- private static func monitoringTextSummary(for log: MonitoringLog) -> String {
- var lines = [
- "DomainDig Monitoring",
- "====================",
- "Trigger: \(log.trigger.title)",
- "Timestamp: \(log.timestamp.formatted(date: .abbreviated, time: .shortened))",
- "Checked: \(log.domainsChecked)",
- "Changes: \(log.changesFound)",
- "Alerts: \(log.alertsTriggered)",
- ""
- ]
-
- if log.checkedDomains.isEmpty {
- lines.append("No domains were checked.")
- } else {
- for result in log.checkedDomains {
- let severity = result.alertSeverity?.title ?? "None"
- lines.append("\(result.domain): \(result.summaryMessage) [alert: \(severity)]")
- }
- }
-
- if !log.errors.isEmpty {
- lines.append("")
- lines.append("Errors:")
- lines.append(contentsOf: log.errors.map { "- \($0)" })
- }
-
- return lines.joined(separator: "\n")
- }
-
- private static var usageText: String {
- """
- usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing]
- domaindig history <domain> [--json]
- domaindig diff <domain> --from <id> --to <id> [--json]
- domaindig monitor [--json]
- domaindig backup export [path]
- domaindig backup import <path> [--replace]
- domaindig backup validate <path> [--json]
-
- note: the CLI remains local-only and does not sync with iCloud yet.
- """
- }
-}