Commit 6e273c2676
Unsigned
Layout: unified · split
DomainDig/DataAccessService.swift +2
| @@ -11,6 +11,8 @@ enum DataAccessService { | ||
| 11 | 11 | return FeatureAccessService.hasAccess(to: .extendedSubdomains) |
| 12 | 12 | case .domainPricing: |
| 13 | 13 | return FeatureAccessService.hasAccess(to: .domainPricing) |
| 14 | case .reputation: | |
| 15 | return FeatureAccessService.hasAccess(to: .reputation) | |
| 14 | 16 | } |
| 15 | 17 | } |
| 16 | 18 | } |
DomainDig/DomainInsightEngine.swift +16
| @@ -303,6 +303,19 @@ enum DomainInsightEngine { | ||
| 303 | 303 | } |
| 304 | 304 | } |
| 305 | 305 | |
| 306 | if let reputation = snapshot.reputation { | |
| 307 | switch reputation.status { | |
| 308 | case .listed: | |
| 309 | score += 25 | |
| 310 | let sourceList = reputation.listedSources.isEmpty ? "" : " (\(reputation.listedSources.joined(separator: ", ")))" | |
| 311 | factors.append(.init(description: "Domain is flagged by a configured reputation source\(sourceList)", impact: .negative)) | |
| 312 | case .clean: | |
| 313 | factors.append(.init(description: "Domain is clean against the configured reputation source", impact: .positive)) | |
| 314 | case .unknown: | |
| 315 | break | |
| 316 | } | |
| 317 | } | |
| 318 | ||
| 306 | 319 | let clampedScore = min(max(score, 0), 100) |
| 307 | 320 | let level: RiskLevel |
| 308 | 321 | switch clampedScore { |
| @@ -327,6 +340,9 @@ enum DomainInsightEngine { | ||
| 327 | 340 | ) -> [String] { |
| 328 | 341 | var items: [String] = [] |
| 329 | 342 | |
| 343 | if snapshot.reputation?.status == .listed { | |
| 344 | items.append("Domain is flagged by a configured reputation source") | |
| 345 | } | |
| 330 | 346 | if let group = subdomainGroups.first(where: { $0.label == "staging" || $0.label == "dev" }) { |
| 331 | 347 | items.append("Multiple \(group.label) subdomains suggest non-production environments are exposed") |
| 332 | 348 | } |
DomainDig/DomainMonitoringService.swift +4
| @@ -516,6 +516,7 @@ final class DomainMonitoringService { | ||
| 516 | 516 | extendedSubdomains: snapshot.extendedSubdomains, |
| 517 | 517 | dnsHistory: snapshot.dnsHistory, |
| 518 | 518 | domainPricing: snapshot.domainPricing, |
| 519 | reputation: snapshot.reputation, | |
| 519 | 520 | portScanResults: snapshot.portScanResults, |
| 520 | 521 | hstsPreloaded: snapshot.hstsPreloaded, |
| 521 | 522 | availabilityResult: snapshot.availabilityResult, |
| @@ -554,6 +555,7 @@ final class DomainMonitoringService { | ||
| 554 | 555 | extendedSubdomainsError: snapshot.extendedSubdomainsError, |
| 555 | 556 | dnsHistoryError: snapshot.dnsHistoryError, |
| 556 | 557 | domainPricingError: snapshot.domainPricingError, |
| 558 | reputationError: snapshot.reputationError, | |
| 557 | 559 | portScanError: snapshot.portScanError |
| 558 | 560 | ) |
| 559 | 561 | |
| @@ -930,6 +932,8 @@ final class DomainMonitoringService { | ||
| 930 | 932 | dnsHistoryError: previousSnapshot.dnsHistoryError, |
| 931 | 933 | domainPricing: previousSnapshot.domainPricing, |
| 932 | 934 | domainPricingError: previousSnapshot.domainPricingError, |
| 935 | reputation: previousSnapshot.reputation, | |
| 936 | reputationError: previousSnapshot.reputationError, | |
| 933 | 937 | portScanResults: previousSnapshot.portScanResults, |
| 934 | 938 | portScanError: previousSnapshot.portScanError, |
| 935 | 939 | changeSummary: previousSnapshot.changeSummary, |
DomainDig/DomainViewModel.swift +51
| @@ -232,6 +232,9 @@ final class DomainViewModel { | ||
| 232 | 232 | var domainPricing: DomainPricingInsight? |
| 233 | 233 | var domainPricingLoading = false |
| 234 | 234 | var domainPricingError: String? |
| 235 | var reputation: DomainReputationResult? | |
| 236 | var reputationLoading = false | |
| 237 | var reputationError: String? | |
| 235 | 238 | var usageCredits: [UsageCreditFeature: UsageCreditStatus] = DomainViewModel.defaultUsageCredits() |
| 236 | 239 | |
| 237 | 240 | var portScanResults: [PortScanResult] = [] |
| @@ -642,6 +645,8 @@ final class DomainViewModel { | ||
| 642 | 645 | dnsHistoryError: dnsHistoryError, |
| 643 | 646 | domainPricing: domainPricing, |
| 644 | 647 | domainPricingError: domainPricingError, |
| 648 | reputation: reputation, | |
| 649 | reputationError: reputationError, | |
| 645 | 650 | portScanResults: allPortScanResults, |
| 646 | 651 | portScanError: combinedPortScanError, |
| 647 | 652 | changeSummary: currentChangeSummary, |
| @@ -1883,6 +1888,11 @@ final class DomainViewModel { | ||
| 1883 | 1888 | await refreshDomainPricing(for: snapshot.domain, persistAfterFetch: false) |
| 1884 | 1889 | } |
| 1885 | 1890 | |
| 1891 | if DataAccessService.hasAccess(to: .reputation), reputation == nil { | |
| 1892 | DomainDebugLog.debug("DomainViewModel.performLookup loadingReputation domain=\(domain)") | |
| 1893 | await refreshReputation(for: snapshot.domain, persistAfterFetch: false) | |
| 1894 | } | |
| 1895 | ||
| 1886 | 1896 | guard snapshot.statusMessage == nil else { |
| 1887 | 1897 | return history.first(where: { $0.id == snapshot.historyEntryID }) |
| 1888 | 1898 | } |
| @@ -2053,6 +2063,8 @@ final class DomainViewModel { | ||
| 2053 | 2063 | dnsHistoryError: previousSnapshot.dnsHistoryError, |
| 2054 | 2064 | domainPricing: previousSnapshot.domainPricing, |
| 2055 | 2065 | domainPricingError: previousSnapshot.domainPricingError, |
| 2066 | reputation: previousSnapshot.reputation, | |
| 2067 | reputationError: previousSnapshot.reputationError, | |
| 2056 | 2068 | portScanResults: previousSnapshot.portScanResults, |
| 2057 | 2069 | portScanError: previousSnapshot.portScanError, |
| 2058 | 2070 | changeSummary: previousSnapshot.changeSummary, |
| @@ -2474,6 +2486,7 @@ final class DomainViewModel { | ||
| 2474 | 2486 | extendedSubdomains: snapshot.extendedSubdomains, |
| 2475 | 2487 | dnsHistory: snapshot.dnsHistory, |
| 2476 | 2488 | domainPricing: snapshot.domainPricing, |
| 2489 | reputation: snapshot.reputation, | |
| 2477 | 2490 | portScanResults: snapshot.portScanResults, |
| 2478 | 2491 | hstsPreloaded: snapshot.hstsPreloaded, |
| 2479 | 2492 | availabilityResult: snapshot.availabilityResult, |
| @@ -2516,6 +2529,7 @@ final class DomainViewModel { | ||
| 2516 | 2529 | extendedSubdomainsError: snapshot.extendedSubdomainsError, |
| 2517 | 2530 | dnsHistoryError: snapshot.dnsHistoryError, |
| 2518 | 2531 | domainPricingError: snapshot.domainPricingError, |
| 2532 | reputationError: snapshot.reputationError, | |
| 2519 | 2533 | portScanError: snapshot.portScanError |
| 2520 | 2534 | ) |
| 2521 | 2535 | |
| @@ -3844,6 +3858,8 @@ final class DomainViewModel { | ||
| 3844 | 3858 | dnsHistoryError: nil, |
| 3845 | 3859 | domainPricing: nil, |
| 3846 | 3860 | domainPricingError: nil, |
| 3861 | reputation: nil, | |
| 3862 | reputationError: nil, | |
| 3847 | 3863 | portScanResults: [], |
| 3848 | 3864 | portScanError: nil, |
| 3849 | 3865 | changeSummary: trackedDomain.lastChangeSummary, |
| @@ -4005,6 +4021,18 @@ final class DomainViewModel { | ||
| 4005 | 4021 | rows.append(InfoRowViewData(label: "Auction", value: auctionSignal, tone: .secondary)) |
| 4006 | 4022 | } |
| 4007 | 4023 | } |
| 4024 | if let reputation = snapshot.reputation { | |
| 4025 | let tone: ResultTone | |
| 4026 | switch reputation.status { | |
| 4027 | case .clean: tone = .success | |
| 4028 | case .listed: tone = .failure | |
| 4029 | case .unknown: tone = .secondary | |
| 4030 | } | |
| 4031 | let value = reputation.status == .listed && !reputation.listedSources.isEmpty | |
| 4032 | ? "\(reputation.status.title) (\(reputation.listedSources.joined(separator: ", ")))" | |
| 4033 | : reputation.status.title | |
| 4034 | rows.append(InfoRowViewData(label: "Reputation", value: value, tone: tone)) | |
| 4035 | } | |
| 4008 | 4036 | if let certificateStatus = certificateBadgeLabel(from: snapshot) { |
| 4009 | 4037 | rows.insert( |
| 4010 | 4038 | InfoRowViewData( |
| @@ -4700,6 +4728,29 @@ final class DomainViewModel { | ||
| 4700 | 4728 | } |
| 4701 | 4729 | } |
| 4702 | 4730 | |
| 4731 | private func refreshReputation(for domain: String, persistAfterFetch: Bool) async { | |
| 4732 | reputationLoading = true | |
| 4733 | let outcome = await ExternalDataService.shared.reputation(domain: domain) | |
| 4734 | ||
| 4735 | switch outcome.value { | |
| 4736 | case let .success(result): | |
| 4737 | reputation = result | |
| 4738 | reputationError = nil | |
| 4739 | case let .empty(message): | |
| 4740 | reputation = nil | |
| 4741 | reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable") | |
| 4742 | case let .error(message): | |
| 4743 | reputation = nil | |
| 4744 | reputationError = conciseExternalMessage(message, fallback: "Reputation check unavailable") | |
| 4745 | } | |
| 4746 | ||
| 4747 | reputationLoading = false | |
| 4748 | ||
| 4749 | if persistAfterFetch { | |
| 4750 | _ = saveHistoryEntry(replaceLatest: true) | |
| 4751 | } | |
| 4752 | } | |
| 4753 | ||
| 4703 | 4754 | private func conciseExternalMessage(_ message: String, fallback: String) -> String { |
| 4704 | 4755 | let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) |
| 4705 | 4756 | if trimmed.isEmpty { |
DomainDig/ExternalDataService.swift +66 −1
| @@ -8,12 +8,14 @@ actor ExternalDataService { | ||
| 8 | 8 | case dnsHistory(String) |
| 9 | 9 | case extendedSubdomains(String) |
| 10 | 10 | case pricing(String) |
| 11 | case reputation(String) | |
| 11 | 12 | } |
| 12 | 13 | |
| 13 | 14 | private enum RateLimitBucket: Hashable { |
| 14 | 15 | case history |
| 15 | 16 | case subdomains |
| 16 | 17 | case pricing |
| 18 | case reputation | |
| 17 | 19 | |
| 18 | 20 | var minimumSpacing: TimeInterval { |
| 19 | 21 | switch self { |
| @@ -23,6 +25,8 @@ actor ExternalDataService { | ||
| 23 | 25 | return 1.5 |
| 24 | 26 | case .pricing: |
| 25 | 27 | return 1.0 |
| 28 | case .reputation: | |
| 29 | return 1.0 | |
| 26 | 30 | } |
| 27 | 31 | } |
| 28 | 32 | } |
| @@ -32,6 +36,7 @@ actor ExternalDataService { | ||
| 32 | 36 | case dnsHistory(ServiceResult<[DNSHistoryEvent]>) |
| 33 | 37 | case extendedSubdomains(ServiceResult<[DiscoveredSubdomain]>) |
| 34 | 38 | case pricing(ServiceResult<DomainPricingInsight>) |
| 39 | case reputation(ServiceResult<DomainReputationResult>) | |
| 35 | 40 | } |
| 36 | 41 | |
| 37 | 42 | private struct CacheEntry { |
| @@ -44,6 +49,7 @@ actor ExternalDataService { | ||
| 44 | 49 | let dnsHistoryURL: String? |
| 45 | 50 | let extendedSubdomainsURL: String? |
| 46 | 51 | let pricingURL: String? |
| 52 | let reputationURL: String? | |
| 47 | 53 | } |
| 48 | 54 | |
| 49 | 55 | private let ttl: TimeInterval = 900 |
| @@ -161,6 +167,25 @@ actor ExternalDataService { | ||
| 161 | 167 | ) |
| 162 | 168 | } |
| 163 | 169 | |
| 170 | /// Checks a domain against a configured blocklist/reputation endpoint. With | |
| 171 | /// no endpoint configured (the default, since DomainDig ships no bundled | |
| 172 | /// third-party reputation dependency) this resolves to `.empty` and callers | |
| 173 | /// treat the result as unavailable rather than "clean". | |
| 174 | func reputation(domain: String) async -> CachedLookupResult<ServiceResult<DomainReputationResult>> { | |
| 175 | let normalizedDomain = Self.normalize(domain) | |
| 176 | return await execute( | |
| 177 | key: .reputation(normalizedDomain), | |
| 178 | rateLimitBucket: .reputation, | |
| 179 | extract: { payload in | |
| 180 | guard case let .reputation(result) = payload else { return nil } | |
| 181 | return result | |
| 182 | }, | |
| 183 | operation: { [configuration = configuration()] in | |
| 184 | .reputation(await self.fetchReputation(domain: normalizedDomain, configuration: configuration)) | |
| 185 | } | |
| 186 | ) | |
| 187 | } | |
| 188 | ||
| 164 | 189 | private func execute<T>( |
| 165 | 190 | key: RequestKey, |
| 166 | 191 | rateLimitBucket: RateLimitBucket, |
| @@ -213,7 +238,9 @@ actor ExternalDataService { | ||
| 213 | 238 | extendedSubdomainsURL: defaults.string(forKey: "externalData.extendedSubdomainsURL") |
| 214 | 239 | ?? Bundle.main.object(forInfoDictionaryKey: "ExternalExtendedSubdomainsURL") as? String, |
| 215 | 240 | pricingURL: defaults.string(forKey: "externalData.pricingURL") |
| 216 | ?? Bundle.main.object(forInfoDictionaryKey: "ExternalPricingURL") as? String | |
| 241 | ?? Bundle.main.object(forInfoDictionaryKey: "ExternalPricingURL") as? String, | |
| 242 | reputationURL: defaults.string(forKey: "externalData.reputationURL") | |
| 243 | ?? Bundle.main.object(forInfoDictionaryKey: "ExternalReputationURL") as? String | |
| 217 | 244 | ) |
| 218 | 245 | } |
| 219 | 246 | |
| @@ -290,6 +317,28 @@ actor ExternalDataService { | ||
| 290 | 317 | } |
| 291 | 318 | } |
| 292 | 319 | |
| 320 | private func fetchReputation( | |
| 321 | domain: String, | |
| 322 | configuration: Configuration | |
| 323 | ) async -> ServiceResult<DomainReputationResult> { | |
| 324 | guard let template = configuration.reputationURL, | |
| 325 | let url = Self.url(from: template, domain: domain) else { | |
| 326 | return .empty("No reputation source configured") | |
| 327 | } | |
| 328 | ||
| 329 | switch await requestData(url: url) { | |
| 330 | case let .success(data): | |
| 331 | guard let reputation = parseReputation(from: data) else { | |
| 332 | return .error("Invalid external response") | |
| 333 | } | |
| 334 | return .success(reputation) | |
| 335 | case let .empty(message): | |
| 336 | return .empty(message) | |
| 337 | case let .error(message): | |
| 338 | return .error(message) | |
| 339 | } | |
| 340 | } | |
| 341 | ||
| 293 | 342 | private func requestData(url: URL) async -> ServiceResult<Data> { |
| 294 | 343 | for attempt in 0..<2 { |
| 295 | 344 | do { |
| @@ -403,6 +452,22 @@ actor ExternalDataService { | ||
| 403 | 452 | ) |
| 404 | 453 | } |
| 405 | 454 | |
| 455 | private func parseReputation(from data: Data) -> DomainReputationResult? { | |
| 456 | guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { | |
| 457 | return nil | |
| 458 | } | |
| 459 | ||
| 460 | let listedSources = json["listed_sources"] as? [String] ?? [] | |
| 461 | let status: DomainReputationStatus | |
| 462 | if let statusString = json["status"] as? String, let parsed = DomainReputationStatus(rawValue: statusString) { | |
| 463 | status = parsed | |
| 464 | } else { | |
| 465 | status = listedSources.isEmpty ? .clean : .listed | |
| 466 | } | |
| 467 | ||
| 468 | return DomainReputationResult(status: status, listedSources: listedSources, checkedAt: Date()) | |
| 469 | } | |
| 470 | ||
| 406 | 471 | private static func localOwnershipHistory( |
| 407 | 472 | domain: String, |
| 408 | 473 | currentOwnership: DomainOwnership?, |
DomainDig/FeatureAccessService.swift +5 −2
| @@ -32,6 +32,7 @@ enum FeatureCapability: String, CaseIterable, Identifiable { | ||
| 32 | 32 | case dnsHistory |
| 33 | 33 | case extendedSubdomains |
| 34 | 34 | case domainPricing |
| 35 | case reputation | |
| 35 | 36 | |
| 36 | 37 | var id: String { rawValue } |
| 37 | 38 | |
| @@ -61,6 +62,8 @@ enum FeatureCapability: String, CaseIterable, Identifiable { | ||
| 61 | 62 | return "Extended subdomains" |
| 62 | 63 | case .domainPricing: |
| 63 | 64 | return "Domain pricing" |
| 65 | case .reputation: | |
| 66 | return "Domain reputation" | |
| 64 | 67 | } |
| 65 | 68 | } |
| 66 | 69 | } |
| @@ -157,7 +160,7 @@ enum FeatureAccessService { | ||
| 157 | 160 | switch capability { |
| 158 | 161 | case .workflows, .batchOperations, .automatedMonitoring, .localAlerts, .advancedExports: |
| 159 | 162 | return "Available in Pro" |
| 160 | case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing: | |
| 163 | case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing, .reputation: | |
| 161 | 164 | return "Available in Pro+" |
| 162 | 165 | case .limitedTracking: |
| 163 | 166 | return "Tracking is limited on Free" |
| @@ -219,7 +222,7 @@ enum FeatureAccessService { | ||
| 219 | 222 | static func upgradePrompt(for capability: FeatureCapability) -> UpgradePromptContext { |
| 220 | 223 | let title: String |
| 221 | 224 | switch capability { |
| 222 | case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing: | |
| 225 | case .ownershipHistory, .dnsHistory, .extendedSubdomains, .domainPricing, .reputation: | |
| 223 | 226 | title = "Available in Pro+" |
| 224 | 227 | default: |
| 225 | 228 | title = "Available in Pro" |
DomainDig/Models.swift +32 −1
| @@ -391,6 +391,7 @@ enum DataCapability: String, Codable { | ||
| 391 | 391 | case dnsHistory |
| 392 | 392 | case extendedSubdomains |
| 393 | 393 | case domainPricing |
| 394 | case reputation | |
| 394 | 395 | } |
| 395 | 396 | |
| 396 | 397 | enum UsageCreditFeature: String, Codable, CaseIterable, Identifiable, Sendable { |
| @@ -724,6 +725,29 @@ struct DomainPricingInsight: Codable, Equatable, Sendable { | ||
| 724 | 725 | let collectedAt: Date |
| 725 | 726 | } |
| 726 | 727 | |
| 728 | enum DomainReputationStatus: String, Codable, Sendable { | |
| 729 | case clean | |
| 730 | case listed | |
| 731 | case unknown | |
| 732 | ||
| 733 | var title: String { | |
| 734 | switch self { | |
| 735 | case .clean: | |
| 736 | return "Clean" | |
| 737 | case .listed: | |
| 738 | return "Listed" | |
| 739 | case .unknown: | |
| 740 | return "Unknown" | |
| 741 | } | |
| 742 | } | |
| 743 | } | |
| 744 | ||
| 745 | struct DomainReputationResult: Codable, Equatable, Sendable { | |
| 746 | let status: DomainReputationStatus | |
| 747 | let listedSources: [String] | |
| 748 | let checkedAt: Date | |
| 749 | } | |
| 750 | ||
| 727 | 751 | enum HistoryDateFilter: String, CaseIterable, Identifiable { |
| 728 | 752 | case today |
| 729 | 753 | case last7Days |
| @@ -2285,6 +2309,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2285 | 2309 | var extendedSubdomains: [DiscoveredSubdomain] |
| 2286 | 2310 | var dnsHistory: [DNSHistoryEvent] |
| 2287 | 2311 | var domainPricing: DomainPricingInsight? |
| 2312 | var reputation: DomainReputationResult? | |
| 2288 | 2313 | var portScanResults: [PortScanResult] |
| 2289 | 2314 | var hstsPreloaded: Bool? |
| 2290 | 2315 | var availabilityResult: DomainAvailabilityResult? |
| @@ -2327,6 +2352,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2327 | 2352 | var extendedSubdomainsError: String? |
| 2328 | 2353 | var dnsHistoryError: String? |
| 2329 | 2354 | var domainPricingError: String? |
| 2355 | var reputationError: String? | |
| 2330 | 2356 | var portScanError: String? |
| 2331 | 2357 | |
| 2332 | 2358 | init(domain: String, timestamp: Date, trackedDomainID: UUID? = nil, note: String? = nil, dnsSections: [DNSSection], |
| @@ -2344,6 +2370,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2344 | 2370 | ptrRecord: String? = nil, redirectChain: [RedirectHop] = [], subdomains: [DiscoveredSubdomain] = [], |
| 2345 | 2371 | extendedSubdomains: [DiscoveredSubdomain] = [], dnsHistory: [DNSHistoryEvent] = [], |
| 2346 | 2372 | domainPricing: DomainPricingInsight? = nil, |
| 2373 | reputation: DomainReputationResult? = nil, | |
| 2347 | 2374 | portScanResults: [PortScanResult] = [], |
| 2348 | 2375 | hstsPreloaded: Bool? = nil, availabilityResult: DomainAvailabilityResult? = nil, |
| 2349 | 2376 | suggestions: [DomainSuggestionResult] = [], appVersion: String = "2.7.0", |
| @@ -2362,7 +2389,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2362 | 2389 | emailSecurityError: String? = nil, ownershipError: String? = nil, ownershipHistoryError: String? = nil, |
| 2363 | 2390 | ptrError: String? = nil, redirectChainError: String? = nil, subdomainsError: String? = nil, |
| 2364 | 2391 | extendedSubdomainsError: String? = nil, dnsHistoryError: String? = nil, |
| 2365 | domainPricingError: String? = nil, portScanError: String? = nil) { | |
| 2392 | domainPricingError: String? = nil, reputationError: String? = nil, portScanError: String? = nil) { | |
| 2366 | 2393 | self.domain = domain |
| 2367 | 2394 | self.timestamp = timestamp |
| 2368 | 2395 | self.trackedDomainID = trackedDomainID |
| @@ -2390,6 +2417,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2390 | 2417 | self.extendedSubdomains = extendedSubdomains |
| 2391 | 2418 | self.dnsHistory = dnsHistory |
| 2392 | 2419 | self.domainPricing = domainPricing |
| 2420 | self.reputation = reputation | |
| 2393 | 2421 | self.portScanResults = portScanResults |
| 2394 | 2422 | self.hstsPreloaded = hstsPreloaded |
| 2395 | 2423 | self.availabilityResult = availabilityResult |
| @@ -2432,6 +2460,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2432 | 2460 | self.extendedSubdomainsError = extendedSubdomainsError |
| 2433 | 2461 | self.dnsHistoryError = dnsHistoryError |
| 2434 | 2462 | self.domainPricingError = domainPricingError |
| 2463 | self.reputationError = reputationError | |
| 2435 | 2464 | self.portScanError = portScanError |
| 2436 | 2465 | } |
| 2437 | 2466 | |
| @@ -2465,6 +2494,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2465 | 2494 | extendedSubdomains = try container.decodeIfPresent([DiscoveredSubdomain].self, forKey: .extendedSubdomains) ?? [] |
| 2466 | 2495 | dnsHistory = try container.decodeIfPresent([DNSHistoryEvent].self, forKey: .dnsHistory) ?? [] |
| 2467 | 2496 | domainPricing = try container.decodeIfPresent(DomainPricingInsight.self, forKey: .domainPricing) |
| 2497 | reputation = try container.decodeIfPresent(DomainReputationResult.self, forKey: .reputation) | |
| 2468 | 2498 | portScanResults = try container.decodeIfPresent([PortScanResult].self, forKey: .portScanResults) ?? [] |
| 2469 | 2499 | hstsPreloaded = try container.decodeIfPresent(Bool.self, forKey: .hstsPreloaded) |
| 2470 | 2500 | availabilityResult = try container.decodeIfPresent(DomainAvailabilityResult.self, forKey: .availabilityResult) |
| @@ -2510,6 +2540,7 @@ struct HistoryEntry: Identifiable, Codable { | ||
| 2510 | 2540 | extendedSubdomainsError = try container.decodeIfPresent(String.self, forKey: .extendedSubdomainsError) |
| 2511 | 2541 | dnsHistoryError = try container.decodeIfPresent(String.self, forKey: .dnsHistoryError) |
| 2512 | 2542 | domainPricingError = try container.decodeIfPresent(String.self, forKey: .domainPricingError) |
| 2543 | reputationError = try container.decodeIfPresent(String.self, forKey: .reputationError) | |
| 2513 | 2544 | portScanError = try container.decodeIfPresent(String.self, forKey: .portScanError) |
| 2514 | 2545 | } |
| 2515 | 2546 | |
DomainInspectionService.swift +2
| @@ -369,6 +369,8 @@ struct DomainInspectionService { | ||
| 369 | 369 | dnsHistoryError: nil, |
| 370 | 370 | domainPricing: nil, |
| 371 | 371 | domainPricingError: nil, |
| 372 | reputation: nil, | |
| 373 | reputationError: nil, | |
| 372 | 374 | portScanResults: portScanResults.value, |
| 373 | 375 | portScanError: portScanResults.message, |
| 374 | 376 | changeSummary: nil, |
DomainReportBuilder.swift +2
| @@ -38,6 +38,7 @@ struct DomainReport: Codable { | ||
| 38 | 38 | let extendedSubdomains: [String] |
| 39 | 39 | let dnsHistory: [DNSHistoryEvent] |
| 40 | 40 | let domainPricing: DomainPricingInsight? |
| 41 | let reputation: DomainReputationResult? | |
| 41 | 42 | let subdomainGroups: [SubdomainGroup] |
| 42 | 43 | let riskAssessment: DomainRiskAssessment |
| 43 | 44 | let insights: [String] |
| @@ -360,6 +361,7 @@ struct DomainReportBuilder { | ||
| 360 | 361 | extendedSubdomains: snapshot.extendedSubdomains.map(\.hostname), |
| 361 | 362 | dnsHistory: snapshot.dnsHistory, |
| 362 | 363 | domainPricing: snapshot.domainPricing, |
| 364 | reputation: snapshot.reputation, | |
| 363 | 365 | subdomainGroups: analysis.subdomainGroups, |
| 364 | 366 | riskAssessment: analysis.riskAssessment, |
| 365 | 367 | insights: analysis.insights, |
DomainReportExporter.swift +8 −1
| @@ -69,7 +69,8 @@ enum DomainReportExporter { | ||
| 69 | 69 | "Email: \(report.email.summary)", |
| 70 | 70 | "Subdomains: \(report.subdomains.count)", |
| 71 | 71 | "Extended Subdomains: \(report.extendedSubdomains.count)", |
| 72 | "External Price: \(report.domainPricing?.estimatedPrice ?? "Unavailable")" | |
| 72 | "External Price: \(report.domainPricing?.estimatedPrice ?? "Unavailable")", | |
| 73 | "Reputation: \(report.reputation?.status.title ?? "Unavailable")" | |
| 73 | 74 | ] |
| 74 | 75 | } |
| 75 | 76 | |
| @@ -406,6 +407,8 @@ enum DomainReportExporter { | ||
| 406 | 407 | "pricing_resale_signal", |
| 407 | 408 | "pricing_auction_signal", |
| 408 | 409 | "pricing_source", |
| 410 | "reputation_status", | |
| 411 | "reputation_listed_sources", | |
| 409 | 412 | "open_ports", |
| 410 | 413 | "reachability_summary", |
| 411 | 414 | "geolocation_summary", |
| @@ -438,6 +441,8 @@ enum DomainReportExporter { | ||
| 438 | 441 | let subdomainGroups = report.subdomainGroups.map { "\($0.label):\($0.subdomains.count)" }.joined(separator: " | ") |
| 439 | 442 | let ownershipHistory = report.ownershipHistory.map { "\($0.date.ISO8601Format()) \($0.summary)" }.joined(separator: " | ") |
| 440 | 443 | let dnsHistory = report.dnsHistory.map { "\($0.date.ISO8601Format()) \($0.summary)" }.joined(separator: " | ") |
| 444 | let reputationStatus = report.reputation?.status.rawValue ?? "" | |
| 445 | let reputationListedSources = report.reputation?.listedSources.joined(separator: " | ") ?? "" | |
| 441 | 446 | |
| 442 | 447 | return [ |
| 443 | 448 | report.domain, |
| @@ -483,6 +488,8 @@ enum DomainReportExporter { | ||
| 483 | 488 | report.domainPricing?.resaleSignal ?? "", |
| 484 | 489 | report.domainPricing?.auctionSignal ?? "", |
| 485 | 490 | report.domainPricing?.source ?? "", |
| 491 | reputationStatus, | |
| 492 | reputationListedSources, | |
| 486 | 493 | openPorts, |
| 487 | 494 | report.network.reachabilitySummary, |
| 488 | 495 | report.network.geolocationSummary, |
LookupSnapshot.swift +4
| @@ -68,6 +68,8 @@ struct LookupSnapshot { | ||
| 68 | 68 | let dnsHistoryError: String? |
| 69 | 69 | let domainPricing: DomainPricingInsight? |
| 70 | 70 | let domainPricingError: String? |
| 71 | let reputation: DomainReputationResult? | |
| 72 | let reputationError: String? | |
| 71 | 73 | let portScanResults: [PortScanResult] |
| 72 | 74 | let portScanError: String? |
| 73 | 75 | let changeSummary: DomainChangeSummary? |
| @@ -150,6 +152,8 @@ extension HistoryEntry { | ||
| 150 | 152 | dnsHistoryError: dnsHistoryError, |
| 151 | 153 | domainPricing: domainPricing, |
| 152 | 154 | domainPricingError: domainPricingError, |
| 155 | reputation: reputation, | |
| 156 | reputationError: reputationError, | |
| 153 | 157 | portScanResults: portScanResults, |
| 154 | 158 | portScanError: portScanError, |
| 155 | 159 | changeSummary: changeSummary, |