summaryrefslogtreecommitdiff
path: root/DomainDigCLI.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-22 13:47:37 -0500
committerChristian Cleberg <[email protected]>2026-04-22 13:47:37 -0500
commit6c23daaaba2a954220ece2afa7193dbdcad55c22 (patch)
treefebfca71eba8a025374d993e7a39fdb13194af25 /DomainDigCLI.swift
parentdcf4135ec376d357b8fafc0101a75b674e76329a (diff)
downloaddomain-dig-6c23daaaba2a954220ece2afa7193dbdcad55c22.tar.gz
domain-dig-6c23daaaba2a954220ece2afa7193dbdcad55c22.tar.bz2
domain-dig-6c23daaaba2a954220ece2afa7193dbdcad55c22.zip
feat(v3.2.0): add Data+ tier and external data integrations
- introduce Data+ tier for advanced data features - add ownership history and DNS history - expand subdomain discovery with external sources - add domain pricing insights - implement local usage/credit system - integrate external data service layer with rate limiting - extend export and CLI for Data+ features
Diffstat (limited to 'DomainDigCLI.swift')
-rw-r--r--DomainDigCLI.swift208
1 files changed, 205 insertions, 3 deletions
diff --git a/DomainDigCLI.swift b/DomainDigCLI.swift
index d0a4d86..a6d2cf3 100644
--- a/DomainDigCLI.swift
+++ b/DomainDigCLI.swift
@@ -6,12 +6,17 @@ struct DomainDigCLI {
let arguments = Array(CommandLine.arguments.dropFirst())
guard let command = CommandLine.arguments.first else {
- fputs("usage: domaindig <domain> [--json]\n", stderr)
+ fputs("usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing] [--show-usage]\n", stderr)
Foundation.exit(1)
}
_ = command
let wantsJSON = arguments.contains("--json") || arguments.contains("-j")
+ let wantsOwnershipHistory = arguments.contains("--ownership-history")
+ let wantsDNSHistory = arguments.contains("--dns-history")
+ let wantsExtendedSubdomains = arguments.contains("--extended-subdomains")
+ let wantsPricing = arguments.contains("--pricing")
+ let wantsUsage = arguments.contains("--show-usage")
let domains = arguments.filter { !$0.hasPrefix("-") }
let requestedDomains = domains
@@ -19,18 +24,29 @@ struct DomainDigCLI {
.filter { !$0.isEmpty }
guard !requestedDomains.isEmpty else {
- fputs("usage: domaindig <domain> [--json]\n", stderr)
+ fputs("usage: domaindig <domain> [--json] [--ownership-history] [--dns-history] [--extended-subdomains] [--pricing] [--show-usage]\n", stderr)
Foundation.exit(1)
}
let inspectionService = DomainInspectionService()
+ let reportBuilder = DomainReportBuilder()
var reports: [DomainReport] = []
var seen = Set<String>()
+ var usageImpact: [String] = []
for domain in requestedDomains {
let normalizedDomain = domain.lowercased()
guard seen.insert(normalizedDomain).inserted else { continue }
- reports.append(await inspectionService.inspect(domain: domain))
+ let snapshot = await inspectionService.inspectSnapshot(domain: domain)
+ let enrichedSnapshot = await enrichSnapshot(
+ snapshot,
+ wantsOwnershipHistory: wantsOwnershipHistory,
+ wantsDNSHistory: wantsDNSHistory,
+ wantsExtendedSubdomains: wantsExtendedSubdomains,
+ wantsPricing: wantsPricing,
+ usageImpact: &usageImpact
+ )
+ reports.append(reportBuilder.build(from: enrichedSnapshot))
}
do {
@@ -47,6 +63,9 @@ struct DomainDigCLI {
title: "DomainDig Batch Report"
)
}
+ if wantsUsage, !usageImpact.isEmpty {
+ FileHandle.standardError.write(Data(("Data+ usage impact: " + usageImpact.joined(separator: ", ") + "\n").utf8))
+ }
FileHandle.standardOutput.write(data)
if data.last != 0x0A {
FileHandle.standardOutput.write(Data([0x0A]))
@@ -56,4 +75,187 @@ struct DomainDigCLI {
Foundation.exit(1)
}
}
+
+ private static func enrichSnapshot(
+ _ snapshot: LookupSnapshot,
+ wantsOwnershipHistory: Bool,
+ wantsDNSHistory: Bool,
+ wantsExtendedSubdomains: Bool,
+ wantsPricing: Bool,
+ usageImpact: inout [String]
+ ) async -> LookupSnapshot {
+ guard FeatureAccessService.currentTier == .dataPlus 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,
+ await UsageCreditService.shared.canUse(.ownershipHistory) {
+ 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
+ if outcome.source != .cached {
+ _ = await UsageCreditService.shared.consume(.ownershipHistory)
+ usageImpact.append("ownership history -1")
+ }
+ case let .empty(message):
+ ownershipHistoryError = message
+ if outcome.source != .cached {
+ _ = await UsageCreditService.shared.consume(.ownershipHistory)
+ usageImpact.append("ownership history -1")
+ }
+ case let .error(message):
+ ownershipHistoryError = message
+ }
+ }
+
+ if wantsDNSHistory,
+ await UsageCreditService.shared.canUse(.dnsHistory) {
+ 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
+ if outcome.source != .cached {
+ _ = await UsageCreditService.shared.consume(.dnsHistory)
+ usageImpact.append("dns history -1")
+ }
+ case let .empty(message):
+ dnsHistoryError = message
+ if outcome.source != .cached {
+ _ = await UsageCreditService.shared.consume(.dnsHistory)
+ usageImpact.append("dns history -1")
+ }
+ case let .error(message):
+ dnsHistoryError = message
+ }
+ }
+
+ if wantsExtendedSubdomains,
+ await UsageCreditService.shared.canUse(.extendedSubdomains) {
+ let outcome = await ExternalDataService.shared.extendedSubdomains(
+ domain: snapshot.domain,
+ existing: snapshot.subdomains
+ )
+ switch outcome.value {
+ case let .success(results):
+ extendedSubdomains = results
+ extendedSubdomainsError = nil
+ if outcome.source != .cached {
+ _ = await UsageCreditService.shared.consume(.extendedSubdomains)
+ usageImpact.append("extended subdomains -1")
+ }
+ case let .empty(message):
+ extendedSubdomainsError = message
+ if outcome.source != .cached {
+ _ = await UsageCreditService.shared.consume(.extendedSubdomains)
+ usageImpact.append("extended subdomains -1")
+ }
+ 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,
+ 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,
+ 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] {
+ guard let data = UserDefaults.standard.data(forKey: "lookupHistory"),
+ let entries = try? JSONDecoder().decode([HistoryEntry].self, from: data) else {
+ return []
+ }
+ return entries
+ }
}