summaryrefslogtreecommitdiff
path: root/DomainReportBuilder.swift
diff options
context:
space:
mode:
Diffstat (limited to 'DomainReportBuilder.swift')
-rw-r--r--DomainReportBuilder.swift190
1 files changed, 190 insertions, 0 deletions
diff --git a/DomainReportBuilder.swift b/DomainReportBuilder.swift
new file mode 100644
index 0000000..8cb5c82
--- /dev/null
+++ b/DomainReportBuilder.swift
@@ -0,0 +1,190 @@
+import Foundation
+
+struct DomainReport: Codable {
+ let domain: String
+ let timestamp: Date
+ let availability: DomainAvailabilityStatus
+ let ownership: DomainOwnership?
+ let dns: DNSResultSummary
+ let web: WebResultSummary
+ let email: EmailSecuritySummary
+ let network: NetworkSummary
+ let subdomains: [String]
+ let changeSummary: DomainChangeSummary?
+}
+
+struct DNSResultSummary: Codable {
+ let resolverDisplayName: String
+ let resolverURLString: String
+ let lookupDurationMs: Int?
+ let recordSections: [DNSSection]
+ let primaryIP: String?
+ let ptrRecord: String?
+ let dnssecSigned: Bool?
+ let error: String?
+ let ptrError: String?
+}
+
+struct WebResultSummary: Codable {
+ let tls: SSLCertificateInfo?
+ let tlsStatus: String
+ let certificateWarningLevel: CertificateWarningLevel
+ let hstsPreloaded: Bool?
+ let headers: [HTTPHeader]
+ let headerCount: Int
+ let securityGrade: String?
+ let statusCode: Int?
+ let responseTimeMs: Int?
+ let httpProtocol: String?
+ let http3Advertised: Bool
+ let redirectChain: [RedirectHop]
+ let finalURL: String?
+ let tlsError: String?
+ let headersError: String?
+ let redirectError: String?
+}
+
+struct EmailSecuritySummary: Codable {
+ let records: EmailSecurityResult?
+ let summary: String
+ let error: String?
+}
+
+struct NetworkSummary: Codable {
+ let primaryIP: String?
+ let reachability: [PortReachability]
+ let reachabilitySummary: String
+ let reachabilityError: String?
+ let geolocation: IPGeolocation?
+ let geolocationSummary: String
+ let geolocationError: String?
+ let portScan: [PortScanResult]
+ let openPorts: [UInt16]
+ let portScanError: String?
+}
+
+struct DomainReportBuilder {
+ func build(from snapshot: LookupSnapshot, previousSnapshot: LookupSnapshot? = nil) -> DomainReport {
+ let primaryIP = primaryIPAddress(from: snapshot)
+
+ return DomainReport(
+ domain: snapshot.domain,
+ timestamp: snapshot.timestamp,
+ availability: snapshot.availabilityResult?.status ?? .unknown,
+ ownership: snapshot.ownership,
+ dns: DNSResultSummary(
+ resolverDisplayName: snapshot.resolverDisplayName,
+ resolverURLString: snapshot.resolverURLString,
+ lookupDurationMs: snapshot.totalLookupDurationMs,
+ recordSections: snapshot.dnsSections,
+ primaryIP: primaryIP,
+ ptrRecord: snapshot.ptrRecord,
+ dnssecSigned: dnssecSigned(from: snapshot),
+ error: snapshot.dnsError,
+ ptrError: snapshot.ptrError
+ ),
+ web: WebResultSummary(
+ tls: snapshot.sslInfo,
+ tlsStatus: tlsStatus(from: snapshot),
+ certificateWarningLevel: DomainDiffService.certificateWarningLevel(for: snapshot),
+ hstsPreloaded: snapshot.hstsPreloaded,
+ headers: snapshot.httpHeaders,
+ headerCount: snapshot.httpHeaders.count,
+ securityGrade: snapshot.httpSecurityGrade,
+ statusCode: snapshot.httpStatusCode,
+ responseTimeMs: snapshot.httpResponseTimeMs,
+ httpProtocol: snapshot.httpProtocol,
+ http3Advertised: snapshot.http3Advertised,
+ redirectChain: snapshot.redirectChain,
+ finalURL: snapshot.redirectChain.last?.url,
+ tlsError: snapshot.sslError,
+ headersError: snapshot.httpHeadersError,
+ redirectError: snapshot.redirectChainError
+ ),
+ email: EmailSecuritySummary(
+ records: snapshot.emailSecurity,
+ summary: emailSummary(from: snapshot),
+ error: snapshot.emailSecurityError
+ ),
+ network: NetworkSummary(
+ primaryIP: primaryIP,
+ reachability: snapshot.reachabilityResults,
+ reachabilitySummary: reachabilitySummary(from: snapshot),
+ reachabilityError: snapshot.reachabilityError,
+ geolocation: snapshot.ipGeolocation,
+ geolocationSummary: geolocationSummary(from: snapshot),
+ geolocationError: snapshot.ipGeolocationError,
+ portScan: snapshot.portScanResults,
+ openPorts: snapshot.portScanResults.filter(\.open).map(\.port),
+ portScanError: snapshot.portScanError
+ ),
+ subdomains: snapshot.subdomains.map(\.hostname),
+ changeSummary: snapshot.changeSummary ?? previousSnapshot.map {
+ DomainDiffService.summary(from: $0, to: snapshot, generatedAt: snapshot.timestamp)
+ }
+ )
+ }
+
+ func build(from entry: HistoryEntry, previousSnapshot: LookupSnapshot? = nil) -> DomainReport {
+ build(from: entry.snapshot, previousSnapshot: previousSnapshot)
+ }
+
+ private func primaryIPAddress(from snapshot: LookupSnapshot) -> String? {
+ snapshot.dnsSections.first(where: { $0.recordType == .A })?.records.first?.value
+ }
+
+ private func dnssecSigned(from snapshot: LookupSnapshot) -> Bool? {
+ snapshot.dnsSections.compactMap(\.dnssecSigned).first
+ }
+
+ private func tlsStatus(from snapshot: LookupSnapshot) -> String {
+ if snapshot.sslInfo != nil {
+ return "valid"
+ }
+ if let sslError = snapshot.sslError {
+ return sslError.localizedCaseInsensitiveContains("certificate") ? "invalid" : "failed"
+ }
+ return "unavailable"
+ }
+
+ private func emailSummary(from snapshot: LookupSnapshot) -> String {
+ guard let emailSecurity = snapshot.emailSecurity else {
+ return snapshot.emailSecurityError ?? "Unavailable"
+ }
+
+ return [
+ "SPF \(emailSecurity.spf.found ? "Yes" : "No")",
+ "DMARC \(emailSecurity.dmarc.found ? "Yes" : "No")",
+ "DKIM \(emailSecurity.dkim.found ? "Yes" : "No")",
+ "BIMI \(emailSecurity.bimi.found ? "Yes" : "No")",
+ "MTA-STS \(emailSecurity.mtaSts?.txtFound == true ? "Yes" : "No")"
+ ].joined(separator: " / ")
+ }
+
+ private func reachabilitySummary(from snapshot: LookupSnapshot) -> String {
+ guard !snapshot.reachabilityResults.isEmpty else {
+ return snapshot.reachabilityError ?? "Unavailable"
+ }
+
+ return snapshot.reachabilityResults
+ .sorted { $0.port < $1.port }
+ .map { "\($0.port):\($0.reachable ? "open" : "closed")" }
+ .joined(separator: ", ")
+ }
+
+ private func geolocationSummary(from snapshot: LookupSnapshot) -> String {
+ guard let geolocation = snapshot.ipGeolocation else {
+ return snapshot.ipGeolocationError ?? "Unavailable"
+ }
+
+ let parts = [geolocation.city, geolocation.region, geolocation.country_name]
+ .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
+ .filter { !$0.isEmpty }
+
+ if !parts.isEmpty {
+ return parts.joined(separator: ", ")
+ }
+
+ return geolocation.ip
+ }
+}