krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

6e273c2676ce29cef057d117e4427e031886e743

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-07-17T15:39:17Z

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.
 DomainDig/DataAccessService.swift       |  2 +
 DomainDig/DomainInsightEngine.swift     | 16 ++++++++
 DomainDig/DomainMonitoringService.swift |  4 ++
 DomainDig/DomainViewModel.swift         | 51 +++++++++++++++++++++++++
 DomainDig/ExternalDataService.swift     | 67 ++++++++++++++++++++++++++++++++-
 DomainDig/FeatureAccessService.swift    |  7 +++-
 DomainDig/Models.swift                  | 33 +++++++++++++++-
 DomainInspectionService.swift           |  2 +
 DomainReportBuilder.swift               |  2 +
 DomainReportExporter.swift              |  9 ++++-
 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,