1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
import Foundation
struct DomainInspectionService {
private let reportBuilder = DomainReportBuilder()
func inspect(domain: String) async -> DomainReport {
let snapshot = await inspectSnapshot(domain: domain)
return reportBuilder.build(from: snapshot)
}
func inspectSnapshot(domain: String) async -> LookupSnapshot {
let normalizedDomain = normalize(domain)
let startedAt = Date()
let resolverDisplayName = DNSLookupService.currentResolverDisplayName()
let resolverURLString = DNSLookupService.currentResolverURLString()
async let dnsResult = DNSLookupService.lookupAll(domain: normalizedDomain)
async let availabilityResult = DomainAvailabilityService.check(domain: normalizedDomain)
async let sslResult = SSLCheckService.check(domain: normalizedDomain)
async let hstsResult = SSLCheckService.checkHSTSPreload(domain: normalizedDomain)
async let httpResult = HTTPHeadersService.fetch(domain: normalizedDomain)
async let reachabilityResult = ReachabilityService.checkAll(domain: normalizedDomain)
async let ownershipResult = DomainOwnershipService.lookup(domain: normalizedDomain)
async let redirectResult = RedirectChainService.trace(domain: normalizedDomain)
async let subdomainResult = SubdomainDiscoveryService.discover(for: normalizedDomain)
async let portScanResult = PortScanService.scanAll(domain: normalizedDomain)
let resolvedDNS = await dnsResult
let availability = await availabilityResult
let resolvedSSL = await sslResult
let hsts = await hstsResult
let http = await httpResult
let reachability = await reachabilityResult
let resolvedOwnership = await ownershipResult
let redirects = await redirectResult
let resolvedSubdomains = await subdomainResult
let ports = await portScanResult
let dnsSections = mapServiceResult(resolvedDNS, emptyValue: [])
let sslInfo = mapOptionalValueServiceResult(resolvedSSL)
let httpHeadersResult = mapHTTPResult(http)
let reachabilityResultValue = mapServiceResult(reachability, emptyValue: [])
let redirectChain = mapServiceResult(redirects, emptyValue: [])
let ownership = mapOptionalValueServiceResult(resolvedOwnership)
let subdomains = mapServiceResult(resolvedSubdomains, emptyValue: [])
let portScanResults = await mapPortScanResult(ports, domain: normalizedDomain)
let txtRecords = dnsSections.value.first(where: { $0.recordType == .TXT })?.records ?? []
let primaryIP = dnsSections.value.first(where: { $0.recordType == .A })?.records.first?.value
async let emailResult = EmailSecurityService.analyze(domain: normalizedDomain, txtRecords: txtRecords)
async let suggestions = availability.status == .registered
? DomainAvailabilityService.suggestions(for: normalizedDomain)
: []
let resolvedEmail = await emailResult
let resolvedSuggestions = await suggestions
let ptrResult: ServiceResult<String>?
let geoResult: ServiceResult<IPGeolocation>?
if let primaryIP {
ptrResult = await ReverseDNSService.lookup(ip: primaryIP, resolverURLString: resolverURLString)
geoResult = await IPGeolocationService.lookup(ip: primaryIP)
} else {
ptrResult = nil
geoResult = nil
}
let emailSecurity = mapOptionalValueServiceResult(resolvedEmail)
let ptrRecord = mapOptionalServiceResult(ptrResult, missingMessage: "No A record available")
let geolocation = mapOptionalServiceResult(geoResult, missingMessage: "No A record available")
return LookupSnapshot(
historyEntryID: nil,
domain: availability.domain,
timestamp: Date(),
trackedDomainID: nil,
resolverDisplayName: resolverDisplayName,
resolverURLString: resolverURLString,
totalLookupDurationMs: Int(Date().timeIntervalSince(startedAt) * 1000),
dnsSections: dnsSections.value,
dnsError: dnsSections.message,
availabilityResult: availability,
suggestions: resolvedSuggestions,
sslInfo: sslInfo.value,
sslError: sslInfo.message,
hstsPreloaded: hsts,
httpHeaders: httpHeadersResult.headers,
httpSecurityGrade: httpHeadersResult.securityGrade,
httpStatusCode: httpHeadersResult.statusCode,
httpResponseTimeMs: httpHeadersResult.responseTimeMs,
httpProtocol: httpHeadersResult.httpProtocol,
http3Advertised: httpHeadersResult.http3Advertised,
httpHeadersError: httpHeadersResult.error,
reachabilityResults: reachabilityResultValue.value,
reachabilityError: reachabilityResultValue.message,
ipGeolocation: geolocation.value,
ipGeolocationError: geolocation.message,
emailSecurity: emailSecurity.value,
emailSecurityError: emailSecurity.message,
ownership: ownership.value,
ownershipError: ownership.message,
ptrRecord: ptrRecord.value,
ptrError: ptrRecord.message,
redirectChain: redirectChain.value,
redirectChainError: redirectChain.message,
subdomains: subdomains.value,
subdomainsError: subdomains.message,
portScanResults: portScanResults.value,
portScanError: portScanResults.message,
changeSummary: nil,
isLive: false
)
}
private func normalize(_ domain: String) -> String {
domain
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "https://", with: "")
.replacingOccurrences(of: "http://", with: "")
.components(separatedBy: "/").first?
.lowercased() ?? domain.lowercased()
}
private func mapServiceResult<Value>(_ result: ServiceResult<Value>, emptyValue: Value) -> (value: Value, message: String?) {
switch result {
case let .success(value):
return (value, nil)
case let .empty(message), let .error(message):
return (emptyValue, message)
}
}
private func mapOptionalValueServiceResult<Value>(_ result: ServiceResult<Value>) -> (value: Value?, message: String?) {
switch result {
case let .success(value):
return (value, nil)
case let .empty(message), let .error(message):
return (nil, message)
}
}
private func mapOptionalServiceResult<Value>(
_ result: ServiceResult<Value>?,
missingMessage: String
) -> (value: Value?, message: String?) {
guard let result else {
return (nil, missingMessage)
}
switch result {
case let .success(value):
return (value, nil)
case let .empty(message), let .error(message):
return (nil, message)
}
}
private func mapPortScanResult(_ result: ServiceResult<[PortScanResult]>, domain: String) async -> (value: [PortScanResult], message: String?) {
switch result {
case let .success(results):
return (await enrichOpenPortBanners(in: results, domain: domain), nil)
case let .empty(message), let .error(message):
return ([], message)
}
}
private func mapHTTPResult(_ result: ServiceResult<HTTPHeadersResult>) -> (
headers: [HTTPHeader],
securityGrade: String?,
statusCode: Int?,
responseTimeMs: Int?,
httpProtocol: String?,
http3Advertised: Bool,
error: String?
) {
switch result {
case let .success(value):
return (
headers: value.headers,
securityGrade: HTTPSecurityGrade.grade(for: value.headers).rawValue,
statusCode: value.statusCode,
responseTimeMs: value.responseTimeMs,
httpProtocol: value.httpProtocol,
http3Advertised: value.http3Advertised,
error: nil
)
case let .empty(message), let .error(message):
return (
headers: [],
securityGrade: nil,
statusCode: nil,
responseTimeMs: nil,
httpProtocol: nil,
http3Advertised: false,
error: message
)
}
}
private func enrichOpenPortBanners(in results: [PortScanResult], domain: String) async -> [PortScanResult] {
let banners = await withTaskGroup(of: (UInt16, String?).self, returning: [UInt16: String].self) { group in
for result in results where result.open {
group.addTask {
let banner = await PortScanService.grabBanner(host: domain, port: result.port)
return (result.port, banner)
}
}
var collected: [UInt16: String] = [:]
for await (port, banner) in group {
if let banner {
collected[port] = banner
}
}
return collected
}
return results.map { result in
var updated = result
updated.banner = banners[result.port]
return updated
}
}
}
|