diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 10:39:17 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 10:57:30 -0500 |
| commit | 6e273c2676ce29cef057d117e4427e031886e743 (patch) | |
| tree | c16273cd4b3e022f2322cb9061b8e735542eea55 | |
| parent | d62480233819184db2e55741b05375818ebf3881 (diff) | |
| download | domain-dig-6e273c2676ce29cef057d117e4427e031886e743.tar.gz domain-dig-6e273c2676ce29cef057d117e4427e031886e743.tar.bz2 domain-dig-6e273c2676ce29cef057d117e4427e031886e743.zip | |
v4.7.0: Add domain reputation/blocklist data source
- New DomainReputationResult model (status: clean/listed/unknown, listed
sources, checked-at) and a `reputation(domain:)` method on ExternalDataService,
mirroring the existing pluggable-URL enrichment pattern (ownership history,
DNS history, extended subdomains, pricing). With no endpoint configured
(the default; DomainDig ships no bundled third-party reputation dependency)
it resolves to unavailable rather than "clean".
- New .reputation FeatureCapability/DataCapability, gated Pro+ like domainPricing.
- Threaded reputation/reputationError through LookupSnapshot and HistoryEntry
(backward-compatible decode) so results persist with history entries.
- Auto-fetched in performLookup alongside pricing; surfaced as a "Reputation"
info row, folded into DomainInsightEngine's risk score/factors and top-level
insights (a listed domain raises risk score and adds a factor/insight), and
exported in text, CSV, and JSON report output.
- Reputation-driven risk changes ride the existing change-severity pipeline, so
a listed status flip is visible to monitoring the same way any other risk
delta is, without bespoke monitoring wiring.
| -rw-r--r-- | DomainDig/DataAccessService.swift | 2 | ||||
| -rw-r--r-- | DomainDig/DomainInsightEngine.swift | 16 | ||||
| -rw-r--r-- | DomainDig/DomainMonitoringService.swift | 4 | ||||
| -rw-r--r-- | DomainDig/DomainViewModel.swift | 51 | ||||
| -rw-r--r-- | DomainDig/ExternalDataService.swift | 67 | ||||
| -rw-r--r-- | DomainDig/FeatureAccessService.swift | 7 | ||||
| -rw-r--r-- | DomainDig/Models.swift | 33 | ||||
| -rw-r--r-- | DomainInspectionService.swift | 2 | ||||
| -rw-r--r-- | DomainReportBuilder.swift | 2 | ||||
| -rw-r--r-- | DomainReportExporter.swift | 9 | ||||
| -rw-r--r-- | LookupSnapshot.swift | 4 |
11 files changed, 192 insertions, 5 deletions
diff --git a/DomainDig/DataAccessService.swift b/DomainDig/DataAccessService.swift index 98e215d..43b664e 100644 --- a/DomainDig/DataAccessService.swift +++ b/DomainDig/DataAccessService.swift @@ -11,6 +11,8 @@ enum DataAccessService { return FeatureAccessService.hasAccess(to: .extendedSubdomains) case .domainPricing: return FeatureAccessService.hasAccess(to: .domainPricing) + case .reputation: + return FeatureAccessService.hasAccess(to: .reputation) } } } diff --git a/DomainDig/DomainInsightEngine.swift b/DomainDig/DomainInsightEngine.swift index 2547727..f1de9c0 100644 --- a/DomainDig/DomainInsightEngine.swift +++ b/DomainDig/DomainInsightEngine.swift @@ -303,6 +303,19 @@ enum DomainInsightEngine { } } + if let reputation = snapshot.reputation { + switch reputation.status { + case .listed: + score += 25 + let sourceList = reputation.listedSources.isEmpty ? "" : " (\(reputation.listedSources.joined(separator: ", ")))" + factors.append(.init(description: "Domain is flagged by a configured reputation source\(sourceList)", impact: .negative)) + case .clean: + factors.append(.init(description: "Domain is clean against the configured reputation source", impact: .positive)) + case .unknown: + break + } + } + let clampedScore = min(max(score, 0), 100) let level: RiskLevel switch clampedScore { @@ -327,6 +340,9 @@ enum DomainInsightEngine { ) -> [String] { var items: [String] = [] + if snapshot.reputation?.status == .listed { + items.append("Domain is flagged by a configured reputation source") + } if let group = subdomainGroups.first(where: { $0.label == "staging" || $0.label == "dev" }) { items.append("Multiple \(group.label) subdomains suggest non-production environments are exposed") } diff --git a/DomainDig/DomainMonitoringService.swift b/DomainDig/DomainMonitoringService.swift index 3229fb7..6fb5fc9 100644 --- a/DomainDig/DomainMonitoringService.swift +++ b/DomainDig/DomainMonitoringService.swift @@ -516,6 +516,7 @@ final class DomainMonitoringService { extendedSubdomains: snapshot.extendedSubdomains, dnsHistory: snapshot.dnsHistory, domainPricing: snapshot.domainPricing, + reputation: snapshot.reputation, portScanResults: snapshot.portScanResults, hstsPreloaded: snapshot.hstsPreloaded, availabilityResult: snapshot.availabilityResult, @@ -554,6 +555,7 @@ final class DomainMonitoringService { extendedSubdomainsError: snapshot.extendedSubdomainsError, dnsHistoryError: snapshot.dnsHistoryError, domainPricingError: snapshot.domainPricingError, + reputationError: snapshot.reputationError, portScanError: snapshot.portScanError ) @@ -930,6 +932,8 @@ final class DomainMonitoringService { dnsHistoryError: previousSnapshot.dnsHistoryError, domainPricing: previousSnapshot.domainPricing, domainPricingError: previousSnapshot.domainPricingError, + reputation: previousSnapshot.reputation, + reputationError: previousSnapshot.reputationError, portScanResults: previousSnapshot.portScanResults, portScanError: previousSnapshot.portScanError, changeSummary: previousSnapshot.changeSummary, diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift index dcae907..3c92ff6 100644 --- a/DomainDig/DomainViewModel.swift +++ b/DomainDig/DomainViewModel.swift @@ -232,6 +232,9 @@ final class DomainViewModel { var domainPricing: DomainPricingInsight? var domainPricingLoading = false var domainPricingError: String? + var reputation: DomainReputationResult? + var reputationLoading = false + var reputationError: String? var usageCredits: [UsageCreditFeature: UsageCreditStatus] = DomainViewModel.defaultUsageCredits() var portScanResults: [PortScanResult] = [] @@ -642,6 +645,8 @@ final class DomainViewModel { dnsHistoryError: dnsHistoryError, domainPricing: domainPricing, domainPricingError: domainPricingError, + reputation: reputation, + reputationError: reputationError, portScanResults: allPortScanResults, portScanError: combinedPortScanError, changeSummary: currentChangeSummary, @@ -1883,6 +1888,11 @@ final class DomainViewModel { await refreshDomainPricing(for: snapshot.domain, persistAfterFetch: false) } + if DataAccessService.hasAccess(to: .reputation), reputation == nil { + DomainDebugLog.debug("DomainViewModel.performLookup loadingReputation domain=\(domain)") + await refreshReputation(for: snapshot.domain, persistAfterFetch: false) + } + guard snapshot.statusMessage == nil else { return history.first(where: { $0.id == snapshot.historyEntryID }) } @@ -2053,6 +2063,8 @@ final class DomainViewModel { dnsHistoryError: previousSnapshot.dnsHistoryError, domainPricing: previousSnapshot.domainPricing, domainPricingError: previousSnapshot.domainPricingError, + reputation: previousSnapshot.reputation, + reputationError: previousSnapshot.reputationError, portScanResults: previousSnapshot.portScanResults, portScanError: previousSnapshot.portScanError, changeSummary: previousSnapshot.changeSummary, @@ -2474,6 +2486,7 @@ final class DomainViewModel { extendedSubdomains: snapshot.extendedSubdomains, dnsHistory: snapshot.dnsHistory, domainPricing: snapshot.domainPricing, + reputation: snapshot.reputation, portScanResults: snapshot.portScanResults, hstsPreloaded: snapshot.hstsPreloaded, availabilityResult: snapshot.availabilityResult, @@ -2516,6 +2529,7 @@ final class DomainViewModel { extendedSubdomainsError: snapshot.extendedSubdomainsError, dnsHistoryError: snapshot.dnsHistoryError, domainPricingError: snapshot.domainPricingError, + reputationError: snapshot.reputationError, portScanError: snapshot.portScanError ) @@ -3844,6 +3858,8 @@ final class DomainViewModel { dnsHistoryError: nil, domainPricing: nil, domainPricingError: nil, + reputation: nil, + reputationError: nil, portScanResults: [], portScanError: nil, changeSummary: trackedDomain.lastChangeSummary, @@ -4005,6 +4021,18 @@ final class DomainViewModel { rows.append(InfoRowViewData(label: "Auction", value: auctionSignal, tone: .secondary)) } } + if let reputation = snapshot.reputation { + let tone: ResultTone + switch reputation.status { + case .clean: tone = .success + case .listed: tone = .failure + case .unknown: tone = .secondary + } + let value = reputation.status == .listed && !reputation.listedSources.isEmpty + ? "\(reputation.status.title) (\(reputation.listedSources.joined(separator: ", ")))" + : reputation.status.title + rows.append(InfoRowViewData(label: "Reputation", value: value, tone: tone)) + } if let certificateStatus = certificateBadgeLabel(from: snapshot) { rows.insert( InfoRowViewData( @@ -4700,6 +4728,29 @@ final class DomainViewModel { } } + private func refreshReputation(for domain: String, persistAfterFetch: Bool) async { + reputationLoading = true + let outcome = await ExternalDataService.shared.reputation(domain: domain) + + switch outcome.value { + case let .success(result): + reputation = result + reputationError = nil + case let .empty(message): + reputation = nil + reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable") + case let .error(message): + reputation = nil + reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable") + } + + reputationLoading = false + + if persistAfterFetch { + _ = saveHistoryEntry(replaceLatest: true) + } + } + private func conciseExternalMessage(_ message: String, fallback: String) -> String { let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { diff --git a/DomainDig/ExternalDataService.swift b/DomainDig/ExternalDataService.swift index 65f117e..43cc489 100644 --- a/DomainDig/ExternalDataService.swift +++ b/DomainDig/ExternalDataService.swift @@ -8,12 +8,14 @@ actor ExternalDataService { case dnsHistory(String) case extendedSubdomains(String) case pricing(String) + case reputation(String) } private enum RateLimitBucket: Hashable { case history case subdomains case pricing + case reputation var minimumSpacing: TimeInterval { switch self { @@ -23,6 +25,8 @@ actor ExternalDataService { return 1.5 case .pricing: return 1.0 + case .reputation: + return 1.0 } } } @@ -32,6 +36,7 @@ actor ExternalDataService { case dnsHistory(ServiceResult<[DNSHistoryEvent]>) case extendedSubdomains(ServiceResult<[DiscoveredSubdomain]>) case pricing(ServiceResult<DomainPricingInsight>) + case reputation(ServiceResult<DomainReputationResult>) } private struct CacheEntry { @@ -44,6 +49,7 @@ actor ExternalDataService { let dnsHistoryURL: String? let extendedSubdomainsURL: String? let pricingURL: String? + let reputationURL: String? } private let ttl: TimeInterval = 900 @@ -161,6 +167,25 @@ actor ExternalDataService { ) } + /// Checks a domain against a configured blocklist/reputation endpoint. With + /// no endpoint configured (the default, since DomainDig ships no bundled + /// third-party reputation dependency) this resolves to `.empty` and callers + /// treat the result as unavailable rather than "clean". + func reputation(domain: String) async -> CachedLookupResult<ServiceResult<DomainReputationResult>> { + let normalizedDomain = Self.normalize(domain) + return await execute( + key: .reputation(normalizedDomain), + rateLimitBucket: .reputation, + extract: { payload in + guard case let .reputation(result) = payload else { return nil } + return result + }, + operation: { [configuration = configuration()] in + .reputation(await self.fetchReputation(domain: normalizedDomain, configuration: configuration)) + } + ) + } + private func execute<T>( key: RequestKey, rateLimitBucket: RateLimitBucket, @@ -213,7 +238,9 @@ actor ExternalDataService { extendedSubdomainsURL: defaults.string(forKey: "externalData.extendedSubdomainsURL") ?? Bundle.main.object(forInfoDictionaryKey: "ExternalExtendedSubdomainsURL") as? String, pricingURL: defaults.string(forKey: "externalData.pricingURL") - ?? Bundle.main.object(forInfoDictionaryKey: "ExternalPricingURL") as? String + ?? Bundle.main.object(forInfoDictionaryKey: "ExternalPricingURL") as? String, + reputationURL: defaults.string(forKey: "externalData.reputationURL") + ?? Bundle.main.object(forInfoDictionaryKey: "ExternalReputationURL") as? String ) } @@ -290,6 +317,28 @@ actor ExternalDataService { } } + private func fetchReputation( + domain: String, + configuration: Configuration + ) async -> ServiceResult<DomainReputationResult> { + guard let template = configuration.reputationURL, + let url = Self.url(from: template, domain: domain) else { + return .empty("No reputation source configured") + } + + switch await requestData(url: url) { + case let .success(data): + guard let reputation = parseReputation(from: data) else { + return .error("Invalid external response") + } + return .success(reputation) + case let .empty(message): + return .empty(message) + case let .error(message): + return .error(message) + } + } + private func requestData(url: URL) async -> ServiceResult<Data> { for attempt in 0..<2 { do { @@ -403,6 +452,22 @@ actor ExternalDataService { ) } + private func parseReputation(from data: Data) -> DomainReputationResult? { + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return nil + } + + let listedSources = json["listed_sources"] as? [String] ?? [] + let status: DomainReputationStatus + if let statusString = json["status"] as? String, let parsed = DomainReputationStatus(rawValue: statusString) { + status = parsed + } else { + status = listedSources.isEmpty ? .clean : .listed + } + + return DomainReputationResult(status: status, listedSources: listedSources, checkedAt: Date()) + } + private static func localOwnershipHistory( domain: String, currentOwnership: DomainOwnership?, diff --git a/DomainDig/FeatureAccessService.swift b/DomainDig/FeatureAccessService.swift index a601b33..1598354 100644 --- a/DomainDig/FeatureAccessService.swift +++ b/DomainDig/FeatureAccessService.swift @@ -32,6 +32,7 @@ enum FeatureCapability: String, CaseIterable, Identifiable { case dnsHistory case extendedSubdomains case domainPricing + case reputation var id: String { rawValue } @@ -61,6 +62,8 @@ enum FeatureCapability: String, CaseIterable, Identifiable { return "Extended subdomains" case .domainPricing: return "Domain pricing" + case .reputation: + return "Domain reputation" } } } @@ -157,7 +160,7 @@ enum FeatureAccessService { switch capability { case .workflows, .batchOperations, .automatedMonitoring, .localAlerts, .advancedExports: return "Available in Pro" - case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing: + case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing, .reputation: return "Available in Pro+" case .limitedTracking: return "Tracking is limited on Free" @@ -219,7 +222,7 @@ enum FeatureAccessService { static func upgradePrompt(for capability: FeatureCapability) -> UpgradePromptContext { let title: String switch capability { - case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing: + case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing, .reputation: title = "Available in Pro+" default: title = "Available in Pro" diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift index 376d264..d14ff4a 100644 --- a/DomainDig/Models.swift +++ b/DomainDig/Models.swift @@ -391,6 +391,7 @@ enum DataCapability: String, Codable { case dnsHistory case extendedSubdomains case domainPricing + case reputation } enum UsageCreditFeature: String, Codable, CaseIterable, Identifiable, Sendable { @@ -724,6 +725,29 @@ struct DomainPricingInsight: Codable, Equatable, Sendable { let collectedAt: Date } +enum DomainReputationStatus: String, Codable, Sendable { + case clean + case listed + case unknown + + var title: String { + switch self { + case .clean: + return "Clean" + case .listed: + return "Listed" + case .unknown: + return "Unknown" + } + } +} + +struct DomainReputationResult: Codable, Equatable, Sendable { + let status: DomainReputationStatus + let listedSources: [String] + let checkedAt: Date +} + enum HistoryDateFilter: String, CaseIterable, Identifiable { case today case last7Days @@ -2285,6 +2309,7 @@ struct HistoryEntry: Identifiable, Codable { var extendedSubdomains: [DiscoveredSubdomain] var dnsHistory: [DNSHistoryEvent] var domainPricing: DomainPricingInsight? + var reputation: DomainReputationResult? var portScanResults: [PortScanResult] var hstsPreloaded: Bool? var availabilityResult: DomainAvailabilityResult? @@ -2327,6 +2352,7 @@ struct HistoryEntry: Identifiable, Codable { var extendedSubdomainsError: String? var dnsHistoryError: String? var domainPricingError: String? + var reputationError: String? var portScanError: String? init(domain: String, timestamp: Date, trackedDomainID: UUID? = nil, note: String? = nil, dnsSections: [DNSSection], @@ -2344,6 +2370,7 @@ struct HistoryEntry: Identifiable, Codable { ptrRecord: String? = nil, redirectChain: [RedirectHop] = [], subdomains: [DiscoveredSubdomain] = [], extendedSubdomains: [DiscoveredSubdomain] = [], dnsHistory: [DNSHistoryEvent] = [], domainPricing: DomainPricingInsight? = nil, + reputation: DomainReputationResult? = nil, portScanResults: [PortScanResult] = [], hstsPreloaded: Bool? = nil, availabilityResult: DomainAvailabilityResult? = nil, suggestions: [DomainSuggestionResult] = [], appVersion: String = "2.7.0", @@ -2362,7 +2389,7 @@ struct HistoryEntry: Identifiable, Codable { emailSecurityError: String? = nil, ownershipError: String? = nil, ownershipHistoryError: String? = nil, ptrError: String? = nil, redirectChainError: String? = nil, subdomainsError: String? = nil, extendedSubdomainsError: String? = nil, dnsHistoryError: String? = nil, - domainPricingError: String? = nil, portScanError: String? = nil) { + domainPricingError: String? = nil, reputationError: String? = nil, portScanError: String? = nil) { self.domain = domain self.timestamp = timestamp self.trackedDomainID = trackedDomainID @@ -2390,6 +2417,7 @@ struct HistoryEntry: Identifiable, Codable { self.extendedSubdomains = extendedSubdomains self.dnsHistory = dnsHistory self.domainPricing = domainPricing + self.reputation = reputation self.portScanResults = portScanResults self.hstsPreloaded = hstsPreloaded self.availabilityResult = availabilityResult @@ -2432,6 +2460,7 @@ struct HistoryEntry: Identifiable, Codable { self.extendedSubdomainsError = extendedSubdomainsError self.dnsHistoryError = dnsHistoryError self.domainPricingError = domainPricingError + self.reputationError = reputationError self.portScanError = portScanError } @@ -2465,6 +2494,7 @@ struct HistoryEntry: Identifiable, Codable { extendedSubdomains = try container.decodeIfPresent([DiscoveredSubdomain].self, forKey: .extendedSubdomains) ?? [] dnsHistory = try container.decodeIfPresent([DNSHistoryEvent].self, forKey: .dnsHistory) ?? [] domainPricing = try container.decodeIfPresent(DomainPricingInsight.self, forKey: .domainPricing) + reputation = try container.decodeIfPresent(DomainReputationResult.self, forKey: .reputation) portScanResults = try container.decodeIfPresent([PortScanResult].self, forKey: .portScanResults) ?? [] hstsPreloaded = try container.decodeIfPresent(Bool.self, forKey: .hstsPreloaded) availabilityResult = try container.decodeIfPresent(DomainAvailabilityResult.self, forKey: .availabilityResult) @@ -2510,6 +2540,7 @@ struct HistoryEntry: Identifiable, Codable { extendedSubdomainsError = try container.decodeIfPresent(String.self, forKey: .extendedSubdomainsError) dnsHistoryError = try container.decodeIfPresent(String.self, forKey: .dnsHistoryError) domainPricingError = try container.decodeIfPresent(String.self, forKey: .domainPricingError) + reputationError = try container.decodeIfPresent(String.self, forKey: .reputationError) portScanError = try container.decodeIfPresent(String.self, forKey: .portScanError) } diff --git a/DomainInspectionService.swift b/DomainInspectionService.swift index 0562195..3cdb21c 100644 --- a/DomainInspectionService.swift +++ b/DomainInspectionService.swift @@ -369,6 +369,8 @@ struct DomainInspectionService { dnsHistoryError: nil, domainPricing: nil, domainPricingError: nil, + reputation: nil, + reputationError: nil, portScanResults: portScanResults.value, portScanError: portScanResults.message, changeSummary: nil, diff --git a/DomainReportBuilder.swift b/DomainReportBuilder.swift index 50c46db..4068c0a 100644 --- a/DomainReportBuilder.swift +++ b/DomainReportBuilder.swift @@ -38,6 +38,7 @@ struct DomainReport: Codable { let extendedSubdomains: [String] let dnsHistory: [DNSHistoryEvent] let domainPricing: DomainPricingInsight? + let reputation: DomainReputationResult? let subdomainGroups: [SubdomainGroup] let riskAssessment: DomainRiskAssessment let insights: [String] @@ -360,6 +361,7 @@ struct DomainReportBuilder { extendedSubdomains: snapshot.extendedSubdomains.map(\.hostname), dnsHistory: snapshot.dnsHistory, domainPricing: snapshot.domainPricing, + reputation: snapshot.reputation, subdomainGroups: analysis.subdomainGroups, riskAssessment: analysis.riskAssessment, insights: analysis.insights, diff --git a/DomainReportExporter.swift b/DomainReportExporter.swift index 55b7372..05ecff9 100644 --- a/DomainReportExporter.swift +++ b/DomainReportExporter.swift @@ -69,7 +69,8 @@ enum DomainReportExporter { "Email: \(report.email.summary)", "Subdomains: \(report.subdomains.count)", "Extended Subdomains: \(report.extendedSubdomains.count)", - "External Price: \(report.domainPricing?.estimatedPrice ?? "Unavailable")" + "External Price: \(report.domainPricing?.estimatedPrice ?? "Unavailable")", + "Reputation: \(report.reputation?.status.title ?? "Unavailable")" ] } @@ -406,6 +407,8 @@ enum DomainReportExporter { "pricing_resale_signal", "pricing_auction_signal", "pricing_source", + "reputation_status", + "reputation_listed_sources", "open_ports", "reachability_summary", "geolocation_summary", @@ -438,6 +441,8 @@ enum DomainReportExporter { let subdomainGroups = report.subdomainGroups.map { "\($0.label):\($0.subdomains.count)" }.joined(separator: " | ") let ownershipHistory = report.ownershipHistory.map { "\($0.date.ISO8601Format()) \($0.summary)" }.joined(separator: " | ") let dnsHistory = report.dnsHistory.map { "\($0.date.ISO8601Format()) \($0.summary)" }.joined(separator: " | ") + let reputationStatus = report.reputation?.status.rawValue ?? "" + let reputationListedSources = report.reputation?.listedSources.joined(separator: " | ") ?? "" return [ report.domain, @@ -483,6 +488,8 @@ enum DomainReportExporter { report.domainPricing?.resaleSignal ?? "", report.domainPricing?.auctionSignal ?? "", report.domainPricing?.source ?? "", + reputationStatus, + reputationListedSources, openPorts, report.network.reachabilitySummary, report.network.geolocationSummary, diff --git a/LookupSnapshot.swift b/LookupSnapshot.swift index f9400f5..e0ccbbd 100644 --- a/LookupSnapshot.swift +++ b/LookupSnapshot.swift @@ -68,6 +68,8 @@ struct LookupSnapshot { let dnsHistoryError: String? let domainPricing: DomainPricingInsight? let domainPricingError: String? + let reputation: DomainReputationResult? + let reputationError: String? let portScanResults: [PortScanResult] let portScanError: String? let changeSummary: DomainChangeSummary? @@ -150,6 +152,8 @@ extension HistoryEntry { dnsHistoryError: dnsHistoryError, domainPricing: domainPricing, domainPricingError: domainPricingError, + reputation: reputation, + reputationError: reputationError, portScanResults: portScanResults, portScanError: portScanError, changeSummary: changeSummary, |
