From 9f75530089bb8cf497ceee7de0419f92406e015a Mon Sep 17 00:00:00 2001 From: hewigovens <360470+hewigovens@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:31:04 +0900 Subject: [PATCH 1/2] Add redirect-aware TLS inspection UI --- .gitignore | 1 + Apps/macOS/Sources/InspectMacRootView.swift | 38 +- DEVELOPMENT.md | 3 + .../MacShareExtensionRequestHandler.swift | 8 +- .../Sources/SafariWebExtensionInspector.swift | 5 +- Packages/InspectCore/Package.resolved | 2 +- .../Core/Collection+SafeSubscript.swift | 5 + .../InspectCore/Sources/Core/SSLLabs.swift | 14 + .../Sources/Core/TLSInspection.swift | 40 ++ .../Sources/Core/TLSInspectionReport.swift | 24 +- .../Sources/Core/TLSInspector.swift | 128 +++--- .../Sources/Core/TLSMonitorProbeEngine.swift | 5 +- .../Sources/Core/TrustSummary.swift | 27 ++ .../CertificateDetailView+iOS.swift | 133 +++--- .../CertificateDetailView+macOS.swift | 378 +++++++++--------- .../Certificate/CertificateDetailView.swift | 78 +++- .../InspectionChainAndRecentCards.swift | 30 +- .../Inspection/InspectionNavigation.swift | 66 +-- .../Inspection/InspectionRedirectsCard.swift | 66 +++ .../Inspection/InspectionResultsContent.swift | 33 +- .../Inspection/InspectionRootView.swift | 79 ++-- .../Feature/Inspection/InspectionStore.swift | 18 +- .../InspectionSummaryAndSecurityCards.swift | 85 ++-- .../InspectionMonitorHostDetailView.swift | 4 +- justfile | 14 +- project.local.yml.example | 7 + project.yml | 4 + scripts/xcodegen_generate.sh | 8 + 28 files changed, 828 insertions(+), 475 deletions(-) create mode 100644 Packages/InspectCore/Sources/Core/Collection+SafeSubscript.swift create mode 100644 Packages/InspectCore/Sources/Core/SSLLabs.swift create mode 100644 Packages/InspectCore/Sources/Core/TLSInspection.swift create mode 100644 Packages/InspectCore/Sources/Core/TrustSummary.swift create mode 100644 Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift create mode 100644 project.local.yml.example create mode 100755 scripts/xcodegen_generate.sh diff --git a/.gitignore b/.gitignore index d6386fd..9fd5782 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ Pods/ fastlane/README.md fastlane/report.xml Configs/LocalOverrides.xcconfig +project.local.yml .asc/ Rust/**/target/ Inspect.xcodeproj/ diff --git a/Apps/macOS/Sources/InspectMacRootView.swift b/Apps/macOS/Sources/InspectMacRootView.swift index 41d5773..cfc3e25 100644 --- a/Apps/macOS/Sources/InspectMacRootView.swift +++ b/Apps/macOS/Sources/InspectMacRootView.swift @@ -1,4 +1,5 @@ import AppKit +import InspectCore import InspectKit import SwiftUI @@ -56,7 +57,8 @@ struct InspectMacRootView: View { case let .report(report, opensCertificateDetail): if opensCertificateDetail { externalCertificateRoute = InspectionCertificateRoute( - report: report, + inspection: TLSInspection(report: report), + initialReportIndex: 0, initialSelectionIndex: 0 ) } else { @@ -77,7 +79,8 @@ struct InspectMacRootView: View { case let .report(report, opensCertificateDetail): if opensCertificateDetail { externalCertificateRoute = InspectionCertificateRoute( - report: report, + inspection: TLSInspection(report: report), + initialReportIndex: 0, initialSelectionIndex: 0 ) } @@ -99,26 +102,35 @@ struct InspectMacRootView: View { @ViewBuilder private var detailView: some View { let inspectSessionID = appModel.inspectSessionID - - switch appModel.selectedSection { - case .inspect: + ZStack { InspectionRootView( showsMonitorCard: false, showsAboutCard: false ) .id(inspectSessionID) - case .monitor: + .opacity(appModel.selectedSection == .inspect ? 1 : 0) + .allowsHitTesting(appModel.selectedSection == .inspect) + .accessibilityHidden(appModel.selectedSection != .inspect) + InspectionMonitorView { await manager.refresh() } - case .settings: + .opacity(appModel.selectedSection == .monitor ? 1 : 0) + .allowsHitTesting(appModel.selectedSection == .monitor) + .accessibilityHidden(appModel.selectedSection != .monitor) + InspectMacSettingsView(manager: manager) - case nil: - ContentUnavailableView( - "Select a Section", - systemImage: "sidebar.left", - description: Text("Choose Inspect, Monitor, or Settings from the sidebar.") - ) + .opacity(appModel.selectedSection == .settings ? 1 : 0) + .allowsHitTesting(appModel.selectedSection == .settings) + .accessibilityHidden(appModel.selectedSection != .settings) + + if appModel.selectedSection == nil { + ContentUnavailableView( + "Select a Section", + systemImage: "sidebar.left", + description: Text("Choose Inspect, Monitor, or Settings from the sidebar.") + ) + } } } } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 504cf24..56a2ae9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -112,9 +112,12 @@ One-time setup: ```bash cp Configs/LocalOverrides.xcconfig.example Configs/LocalOverrides.xcconfig +cp project.local.yml.example project.local.yml cp .env.example .env ``` +For local Xcode signing, set your `DEVELOPMENT_TEAM` in `project.local.yml`. The generate script merges that file into `project.yml` so the generated project gets automatic signing target attributes as well as build settings. + Required `.env` values: - `ASC_APP_ID` diff --git a/Extensions/MacShare/Sources/MacShareExtensionRequestHandler.swift b/Extensions/MacShare/Sources/MacShareExtensionRequestHandler.swift index 815cd2f..7ca7f1c 100644 --- a/Extensions/MacShare/Sources/MacShareExtensionRequestHandler.swift +++ b/Extensions/MacShare/Sources/MacShareExtensionRequestHandler.swift @@ -16,7 +16,10 @@ final class MacShareExtensionRequestHandler { return } - let report = try await TLSInspector().inspect(input: input) + let inspection = try await TLSInspector().inspect(input: input) + guard let report = inspection.primaryReport else { + throw InspectionError.missingServerTrust + } let token = try InspectionSharedReportStore.save(report) InspectionSharedPendingReportStore.save(token: token) _ = await activateParentApp() @@ -33,7 +36,8 @@ final class MacShareExtensionRequestHandler { let bundleIdentifier = Bundle(url: appURL)?.bundleIdentifier if let bundleIdentifier, - let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first { + let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first + { return runningApp.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) } diff --git a/Extensions/SafariWeb/Sources/SafariWebExtensionInspector.swift b/Extensions/SafariWeb/Sources/SafariWebExtensionInspector.swift index 07d389e..9afe7c3 100644 --- a/Extensions/SafariWeb/Sources/SafariWebExtensionInspector.swift +++ b/Extensions/SafariWeb/Sources/SafariWebExtensionInspector.swift @@ -16,7 +16,10 @@ final class SafariExtensionInspector { ) { Task { do { - let report = try await TLSInspector().inspect(url: url) + let inspection = try await TLSInspector().inspect(url: url) + guard let report = inspection.primaryReport else { + throw InspectionError.missingServerTrust + } DispatchQueue.main.async { completion.completion(.success(report)) } diff --git a/Packages/InspectCore/Package.resolved b/Packages/InspectCore/Package.resolved index 3703f54..241f9a9 100644 --- a/Packages/InspectCore/Package.resolved +++ b/Packages/InspectCore/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "581f219c30a0b6635c7c2db07194f53b8cea7d9666aec7fd07abe191a3e3411c", + "originHash" : "84ea59c0d4b63644462385a299340e3fb79f00e09ab67e2e11f53f1858e81e8d", "pins" : [ { "identity" : "swift-asn1", diff --git a/Packages/InspectCore/Sources/Core/Collection+SafeSubscript.swift b/Packages/InspectCore/Sources/Core/Collection+SafeSubscript.swift new file mode 100644 index 0000000..24eb242 --- /dev/null +++ b/Packages/InspectCore/Sources/Core/Collection+SafeSubscript.swift @@ -0,0 +1,5 @@ +public extension Collection { + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } +} diff --git a/Packages/InspectCore/Sources/Core/SSLLabs.swift b/Packages/InspectCore/Sources/Core/SSLLabs.swift new file mode 100644 index 0000000..7202081 --- /dev/null +++ b/Packages/InspectCore/Sources/Core/SSLLabs.swift @@ -0,0 +1,14 @@ +import Foundation + +enum SSLLabs { + static let analyzeBaseURL = "https://www.ssllabs.com/ssltest/analyze.html" + + static func analyzeURL(host: String) -> URL? { + var components = URLComponents(string: analyzeBaseURL) + components?.queryItems = [ + URLQueryItem(name: "hideResults", value: "on"), + URLQueryItem(name: "d", value: host), + ] + return components?.url + } +} diff --git a/Packages/InspectCore/Sources/Core/TLSInspection.swift b/Packages/InspectCore/Sources/Core/TLSInspection.swift new file mode 100644 index 0000000..6edce00 --- /dev/null +++ b/Packages/InspectCore/Sources/Core/TLSInspection.swift @@ -0,0 +1,40 @@ +import Foundation + +public struct TLSInspection: Identifiable, Sendable, Equatable, Codable { + public let id: UUID + public let requestedURL: URL + public let reports: [TLSInspectionReport] + + public init( + id: UUID = UUID(), + requestedURL: URL, + reports: [TLSInspectionReport] + ) { + self.id = id + self.requestedURL = requestedURL + self.reports = reports + } + + public init(report: TLSInspectionReport) { + self.init( + requestedURL: report.requestedURL, + reports: [report] + ) + } + + public var primaryReport: TLSInspectionReport? { + reports.first + } + + public var finalReport: TLSInspectionReport? { + reports.last + } + + public var didRedirect: Bool { + reports.count > 1 + } + + public var combinedSecurity: SecurityAssessment { + SecurityAssessment(findings: reports.flatMap { $0.security.findings }) + } +} diff --git a/Packages/InspectCore/Sources/Core/TLSInspectionReport.swift b/Packages/InspectCore/Sources/Core/TLSInspectionReport.swift index 5c4209a..deb46b3 100644 --- a/Packages/InspectCore/Sources/Core/TLSInspectionReport.swift +++ b/Packages/InspectCore/Sources/Core/TLSInspectionReport.swift @@ -38,28 +38,6 @@ public struct TLSInspectionReport: Identifiable, Sendable, Equatable, Codable { } public var sslLabsURL: URL? { - URL(string: "https://www.ssllabs.com/ssltest/analyze.html?hideResults=on&d=\(host)") - } -} - -public struct TrustSummary: Sendable, Equatable, Codable { - public let evaluated: Bool - public let isTrusted: Bool - public let failureReason: String? - - public init(evaluated: Bool, isTrusted: Bool, failureReason: String?) { - self.evaluated = evaluated - self.isTrusted = isTrusted - self.failureReason = failureReason - } - - public var badgeText: String { - if isTrusted { - return "Trusted" - } - if evaluated { - return "Failed" - } - return "Unchecked" + SSLLabs.analyzeURL(host: host) } } diff --git a/Packages/InspectCore/Sources/Core/TLSInspector.swift b/Packages/InspectCore/Sources/Core/TLSInspector.swift index 6aaac65..7feddd8 100644 --- a/Packages/InspectCore/Sources/Core/TLSInspector.swift +++ b/Packages/InspectCore/Sources/Core/TLSInspector.swift @@ -6,27 +6,31 @@ public final class TLSInspector { public init() {} - public func inspect(input: String) async throws -> TLSInspectionReport { + public func inspect(input: String) async throws -> TLSInspection { try await inspect(url: URLInputNormalizer.normalize(input: input)) } - public func inspect(url: URL) async throws -> TLSInspectionReport { + public func inspect(url: URL) async throws -> TLSInspection { try await RequestRunner(url: URLInputNormalizer.normalize(url: url), parser: parser).run() } } private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate, @unchecked Sendable { + private struct CapturedTrustEvent { + let requestURL: URL? + let host: String + let trust: SecTrust + let trustEvaluationSucceeded: Bool + let trustFailureReason: String? + } + private let url: URL private let parser: CertificateParser private var session: URLSession? - private var continuation: CheckedContinuation? - private var capturedTrust: SecTrust? - private var trustEvaluationSucceeded = false - private var trustFailureReason: String? - private var networkProtocolName: String? - private var tlsVersion: String? - private var cipherSuite: String? + private var continuation: CheckedContinuation? + private var capturedTrustEvents: [CapturedTrustEvent] = [] + private var transactionMetrics: [URLSessionTaskTransactionMetrics] = [] private var hasCompleted = false init(url: URL, parser: CertificateParser) { @@ -34,7 +38,7 @@ private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionT self.parser = parser } - func run() async throws -> TLSInspectionReport { + func run() async throws -> TLSInspection { try await withCheckedThrowingContinuation { continuation in self.continuation = continuation @@ -56,31 +60,38 @@ private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionT } func urlSession( - _ session: URLSession, + _: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let serverTrust = challenge.protectionSpace.serverTrust else { + let serverTrust = challenge.protectionSpace.serverTrust + else { completionHandler(.performDefaultHandling, nil) return } - capturedTrust = serverTrust - var error: CFError? - trustEvaluationSucceeded = SecTrustEvaluateWithError(serverTrust, &error) - trustFailureReason = (error as Error?)?.localizedDescription + let trustEvaluationSucceeded = SecTrustEvaluateWithError(serverTrust, &error) + let trustFailureReason = (error as Error?)?.localizedDescription + capturedTrustEvents.append( + CapturedTrustEvent( + requestURL: task.currentRequest?.url ?? task.originalRequest?.url, + host: challenge.protectionSpace.host, + trust: serverTrust, + trustEvaluationSucceeded: trustEvaluationSucceeded, + trustFailureReason: trustFailureReason + ) + ) completionHandler(.useCredential, URLCredential(trust: serverTrust)) } - func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - guard let transaction = metrics.transactionMetrics.last else { return } - networkProtocolName = transaction.networkProtocolName - tlsVersion = transaction.negotiatedTLSProtocolVersion.map(Self.tlsVersionName) - cipherSuite = transaction.negotiatedTLSCipherSuite.map(Self.cipherSuiteName) + func urlSession(_: URLSession, task _: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + transactionMetrics = metrics.transactionMetrics.filter { transaction in + transaction.request.url?.scheme?.caseInsensitiveCompare("https") == .orderedSame + } } static func tlsVersionName(_ version: tls_protocol_version_t) -> String { @@ -112,13 +123,13 @@ private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionT } } - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive _: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { completionHandler(.allow) } - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {} + func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive _: Data) {} - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + func urlSession(_ session: URLSession, task _: URLSessionTask, didCompleteWithError error: Error?) { guard hasCompleted == false else { return } @@ -128,37 +139,60 @@ private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionT session.finishTasksAndInvalidate() } - guard let trust = capturedTrust else { + let reports = buildReports() + guard reports.isEmpty == false else { continuation?.resume(throwing: error ?? InspectionError.missingServerTrust) continuation = nil return } - let chain = (SecTrustCopyCertificateChain(trust) as? [SecCertificate]) ?? [] - let certificates = parser.parse(certificates: chain) - let trustSummary = TrustSummary( - evaluated: true, - isTrusted: trustEvaluationSucceeded, - failureReason: trustFailureReason - ) - let security = SecurityAnalyzer().analyze( - requestedURL: url, - trust: trustSummary, - certificates: certificates - ) - - let report = TLSInspectionReport( + let inspection = TLSInspection( requestedURL: url, - host: url.host ?? url.absoluteString, - networkProtocolName: networkProtocolName, - tlsVersion: tlsVersion, - cipherSuite: cipherSuite, - trust: trustSummary, - security: security, - certificates: certificates + reports: reports ) - continuation?.resume(returning: report) + continuation?.resume(returning: inspection) continuation = nil } + + private func buildReports() -> [TLSInspectionReport] { + capturedTrustEvents.enumerated().compactMap { index, event in + let transaction = transactionMetrics[safe: index] + let requestURL = transaction?.request.url ?? event.requestURL ?? makeFallbackURL(host: event.host) + guard let requestURL else { + return nil + } + + let chain = (SecTrustCopyCertificateChain(event.trust) as? [SecCertificate]) ?? [] + let certificates = parser.parse(certificates: chain) + let trustSummary = TrustSummary( + evaluated: true, + isTrusted: event.trustEvaluationSucceeded, + failureReason: event.trustFailureReason + ) + let security = SecurityAnalyzer().analyze( + requestedURL: requestURL, + trust: trustSummary, + certificates: certificates + ) + + return TLSInspectionReport( + requestedURL: requestURL, + host: requestURL.host ?? event.host, + networkProtocolName: transaction?.networkProtocolName, + tlsVersion: transaction?.negotiatedTLSProtocolVersion.map(Self.tlsVersionName), + cipherSuite: transaction?.negotiatedTLSCipherSuite.map(Self.cipherSuiteName), + trust: trustSummary, + security: security, + certificates: certificates + ) + } + } + + private func makeFallbackURL(host: String) -> URL? { + var components = URLComponents() + components.scheme = "https" + components.host = host + return components.url + } } diff --git a/Packages/InspectCore/Sources/Core/TLSMonitorProbeEngine.swift b/Packages/InspectCore/Sources/Core/TLSMonitorProbeEngine.swift index 390fcea..56f8000 100644 --- a/Packages/InspectCore/Sources/Core/TLSMonitorProbeEngine.swift +++ b/Packages/InspectCore/Sources/Core/TLSMonitorProbeEngine.swift @@ -8,7 +8,10 @@ public struct LiveTLSInspectionClient: TLSInspectionClient { public init() {} public func inspect(url: URL) async throws -> TLSInspectionReport { - try await TLSInspector().inspect(url: url) + guard let report = try await TLSInspector().inspect(url: url).primaryReport else { + throw InspectionError.missingServerTrust + } + return report } } diff --git a/Packages/InspectCore/Sources/Core/TrustSummary.swift b/Packages/InspectCore/Sources/Core/TrustSummary.swift new file mode 100644 index 0000000..a3b33e7 --- /dev/null +++ b/Packages/InspectCore/Sources/Core/TrustSummary.swift @@ -0,0 +1,27 @@ +public struct TrustSummary: Sendable, Equatable, Codable { + public let evaluated: Bool + public let isTrusted: Bool + public let failureReason: String? + + public init(evaluated: Bool, isTrusted: Bool, failureReason: String?) { + self.evaluated = evaluated + self.isTrusted = isTrusted + self.failureReason = failureReason + } + + public static let unchecked = TrustSummary( + evaluated: false, + isTrusted: false, + failureReason: nil + ) + + public var badgeText: String { + if isTrusted { + return "Trusted" + } + if evaluated { + return "Failed" + } + return "Unchecked" + } +} diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift index a5b7db3..56ddf6d 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift @@ -1,79 +1,98 @@ #if !os(macOS) -import SwiftUI + import SwiftUI -extension CertificateDetailView { - @ViewBuilder - var platformContent: some View { - List { - chainSection + extension CertificateDetailView { + var platformContent: some View { + List { + if inspection.didRedirect { + reportSection + } - Section { - RevocationStatusBadge( - status: revocationStatus, - onCheck: checkRevocation - ) - } header: { - sectionHeader("Revocation") - } + chainSection - if let selectedContent { - ForEach(selectedContent.sections) { section in - detailSection(section) - } - } else { Section { - Text("No certificate details were available for this inspection.") - .foregroundStyle(.secondary) + RevocationStatusBadge( + status: revocationStatus, + onCheck: checkRevocation + ) } header: { - sectionHeader("Certificate") + sectionHeader("Revocation") + } + + if let selectedContent { + ForEach(selectedContent.sections) { section in + detailSection(section) + } + } else { + Section { + Text("No certificate details were available for this inspection.") + .foregroundStyle(.secondary) + } header: { + sectionHeader("Certificate") + } } } } - } - var chainSection: some View { - Section { - CompactCertificateChainPanel( - nodes: chainNodes, - trust: report.trust, - selectedIndex: selectedIndex, - onSelect: updateSelection(to:) - ) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - } header: { - sectionHeader("Certificate Chain") + var reportSection: some View { + Section { + Picker("TLS Hop", selection: $selectedReportIndex) { + ForEach(Array(inspection.reports.enumerated()), id: \.element.id) { index, report in + Text("Hop \(index + 1) • \(report.host)").tag(index) + } + } + .pickerStyle(.navigationLink) + .onChange(of: selectedReportIndex) { _, newValue in + updateReportSelection(to: newValue) + } + } header: { + sectionHeader("TLS Hop") + } } - } - func detailSection(_ section: CertificateDetailSection) -> some View { - Section { - ForEach(section.rows) { row in - detailRow(row) + var chainSection: some View { + Section { + CompactCertificateChainPanel( + nodes: chainNodes, + trust: selectedReport?.trust ?? .unchecked, + selectedIndex: selectedIndex, + onSelect: updateSelection(to:) + ) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } header: { + sectionHeader(inspection.didRedirect ? "Certificate Chain For Selected Hop" : "Certificate Chain") } - } header: { - sectionHeader(section.title) } - } - @ViewBuilder - func detailRow(_ row: DetailLine) -> some View { - switch row.style { - case .inline: - InlineDetailRow(label: row.label, value: row.value) { - copy(row: row) + func detailSection(_ section: CertificateDetailSection) -> some View { + Section { + ForEach(section.rows) { row in + detailRow(row) + } + } header: { + sectionHeader(section.title) } - case .stacked: - StackedDetailRow(label: row.label, value: row.value, monospaced: row.monospaced) { - copy(row: row) + } + + @ViewBuilder + func detailRow(_ row: DetailLine) -> some View { + switch row.style { + case .inline: + InlineDetailRow(label: row.label, value: row.value) { + copy(row: row) + } + case .stacked: + StackedDetailRow(label: row.label, value: row.value, monospaced: row.monospaced) { + copy(row: row) + } } } - } - func sectionHeader(_ title: String) -> some View { - Text(title.uppercased()) - .font(.inspectDetailCaptionSemibold) - .foregroundStyle(.secondary) + func sectionHeader(_ title: String) -> some View { + Text(title.uppercased()) + .font(.inspectDetailCaptionSemibold) + .foregroundStyle(.secondary) + } } -} #endif diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift index ec872f0..3a6a7df 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift @@ -1,229 +1,251 @@ #if os(macOS) -import InspectCore -import SwiftUI - -extension CertificateDetailView { - @ViewBuilder - var platformContent: some View { - HSplitView { - ScrollView { - VStack(spacing: 16) { - macOverviewCard - - InspectCard { - VStack(alignment: .leading, spacing: 14) { - Text("Certificate Chain") - .font(.inspectRootHeadline) + import InspectCore + import SwiftUI - CompactCertificateChainPanel( - nodes: chainNodes, - trust: report.trust, - selectedIndex: selectedIndex, - onSelect: updateSelection(to:) - ) + extension CertificateDetailView { + var platformContent: some View { + HSplitView { + ScrollView { + VStack(spacing: 16) { + macOverviewCard + + InspectCard { + VStack(alignment: .leading, spacing: 14) { + if inspection.didRedirect { + VStack(alignment: .leading, spacing: 10) { + Text("TLS Hop") + .font(.inspectRootHeadline) + + Picker("TLS Hop", selection: $selectedReportIndex) { + ForEach(Array(inspection.reports.enumerated()), id: \.element.id) { index, report in + Text("Hop \(index + 1) • \(report.host)").tag(index) + } + } + .labelsHidden() + .onChange(of: selectedReportIndex) { _, newValue in + updateReportSelection(to: newValue) + } + } + } + + Text("Certificate Chain") + .font(.inspectRootHeadline) + + CompactCertificateChainPanel( + nodes: chainNodes, + trust: selectedReport?.trust ?? .unchecked, + selectedIndex: selectedIndex, + onSelect: updateSelection(to:) + ) + } } } + .padding(EdgeInsets(top: 52, leading: 16, bottom: 16, trailing: 16)) } - .padding(EdgeInsets(top: 52, leading: 16, bottom: 16, trailing: 16)) - } - .frame(minWidth: 260, idealWidth: 290, maxWidth: 320) + .frame(minWidth: 260, idealWidth: 290, maxWidth: 320) - ScrollView { - VStack(alignment: .leading, spacing: 16) { - if let selectedCertificate { - macSelectedCertificateCard(selectedCertificate) - } + ScrollView { + VStack(alignment: .leading, spacing: 16) { + if let selectedCertificate { + macSelectedCertificateCard(selectedCertificate) + } - InspectCard { - RevocationStatusBadge( - status: revocationStatus, - onCheck: checkRevocation - ) - } + InspectCard { + RevocationStatusBadge( + status: revocationStatus, + onCheck: checkRevocation + ) + } - if let selectedContent { - ForEach(selectedContent.sections) { section in - MacCertificateSectionCard(section: section) { row in - copy(row: row) + if let selectedContent { + ForEach(selectedContent.sections) { section in + MacCertificateSectionCard(section: section) { row in + copy(row: row) + } + } + } else { + InspectCard { + Text("No certificate details were available for this inspection.") + .foregroundStyle(.secondary) } - } - } else { - InspectCard { - Text("No certificate details were available for this inspection.") - .foregroundStyle(.secondary) } } + .padding(EdgeInsets(top: 52, leading: 16, bottom: 16, trailing: 16)) } - .padding(EdgeInsets(top: 52, leading: 16, bottom: 16, trailing: 16)) + .frame(minWidth: 420, idealWidth: 520, maxWidth: .infinity) } - .frame(minWidth: 420, idealWidth: 520, maxWidth: .infinity) - } - .background { - ZStack { - Color.certificateGroupedBackground - InspectBackground() - .opacity(0.22) + .background { + ZStack { + Color.certificateGroupedBackground + InspectBackground() + .opacity(0.22) + } + .ignoresSafeArea() } - .ignoresSafeArea() } - } - - var macOverviewCard: some View { - InspectCard { - VStack(alignment: .leading, spacing: 14) { - HStack(alignment: .top, spacing: 12) { - SmallFeatureGlyph( - symbol: selectedCertificateGlyph, - tint: selectedCertificateTint - ) - - VStack(alignment: .leading, spacing: 4) { - Text(selectedCertificate?.subjectSummary ?? report.host) - .font(.inspectRootTitle3) - .lineLimit(3) - - Text(report.host) - .font(.inspectRootSubheadline) - .foregroundStyle(.secondary) - } - } - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - Badge( - text: report.trust.badgeText, - tint: report.trust.isTrusted ? .green : .orange - ) - Badge( - text: selectedCertificate.map(certificateRoleTitle) ?? "Certificate", + var macOverviewCard: some View { + InspectCard { + VStack(alignment: .leading, spacing: 14) { + HStack(alignment: .top, spacing: 12) { + SmallFeatureGlyph( + symbol: selectedCertificateGlyph, tint: selectedCertificateTint ) + + VStack(alignment: .leading, spacing: 4) { + Text(selectedCertificate?.subjectSummary ?? selectedReport?.host ?? inspection.primaryReport?.host ?? inspection.requestedURL.absoluteString) + .font(.inspectRootTitle3) + .lineLimit(3) + + Text(selectedReport?.host ?? inspection.primaryReport?.host ?? inspection.requestedURL.absoluteString) + .font(.inspectRootSubheadline) + .foregroundStyle(.secondary) + } } - HStack(spacing: 8) { - Badge(text: protocolTitle, tint: .blue) - if let selectedCertificate { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { Badge( - text: selectedCertificate.validity.status.rawValue, - tint: validityTint(for: selectedCertificate) + text: (selectedReport?.trust ?? .unchecked).badgeText, + tint: (selectedReport?.trust.isTrusted == true) ? .green : .orange + ) + Badge( + text: selectedCertificate.map(certificateRoleTitle) ?? "Certificate", + tint: selectedCertificateTint ) } + + HStack(spacing: 8) { + Badge(text: protocolTitle, tint: .blue) + if let selectedCertificate { + Badge( + text: selectedCertificate.validity.status.rawValue, + tint: validityTint(for: selectedCertificate) + ) + } + } } - } - if let selectedCertificate { - VStack(alignment: .leading, spacing: 12) { - MacCertificateQuickFact(title: "Issued By", value: selectedCertificate.issuerSummary) - MacCertificateQuickFact(title: "Chain Position", value: "\(selectedIndex + 1) of \(report.certificates.count)") + if let selectedCertificate { + VStack(alignment: .leading, spacing: 12) { + MacCertificateQuickFact(title: "Issued By", value: selectedCertificate.issuerSummary) + MacCertificateQuickFact( + title: "Chain Position", + value: "\(selectedIndex + 1) of \((selectedReport?.certificates.count ?? 0))" + ) + if inspection.didRedirect { + MacCertificateQuickFact(title: "TLS Hop", value: "Hop \(selectedReportIndex + 1) of \(inspection.reports.count)") + } + } } - } - if let failureReason = report.trust.failureReason, report.trust.isTrusted == false { - Text(failureReason) - .font(.inspectRootFootnote) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) + if let failureReason = selectedReport?.trust.failureReason, selectedReport?.trust.isTrusted == false { + Text(failureReason) + .font(.inspectRootFootnote) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } } } } - } - func macSelectedCertificateCard(_ certificate: CertificateDetails) -> some View { - InspectCard { - VStack(alignment: .leading, spacing: 14) { - HStack(alignment: .center, spacing: 12) { - SmallFeatureGlyph(symbol: selectedCertificateGlyph, tint: selectedCertificateTint) + func macSelectedCertificateCard(_ certificate: CertificateDetails) -> some View { + InspectCard { + VStack(alignment: .leading, spacing: 14) { + HStack(alignment: .center, spacing: 12) { + SmallFeatureGlyph(symbol: selectedCertificateGlyph, tint: selectedCertificateTint) - VStack(alignment: .leading, spacing: 2) { - Text(certificate.subjectSummary) - .font(.inspectRootHeadline) - .lineLimit(2) + VStack(alignment: .leading, spacing: 2) { + Text(certificate.subjectSummary) + .font(.inspectRootHeadline) + .lineLimit(2) - Text(certificate.issuerSummary) - .font(.inspectRootSubheadline) - .foregroundStyle(.secondary) - .lineLimit(2) + Text(certificate.issuerSummary) + .font(.inspectRootSubheadline) + .foregroundStyle(.secondary) + .lineLimit(2) + } } - } - HStack(spacing: 10) { - MacCertificateStatPill(title: "Role", value: certificateRoleTitle(certificate)) - MacCertificateStatPill(title: "Version", value: certificate.version) - MacCertificateStatPill(title: "Algorithm", value: certificate.signatureAlgorithm) + HStack(spacing: 10) { + MacCertificateStatPill(title: "Role", value: certificateRoleTitle(certificate)) + MacCertificateStatPill(title: "Version", value: certificate.version) + MacCertificateStatPill(title: "Algorithm", value: certificate.signatureAlgorithm) + } } } } - } - - var selectedCertificateGlyph: String { - guard let selectedCertificate else { - return "doc.text.magnifyingglass" - } - if selectedCertificate.isLeaf, report.trust.isTrusted == false { - return "xmark.seal.fill" - } - if selectedCertificate.isLeaf { - return "network" - } - if selectedCertificate.isRoot { - return "checkmark.shield.fill" - } - return "seal.fill" - } + var selectedCertificateGlyph: String { + guard let selectedCertificate else { + return "doc.text.magnifyingglass" + } - var selectedCertificateTint: Color { - guard let selectedCertificate else { - return .inspectAccent + if selectedCertificate.isLeaf, selectedReport?.trust.isTrusted == false { + return "xmark.seal.fill" + } + if selectedCertificate.isLeaf { + return "network" + } + if selectedCertificate.isRoot { + return "checkmark.shield.fill" + } + return "seal.fill" } - if selectedCertificate.isLeaf, report.trust.isTrusted == false { - return .red - } - if selectedCertificate.isLeaf { - return .blue - } - if selectedCertificate.isRoot { - return .orange - } - return .indigo - } + var selectedCertificateTint: Color { + guard let selectedCertificate else { + return .inspectAccent + } - func validityTint(for certificate: CertificateDetails) -> Color { - switch certificate.validity.status { - case .valid: - return .green - case .expired: - return .red - case .notYetValid: - return .orange + if selectedCertificate.isLeaf, selectedReport?.trust.isTrusted == false { + return .red + } + if selectedCertificate.isLeaf { + return .blue + } + if selectedCertificate.isRoot { + return .orange + } + return .indigo } - } - func certificateRoleTitle(_ certificate: CertificateDetails) -> String { - if certificate.isLeaf { - return "Leaf certificate" - } - if certificate.isRoot { - return "Root certificate" + func validityTint(for certificate: CertificateDetails) -> Color { + switch certificate.validity.status { + case .valid: + return .green + case .expired: + return .red + case .notYetValid: + return .orange + } } - return "Intermediate certificate" - } - var protocolTitle: String { - switch report.networkProtocolName?.lowercased() { - case "h2": - return "HTTP/2" - case "h3": - return "HTTP/3" - case "http/1.1": - return "HTTP/1.1" - case let value?: - return value.uppercased() - default: - return "Protocol Unknown" + func certificateRoleTitle(_ certificate: CertificateDetails) -> String { + if certificate.isLeaf { + return "Leaf certificate" + } + if certificate.isRoot { + return "Root certificate" + } + return "Intermediate certificate" + } + + var protocolTitle: String { + switch selectedReport?.networkProtocolName?.lowercased() { + case "h2": + return "HTTP/2" + case "h3": + return "HTTP/3" + case "http/1.1": + return "HTTP/1.1" + case let value?: + return value.uppercased() + default: + return "Protocol Unknown" + } } } -} #endif diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView.swift index f453ea2..dead053 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView.swift @@ -3,7 +3,8 @@ import SwiftUI public struct CertificateDetailView: View { @Environment(\.openURL) private var openURL - let report: TLSInspectionReport + let inspection: TLSInspection + @State var selectedReportIndex: Int @State var selectedIndex: Int @State var selectedContent: CertificateDetailContent? @State var copyFeedback: String? @@ -11,14 +12,35 @@ public struct CertificateDetailView: View { @State var presentsSSLLabs = false @State var revocationStatus: RevocationStatus = .unchecked - public init(report: TLSInspectionReport, initialSelectionIndex: Int = 0) { - self.report = report + public init(inspection: TLSInspection, initialReportIndex: Int = 0, initialSelectionIndex: Int = 0) { + self.inspection = inspection - let selectedIndex = report.certificates.indices.contains(initialSelectionIndex) + let selectedReportIndex = inspection.reports.indices.contains(initialReportIndex) + ? initialReportIndex + : 0 + let selectedIndex = inspection.reports[safe: selectedReportIndex]?.certificates.indices.contains(initialSelectionIndex) == true ? initialSelectionIndex : 0 + _selectedReportIndex = State(initialValue: selectedReportIndex) _selectedIndex = State(initialValue: selectedIndex) - _selectedContent = State(initialValue: Self.selectedContent(in: report, index: selectedIndex)) + _selectedContent = State( + initialValue: Self.selectedContent( + in: inspection.reports[safe: selectedReportIndex], + index: selectedIndex + ) + ) + } + + public init(report: TLSInspectionReport, initialSelectionIndex: Int = 0) { + self.init( + inspection: TLSInspection(report: report), + initialReportIndex: 0, + initialSelectionIndex: initialSelectionIndex + ) + } + + var selectedReport: TLSInspectionReport? { + inspection.reports[safe: selectedReportIndex] } var selectedCertificate: CertificateDetails? { @@ -32,13 +54,13 @@ public struct CertificateDetailView: View { return CertificateExportWriter.writeTemporaryCertificate( selectedCertificate, - host: report.host, + host: selectedReport?.host ?? inspection.primaryReport?.host ?? inspection.requestedURL.absoluteString, indexInChain: selectedIndex ) } var chainNodes: [CertificateChainNode] { - Array(report.certificates.enumerated().reversed().enumerated()).map { depth, entry in + Array((selectedReport?.certificates ?? []).enumerated().reversed().enumerated()).map { depth, entry in CertificateChainNode( originalIndex: entry.offset, depth: depth, @@ -75,7 +97,7 @@ public struct CertificateDetailView: View { Label("Copy as PEM", systemImage: "doc.on.doc") } - if report.certificates.count > 1 { + if let selectedReport, selectedReport.certificates.count > 1 { Button { copyFullChainPEM() } label: { @@ -92,7 +114,7 @@ public struct CertificateDetailView: View { } .disabled(revocationStatus == .checking) - if let sslLabsURL = report.sslLabsURL { + if let sslLabsURL = selectedReport?.sslLabsURL { Button { openSSLLabs(sslLabsURL) } label: { @@ -104,11 +126,11 @@ public struct CertificateDetailView: View { } } } - .inspectSafariSheet(url: report.sslLabsURL, isPresented: $presentsSSLLabs) + .inspectSafariSheet(url: selectedReport?.sslLabsURL, isPresented: $presentsSSLLabs) } - static func selectedContent(in report: TLSInspectionReport, index: Int) -> CertificateDetailContent? { - guard report.certificates.indices.contains(index) else { + static func selectedContent(in report: TLSInspectionReport?, index: Int) -> CertificateDetailContent? { + guard let report, report.certificates.indices.contains(index) else { return nil } @@ -117,7 +139,20 @@ public struct CertificateDetailView: View { func updateSelection(to index: Int) { selectedIndex = index - selectedContent = Self.selectedContent(in: report, index: index) + selectedContent = Self.selectedContent(in: selectedReport, index: index) + } + + func updateReportSelection(to index: Int) { + selectedReportIndex = index + let nextCertificateIndex = inspection.reports[safe: index]?.certificates.indices.contains(selectedIndex) == true + ? selectedIndex + : 0 + selectedIndex = nextCertificateIndex + selectedContent = Self.selectedContent( + in: inspection.reports[safe: index], + index: nextCertificateIndex + ) + revocationStatus = .unchecked } func copy(row: DetailLine) { @@ -156,17 +191,22 @@ public struct CertificateDetailView: View { } func copyFullChainPEM() { - let pem = CertificateExportWriter.fullChainPEM(from: report.certificates) + let certificates = selectedReport?.certificates ?? [] + let pem = CertificateExportWriter.fullChainPEM(from: certificates) InspectClipboard.copy(pem) - showCopyFeedback("Copied \(report.certificates.count) certificates") + showCopyFeedback("Copied \(certificates.count) certificates") } func checkRevocation() { + guard let selectedReport else { + return + } + revocationStatus = .checking Task { let result = await RevocationChecker.check( - certificates: report.certificates, - host: report.host + certificates: selectedReport.certificates, + host: selectedReport.host ) await MainActor.run { withAnimation(.easeInOut(duration: 0.2)) { @@ -178,9 +218,9 @@ public struct CertificateDetailView: View { func openSSLLabs(_ url: URL) { #if os(macOS) - openURL(url) + openURL(url) #else - presentsSSLLabs = true + presentsSSLLabs = true #endif } } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionChainAndRecentCards.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionChainAndRecentCards.swift index 1b44b2c..aeed250 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionChainAndRecentCards.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionChainAndRecentCards.swift @@ -2,8 +2,9 @@ import InspectCore import SwiftUI struct InspectionChainCard: View { - let report: TLSInspectionReport - let onOpenCertificateDetail: (TLSInspectionReport, Int) -> Void + let inspection: TLSInspection + let selectedReportIndex: Int + let onOpenCertificateDetail: (TLSInspection, Int, Int) -> Void var body: some View { InspectCard { @@ -11,17 +12,26 @@ struct InspectionChainCard: View { Text("Certificate Chain") .font(.inspectRootHeadline) - ForEach(Array(report.certificates.enumerated()), id: \.element.id) { index, certificate in - certificateRow(certificate: certificate, at: index) + ForEach(Array(selectedReport.certificates.enumerated()), id: \.element.id) { certificateIndex, certificate in + certificateRow( + certificate: certificate, + report: selectedReport, + reportIndex: selectedReportIndex, + certificateIndex: certificateIndex + ) } } } } - @ViewBuilder - private func certificateRow(certificate: CertificateDetails, at index: Int) -> some View { + private func certificateRow( + certificate: CertificateDetails, + report: TLSInspectionReport, + reportIndex: Int, + certificateIndex: Int + ) -> some View { Button { - onOpenCertificateDetail(report, index) + onOpenCertificateDetail(inspection, reportIndex, certificateIndex) } label: { CertificateRow( certificate: certificate, @@ -30,7 +40,11 @@ struct InspectionChainCard: View { } .buttonStyle(.plain) .contentShape(Rectangle()) - .accessibilityIdentifier("chain.certificate.\(index)") + .accessibilityIdentifier("chain.hop.\(reportIndex).certificate.\(certificateIndex)") + } + + private var selectedReport: TLSInspectionReport { + inspection.reports[selectedReportIndex] } } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift index 1072757..c824dab 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift @@ -1,13 +1,13 @@ -import InspectCore import Foundation +import InspectCore import SwiftUI private struct FocusInspectionInputKey: FocusedValueKey { typealias Value = () -> Void } -extension FocusedValues { - public var focusInspectionInput: (() -> Void)? { +public extension FocusedValues { + var focusInspectionInput: (() -> Void)? { get { self[FocusInspectionInputKey.self] } set { self[FocusInspectionInputKey.self] = newValue } } @@ -15,11 +15,13 @@ extension FocusedValues { public struct InspectionCertificateRoute: Identifiable { public let id = UUID() - public let report: TLSInspectionReport + public let inspection: TLSInspection + public let initialReportIndex: Int public let initialSelectionIndex: Int - public init(report: TLSInspectionReport, initialSelectionIndex: Int) { - self.report = report + public init(inspection: TLSInspection, initialReportIndex: Int, initialSelectionIndex: Int) { + self.inspection = inspection + self.initialReportIndex = initialReportIndex self.initialSelectionIndex = initialSelectionIndex } } @@ -35,40 +37,42 @@ extension InspectionCertificateRoute: Hashable { } #if os(macOS) -private struct CertificateDetailSheet: View { - @Environment(\.dismiss) private var dismiss - let route: InspectionCertificateRoute + private struct CertificateDetailSheet: View { + @Environment(\.dismiss) private var dismiss + let route: InspectionCertificateRoute - var body: some View { - NavigationStack { - CertificateDetailView( - report: route.report, - initialSelectionIndex: route.initialSelectionIndex - ) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { dismiss() } + var body: some View { + NavigationStack { + CertificateDetailView( + inspection: route.inspection, + initialReportIndex: route.initialReportIndex, + initialSelectionIndex: route.initialSelectionIndex + ) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { dismiss() } + } } } + .frame(width: 960, height: 720) } - .frame(width: 960, height: 720) } -} #endif -extension View { - public func certificateDetailDestination(_ route: Binding) -> some View { +public extension View { + func certificateDetailDestination(_ route: Binding) -> some View { #if os(macOS) - self.sheet(item: route) { route in - CertificateDetailSheet(route: route) - } + sheet(item: route) { route in + CertificateDetailSheet(route: route) + } #else - self.navigationDestination(item: route) { route in - CertificateDetailView( - report: route.report, - initialSelectionIndex: route.initialSelectionIndex - ) - } + navigationDestination(item: route) { route in + CertificateDetailView( + inspection: route.inspection, + initialReportIndex: route.initialReportIndex, + initialSelectionIndex: route.initialSelectionIndex + ) + } #endif } } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift new file mode 100644 index 0000000..c2ce89b --- /dev/null +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift @@ -0,0 +1,66 @@ +import InspectCore +import SwiftUI + +struct InspectionRedirectsCard: View { + let inspection: TLSInspection + @Binding var selectedReportIndex: Int + + var body: some View { + InspectCard { + VStack(alignment: .leading, spacing: 14) { + Text("Redirects") + .font(.inspectRootHeadline) + + ForEach(Array(inspection.reports.enumerated()), id: \.element.id) { index, report in + Button { + selectedReportIndex = index + } label: { + HStack(spacing: 12) { + Text("Hop \(index + 1)") + .font(.inspectRootCaption) + .foregroundStyle(.secondary) + + Text(report.host) + .font(.inspectRootSubheadlineSemibold) + .foregroundStyle(.primary) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + + if let badgeTitle = badgeTitle(for: index) { + Badge( + text: badgeTitle, + tint: index == 0 ? .blue : .indigo + ) + } + + if selectedReportIndex == index { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(Color.inspectAccent) + } else { + Image(systemName: "circle") + .foregroundStyle(.tertiary) + } + } + .padding(.vertical, 8) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + + if inspection.reports.indices.contains(index + 1) { + Divider() + } + } + } + } + } + + private func badgeTitle(for index: Int) -> String? { + if index == 0 { + return "Origin" + } + if index == inspection.reports.count - 1 { + return "Final" + } + return nil + } +} diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift index 4a2da71..f9b6a2a 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift @@ -4,12 +4,13 @@ import SwiftUI struct InspectionResultsContent: View { let isLoading: Bool let errorMessage: String? - let report: TLSInspectionReport? + let inspection: TLSInspection? + @Binding var selectedReportIndex: Int let recentItems: [RecentLookupItem] let currentReportURL: URL? let onInspectRecent: (String) async -> Void let onClearRecents: () -> Void - let onOpenCertificateDetail: (TLSInspectionReport, Int) -> Void + let onOpenCertificateDetail: (TLSInspection, Int, Int) -> Void let isInputFocused: FocusState.Binding var body: some View { @@ -28,15 +29,27 @@ struct InspectionResultsContent: View { .id("error") } - if let report { + if let inspection, let report = selectedReport { + if inspection.didRedirect { + InspectionRedirectsCard( + inspection: inspection, + selectedReportIndex: $selectedReportIndex + ) + .id("hop-picker") + } + InspectionChainCard( - report: report, + inspection: inspection, + selectedReportIndex: selectedReportIndex, onOpenCertificateDetail: onOpenCertificateDetail ) - .id("chain") - InspectionSummaryCard(report: report) - .id("summary") - InspectionSecurityCard(assessment: report.security) + .id("chain") + InspectionSummaryCard( + report: report, + reportIndex: selectedReportIndex + ) + .id("summary") + InspectionSecurityCard(report: report) .id("security") } @@ -52,4 +65,8 @@ struct InspectionResultsContent: View { } } } + + private var selectedReport: TLSInspectionReport? { + inspection?.reports[safe: selectedReportIndex] + } } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift index ffb62af..5803d06 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift @@ -10,6 +10,7 @@ public struct InspectionRootView: View { @State private var store = InspectionStore() @State private var monitorStore: InspectionMonitorStore @State private var certificateRoute: InspectionCertificateRoute? + @State private var selectedReportIndex = 0 private let initialURL: URL? private let closeAction: (() -> Void)? @@ -65,17 +66,19 @@ public struct InspectionRootView: View { } .onReceive(NotificationCenter.default.publisher(for: InspectionExternalInputCenter.notification)) { _ in guard presentation == .app, - let request = InspectionExternalInputCenter.consumePendingRequest() else { + let request = InspectionExternalInputCenter.consumePendingRequest() + else { return } handleExternalRequest(request) } - .onChange(of: store.report?.id) { _, _ in - guard screenshotScenario == nil, let report = store.report else { + .onChange(of: store.inspection?.id) { _, _ in + guard screenshotScenario == nil, let report = store.inspection?.primaryReport else { return } + selectedReportIndex = 0 monitorStore.recordInspection(report) maybeRequestReview(for: report) } @@ -83,10 +86,10 @@ public struct InspectionRootView: View { @ViewBuilder private var content: some View { - if presentation == .app, screenshotScenario?.showsCertificateDetail == true, let report = store.report { + if presentation == .app, screenshotScenario?.showsCertificateDetail == true, let report = store.inspection?.primaryReport { CertificateDetailView(report: report, initialSelectionIndex: 0) .ensureNavigationBarVisible() - } else if presentation == .actionExtension, let report = store.report { + } else if presentation == .actionExtension, let report = store.inspection?.primaryReport { CertificateDetailView(report: report, initialSelectionIndex: 0) .toolbar { if let closeAction { @@ -110,7 +113,8 @@ public struct InspectionRootView: View { } private var rootContent: some View { - let report = store.report + let inspection = store.inspection + let report = inspection?.primaryReport let recentItems = screenshotScenario?.showsRecents == false ? [] : store.recentInputs.map(RecentLookupItem.init) @@ -125,9 +129,9 @@ public struct InspectionRootView: View { ScrollView { Group { if usesRegularDashboardLayout { - regularWidthContent(report: report, recentItems: recentItems) + regularWidthContent(inspection: inspection, report: report, recentItems: recentItems) } else { - compactWidthContent(report: report, recentItems: recentItems) + compactWidthContent(inspection: inspection, report: report, recentItems: recentItems) } } .frame(maxWidth: rootContentMaxWidth, alignment: .leading) @@ -151,7 +155,7 @@ public struct InspectionRootView: View { .certificateDetailDestination($certificateRoute) } - private func compactWidthContent(report: TLSInspectionReport?, recentItems: [RecentLookupItem]) -> some View { + private func compactWidthContent(inspection: TLSInspection?, report: TLSInspectionReport?, recentItems: [RecentLookupItem]) -> some View { LazyVStack(spacing: rootStackSpacing) { InspectionInputCard( store: store, @@ -162,7 +166,8 @@ public struct InspectionRootView: View { if presentation == .app, showsMonitorCard, - screenshotScenario?.showsMonitorCard != false { + screenshotScenario?.showsMonitorCard != false + { InspectionMonitorCard(store: monitorStore) .id("monitor") } @@ -170,7 +175,8 @@ public struct InspectionRootView: View { InspectionResultsContent( isLoading: store.isLoading, errorMessage: store.errorMessage, - report: report, + inspection: inspection, + selectedReportIndex: $selectedReportIndex, recentItems: recentItems, currentReportURL: report?.requestedURL, onInspectRecent: { recentInput in @@ -190,7 +196,7 @@ public struct InspectionRootView: View { } } - private func regularWidthContent(report: TLSInspectionReport?, recentItems: [RecentLookupItem]) -> some View { + private func regularWidthContent(inspection: TLSInspection?, report: TLSInspectionReport?, recentItems: [RecentLookupItem]) -> some View { VStack(alignment: .leading, spacing: 18) { InspectionInputCard( store: store, @@ -200,10 +206,10 @@ public struct InspectionRootView: View { .id("input") HStack(alignment: .top, spacing: 18) { - regularMainColumn(report: report) + regularMainColumn(inspection: inspection, report: report) .frame(maxWidth: .infinity, alignment: .top) - regularSideRail(report: report, recentItems: recentItems) + regularSideRail(inspection: inspection, recentItems: recentItems) .frame(width: regularSideRailWidth, alignment: .top) } } @@ -221,8 +227,7 @@ public struct InspectionRootView: View { InspectReviewRequester.requestReview() } - @ViewBuilder - private func regularMainColumn(report: TLSInspectionReport?) -> some View { + private func regularMainColumn(inspection: TLSInspection?, report: TLSInspectionReport?) -> some View { VStack(alignment: .leading, spacing: 18) { if store.isLoading { InspectionLoadingCard() @@ -238,10 +243,21 @@ public struct InspectionRootView: View { .id("error") } - if let report { - InspectionSummaryCard(report: report) - .id("summary") - InspectionSecurityCard(assessment: report.security) + if let inspection, let report { + if inspection.didRedirect { + InspectionRedirectsCard( + inspection: inspection, + selectedReportIndex: $selectedReportIndex + ) + .id("hop-picker") + } + + InspectionSummaryCard( + report: report, + reportIndex: selectedReportIndex + ) + .id("summary") + InspectionSecurityCard(report: report) .id("security") } else { InspectionWorkspaceCard() @@ -250,12 +266,12 @@ public struct InspectionRootView: View { } } - @ViewBuilder - private func regularSideRail(report: TLSInspectionReport?, recentItems: [RecentLookupItem]) -> some View { + private func regularSideRail(inspection: TLSInspection?, recentItems: [RecentLookupItem]) -> some View { VStack(alignment: .leading, spacing: 18) { - if let report { + if let inspection { InspectionChainCard( - report: report, + inspection: inspection, + selectedReportIndex: selectedReportIndex, onOpenCertificateDetail: openCertificateDetail ) .id("chain") @@ -267,7 +283,7 @@ public struct InspectionRootView: View { } else { InspectionRecentCard( items: recentItems, - currentReportURL: report?.requestedURL, + currentReportURL: inspection?.requestedURL, onInspectRecent: { recentInput in await store.inspectRecent(recentInput) }, @@ -281,7 +297,8 @@ public struct InspectionRootView: View { if presentation == .app, showsMonitorCard, - screenshotScenario?.showsMonitorCard != false { + screenshotScenario?.showsMonitorCard != false + { InspectionMonitorCard(store: monitorStore) .id("monitor") } @@ -324,10 +341,11 @@ public struct InspectionRootView: View { ) } - private func openCertificateDetail(_ report: TLSInspectionReport, _ index: Int) { + private func openCertificateDetail(_ inspection: TLSInspection, _ reportIndex: Int, _ certificateIndex: Int) { certificateRoute = InspectionCertificateRoute( - report: report, - initialSelectionIndex: index + inspection: inspection, + initialReportIndex: reportIndex, + initialSelectionIndex: certificateIndex ) } @@ -342,7 +360,8 @@ public struct InspectionRootView: View { } certificateRoute = InspectionCertificateRoute( - report: report, + inspection: TLSInspection(report: report), + initialReportIndex: 0, initialSelectionIndex: 0 ) } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionStore.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionStore.swift index 345f8e9..85103b3 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionStore.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionStore.swift @@ -1,19 +1,19 @@ import Foundation -import Observation import InspectCore +import Observation @MainActor @Observable public final class InspectionStore { public var input = "" - public var report: TLSInspectionReport? + public var inspection: TLSInspection? public var isLoading = false public var errorMessage: String? public private(set) var recentInputs: [String] private var hasConsumedInitialURL = false public init() { - self.recentInputs = RecentInputStore.load() + recentInputs = RecentInputStore.load() } public func bootstrap(initialURL: URL?) { @@ -40,7 +40,7 @@ public final class InspectionStore { case let .report(report, _): isLoading = false input = report.requestedURL.absoluteString - self.report = report + inspection = TLSInspection(report: report) RecentInputStore.record(report.requestedURL.absoluteString) recentInputs = RecentInputStore.load() } @@ -57,7 +57,7 @@ public final class InspectionStore { } public func inspectRecent(_ recentInput: String) async { - guard normalizedURL(from: recentInput) != report?.requestedURL else { + guard normalizedURL(from: recentInput) != inspection?.requestedURL else { return } @@ -75,10 +75,10 @@ public final class InspectionStore { errorMessage = nil do { - let report = try await TLSInspector().inspect(input: candidate) - self.report = report - self.input = report.requestedURL.absoluteString - RecentInputStore.record(report.requestedURL.absoluteString) + let inspection = try await TLSInspector().inspect(input: candidate) + self.inspection = inspection + input = inspection.requestedURL.absoluteString + RecentInputStore.record(inspection.requestedURL.absoluteString) recentInputs = RecentInputStore.load() } catch { errorMessage = error.localizedDescription diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionSummaryAndSecurityCards.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionSummaryAndSecurityCards.swift index 7d504c7..1ef06ab 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionSummaryAndSecurityCards.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionSummaryAndSecurityCards.swift @@ -4,70 +4,70 @@ import SwiftUI struct InspectionSummaryCard: View { @Environment(\.openURL) private var openURL let report: TLSInspectionReport + let reportIndex: Int @State private var presentsSSLLabs = false var body: some View { InspectCard { VStack(alignment: .leading, spacing: 16) { - Text(report.host) - .font(.inspectRootTitle3) - .lineLimit(2) + Text("Connection Summary") + .font(.inspectRootHeadline) LazyVGrid( columns: Array( repeating: GridItem(.flexible(minimum: 0), spacing: badgeSpacing), - count: min(badges.count, InspectLayout.Summary.maxBadgesPerRow) + count: min(badges(for: selectedReport).count, InspectLayout.Summary.maxBadgesPerRow) ), alignment: .leading, spacing: badgeSpacing ) { - ForEach(badges) { badge in + ForEach(badges(for: selectedReport)) { badge in Badge(text: badge.text, tint: badge.tint) .frame(maxWidth: .infinity) } } - if let leaf = report.leafCertificate { + if let leaf = selectedReport.leafCertificate { VStack(alignment: .leading, spacing: 12) { InspectionSummaryField(title: "Issued To", value: leaf.subjectSummary) InspectionSummaryField(title: "Issued By", value: leaf.issuerSummary) InspectionSummaryField(title: "Validity", value: leaf.validity.status.rawValue) - if let cipherSuite = report.cipherSuite { + if let cipherSuite = selectedReport.cipherSuite { InspectionSummaryField(title: "Cipher Suite", value: cipherSuite) } } .frame(maxWidth: .infinity, alignment: .leading) } - if let failureReason = report.trust.failureReason, report.trust.isTrusted == false { + if let failureReason = selectedReport.trust.failureReason, selectedReport.trust.isTrusted == false { Text(failureReason) .font(.inspectRootFootnote) .foregroundStyle(.secondary) } - if let sslLabsURL = report.sslLabsURL { + if let sslLabsURL = selectedReport.sslLabsURL { Button { openSSLLabs(sslLabsURL) } label: { Label("Open in SSL Labs", systemImage: "arrow.up.right.square") } .font(.inspectRootSubheadlineSemibold) - .accessibilityIdentifier("action.open-ssllabs") + .accessibilityIdentifier("action.open-ssllabs.\(reportIndex)") } } } - .inspectSafariSheet(url: report.sslLabsURL, isPresented: $presentsSSLLabs) + .inspectSafariSheet(url: selectedReport.sslLabsURL, isPresented: $presentsSSLLabs) } private func openSSLLabs(_ url: URL) { #if os(macOS) - openURL(url) + openURL(url) #else - presentsSSLLabs = true + presentsSSLLabs = true #endif } - private var protocolTitle: String { + private func protocolTitle(for report: TLSInspectionReport) -> String { switch report.networkProtocolName?.lowercased() { case "h2": return "HTTP/2" @@ -86,13 +86,17 @@ struct InspectionSummaryCard: View { InspectLayout.Summary.badgeSpacing } - private var badges: [InspectionSummaryBadge] { + private var selectedReport: TLSInspectionReport { + report + } + + private func badges(for report: TLSInspectionReport) -> [InspectionSummaryBadge] { var values = [ InspectionSummaryBadge( text: report.trust.badgeText, tint: report.trust.isTrusted ? .green : .orange ), - InspectionSummaryBadge(text: protocolTitle, tint: .blue) + InspectionSummaryBadge(text: protocolTitle(for: report), tint: .blue), ] if let tlsVersion = report.tlsVersion { @@ -135,7 +139,9 @@ private struct InspectionSummaryBadge: Identifiable { let text: String let tint: Color - var id: String { text } + var id: String { + text + } } private struct InspectionSummaryField: View { @@ -158,36 +164,39 @@ private struct InspectionSummaryField: View { } struct InspectionSecurityCard: View { - let assessment: SecurityAssessment + let report: TLSInspectionReport var body: some View { InspectCard { VStack(alignment: .leading, spacing: 14) { - HStack { - Text("Security Signals") - .font(.inspectRootHeadline) + Text("Security Signals") + .font(.inspectRootHeadline) - if assessment.showsHeadline { - Spacer() - Text(assessment.headline) + if report.security.findings.isEmpty { + Text("No security findings for this hop.") + .font(.inspectRootCaption) + .foregroundStyle(.secondary) + } else { + if report.security.showsHeadline { + Text(report.security.headline) .font(.inspectRootCaptionSemibold) .foregroundStyle(.secondary) } - } - ForEach(assessment.findings) { finding in - HStack(alignment: .top, spacing: 12) { - Circle() - .fill(color(for: finding.severity)) - .frame(width: 10, height: 10) - .padding(.top, 6) - - VStack(alignment: .leading, spacing: 3) { - Text(finding.title) - .font(.inspectRootSubheadlineSemibold) - Text(finding.message) - .font(.inspectRootCaption) - .foregroundStyle(.secondary) + ForEach(report.security.findings) { finding in + HStack(alignment: .top, spacing: 12) { + Circle() + .fill(color(for: finding.severity)) + .frame(width: 10, height: 10) + .padding(.top, 6) + + VStack(alignment: .leading, spacing: 3) { + Text(finding.title) + .font(.inspectRootSubheadlineSemibold) + Text(finding.message) + .font(.inspectRootCaption) + .foregroundStyle(.secondary) + } } } } diff --git a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorHostDetailView.swift b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorHostDetailView.swift index e30f187..df5bfe6 100644 --- a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorHostDetailView.swift +++ b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorHostDetailView.swift @@ -68,7 +68,6 @@ struct InspectionMonitorHostDetailView: View { } } - @ViewBuilder private var certificateCard: some View { InspectCard { VStack(alignment: .leading, spacing: 14) { @@ -85,7 +84,8 @@ struct InspectionMonitorHostDetailView: View { Button { certificateRoute = InspectionCertificateRoute( - report: report, + inspection: TLSInspection(report: report), + initialReportIndex: 0, initialSelectionIndex: 0 ) } label: { diff --git a/justfile b/justfile index 3f7e82a..cc76522 100644 --- a/justfile +++ b/justfile @@ -5,22 +5,22 @@ default: @just --list --list-submodules generate: - xcodegen generate + ./scripts/xcodegen_generate.sh test-ios-sim device_id="863DCA4D-25BC-4E56-B6DA-D94FEC42A174": - xcodegen generate + ./scripts/xcodegen_generate.sh xcodebuild -project Inspect.xcodeproj -scheme Inspect -destination "platform=iOS Simulator,id={{device_id}}" test | xcbeautify build-macos: - xcodegen generate + ./scripts/xcodegen_generate.sh xcodebuild -project Inspect.xcodeproj -scheme InspectMac -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build | xcbeautify run-mac derived_data="target/DerivedData/InspectMac": #!/usr/bin/env bash set -euo pipefail DERIVED_DATA="{{derived_data}}" - xcodegen generate - xcodebuild -project Inspect.xcodeproj -scheme InspectMac -destination 'platform=macOS' -derivedDataPath "$DERIVED_DATA" build | xcbeautify + ./scripts/xcodegen_generate.sh + xcodebuild -project Inspect.xcodeproj -scheme InspectMac -destination 'platform=macOS' -allowProvisioningUpdates -derivedDataPath "$DERIVED_DATA" build | xcbeautify APP_PATH="$DERIVED_DATA/Build/Products/Debug/Inspect.app" if [[ ! -d "$APP_PATH" ]]; then echo "Built app not found at $APP_PATH" >&2 @@ -29,7 +29,7 @@ run-mac derived_data="target/DerivedData/InspectMac": open -n "$APP_PATH" test-macos: - xcodegen generate + ./scripts/xcodegen_generate.sh xcodebuild -project Inspect.xcodeproj -scheme InspectMac -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO test | xcbeautify testflight: @@ -86,7 +86,7 @@ run-ios-device log_file="target/ios-device-console.log" tunnel_log_file="target/ LOG_FILE="{{log_file}}" TUNNEL_LOG_FILE="{{tunnel_log_file}}" APP_GROUP="{{app_group}}" - xcodegen generate + ./scripts/xcodegen_generate.sh if [[ -z "${DEVICE_ID:-}" ]]; then DEVICE_ID="$( xcrun xctrace list devices 2>/dev/null \ diff --git a/project.local.yml.example b/project.local.yml.example new file mode 100644 index 0000000..ccab7a2 --- /dev/null +++ b/project.local.yml.example @@ -0,0 +1,7 @@ +name: Inspect + +settings: + base: + DEVELOPMENT_TEAM: V28VJH6B6S + CODE_SIGN_STYLE: Automatic + diff --git a/project.yml b/project.yml index ddb9580..72e3a6a 100644 --- a/project.yml +++ b/project.yml @@ -1,5 +1,9 @@ name: Inspect +include: + - path: project.local.yml + enable: ${INCLUDE_LOCAL_PROJECT_YML} + options: bundleIdPrefix: in.fourplex createIntermediateGroups: true diff --git a/scripts/xcodegen_generate.sh b/scripts/xcodegen_generate.sh new file mode 100755 index 0000000..0a10b56 --- /dev/null +++ b/scripts/xcodegen_generate.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -f "project.local.yml" ]]; then + INCLUDE_LOCAL_PROJECT_YML=YES xcodegen generate -s project.yml +else + INCLUDE_LOCAL_PROJECT_YML=NO xcodegen generate -s project.yml +fi From 6d0a405ce04b2fdbe8301eaba1ab8c8b621b6a2e Mon Sep 17 00:00:00 2001 From: hewigovens <360470+hewigovens@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:19:52 +0900 Subject: [PATCH 2/2] v2.5.2: Address PR review, add lint/format, consolidate local overrides - Fix fragile index-based trust/metrics pairing with host-based matching - Hybrid ZStack for macOS root view (state preservation + lazy Settings) - Add just lint/format commands, resolve swiftformat/swiftlint conflicts - Re-enable identifier_name and cyclomatic_complexity lint rules, fix violations - Fix all remaining lint warnings (zero warnings) - Add swiftlint --strict to CI pipeline - Consolidate local signing overrides to project.local.yml only - Remove TLS hop info from CertificateDetailView - Fix vertical centering in redirect rows - Fix stale selectedReportIndex when switching domains - Fix regular layout summary/security cards not reflecting selected hop - Regenerate CT logs: Data-keyed dictionary, tiled_logs support, filter retired - Widen macOS certificate detail panel for long chains - Bump to 2.5.2 (build 86) --- .github/workflows/ci.yml | 16 +- .gitignore | 1 - .swiftlint.yml | 11 +- Apps/iOS/Sources/InspectAppDelegate.swift | 4 +- Apps/iOS/Sources/InspectAppRootView.swift | 20 +- Apps/iOS/Sources/InspectQuickAction.swift | 2 +- Apps/iOS/Sources/InspectSceneDelegate.swift | 9 +- .../iOS/Sources/InspectSettingsSections.swift | 2 +- Apps/iOS/Sources/LiveMonitorManager.swift | 14 +- .../InspectMacLiveMonitorManager.swift | 5 +- Apps/macOS/Sources/InspectMacRootView.swift | 7 +- .../Sources/InspectMacSettingsSections.swift | 2 +- .../Sources/InspectMacTunnelProfile.swift | 2 +- .../InspectMacVerificationSections.swift | 4 +- .../Sources/InspectMacWindowController.swift | 7 +- .../Sources/MacSystemExtensionActivator.swift | 10 +- Configs/LocalOverrides.xcconfig.example | 3 - Configs/Project.xcconfig | 1 - DEVELOPMENT.md | 3 +- .../InspectCore/Sources/Core/ByteReader.swift | 8 +- .../Core/CRLDistributionPointsDecoder.swift | 5 +- .../Sources/Core/CertificateParser.swift | 99 +++++----- .../Core/CertificatePoliciesDecoder.swift | 16 +- .../Core/ExtensionInputExtractor.swift | 31 +-- .../Sources/Core/HostNormalizer.swift | 3 +- .../Sources/Core/InspectDeepLink.swift | 5 +- .../Sources/Core/InspectLogging.swift | 5 +- .../InspectPacketTunnelConfiguration.swift | 2 +- .../Core/InspectPacketTunnelRuntime.swift | 7 +- .../Sources/Core/InspectSharedContainer.swift | 3 +- .../Sources/Core/InspectSharedLog.swift | 12 +- .../Core/InspectTunnelForwardingEngine.swift | 4 +- .../Sources/Core/InspectionError.swift | 4 +- .../InspectionSharedPendingReportStore.swift | 3 +- .../Core/InspectionSharedReportStore.swift | 3 +- .../Sources/Core/KnownCTLogs.swift | 142 ++++++++++---- .../Sources/Core/PacketSummary.swift | 12 +- .../PassiveTLSInspectionReportBuilder.swift | 3 +- .../InspectCore/Sources/Core/SCTDecoder.swift | 9 +- .../Sources/Core/SecurityAnalyzer.swift | 43 ++++- .../Core/TLSClientHelloSNIExtractor.swift | 90 +++++---- .../Sources/Core/TLSFlowObservationFeed.swift | 6 +- .../Sources/Core/TLSInspector.swift | 49 +++-- .../Core/TLSPassiveHandshakeCapture.swift | 12 +- .../Certificate/CertificateDetailModels.swift | 10 +- .../Certificate/CertificateDetailTheme.swift | 12 +- .../CertificateDetailView+iOS.swift | 22 +-- .../CertificateDetailView+macOS.swift | 22 +-- .../Certificate/CertificateExportWriter.swift | 2 +- .../MacCertificateSectionViews.swift | 178 +++++++++--------- .../MacCertificateSummaryViews.swift | 58 +++--- .../Certificate/RevocationStatusView.swift | 4 +- .../InspectionAppStoreScreenshotView.swift | 122 ++++++------ .../InspectionExternalInputCenter.swift | 3 +- .../Inspection/InspectionNavigation.swift | 2 +- .../Inspection/InspectionRecentItems.swift | 2 +- .../Inspection/InspectionRedirectsCard.swift | 19 +- .../Inspection/InspectionResultsContent.swift | 2 +- .../Inspection/InspectionRootView.swift | 8 +- .../InspectionScreenshotFixtures.swift | 10 +- .../Feature/Inspection/RecentLookupIcon.swift | 2 +- ...InspectionLiveMonitorPreferenceStore.swift | 2 +- .../Monitor/InspectionMonitorCard.swift | 2 +- .../Monitor/InspectionMonitorModels.swift | 14 +- .../Monitor/InspectionMonitorStore.swift | 27 +-- .../Monitor/InspectionMonitorView.swift | 4 +- .../Settings/InspectReviewRequester.swift | 30 +-- .../Settings/InspectSettingsIconLabel.swift | 8 +- .../Settings/InspectSettingsRows.swift | 4 +- .../InspectSharedSettingsSections.swift | 4 +- .../InspectionReviewPromptStore.swift | 2 +- .../Feature/Shared/InspectAppRoute.swift | 5 +- .../Shared/InspectPlatformSupport.swift | 152 ++++++++------- .../Shared/InspectSafariController.swift | 44 ++--- .../Feature/Shared/InspectSection.swift | 8 +- .../Shared/InspectionCommonStrings.swift | 4 +- .../Feature/Theme/InspectSurfaceViews.swift | 2 +- .../Theme/InspectionViewModifiers.swift | 2 +- .../Sources/Feature/Theme/Layout.swift | 142 +++++++++----- justfile | 9 + project.local.yml.example | 2 +- project.yml | 8 +- scripts/update_ct_logs.py | 63 ++++--- 83 files changed, 940 insertions(+), 775 deletions(-) delete mode 100644 Configs/LocalOverrides.xcconfig.example delete mode 100644 Configs/Project.xcconfig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8986b48..5df4e5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,6 @@ name: CI on: push: - branches: - - main - - master pull_request: concurrency: @@ -14,8 +11,6 @@ concurrency: jobs: build-and-test: runs-on: macos-26 - env: - CARGO_NET_GIT_FETCH_WITH_CLI: "true" steps: - name: Check out repository @@ -29,11 +24,11 @@ jobs: - name: Show Xcode version run: xcodebuild -version - - name: Install XcodeGen - run: brew install xcodegen + - name: Install tools + run: brew install xcodegen swiftlint - - name: Generate Xcode project - run: xcodegen generate + - name: Lint + run: swiftlint lint --quiet --strict Apps Packages/InspectCore/Sources - name: Test InspectCore package shell: bash @@ -41,6 +36,9 @@ jobs: set -o pipefail swift test --package-path Packages/InspectCore + - name: Generate Xcode project + run: xcodegen generate + - name: Build iOS app for Simulator shell: bash run: | diff --git a/.gitignore b/.gitignore index 9fd5782..bfbd2f0 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ output/ Pods/ fastlane/README.md fastlane/report.xml -Configs/LocalOverrides.xcconfig project.local.yml .asc/ Rust/**/target/ diff --git a/.swiftlint.yml b/.swiftlint.yml index 2e0a7ba..014cbea 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -2,8 +2,10 @@ disabled_rules: - line_length - file_length - type_body_length - - identifier_name - - cyclomatic_complexity + - function_body_length + - opening_brace + - optional_data_string_conversion + - trailing_comma excluded: - Carthage @@ -13,4 +15,9 @@ excluded: - ./QRCode +identifier_name: + excluded: + - i + - id + force_cast: warning diff --git a/Apps/iOS/Sources/InspectAppDelegate.swift b/Apps/iOS/Sources/InspectAppDelegate.swift index 1fef68e..ee39ed1 100644 --- a/Apps/iOS/Sources/InspectAppDelegate.swift +++ b/Apps/iOS/Sources/InspectAppDelegate.swift @@ -2,9 +2,9 @@ import UIKit final class InspectAppDelegate: NSObject, UIApplicationDelegate { func application( - _ application: UIApplication, + _: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions + options _: UIScene.ConnectionOptions ) -> UISceneConfiguration { let configuration = UISceneConfiguration( name: nil, diff --git a/Apps/iOS/Sources/InspectAppRootView.swift b/Apps/iOS/Sources/InspectAppRootView.swift index 4ecd992..09067aa 100644 --- a/Apps/iOS/Sources/InspectAppRootView.swift +++ b/Apps/iOS/Sources/InspectAppRootView.swift @@ -18,20 +18,20 @@ struct InspectAppRootView: View { showsMonitorCard: false, showsAboutCard: false ) - .tabItem { - Label(InspectSection.inspect.title, systemImage: InspectSection.inspect.systemImage) - } - .tag(InspectSection.inspect) - .accessibilityIdentifier("tab.inspect") + .tabItem { + Label(InspectSection.inspect.title, systemImage: InspectSection.inspect.systemImage) + } + .tag(InspectSection.inspect) + .accessibilityIdentifier("tab.inspect") InspectionMonitorView { await liveMonitorManager.refresh() } - .tabItem { - Label(InspectSection.monitor.title, systemImage: InspectSection.monitor.systemImage) - } - .tag(InspectSection.monitor) - .accessibilityIdentifier("tab.monitor") + .tabItem { + Label(InspectSection.monitor.title, systemImage: InspectSection.monitor.systemImage) + } + .tag(InspectSection.monitor) + .accessibilityIdentifier("tab.monitor") InspectSettingsView(manager: liveMonitorManager) .tabItem { diff --git a/Apps/iOS/Sources/InspectQuickAction.swift b/Apps/iOS/Sources/InspectQuickAction.swift index 5b56632..056e2cd 100644 --- a/Apps/iOS/Sources/InspectQuickAction.swift +++ b/Apps/iOS/Sources/InspectQuickAction.swift @@ -1,5 +1,5 @@ -import UIKit import InspectKit +import UIKit enum InspectQuickAction { static let inspectType = "in.fourplex.Inspect.shortcut.inspect" diff --git a/Apps/iOS/Sources/InspectSceneDelegate.swift b/Apps/iOS/Sources/InspectSceneDelegate.swift index bbf2237..5181fa2 100644 --- a/Apps/iOS/Sources/InspectSceneDelegate.swift +++ b/Apps/iOS/Sources/InspectSceneDelegate.swift @@ -2,12 +2,13 @@ import UIKit final class InspectSceneDelegate: NSObject, UIWindowSceneDelegate { func scene( - _ scene: UIScene, - willConnectTo session: UISceneSession, + _: UIScene, + willConnectTo _: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let shortcutItem = connectionOptions.shortcutItem, - let route = InspectQuickAction.route(for: shortcutItem) else { + let route = InspectQuickAction.route(for: shortcutItem) + else { return } @@ -15,7 +16,7 @@ final class InspectSceneDelegate: NSObject, UIWindowSceneDelegate { } func windowScene( - _ windowScene: UIWindowScene, + _: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void ) { diff --git a/Apps/iOS/Sources/InspectSettingsSections.swift b/Apps/iOS/Sources/InspectSettingsSections.swift index 548a29d..8fe1c06 100644 --- a/Apps/iOS/Sources/InspectSettingsSections.swift +++ b/Apps/iOS/Sources/InspectSettingsSections.swift @@ -13,7 +13,7 @@ struct InspectTunnelSettingsSection: View { systemImage: "checkmark.shield", tint: .green ) { - Text(manager.isConfigured ? InspectionCommonStrings.yes : InspectionCommonStrings.no) + Text(manager.isConfigured ? InspectionCommonStrings.yesLabel : InspectionCommonStrings.noLabel) .font(.subheadline.weight(.medium)) .foregroundStyle(.primary) .monospacedDigit() diff --git a/Apps/iOS/Sources/LiveMonitorManager.swift b/Apps/iOS/Sources/LiveMonitorManager.swift index c9553f8..beb25db 100644 --- a/Apps/iOS/Sources/LiveMonitorManager.swift +++ b/Apps/iOS/Sources/LiveMonitorManager.swift @@ -1,10 +1,10 @@ +import Foundation import InspectCore import InspectKit -import Foundation @preconcurrency import NetworkExtension import Observation #if canImport(WidgetKit) -import WidgetKit + import WidgetKit #endif @MainActor @@ -36,10 +36,10 @@ final class LiveMonitorManager { if desiredLiveMonitorEnabled == nil { desiredLiveMonitorEnabled = LiveMonitorTunnelState.isActive(for: manager.connection.status) } - self.lastErrorMessage = nil + lastErrorMessage = nil logger.verbose("Refresh complete. status=\(manager.connection.status.inspectionDescription) configured=\(manager.isEnabled)") } catch { - self.lastErrorMessage = error.localizedDescription + lastErrorMessage = error.localizedDescription logger.critical("Refresh failed: \(error.localizedDescription)") } } @@ -167,9 +167,9 @@ final class LiveMonitorManager { InspectionLiveMonitorPreferenceStore.setEnabled( LiveMonitorTunnelState.isActive(for: currentStatus) ) -#if canImport(WidgetKit) - WidgetCenter.shared.reloadTimelines(ofKind: InspectWidgetKind.liveMonitor) -#endif + #if canImport(WidgetKit) + WidgetCenter.shared.reloadTimelines(ofKind: InspectWidgetKind.liveMonitor) + #endif logger.verbose("Updated state. status=\(currentStatus.inspectionDescription) configured=\(isConfigured)") } diff --git a/Apps/macOS/Sources/InspectMacLiveMonitorManager.swift b/Apps/macOS/Sources/InspectMacLiveMonitorManager.swift index 3296d2e..bf0841a 100644 --- a/Apps/macOS/Sources/InspectMacLiveMonitorManager.swift +++ b/Apps/macOS/Sources/InspectMacLiveMonitorManager.swift @@ -1,6 +1,6 @@ +import Foundation import InspectCore import InspectKit -import Foundation @preconcurrency import NetworkExtension import Observation @@ -184,7 +184,8 @@ final class InspectMacLiveMonitorManager { if preservesActiveStatus, LiveMonitorTunnelState.isActive(for: status), - LiveMonitorTunnelState.isActive(for: refreshedStatus) == false { + LiveMonitorTunnelState.isActive(for: refreshedStatus) == false + { effectiveStatus = status } else { effectiveStatus = refreshedStatus diff --git a/Apps/macOS/Sources/InspectMacRootView.swift b/Apps/macOS/Sources/InspectMacRootView.swift index cfc3e25..2feedf2 100644 --- a/Apps/macOS/Sources/InspectMacRootView.swift +++ b/Apps/macOS/Sources/InspectMacRootView.swift @@ -119,10 +119,9 @@ struct InspectMacRootView: View { .allowsHitTesting(appModel.selectedSection == .monitor) .accessibilityHidden(appModel.selectedSection != .monitor) - InspectMacSettingsView(manager: manager) - .opacity(appModel.selectedSection == .settings ? 1 : 0) - .allowsHitTesting(appModel.selectedSection == .settings) - .accessibilityHidden(appModel.selectedSection != .settings) + if appModel.selectedSection == .settings { + InspectMacSettingsView(manager: manager) + } if appModel.selectedSection == nil { ContentUnavailableView( diff --git a/Apps/macOS/Sources/InspectMacSettingsSections.swift b/Apps/macOS/Sources/InspectMacSettingsSections.swift index ea540c7..a2c0da5 100644 --- a/Apps/macOS/Sources/InspectMacSettingsSections.swift +++ b/Apps/macOS/Sources/InspectMacSettingsSections.swift @@ -12,7 +12,7 @@ struct InspectMacLiveMonitorSettingsSection: View { systemImage: "checkmark.shield", tint: .green ) { - Text(manager.isConfigured ? InspectionCommonStrings.yes : InspectionCommonStrings.no) + Text(manager.isConfigured ? InspectionCommonStrings.yesLabel : InspectionCommonStrings.noLabel) .font(.body.weight(.medium)) } diff --git a/Apps/macOS/Sources/InspectMacTunnelProfile.swift b/Apps/macOS/Sources/InspectMacTunnelProfile.swift index 543e004..7fccdcb 100644 --- a/Apps/macOS/Sources/InspectMacTunnelProfile.swift +++ b/Apps/macOS/Sources/InspectMacTunnelProfile.swift @@ -1,6 +1,6 @@ import Foundation -struct InspectMacTunnelProfile: Sendable { +struct InspectMacTunnelProfile { let localizedDescription: String let serverAddress: String let providerBundleIdentifier: String diff --git a/Apps/macOS/Sources/InspectMacVerificationSections.swift b/Apps/macOS/Sources/InspectMacVerificationSections.swift index d1bd7f6..c31ebb9 100644 --- a/Apps/macOS/Sources/InspectMacVerificationSections.swift +++ b/Apps/macOS/Sources/InspectMacVerificationSections.swift @@ -1,5 +1,5 @@ -import InspectKit import InspectCore +import InspectKit @preconcurrency import NetworkExtension import SwiftUI @@ -32,7 +32,7 @@ struct InspectMacVerificationProfileCard: View { GridRow { Text("Configured") .foregroundStyle(.secondary) - Text(isConfigured ? InspectionCommonStrings.yes : InspectionCommonStrings.no) + Text(isConfigured ? InspectionCommonStrings.yesLabel : InspectionCommonStrings.noLabel) .fontWeight(.medium) } diff --git a/Apps/macOS/Sources/InspectMacWindowController.swift b/Apps/macOS/Sources/InspectMacWindowController.swift index 1577942..9141630 100644 --- a/Apps/macOS/Sources/InspectMacWindowController.swift +++ b/Apps/macOS/Sources/InspectMacWindowController.swift @@ -68,7 +68,8 @@ final class InspectMacWindowController { private func installDockIcon() { guard let iconURL = Bundle.main.url(forResource: "Inspect", withExtension: "icns"), - let iconImage = NSImage(contentsOf: iconURL) else { + let iconImage = NSImage(contentsOf: iconURL) + else { return } @@ -79,7 +80,7 @@ final class InspectMacWindowController { struct InspectMacWindowReader: NSViewRepresentable { let onResolve: @MainActor (NSWindow) -> Void - func makeNSView(context: Context) -> NSView { + func makeNSView(context _: Context) -> NSView { let view = NSView(frame: .zero) DispatchQueue.main.async { if let window = view.window { @@ -89,7 +90,7 @@ struct InspectMacWindowReader: NSViewRepresentable { return view } - func updateNSView(_ nsView: NSView, context: Context) { + func updateNSView(_ nsView: NSView, context _: Context) { DispatchQueue.main.async { if let window = nsView.window { onResolve(window) diff --git a/Apps/macOS/Sources/MacSystemExtensionActivator.swift b/Apps/macOS/Sources/MacSystemExtensionActivator.swift index 03d3062..20f31c8 100644 --- a/Apps/macOS/Sources/MacSystemExtensionActivator.swift +++ b/Apps/macOS/Sources/MacSystemExtensionActivator.swift @@ -18,13 +18,13 @@ final class MacSystemExtensionActivator: NSObject, OSSystemExtensionRequestDeleg } } - func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) { + func requestNeedsUserApproval(_: OSSystemExtensionRequest) { onApprovalRequired?() } func request( _ request: OSSystemExtensionRequest, - didFinishWithResult result: OSSystemExtensionRequest.Result + didFinishWithResult _: OSSystemExtensionRequest.Result ) { finish(request: request, result: .success(())) } @@ -37,9 +37,9 @@ final class MacSystemExtensionActivator: NSObject, OSSystemExtensionRequestDeleg } func request( - _ request: OSSystemExtensionRequest, - actionForReplacingExtension existing: OSSystemExtensionProperties, - withExtension ext: OSSystemExtensionProperties + _: OSSystemExtensionRequest, + actionForReplacingExtension _: OSSystemExtensionProperties, + withExtension _: OSSystemExtensionProperties ) -> OSSystemExtensionRequest.ReplacementAction { .replace } diff --git a/Configs/LocalOverrides.xcconfig.example b/Configs/LocalOverrides.xcconfig.example deleted file mode 100644 index b22d7c5..0000000 --- a/Configs/LocalOverrides.xcconfig.example +++ /dev/null @@ -1,3 +0,0 @@ -// Copy this file to LocalOverrides.xcconfig for local-only signing overrides. -DEVELOPMENT_TEAM = V28VJH6B6S -CODE_SIGN_STYLE = Automatic diff --git a/Configs/Project.xcconfig b/Configs/Project.xcconfig deleted file mode 100644 index 1cc7e47..0000000 --- a/Configs/Project.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include? "LocalOverrides.xcconfig" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 56a2ae9..4b666ad 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -111,12 +111,11 @@ just testflight One-time setup: ```bash -cp Configs/LocalOverrides.xcconfig.example Configs/LocalOverrides.xcconfig cp project.local.yml.example project.local.yml cp .env.example .env ``` -For local Xcode signing, set your `DEVELOPMENT_TEAM` in `project.local.yml`. The generate script merges that file into `project.yml` so the generated project gets automatic signing target attributes as well as build settings. +For local Xcode signing, set your `DEVELOPMENT_TEAM` and `INSPECT_APP_GROUP_IDENTIFIER` in `project.local.yml`. The generate script merges that file into `project.yml` so the generated project gets automatic signing target attributes as well as build settings. Required `.env` values: diff --git a/Packages/InspectCore/Sources/Core/ByteReader.swift b/Packages/InspectCore/Sources/Core/ByteReader.swift index eb8643d..2028244 100644 --- a/Packages/InspectCore/Sources/Core/ByteReader.swift +++ b/Packages/InspectCore/Sources/Core/ByteReader.swift @@ -2,7 +2,9 @@ public struct ByteReader { private let data: [UInt8] public private(set) var offset: Int = 0 - public var remaining: Int { data.count - offset } + public var remaining: Int { + data.count - offset + } public init(_ data: [UInt8]) { self.data = data @@ -32,7 +34,7 @@ public struct ByteReader { public mutating func readUInt64() throws -> UInt64 { guard offset + 8 <= data.count else { throw ByteReaderError.truncated } var value: UInt64 = 0 - for i in 0..<8 { + for i in 0 ..< 8 { value = value << 8 | UInt64(data[offset + i]) } offset += 8 @@ -41,7 +43,7 @@ public struct ByteReader { public mutating func readBytes(_ count: Int) throws -> [UInt8] { guard offset + count <= data.count else { throw ByteReaderError.truncated } - let bytes = Array(data[offset.. [LabeledValue] { guard ext.oid == oid, - let parsed = try? CRLDistributionPoints(ext) else { + let parsed = try? CRLDistributionPoints(ext) + else { return [] } @@ -35,7 +36,7 @@ private struct CRLDistributionPoints { } } - self.uris = collected + uris = collected } private static func parseDistributionPoint(_ node: ASN1Node) throws -> [String] { diff --git a/Packages/InspectCore/Sources/Core/CertificateParser.swift b/Packages/InspectCore/Sources/Core/CertificateParser.swift index 35cc84e..bdb39d3 100644 --- a/Packages/InspectCore/Sources/Core/CertificateParser.swift +++ b/Packages/InspectCore/Sources/Core/CertificateParser.swift @@ -154,11 +154,11 @@ public struct CertificateParser: Sendable { switch constraints { case .notCertificateAuthority: return [LabeledValue(label: "Certificate Authority", value: "No")] - case .isCertificateAuthority(let maxPathLength): + case let .isCertificateAuthority(maxPathLength): if let maxPathLength { return [ LabeledValue(label: "Certificate Authority", value: "Yes"), - LabeledValue(label: "Max Path Length", value: String(maxPathLength)) + LabeledValue(label: "Max Path Length", value: String(maxPathLength)), ] } @@ -259,21 +259,7 @@ public struct CertificateParser: Sendable { } if let decoded = try? AuthorityKeyIdentifier(ext) { - var parts: [String] = [] - - if let keyIdentifier = decoded.keyIdentifier { - parts.append("keyID: \(Array(keyIdentifier).inspectHexString(grouped: true))") - } - - if let serial = decoded.authorityCertSerialNumber { - parts.append("issuerSerial: \(Array(serial.bytes).inspectHexString(grouped: true))") - } - - if let issuer = decoded.authorityCertIssuer { - parts.append("issuer: \(issuer.map(displayValue(for:)).joined(separator: ", "))") - } - - return parts.joined(separator: ", ") + return formatAuthorityKeyIdentifier(decoded) } if let decoded = try? ExtendedKeyUsage(ext) { @@ -292,6 +278,24 @@ public struct CertificateParser: Sendable { return Array(ext.value).inspectHexString(grouped: true) } + private func formatAuthorityKeyIdentifier(_ aki: AuthorityKeyIdentifier) -> String { + var parts: [String] = [] + + if let keyIdentifier = aki.keyIdentifier { + parts.append("keyID: \(Array(keyIdentifier).inspectHexString(grouped: true))") + } + + if let serial = aki.authorityCertSerialNumber { + parts.append("issuerSerial: \(Array(serial.bytes).inspectHexString(grouped: true))") + } + + if let issuer = aki.authorityCertIssuer { + parts.append("issuer: \(issuer.map(displayValue(for:)).joined(separator: ", "))") + } + + return parts.joined(separator: ", ") + } + private func parsePublicKey(_ publicKey: X509.Certificate.PublicKey?, secCertificate: SecCertificate) -> PublicKeyDetails { let fallbackHex = publicKey.map { Array($0.subjectPublicKeyInfoBytes).inspectHexString(grouped: true) } ?? "Unavailable" let description = publicKey.map { String(describing: $0) } ?? "Unavailable" @@ -323,7 +327,8 @@ public struct CertificateParser: Sendable { private func parsePublicKeyDescription(_ description: String) -> (algorithm: String, bitSize: Int?) { if let match = description.firstMatch(of: /RSA(\d+)\.PublicKey/), - let bitSize = Int(match.1) { + let bitSize = Int(match.1) + { return ("RSA", bitSize) } @@ -348,23 +353,23 @@ public struct CertificateParser: Sendable { private func parse(generalName: GeneralName) -> LabeledValue { switch generalName { - case .dnsName(let value): + case let .dnsName(value): return LabeledValue(label: "DNS Name", value: value) - case .rfc822Name(let value): + case let .rfc822Name(value): return LabeledValue(label: "Email", value: value) - case .uniformResourceIdentifier(let value): + case let .uniformResourceIdentifier(value): return LabeledValue(label: "URI", value: value) - case .ipAddress(let value): + case let .ipAddress(value): return LabeledValue(label: "IP Address", value: formatIPAddress(value.bytes)) - case .directoryName(let value): + case let .directoryName(value): return LabeledValue(label: "Directory Name", value: String(describing: value)) - case .registeredID(let value): + case let .registeredID(value): return LabeledValue(label: "Registered ID", value: String(describing: value)) - case .otherName(let value): + case let .otherName(value): return LabeledValue(label: "Other Name", value: String(describing: value)) - case .x400Address(let value): + case let .x400Address(value): return LabeledValue(label: "X.400 Address", value: String(describing: value)) - case .ediPartyName(let value): + case let .ediPartyName(value): return LabeledValue(label: "EDI Party", value: String(describing: value)) } } @@ -393,7 +398,7 @@ public struct CertificateParser: Sendable { private func fingerprints(for derData: Data) -> [LabeledValue] { [ LabeledValue(label: "SHA-256", value: SHA256.hash(data: derData).inspectHexString(grouped: true)), - LabeledValue(label: "SHA-1", value: Insecure.SHA1.hash(data: derData).inspectHexString(grouped: true)) + LabeledValue(label: "SHA-1", value: Insecure.SHA1.hash(data: derData).inspectHexString(grouped: true)), ] } @@ -422,31 +427,21 @@ public struct CertificateParser: Sendable { } } + private static let extensionFriendlyNames: [String: String] = [ + "2.5.29.14": "Subject Key Identifier", + "2.5.29.15": "Key Usage", + "2.5.29.17": "Subject Alternative Name", + "2.5.29.19": "Basic Constraints", + "2.5.29.32": "Certificate Policies", + "2.5.29.35": "Authority Key Identifier", + "2.5.29.37": "Extended Key Usage", + "1.3.6.1.5.5.7.1.1": "Authority Information Access", + "1.3.6.1.4.1.11129.2.4.2": "CT Precertificate SCTs", + "2.5.29.31": "CRL Distribution Points", + ] + private func extensionFriendlyName(for oid: String) -> String { - switch oid { - case "2.5.29.14": - return "Subject Key Identifier" - case "2.5.29.15": - return "Key Usage" - case "2.5.29.17": - return "Subject Alternative Name" - case "2.5.29.19": - return "Basic Constraints" - case "2.5.29.32": - return "Certificate Policies" - case "2.5.29.35": - return "Authority Key Identifier" - case "2.5.29.37": - return "Extended Key Usage" - case "1.3.6.1.5.5.7.1.1": - return "Authority Information Access" - case "1.3.6.1.4.1.11129.2.4.2": - return "CT Precertificate SCTs" - case "2.5.29.31": - return "CRL Distribution Points" - default: - return oid - } + Self.extensionFriendlyNames[oid] ?? oid } private func normalizeSignatureAlgorithm(_ value: String) -> String { diff --git a/Packages/InspectCore/Sources/Core/CertificatePoliciesDecoder.swift b/Packages/InspectCore/Sources/Core/CertificatePoliciesDecoder.swift index 60f40f5..9d3afdb 100644 --- a/Packages/InspectCore/Sources/Core/CertificatePoliciesDecoder.swift +++ b/Packages/InspectCore/Sources/Core/CertificatePoliciesDecoder.swift @@ -31,15 +31,15 @@ private struct CertificatePolicies { let policies: [PolicyInformation] init(_ ext: X509.Certificate.Extension) throws { - self.policies = try DER.sequence( + policies = try DER.sequence( of: PolicyInformation.self, identifier: .sequence, - rootNode: try DER.parse(ext.value) + rootNode: DER.parse(ext.value) ) } } -private struct PolicyInformation: DERParseable, Sendable { +private struct PolicyInformation: DERParseable { let identifier: ASN1ObjectIdentifier let qualifiers: [PolicyQualifier] @@ -64,7 +64,7 @@ private struct PolicyInformation: DERParseable, Sendable { } } -private struct PolicyQualifier: DERParseable, Sendable { +private struct PolicyQualifier: DERParseable { let label: String let value: String @@ -77,12 +77,14 @@ private struct PolicyQualifier: DERParseable, Sendable { let qualifierValue = try ASN1Any(derEncoded: &nodes) if qualifierID == Self.cpsQualifierID, - let cpsURI = try? ASN1IA5String(asn1Any: qualifierValue) { + let cpsURI = try? ASN1IA5String(asn1Any: qualifierValue) + { return PolicyQualifier(label: "CPS URI", value: String(decoding: cpsURI.bytes, as: UTF8.self)) } if qualifierID == Self.userNoticeQualifierID, - let userNotice = try? UserNotice(asn1Any: qualifierValue) { + let userNotice = try? UserNotice(asn1Any: qualifierValue) + { return PolicyQualifier(label: "User Notice", value: userNotice.value) } @@ -99,7 +101,7 @@ private struct PolicyQualifier: DERParseable, Sendable { } } -private struct UserNotice: DERParseable, Sendable { +private struct UserNotice: DERParseable { let value: String init(derEncoded node: ASN1Node) throws { diff --git a/Packages/InspectCore/Sources/Core/ExtensionInputExtractor.swift b/Packages/InspectCore/Sources/Core/ExtensionInputExtractor.swift index 9f4e315..9cc09a9 100644 --- a/Packages/InspectCore/Sources/Core/ExtensionInputExtractor.swift +++ b/Packages/InspectCore/Sources/Core/ExtensionInputExtractor.swift @@ -3,7 +3,7 @@ import UniformTypeIdentifiers @MainActor public enum ExtensionInputExtractor { - nonisolated private static let safariPreprocessingResultsKey = "NSExtensionJavaScriptPreprocessingResultsKey" + private nonisolated static let safariPreprocessingResultsKey = "NSExtensionJavaScriptPreprocessingResultsKey" public static func loadURL(from context: NSExtensionContext?) async -> URL? { guard let input = await loadInputString(from: context) else { @@ -36,9 +36,11 @@ public enum ExtensionInputExtractor { private static func loadSafariPreprocessingInput(from items: [NSExtensionItem]) async -> String? { for item in items { for provider in item.attachments ?? [] - where provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) { + where provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) + { if let payload = await loadSafariPreprocessingPayload(from: provider), - let urlString = stringValue(in: payload, keys: ["url", "URL"]) { + let urlString = stringValue(in: payload, keys: ["url", "URL"]) + { return urlString } } @@ -56,7 +58,7 @@ public enum ExtensionInputExtractor { for typeIdentifier in [ UTType.url.identifier, - UTType.fileURL.identifier + UTType.fileURL.identifier, ] where provider.hasItemConformingToTypeIdentifier(typeIdentifier) { if let value = await loadItemString(from: provider, typeIdentifier: typeIdentifier) { return trimmed(value) @@ -77,7 +79,7 @@ public enum ExtensionInputExtractor { for typeIdentifier in [ UTType.plainText.identifier, - UTType.text.identifier + UTType.text.identifier, ] where provider.hasItemConformingToTypeIdentifier(typeIdentifier) { if let value = await loadItemString(from: provider, typeIdentifier: typeIdentifier) { return trimmed(value) @@ -92,12 +94,14 @@ public enum ExtensionInputExtractor { private static func loadAttributedFallback(from items: [NSExtensionItem]) -> String? { for item in items { if let body = item.attributedContentText?.string, - let trimmedBody = trimmed(body) { + let trimmedBody = trimmed(body) + { return trimmedBody } if let title = item.attributedTitle?.string, - let trimmedTitle = trimmed(title) { + let trimmedTitle = trimmed(title) + { return trimmedTitle } } @@ -113,7 +117,7 @@ public enum ExtensionInputExtractor { } } - nonisolated private static func safariPreprocessingPayload(from item: NSSecureCoding?) -> [String: String]? { + private nonisolated static func safariPreprocessingPayload(from item: NSSecureCoding?) -> [String: String]? { if let dictionary = item as? [String: Any] { if let nested = dictionary[safariPreprocessingResultsKey] as? [String: Any] { return stringDictionary(from: nested) @@ -134,7 +138,7 @@ public enum ExtensionInputExtractor { return nil } - nonisolated private static func stringDictionary(from dictionary: [String: Any]) -> [String: String] { + private nonisolated static func stringDictionary(from dictionary: [String: Any]) -> [String: String] { var result: [String: String] = [:] for (key, value) in dictionary { @@ -148,10 +152,11 @@ public enum ExtensionInputExtractor { return result } - nonisolated private static func stringValue(in dictionary: [String: String], keys: [String]) -> String? { + private nonisolated static func stringValue(in dictionary: [String: String], keys: [String]) -> String? { for key in keys { if let value = dictionary[key], - let trimmedValue = trimmed(value) { + let trimmedValue = trimmed(value) + { return trimmedValue } } @@ -207,7 +212,7 @@ public enum ExtensionInputExtractor { } } - nonisolated private static func stringValue(from item: NSSecureCoding?) -> String? { + private nonisolated static func stringValue(from item: NSSecureCoding?) -> String? { if let url = item as? URL { return url.absoluteString } @@ -231,7 +236,7 @@ public enum ExtensionInputExtractor { return nil } - nonisolated private static func trimmed(_ value: String) -> String? { + private nonisolated static func trimmed(_ value: String) -> String? { let trimmedValue = value.trimmingCharacters(in: .whitespacesAndNewlines) return trimmedValue.isEmpty ? nil : trimmedValue } diff --git a/Packages/InspectCore/Sources/Core/HostNormalizer.swift b/Packages/InspectCore/Sources/Core/HostNormalizer.swift index 9a2e250..5faf802 100644 --- a/Packages/InspectCore/Sources/Core/HostNormalizer.swift +++ b/Packages/InspectCore/Sources/Core/HostNormalizer.swift @@ -38,7 +38,8 @@ public enum HostNormalizer { guard component.isEmpty == false, component.count <= 3, let number = Int(component), - (0...255).contains(number) else { + (0 ... 255).contains(number) + else { return false } } diff --git a/Packages/InspectCore/Sources/Core/InspectDeepLink.swift b/Packages/InspectCore/Sources/Core/InspectDeepLink.swift index 85eb43d..d1ab310 100644 --- a/Packages/InspectCore/Sources/Core/InspectDeepLink.swift +++ b/Packages/InspectCore/Sources/Core/InspectDeepLink.swift @@ -10,7 +10,7 @@ public enum InspectDeepLink: Sendable, Equatable { components.scheme = InspectScheme.scheme components.host = InspectScheme.certificateDetailHost components.queryItems = [ - URLQueryItem(name: InspectScheme.tokenQueryItemName, value: token) + URLQueryItem(name: InspectScheme.tokenQueryItemName, value: token), ] return components.url! } @@ -27,7 +27,8 @@ public enum InspectDeepLink: Sendable, Equatable { .queryItems? .first(where: { $0.name == InspectScheme.tokenQueryItemName })? .value, - token.isEmpty == false else { + token.isEmpty == false + else { return nil } diff --git a/Packages/InspectCore/Sources/Core/InspectLogging.swift b/Packages/InspectCore/Sources/Core/InspectLogging.swift index 0a84022..7f57244 100644 --- a/Packages/InspectCore/Sources/Core/InspectLogging.swift +++ b/Packages/InspectCore/Sources/Core/InspectLogging.swift @@ -23,7 +23,8 @@ public enum InspectLogConfiguration { ) -> InspectLogVerbosity { let defaults = UserDefaults(suiteName: suiteName) ?? .standard guard let rawValue = defaults.string(forKey: defaultsKey), - let verbosity = InspectLogVerbosity(rawValue: rawValue) else { + let verbosity = InspectLogVerbosity(rawValue: rawValue) + else { return .criticalOnly } @@ -48,7 +49,7 @@ public struct InspectRuntimeLogger: Sendable { category: String, scope: String ) { - self.logger = Logger(subsystem: subsystem, category: category) + logger = Logger(subsystem: subsystem, category: category) self.scope = scope } diff --git a/Packages/InspectCore/Sources/Core/InspectPacketTunnelConfiguration.swift b/Packages/InspectCore/Sources/Core/InspectPacketTunnelConfiguration.swift index 0c8b4df..d8e4e87 100644 --- a/Packages/InspectCore/Sources/Core/InspectPacketTunnelConfiguration.swift +++ b/Packages/InspectCore/Sources/Core/InspectPacketTunnelConfiguration.swift @@ -1,7 +1,7 @@ import Foundation import NetworkExtension -struct InspectPacketTunnelConfiguration: Equatable, Sendable { +struct InspectPacketTunnelConfiguration: Equatable { static let liveMonitor = InspectPacketTunnelConfiguration( ipv4Address: "198.18.0.1", ipv4SubnetMask: "255.255.255.0", diff --git a/Packages/InspectCore/Sources/Core/InspectPacketTunnelRuntime.swift b/Packages/InspectCore/Sources/Core/InspectPacketTunnelRuntime.swift index 9793db2..842443f 100644 --- a/Packages/InspectCore/Sources/Core/InspectPacketTunnelRuntime.swift +++ b/Packages/InspectCore/Sources/Core/InspectPacketTunnelRuntime.swift @@ -27,7 +27,7 @@ public final class InspectPacketTunnelRuntime: @unchecked Sendable { ) { self.provider = provider self.observationFeed = observationFeed - self.logger = InspectRuntimeLogger( + logger = InspectRuntimeLogger( subsystem: loggerSubsystem, category: loggerCategory, scope: "InspectTunnel" @@ -124,7 +124,8 @@ public final class InspectPacketTunnelRuntime: @unchecked Sendable { let shouldEmit = stateQueue.sync { () -> Bool in if let lastObservedAt = lastObservationAtByKey[endpointKey], - now.timeIntervalSince(lastObservedAt) < throttleInterval { + now.timeIntervalSince(lastObservedAt) < throttleInterval + { return false } @@ -256,7 +257,7 @@ public final class InspectPacketTunnelRuntime: @unchecked Sendable { private var tunnelFileDescriptor: Int32? { var buffer = [CChar](repeating: 0, count: Int(IFNAMSIZ)) - for fileDescriptor: Int32 in 0...1024 { + for fileDescriptor: Int32 in 0 ... 1024 { var length = socklen_t(buffer.count) let interfaceName: String if getsockopt( diff --git a/Packages/InspectCore/Sources/Core/InspectSharedContainer.swift b/Packages/InspectCore/Sources/Core/InspectSharedContainer.swift index 00048a3..77d0f55 100644 --- a/Packages/InspectCore/Sources/Core/InspectSharedContainer.swift +++ b/Packages/InspectCore/Sources/Core/InspectSharedContainer.swift @@ -14,7 +14,8 @@ public enum InspectSharedContainer { if let value = ProcessInfo.processInfo.environment["INSPECT_APP_GROUP_IDENTIFIER"]? .trimmingCharacters(in: .whitespacesAndNewlines), - value.isEmpty == false { + value.isEmpty == false + { bootstrapLogger.debug("Using app group from environment: \(value, privacy: .public)") return value } diff --git a/Packages/InspectCore/Sources/Core/InspectSharedLog.swift b/Packages/InspectCore/Sources/Core/InspectSharedLog.swift index 7c79f71..f5ed84d 100644 --- a/Packages/InspectCore/Sources/Core/InspectSharedLog.swift +++ b/Packages/InspectCore/Sources/Core/InspectSharedLog.swift @@ -19,7 +19,8 @@ public enum InspectSharedLog { } if FileManager.default.fileExists(atPath: fileURL.path), - let handle = try? FileHandle(forWritingTo: fileURL) { + let handle = try? FileHandle(forWritingTo: fileURL) + { do { try handle.seekToEnd() try handle.write(contentsOf: data) @@ -34,7 +35,8 @@ public enum InspectSharedLog { public static func readTail(maxBytes: Int = 256 * 1024) -> String? { guard let fileURL = logFileURL(), - let handle = try? FileHandle(forReadingFrom: fileURL) else { + let handle = try? FileHandle(forReadingFrom: fileURL) + else { return nil } defer { @@ -49,7 +51,8 @@ public enum InspectSharedLog { if fileSize > UInt64(maxBytes) { try? handle.seek(toOffset: fileSize - UInt64(maxBytes)) guard let data = try? handle.readToEnd(), - let raw = String(data: data, encoding: .utf8) else { + let raw = String(data: data, encoding: .utf8) + else { return nil } @@ -64,7 +67,8 @@ public enum InspectSharedLog { try? handle.seek(toOffset: 0) guard let data = try? handle.readToEnd(), let text = String(data: data, encoding: .utf8), - text.isEmpty == false else { + text.isEmpty == false + else { return nil } diff --git a/Packages/InspectCore/Sources/Core/InspectTunnelForwardingEngine.swift b/Packages/InspectCore/Sources/Core/InspectTunnelForwardingEngine.swift index a0def15..07eaf8c 100644 --- a/Packages/InspectCore/Sources/Core/InspectTunnelForwardingEngine.swift +++ b/Packages/InspectCore/Sources/Core/InspectTunnelForwardingEngine.swift @@ -62,5 +62,7 @@ public protocol InspectTunnelForwardingEngine: AnyObject, Sendable { } public extension InspectTunnelForwardingEngine { - func drainObservations() -> [TLSFlowObservation] { [] } + func drainObservations() -> [TLSFlowObservation] { + [] + } } diff --git a/Packages/InspectCore/Sources/Core/InspectionError.swift b/Packages/InspectCore/Sources/Core/InspectionError.swift index 3b335f3..8dd7253 100644 --- a/Packages/InspectCore/Sources/Core/InspectionError.swift +++ b/Packages/InspectCore/Sources/Core/InspectionError.swift @@ -8,9 +8,9 @@ public enum InspectionError: LocalizedError, Sendable { public var errorDescription: String? { switch self { - case .invalidURL(let raw): + case let .invalidURL(raw): return "'\(raw)' is not a valid HTTPS URL." - case .unsupportedScheme(let scheme): + case let .unsupportedScheme(scheme): return "Only HTTPS URLs are supported. Received: \(scheme ?? "unknown")." case .missingServerTrust: return "The TLS handshake finished without exposing a server trust chain." diff --git a/Packages/InspectCore/Sources/Core/InspectionSharedPendingReportStore.swift b/Packages/InspectCore/Sources/Core/InspectionSharedPendingReportStore.swift index 47eef54..fe2686e 100644 --- a/Packages/InspectCore/Sources/Core/InspectionSharedPendingReportStore.swift +++ b/Packages/InspectCore/Sources/Core/InspectionSharedPendingReportStore.swift @@ -15,7 +15,8 @@ public enum InspectionSharedPendingReportStore { } guard let token = defaults.string(forKey: defaultsKey), - token.isEmpty == false else { + token.isEmpty == false + else { return nil } diff --git a/Packages/InspectCore/Sources/Core/InspectionSharedReportStore.swift b/Packages/InspectCore/Sources/Core/InspectionSharedReportStore.swift index 0e6103b..9c868ec 100644 --- a/Packages/InspectCore/Sources/Core/InspectionSharedReportStore.swift +++ b/Packages/InspectCore/Sources/Core/InspectionSharedReportStore.swift @@ -13,7 +13,8 @@ public enum InspectionSharedReportStore { public static func consume(token: String) -> TLSInspectionReport? { guard let url = fileURL(for: token), let data = try? Data(contentsOf: url), - let report = try? JSONDecoder().decode(TLSInspectionReport.self, from: data) else { + let report = try? JSONDecoder().decode(TLSInspectionReport.self, from: data) + else { return nil } diff --git a/Packages/InspectCore/Sources/Core/KnownCTLogs.swift b/Packages/InspectCore/Sources/Core/KnownCTLogs.swift index 3f38ba9..f863288 100644 --- a/Packages/InspectCore/Sources/Core/KnownCTLogs.swift +++ b/Packages/InspectCore/Sources/Core/KnownCTLogs.swift @@ -2,68 +2,130 @@ // Source: https://www.gstatic.com/ct/log_list/v3/log_list.json // Do not edit manually. +import Foundation + enum KnownCTLogs { static func name(forLogID logID: [UInt8]) -> String { - let hex = logID.inspectHexString(grouped: false) - if let name = knownLogs[hex] { + if let name = knownLogs[Data(logID)] { return name } let truncated = Array(logID.prefix(8)).inspectHexString(grouped: true) return "Unknown (\(truncated)…)" } - // Log ID (SHA-256 of log's public key) → human-readable name - // Last updated: 2026-03-20 - static let knownLogs: [String: String] = [ + /// Log ID (SHA-256 of log's public key) → human-readable name + /// Last updated: 2026-04-02 + private static let knownLogs: [Data: String] = [ // Google - "0E5794BCF3AEA93E331B2C9907B3F790DF9BC23D713225DD21A925AC61C54E21": "Google 'Argon2026h1'", - "D76D7D10D1A7F577C2C7E95FD700BFF982C9335A65E1D0B3017317C0C8C56977": "Google 'Argon2026h2'", - "D6D58DA9D01753F36A4AA0C7574902AFEBC7DC2CD38CD9F764C80C89191E9F02": "Google 'Argon2027h1'", - "969764BF555897ADF743876837084277E9F03AD5F6A4F3366E46A43F0FCAA9C6": "Google 'Xenon2026h1'", - "D809553B944F7AFFC816196F944F85ABB0F8FC5E8755260F15D12E72BB454B14": "Google 'Xenon2026h2'", - "44C2BD0CE9140E64A5C94A01930A5AA1BB35970E00EE111689682A1C44D7B566": "Google 'Xenon2027h1'", + // 0E5794BCF3AEA93E331B2C9907B3F790DF9BC23D713225DD21A925AC61C54E21 + Data([0x0E, 0x57, 0x94, 0xBC, 0xF3, 0xAE, 0xA9, 0x3E, 0x33, 0x1B, 0x2C, 0x99, 0x07, 0xB3, 0xF7, 0x90, 0xDF, 0x9B, 0xC2, 0x3D, 0x71, 0x32, 0x25, 0xDD, 0x21, 0xA9, 0x25, 0xAC, 0x61, 0xC5, 0x4E, 0x21]): "Google 'Argon2026h1'", + // D76D7D10D1A7F577C2C7E95FD700BFF982C9335A65E1D0B3017317C0C8C56977 + Data([0xD7, 0x6D, 0x7D, 0x10, 0xD1, 0xA7, 0xF5, 0x77, 0xC2, 0xC7, 0xE9, 0x5F, 0xD7, 0x00, 0xBF, 0xF9, 0x82, 0xC9, 0x33, 0x5A, 0x65, 0xE1, 0xD0, 0xB3, 0x01, 0x73, 0x17, 0xC0, 0xC8, 0xC5, 0x69, 0x77]): "Google 'Argon2026h2'", + // D6D58DA9D01753F36A4AA0C7574902AFEBC7DC2CD38CD9F764C80C89191E9F02 + Data([0xD6, 0xD5, 0x8D, 0xA9, 0xD0, 0x17, 0x53, 0xF3, 0x6A, 0x4A, 0xA0, 0xC7, 0x57, 0x49, 0x02, 0xAF, 0xEB, 0xC7, 0xDC, 0x2C, 0xD3, 0x8C, 0xD9, 0xF7, 0x64, 0xC8, 0x0C, 0x89, 0x19, 0x1E, 0x9F, 0x02]): "Google 'Argon2027h1'", + // 969764BF555897ADF743876837084277E9F03AD5F6A4F3366E46A43F0FCAA9C6 + Data([0x96, 0x97, 0x64, 0xBF, 0x55, 0x58, 0x97, 0xAD, 0xF7, 0x43, 0x87, 0x68, 0x37, 0x08, 0x42, 0x77, 0xE9, 0xF0, 0x3A, 0xD5, 0xF6, 0xA4, 0xF3, 0x36, 0x6E, 0x46, 0xA4, 0x3F, 0x0F, 0xCA, 0xA9, 0xC6]): "Google 'Xenon2026h1'", + // D809553B944F7AFFC816196F944F85ABB0F8FC5E8755260F15D12E72BB454B14 + Data([0xD8, 0x09, 0x55, 0x3B, 0x94, 0x4F, 0x7A, 0xFF, 0xC8, 0x16, 0x19, 0x6F, 0x94, 0x4F, 0x85, 0xAB, 0xB0, 0xF8, 0xFC, 0x5E, 0x87, 0x55, 0x26, 0x0F, 0x15, 0xD1, 0x2E, 0x72, 0xBB, 0x45, 0x4B, 0x14]): "Google 'Xenon2026h2'", + // 44C2BD0CE9140E64A5C94A01930A5AA1BB35970E00EE111689682A1C44D7B566 + Data([0x44, 0xC2, 0xBD, 0x0C, 0xE9, 0x14, 0x0E, 0x64, 0xA5, 0xC9, 0x4A, 0x01, 0x93, 0x0A, 0x5A, 0xA1, 0xBB, 0x35, 0x97, 0x0E, 0x00, 0xEE, 0x11, 0x16, 0x89, 0x68, 0x2A, 0x1C, 0x44, 0xD7, 0xB5, 0x66]): "Google 'Xenon2027h1'", // Cloudflare - "CB38F715897C84A1445F5BC1DDFBC96EF29A59CD470A690585B0CB14C31458E7": "Cloudflare 'Nimbus2026'", - "4C63DC98E59C1DAB88F61E8A3DDEAE8FAB44A3377B5F9B94C3FBA19CFCC1BE26": "Cloudflare 'Nimbus2027'", + // CB38F715897C84A1445F5BC1DDFBC96EF29A59CD470A690585B0CB14C31458E7 + Data([0xCB, 0x38, 0xF7, 0x15, 0x89, 0x7C, 0x84, 0xA1, 0x44, 0x5F, 0x5B, 0xC1, 0xDD, 0xFB, 0xC9, 0x6E, 0xF2, 0x9A, 0x59, 0xCD, 0x47, 0x0A, 0x69, 0x05, 0x85, 0xB0, 0xCB, 0x14, 0xC3, 0x14, 0x58, 0xE7]): "Cloudflare 'Nimbus2026'", + // 4C63DC98E59C1DAB88F61E8A3DDEAE8FAB44A3377B5F9B94C3FBA19CFCC1BE26 + Data([0x4C, 0x63, 0xDC, 0x98, 0xE5, 0x9C, 0x1D, 0xAB, 0x88, 0xF6, 0x1E, 0x8A, 0x3D, 0xDE, 0xAE, 0x8F, 0xAB, 0x44, 0xA3, 0x37, 0x7B, 0x5F, 0x9B, 0x94, 0xC3, 0xFB, 0xA1, 0x9C, 0xFC, 0xC1, 0xBE, 0x26]): "Cloudflare 'Nimbus2027'", // DigiCert - "6411C46CA412ECA7891CA2022E00BCAB4F2807D41E3527ABEAFED503C97DCDF0": "DigiCert 'Wyvern2026h1'", - "C2317E574519A345EE7F38DEB29041EBC7C2215A22BF7FD5B5AD769AD90E52CD": "DigiCert 'Wyvern2026h2'", - "001A5D1A1C2D9375B6485578F82F71A1AE6EEF397D297C8AE3157BCADEE1A01E": "DigiCert 'Wyvern2027h1'", - "37AA07CC216F2E6D919C709D24D8F731B00F2B147C621CC091A5FA1A84D816DD": "DigiCert 'Wyvern2027h2'", - "499C9B69DE1D7CECFC36DECD8764A6B85BAF0A878019D15552FBE9EB29DDF8C3": "DigiCert 'Sphinx2026h1'", - "944E4387FAECC1EF81F3192426A8186501C7D35F3802013F72677D55372E19D8": "DigiCert 'Sphinx2026h2'", - "46A23967C60DB64687C66F3DF999947693A6A611208457D555E7E3D0A1D9B646": "DigiCert 'sphinx2027h1'", - "1FB0F8A92D8ADDA121776C05E2AA2E15BACBC62B65393695576AAAB52E11D11D": "DigiCert 'sphinx2027h2'", + // 6411C46CA412ECA7891CA2022E00BCAB4F2807D41E3527ABEAFED503C97DCDF0 + Data([0x64, 0x11, 0xC4, 0x6C, 0xA4, 0x12, 0xEC, 0xA7, 0x89, 0x1C, 0xA2, 0x02, 0x2E, 0x00, 0xBC, 0xAB, 0x4F, 0x28, 0x07, 0xD4, 0x1E, 0x35, 0x27, 0xAB, 0xEA, 0xFE, 0xD5, 0x03, 0xC9, 0x7D, 0xCD, 0xF0]): "DigiCert 'Wyvern2026h1'", + // C2317E574519A345EE7F38DEB29041EBC7C2215A22BF7FD5B5AD769AD90E52CD + Data([0xC2, 0x31, 0x7E, 0x57, 0x45, 0x19, 0xA3, 0x45, 0xEE, 0x7F, 0x38, 0xDE, 0xB2, 0x90, 0x41, 0xEB, 0xC7, 0xC2, 0x21, 0x5A, 0x22, 0xBF, 0x7F, 0xD5, 0xB5, 0xAD, 0x76, 0x9A, 0xD9, 0x0E, 0x52, 0xCD]): "DigiCert 'Wyvern2026h2'", + // 001A5D1A1C2D9375B6485578F82F71A1AE6EEF397D297C8AE3157BCADEE1A01E + Data([0x00, 0x1A, 0x5D, 0x1A, 0x1C, 0x2D, 0x93, 0x75, 0xB6, 0x48, 0x55, 0x78, 0xF8, 0x2F, 0x71, 0xA1, 0xAE, 0x6E, 0xEF, 0x39, 0x7D, 0x29, 0x7C, 0x8A, 0xE3, 0x15, 0x7B, 0xCA, 0xDE, 0xE1, 0xA0, 0x1E]): "DigiCert 'Wyvern2027h1'", + // 37AA07CC216F2E6D919C709D24D8F731B00F2B147C621CC091A5FA1A84D816DD + Data([0x37, 0xAA, 0x07, 0xCC, 0x21, 0x6F, 0x2E, 0x6D, 0x91, 0x9C, 0x70, 0x9D, 0x24, 0xD8, 0xF7, 0x31, 0xB0, 0x0F, 0x2B, 0x14, 0x7C, 0x62, 0x1C, 0xC0, 0x91, 0xA5, 0xFA, 0x1A, 0x84, 0xD8, 0x16, 0xDD]): "DigiCert 'Wyvern2027h2'", + // 499C9B69DE1D7CECFC36DECD8764A6B85BAF0A878019D15552FBE9EB29DDF8C3 + Data([0x49, 0x9C, 0x9B, 0x69, 0xDE, 0x1D, 0x7C, 0xEC, 0xFC, 0x36, 0xDE, 0xCD, 0x87, 0x64, 0xA6, 0xB8, 0x5B, 0xAF, 0x0A, 0x87, 0x80, 0x19, 0xD1, 0x55, 0x52, 0xFB, 0xE9, 0xEB, 0x29, 0xDD, 0xF8, 0xC3]): "DigiCert 'Sphinx2026h1'", + // 944E4387FAECC1EF81F3192426A8186501C7D35F3802013F72677D55372E19D8 + Data([0x94, 0x4E, 0x43, 0x87, 0xFA, 0xEC, 0xC1, 0xEF, 0x81, 0xF3, 0x19, 0x24, 0x26, 0xA8, 0x18, 0x65, 0x01, 0xC7, 0xD3, 0x5F, 0x38, 0x02, 0x01, 0x3F, 0x72, 0x67, 0x7D, 0x55, 0x37, 0x2E, 0x19, 0xD8]): "DigiCert 'Sphinx2026h2'", + // 46A23967C60DB64687C66F3DF999947693A6A611208457D555E7E3D0A1D9B646 + Data([0x46, 0xA2, 0x39, 0x67, 0xC6, 0x0D, 0xB6, 0x46, 0x87, 0xC6, 0x6F, 0x3D, 0xF9, 0x99, 0x94, 0x76, 0x93, 0xA6, 0xA6, 0x11, 0x20, 0x84, 0x57, 0xD5, 0x55, 0xE7, 0xE3, 0xD0, 0xA1, 0xD9, 0xB6, 0x46]): "DigiCert 'sphinx2027h1'", + // 1FB0F8A92D8ADDA121776C05E2AA2E15BACBC62B65393695576AAAB52E11D11D + Data([0x1F, 0xB0, 0xF8, 0xA9, 0x2D, 0x8A, 0xDD, 0xA1, 0x21, 0x77, 0x6C, 0x05, 0xE2, 0xAA, 0x2E, 0x15, 0xBA, 0xCB, 0xC6, 0x2B, 0x65, 0x39, 0x36, 0x95, 0x57, 0x6A, 0xAA, 0xB5, 0x2E, 0x11, 0xD1, 0x1D]): "DigiCert 'sphinx2027h2'", // Sectigo - "252F94C22B29E96E9F411A72072B695C5B52FF97A90D2540BBFCDC51EC4DEE0B": "Sectigo 'Mammoth2026h1'", - "94B1C18AB0D057C47BE0AC040E1F2CBC8DC375727BC951F20A526126863BA73C": "Sectigo 'Mammoth2026h2'", - "566CD5A376BE83DFE342B675C49C232498A769BAC382CBAB49A3877D9AB32D01": "Sectigo 'Sabre2026h1'", - "1F56D1AB94704A41DD3FEAFDF4699355302C1431BFE61346089FFFAE795DCC2F": "Sectigo 'Sabre2026h2'", - "D16EA9A568077E6635A03F37A5DDBC03A53C411214D48818F5E931B323CB9504": "Sectigo 'Elephant2026h1'", - "AF67883B57B04EDD8FA6D97EF62EA8EB810AC77160F0245E55D60C2FE785873A": "Sectigo 'Elephant2026h2'", - "604C9AAF7A7F775F01D406FC920DC899EB0B1C7DF8C9521BFAFA17773B978BC9": "Sectigo 'Elephant2027h1'", - "A2490CDCDB8E33A400321760D6D4D51A2036191EA77D968BE26A8A00F6FFFFF7": "Sectigo 'Elephant2027h2'", - "16832DABF0A9250F0FF03AA545FFC8BFC823D0874BF6042927F8E71F3313F5FA": "Sectigo 'Tiger2026h1'", - "C8A3C47FC7B3ADB9356B013F6A7A126DE33A4E43A5C646F997AD3975991DCF9A": "Sectigo 'Tiger2026h2'", - "1C9F682CE9FAF0456950F81B968A87DDDB3210D84CE6C8B2E382524AC4CF599F": "Sectigo 'Tiger2027h1'", - "03802AC262F6E05E03F8BC6F7B9851324FD76A3DF5B7595175E222FB8E9BD5F6": "Sectigo 'Tiger2027h2'", + // D16EA9A568077E6635A03F37A5DDBC03A53C411214D48818F5E931B323CB9504 + Data([0xD1, 0x6E, 0xA9, 0xA5, 0x68, 0x07, 0x7E, 0x66, 0x35, 0xA0, 0x3F, 0x37, 0xA5, 0xDD, 0xBC, 0x03, 0xA5, 0x3C, 0x41, 0x12, 0x14, 0xD4, 0x88, 0x18, 0xF5, 0xE9, 0x31, 0xB3, 0x23, 0xCB, 0x95, 0x04]): "Sectigo 'Elephant2026h1'", + // AF67883B57B04EDD8FA6D97EF62EA8EB810AC77160F0245E55D60C2FE785873A + Data([0xAF, 0x67, 0x88, 0x3B, 0x57, 0xB0, 0x4E, 0xDD, 0x8F, 0xA6, 0xD9, 0x7E, 0xF6, 0x2E, 0xA8, 0xEB, 0x81, 0x0A, 0xC7, 0x71, 0x60, 0xF0, 0x24, 0x5E, 0x55, 0xD6, 0x0C, 0x2F, 0xE7, 0x85, 0x87, 0x3A]): "Sectigo 'Elephant2026h2'", + // 604C9AAF7A7F775F01D406FC920DC899EB0B1C7DF8C9521BFAFA17773B978BC9 + Data([0x60, 0x4C, 0x9A, 0xAF, 0x7A, 0x7F, 0x77, 0x5F, 0x01, 0xD4, 0x06, 0xFC, 0x92, 0x0D, 0xC8, 0x99, 0xEB, 0x0B, 0x1C, 0x7D, 0xF8, 0xC9, 0x52, 0x1B, 0xFA, 0xFA, 0x17, 0x77, 0x3B, 0x97, 0x8B, 0xC9]): "Sectigo 'Elephant2027h1'", + // A2490CDCDB8E33A400321760D6D4D51A2036191EA77D968BE26A8A00F6FFFFF7 + Data([0xA2, 0x49, 0x0C, 0xDC, 0xDB, 0x8E, 0x33, 0xA4, 0x00, 0x32, 0x17, 0x60, 0xD6, 0xD4, 0xD5, 0x1A, 0x20, 0x36, 0x19, 0x1E, 0xA7, 0x7D, 0x96, 0x8B, 0xE2, 0x6A, 0x8A, 0x00, 0xF6, 0xFF, 0xFF, 0xF7]): "Sectigo 'Elephant2027h2'", + // 16832DABF0A9250F0FF03AA545FFC8BFC823D0874BF6042927F8E71F3313F5FA + Data([0x16, 0x83, 0x2D, 0xAB, 0xF0, 0xA9, 0x25, 0x0F, 0x0F, 0xF0, 0x3A, 0xA5, 0x45, 0xFF, 0xC8, 0xBF, 0xC8, 0x23, 0xD0, 0x87, 0x4B, 0xF6, 0x04, 0x29, 0x27, 0xF8, 0xE7, 0x1F, 0x33, 0x13, 0xF5, 0xFA]): "Sectigo 'Tiger2026h1'", + // C8A3C47FC7B3ADB9356B013F6A7A126DE33A4E43A5C646F997AD3975991DCF9A + Data([0xC8, 0xA3, 0xC4, 0x7F, 0xC7, 0xB3, 0xAD, 0xB9, 0x35, 0x6B, 0x01, 0x3F, 0x6A, 0x7A, 0x12, 0x6D, 0xE3, 0x3A, 0x4E, 0x43, 0xA5, 0xC6, 0x46, 0xF9, 0x97, 0xAD, 0x39, 0x75, 0x99, 0x1D, 0xCF, 0x9A]): "Sectigo 'Tiger2026h2'", + // 1C9F682CE9FAF0456950F81B968A87DDDB3210D84CE6C8B2E382524AC4CF599F + Data([0x1C, 0x9F, 0x68, 0x2C, 0xE9, 0xFA, 0xF0, 0x45, 0x69, 0x50, 0xF8, 0x1B, 0x96, 0x8A, 0x87, 0xDD, 0xDB, 0x32, 0x10, 0xD8, 0x4C, 0xE6, 0xC8, 0xB2, 0xE3, 0x82, 0x52, 0x4A, 0xC4, 0xCF, 0x59, 0x9F]): "Sectigo 'Tiger2027h1'", + // 03802AC262F6E05E03F8BC6F7B9851324FD76A3DF5B7595175E222FB8E9BD5F6 + Data([0x03, 0x80, 0x2A, 0xC2, 0x62, 0xF6, 0xE0, 0x5E, 0x03, 0xF8, 0xBC, 0x6F, 0x7B, 0x98, 0x51, 0x32, 0x4F, 0xD7, 0x6A, 0x3D, 0xF5, 0xB7, 0x59, 0x51, 0x75, 0xE2, 0x22, 0xFB, 0x8E, 0x9B, 0xD5, 0xF6]): "Sectigo 'Tiger2027h2'", // Let's Encrypt - "1986D4C728AA6FFEBA036F782A4D0191AACE2D72310FAECE5D70412D254CC7D4": "Let's Encrypt 'Oak2026h1'", - "ACAB30706CEBEC8431F413D2F4915F111E422443B1F2A68C4F3C2B3BA71E02C3": "Let's Encrypt 'Oak2026h2'", + // A5C978925D57461782870DD889660B5C55648B7D0040F2EC076851D1886919F7 + Data([0xA5, 0xC9, 0x78, 0x92, 0x5D, 0x57, 0x46, 0x17, 0x82, 0x87, 0x0D, 0xD8, 0x89, 0x66, 0x0B, 0x5C, 0x55, 0x64, 0x8B, 0x7D, 0x00, 0x40, 0xF2, 0xEC, 0x07, 0x68, 0x51, 0xD1, 0x88, 0x69, 0x19, 0xF7]): "Let's Encrypt 'Sycamore2026h1'", + // 6CFE501943A85EA916BC52D133E4DCC91EF1411C7D258420D173809E1818EB3A + Data([0x6C, 0xFE, 0x50, 0x19, 0x43, 0xA8, 0x5E, 0xA9, 0x16, 0xBC, 0x52, 0xD1, 0x33, 0xE4, 0xDC, 0xC9, 0x1E, 0xF1, 0x41, 0x1C, 0x7D, 0x25, 0x84, 0x20, 0xD1, 0x73, 0x80, 0x9E, 0x18, 0x18, 0xEB, 0x3A]): "Let's Encrypt 'Sycamore2026h2'", + // 8ECA470BACDE6AF3A206B0A47A84B746FE1FC6BF953E25E69B4EE40248F3C6E8 + Data([0x8E, 0xCA, 0x47, 0x0B, 0xAC, 0xDE, 0x6A, 0xF3, 0xA2, 0x06, 0xB0, 0xA4, 0x7A, 0x84, 0xB7, 0x46, 0xFE, 0x1F, 0xC6, 0xBF, 0x95, 0x3E, 0x25, 0xE6, 0x9B, 0x4E, 0xE4, 0x02, 0x48, 0xF3, 0xC6, 0xE8]): "Let's Encrypt 'Sycamore2027h1'", + // E5E36247D92EF4ADA38583B53591DB729FC2F00AE4B6745174D3DDFC6AA25388 + Data([0xE5, 0xE3, 0x62, 0x47, 0xD9, 0x2E, 0xF4, 0xAD, 0xA3, 0x85, 0x83, 0xB5, 0x35, 0x91, 0xDB, 0x72, 0x9F, 0xC2, 0xF0, 0x0A, 0xE4, 0xB6, 0x74, 0x51, 0x74, 0xD3, 0xDD, 0xFC, 0x6A, 0xA2, 0x53, 0x88]): "Let's Encrypt 'Sycamore2027h2'", + // E3238DF28DA288E0AAE0ACF0FA90C985F0B6BFF5D2A527B001FC1C4458C4B6E8 + Data([0xE3, 0x23, 0x8D, 0xF2, 0x8D, 0xA2, 0x88, 0xE0, 0xAA, 0xE0, 0xAC, 0xF0, 0xFA, 0x90, 0xC9, 0x85, 0xF0, 0xB6, 0xBF, 0xF5, 0xD2, 0xA5, 0x27, 0xB0, 0x01, 0xFC, 0x1C, 0x44, 0x58, 0xC4, 0xB6, 0xE8]): "Let's Encrypt 'Willow2026h1'", + // A826CBE30AC6351246533FE065F14F19D96E190813C41DD96D7900B3123C5527 + Data([0xA8, 0x26, 0xCB, 0xE3, 0x0A, 0xC6, 0x35, 0x12, 0x46, 0x53, 0x3F, 0xE0, 0x65, 0xF1, 0x4F, 0x19, 0xD9, 0x6E, 0x19, 0x08, 0x13, 0xC4, 0x1D, 0xD9, 0x6D, 0x79, 0x00, 0xB3, 0x12, 0x3C, 0x55, 0x27]): "Let's Encrypt 'Willow2026h2'", + // A2810018734E176E1D47E09540F381BA546697CD63A84350716EB8094EDAF10D + Data([0xA2, 0x81, 0x00, 0x18, 0x73, 0x4E, 0x17, 0x6E, 0x1D, 0x47, 0xE0, 0x95, 0x40, 0xF3, 0x81, 0xBA, 0x54, 0x66, 0x97, 0xCD, 0x63, 0xA8, 0x43, 0x50, 0x71, 0x6E, 0xB8, 0x09, 0x4E, 0xDA, 0xF1, 0x0D]): "Let's Encrypt 'Willow2027h1'", + // A695A2AD926D6F996E8EFC49014257D8BBF046A7D62589B88DC2D7876C78E52F + Data([0xA6, 0x95, 0xA2, 0xAD, 0x92, 0x6D, 0x6F, 0x99, 0x6E, 0x8E, 0xFC, 0x49, 0x01, 0x42, 0x57, 0xD8, 0xBB, 0xF0, 0x46, 0xA7, 0xD6, 0x25, 0x89, 0xB8, 0x8D, 0xC2, 0xD7, 0x87, 0x6C, 0x78, 0xE5, 0x2F]): "Let's Encrypt 'Willow2027h2'", // TrustAsia - "74DB9D58F7D47E9DFD787A162A991C18CF698DA7C729918C9A18B0450DBA44BC": "TrustAsia 'log2026a'", - "25B7EFDEA1130193ED93079770AA322A26620DE35AC8AA7C75197DE0B1A9E065": "TrustAsia 'log2026b'", - "EDDAEB815C63213449B47BE5077905ABD0D93147C27AC5146B3BC58E43E9B6C7": "TrustAsia 'HETU2027'", + // 74DB9D58F7D47E9DFD787A162A991C18CF698DA7C729918C9A18B0450DBA44BC + Data([0x74, 0xDB, 0x9D, 0x58, 0xF7, 0xD4, 0x7E, 0x9D, 0xFD, 0x78, 0x7A, 0x16, 0x2A, 0x99, 0x1C, 0x18, 0xCF, 0x69, 0x8D, 0xA7, 0xC7, 0x29, 0x91, 0x8C, 0x9A, 0x18, 0xB0, 0x45, 0x0D, 0xBA, 0x44, 0xBC]): "TrustAsia 'log2026a'", + // 25B7EFDEA1130193ED93079770AA322A26620DE35AC8AA7C75197DE0B1A9E065 + Data([0x25, 0xB7, 0xEF, 0xDE, 0xA1, 0x13, 0x01, 0x93, 0xED, 0x93, 0x07, 0x97, 0x70, 0xAA, 0x32, 0x2A, 0x26, 0x62, 0x0D, 0xE3, 0x5A, 0xC8, 0xAA, 0x7C, 0x75, 0x19, 0x7D, 0xE0, 0xB1, 0xA9, 0xE0, 0x65]): "TrustAsia 'log2026b'", + // EDDAEB815C63213449B47BE5077905ABD0D93147C27AC5146B3BC58E43E9B6C7 + Data([0xED, 0xDA, 0xEB, 0x81, 0x5C, 0x63, 0x21, 0x34, 0x49, 0xB4, 0x7B, 0xE5, 0x07, 0x79, 0x05, 0xAB, 0xD0, 0xD9, 0x31, 0x47, 0xC2, 0x7A, 0xC5, 0x14, 0x6B, 0x3B, 0xC5, 0x8E, 0x43, 0xE9, 0xB6, 0xC7]): "TrustAsia 'HETU2027'", + // 573448CC6E1D2C0DC94B69F287D1EFE483C7A25C50C5320BBB3ADEA76F6EB041 + Data([0x57, 0x34, 0x48, 0xCC, 0x6E, 0x1D, 0x2C, 0x0D, 0xC9, 0x4B, 0x69, 0xF2, 0x87, 0xD1, 0xEF, 0xE4, 0x83, 0xC7, 0xA2, 0x5C, 0x50, 0xC5, 0x32, 0x0B, 0xBB, 0x3A, 0xDE, 0xA7, 0x6F, 0x6E, 0xB0, 0x41]): "TrustAsia Luoshu2027", // Geomys - "2ED6A44DEB8F0C864667769C4EDD041F84236755FA3AACA634D0935DFCD59A70": "Bogus placeholder log to unbreak misbehaving CT libraries", + // 717E95F3C2388A6DB1E384493D31E15AA96208762D4200E0050CD067B5A661E2 + Data([0x71, 0x7E, 0x95, 0xF3, 0xC2, 0x38, 0x8A, 0x6D, 0xB1, 0xE3, 0x84, 0x49, 0x3D, 0x31, 0xE1, 0x5A, 0xA9, 0x62, 0x08, 0x76, 0x2D, 0x42, 0x00, 0xE0, 0x05, 0x0C, 0xD0, 0x67, 0xB5, 0xA6, 0x61, 0xE2]): "Geomys 'Tuscolo2026h1'", + // 46AF863D3B3EE59FA577DEA8245D36B0D9ED22A223F4617741229452EE95505F + Data([0x46, 0xAF, 0x86, 0x3D, 0x3B, 0x3E, 0xE5, 0x9F, 0xA5, 0x77, 0xDE, 0xA8, 0x24, 0x5D, 0x36, 0xB0, 0xD9, 0xED, 0x22, 0xA2, 0x23, 0xF4, 0x61, 0x77, 0x41, 0x22, 0x94, 0x52, 0xEE, 0x95, 0x50, 0x5F]): "Geomys 'Tuscolo2026h2'", + // 596E6C338694B25972A256C8A0E8DD904A76E8083DDA873B01083828143CEE59 + Data([0x59, 0x6E, 0x6C, 0x33, 0x86, 0x94, 0xB2, 0x59, 0x72, 0xA2, 0x56, 0xC8, 0xA0, 0xE8, 0xDD, 0x90, 0x4A, 0x76, 0xE8, 0x08, 0x3D, 0xDA, 0x87, 0x3B, 0x01, 0x08, 0x38, 0x28, 0x14, 0x3C, 0xEE, 0x59]): "Geomys 'Tuscolo2027h1'", + // D5DE55EEBA08B60C9FFC18C513BE6A60BA004606BC595B96BB44F62CC57D39FA + Data([0xD5, 0xDE, 0x55, 0xEE, 0xBA, 0x08, 0xB6, 0x0C, 0x9F, 0xFC, 0x18, 0xC5, 0x13, 0xBE, 0x6A, 0x60, 0xBA, 0x00, 0x46, 0x06, 0xBC, 0x59, 0x5B, 0x96, 0xBB, 0x44, 0xF6, 0x2C, 0xC5, 0x7D, 0x39, 0xFA]): "Geomys 'Tuscolo2027h2'", // IPng Networks - "D2FC652FA5F9B738B83755FA5EB15F0B45253F4E8FA3B9B64FD4DE5662D18708": "Bogus RFC6962 log to avoid breaking misbehaving CT libraries", + // 7F3D37E7F8923D8E7165BEB0D3EABEE72A22BE46C0CB84C416D4E4B98264CBC2 + Data([0x7F, 0x3D, 0x37, 0xE7, 0xF8, 0x92, 0x3D, 0x8E, 0x71, 0x65, 0xBE, 0xB0, 0xD3, 0xEA, 0xBE, 0xE7, 0x2A, 0x22, 0xBE, 0x46, 0xC0, 0xCB, 0x84, 0xC4, 0x16, 0xD4, 0xE4, 0xB9, 0x82, 0x64, 0xCB, 0xC2]): "IPng Networks 'Halloumi2026h1'", + // 26E3646E58692123BC343F4724359B3792CD245A88D815D39333FD9918AB4723 + Data([0x26, 0xE3, 0x64, 0x6E, 0x58, 0x69, 0x21, 0x23, 0xBC, 0x34, 0x3F, 0x47, 0x24, 0x35, 0x9B, 0x37, 0x92, 0xCD, 0x24, 0x5A, 0x88, 0xD8, 0x15, 0xD3, 0x93, 0x33, 0xFD, 0x99, 0x18, 0xAB, 0x47, 0x23]): "IPng Networks 'Halloumi2026h2a'", + // 44E822FC2BAB0E92EED0E9FAD69664602776D01760E0890509C923A1B03FC37F + Data([0x44, 0xE8, 0x22, 0xFC, 0x2B, 0xAB, 0x0E, 0x92, 0xEE, 0xD0, 0xE9, 0xFA, 0xD6, 0x96, 0x64, 0x60, 0x27, 0x76, 0xD0, 0x17, 0x60, 0xE0, 0x89, 0x05, 0x09, 0xC9, 0x23, 0xA1, 0xB0, 0x3F, 0xC3, 0x7F]): "IPng Networks 'Halloumi2027h1'", + // 09157F632D46C7F76D95265493BC0F00B395AC5DB3A2B26BFB043DBA4AC63893 + Data([0x09, 0x15, 0x7F, 0x63, 0x2D, 0x46, 0xC7, 0xF7, 0x6D, 0x95, 0x26, 0x54, 0x93, 0xBC, 0x0F, 0x00, 0xB3, 0x95, 0xAC, 0x5D, 0xB3, 0xA2, 0xB2, 0x6B, 0xFB, 0x04, 0x3D, 0xBA, 0x4A, 0xC6, 0x38, 0x93]): "IPng Networks 'Halloumi2027h2'", + // 1A8B9D694A5798C899A0CA88BDF48FC0B45660CCC3600D1F71F469FFC7D1ACA3 + Data([0x1A, 0x8B, 0x9D, 0x69, 0x4A, 0x57, 0x98, 0xC8, 0x99, 0xA0, 0xCA, 0x88, 0xBD, 0xF4, 0x8F, 0xC0, 0xB4, 0x56, 0x60, 0xCC, 0xC3, 0x60, 0x0D, 0x1F, 0x71, 0xF4, 0x69, 0xFF, 0xC7, 0xD1, 0xAC, 0xA3]): "IPng Networks 'Gouda2026h1'", + // 1A8B9D6B0FFEBF81B47939C6D2310A86D6D102D4F046E2182C9DE35F5E2625EF + Data([0x1A, 0x8B, 0x9D, 0x6B, 0x0F, 0xFE, 0xBF, 0x81, 0xB4, 0x79, 0x39, 0xC6, 0xD2, 0x31, 0x0A, 0x86, 0xD6, 0xD1, 0x02, 0xD4, 0xF0, 0x46, 0xE2, 0x18, 0x2C, 0x9D, 0xE3, 0x5F, 0x5E, 0x26, 0x25, 0xEF]): "IPng Networks 'Gouda2026h2'", + // 1A8B9D6B8DD791D1CD0549EDB60355D606B64FAD30DB71FE788F0FC7C8FBC4B1 + Data([0x1A, 0x8B, 0x9D, 0x6B, 0x8D, 0xD7, 0x91, 0xD1, 0xCD, 0x05, 0x49, 0xED, 0xB6, 0x03, 0x55, 0xD6, 0x06, 0xB6, 0x4F, 0xAD, 0x30, 0xDB, 0x71, 0xFE, 0x78, 0x8F, 0x0F, 0xC7, 0xC8, 0xFB, 0xC4, 0xB1]): "IPng Networks 'Gouda2027h1'", + // 1A8B9D695362D86492A7B9E223606E34ECE9E310BA34FB9305785D29CE5757EB + Data([0x1A, 0x8B, 0x9D, 0x69, 0x53, 0x62, 0xD8, 0x64, 0x92, 0xA7, 0xB9, 0xE2, 0x23, 0x60, 0x6E, 0x34, 0xEC, 0xE9, 0xE3, 0x10, 0xBA, 0x34, 0xFB, 0x93, 0x05, 0x78, 0x5D, 0x29, 0xCE, 0x57, 0x57, 0xEB]): "IPng Networks 'Gouda2027h2'", ] } diff --git a/Packages/InspectCore/Sources/Core/PacketSummary.swift b/Packages/InspectCore/Sources/Core/PacketSummary.swift index 63d5853..dcda34c 100644 --- a/Packages/InspectCore/Sources/Core/PacketSummary.swift +++ b/Packages/InspectCore/Sources/Core/PacketSummary.swift @@ -42,7 +42,7 @@ struct PacketSummary { String(bytes[16]), String(bytes[17]), String(bytes[18]), - String(bytes[19]) + String(bytes[19]), ].joined(separator: ".") transport = Self.transport(for: protocolNumber) @@ -64,7 +64,7 @@ struct PacketSummary { } let protocolNumber = bytes[6] - let destinationSlice = bytes[24..<40] + let destinationSlice = bytes[24 ..< 40] transport = Self.transport(for: protocolNumber) remoteHost = Self.ipv6String(from: destinationSlice) @@ -96,8 +96,9 @@ struct PacketSummary { } private static func destinationPort(from bytes: [UInt8], protocolNumber: UInt8, transportOffset: Int) -> Int? { - guard (protocolNumber == IPProtocol.tcp || protocolNumber == IPProtocol.udp), - bytes.count >= transportOffset + 4 else { + guard protocolNumber == IPProtocol.tcp || protocolNumber == IPProtocol.udp, + bytes.count >= transportOffset + 4 + else { return nil } @@ -108,7 +109,8 @@ struct PacketSummary { private static func serverName(from bytes: [UInt8], protocolNumber: UInt8, transportOffset: Int) -> String? { guard protocolNumber == IPProtocol.tcp, - bytes.count >= transportOffset + 20 else { + bytes.count >= transportOffset + 20 + else { return nil } diff --git a/Packages/InspectCore/Sources/Core/PassiveTLSInspectionReportBuilder.swift b/Packages/InspectCore/Sources/Core/PassiveTLSInspectionReportBuilder.swift index 414e051..c170d36 100644 --- a/Packages/InspectCore/Sources/Core/PassiveTLSInspectionReportBuilder.swift +++ b/Packages/InspectCore/Sources/Core/PassiveTLSInspectionReportBuilder.swift @@ -11,7 +11,8 @@ public struct PassiveTLSInspectionReportBuilder: Sendable { guard let certificateChainDER = observation.capturedCertificateChainDER, certificateChainDER.isEmpty == false, let host = observation.passiveInspectionHost, - let requestedURL = makeRequestedURL(host: host, port: observation.remotePort) else { + let requestedURL = makeRequestedURL(host: host, port: observation.remotePort) + else { return nil } diff --git a/Packages/InspectCore/Sources/Core/SCTDecoder.swift b/Packages/InspectCore/Sources/Core/SCTDecoder.swift index 0991acc..42d70a1 100644 --- a/Packages/InspectCore/Sources/Core/SCTDecoder.swift +++ b/Packages/InspectCore/Sources/Core/SCTDecoder.swift @@ -13,7 +13,8 @@ enum SCTDecoder { static func decode(from ext: X509.Certificate.Extension) -> [LabeledValue] { guard ext.oid == oid, let outerNode = try? DER.parse(ext.value), - let octetString = try? ASN1OctetString(derEncoded: outerNode) else { + let octetString = try? ASN1OctetString(derEncoded: outerNode) + else { return [] } return parseList(Array(octetString.bytes)) @@ -51,8 +52,8 @@ enum SCTDecoder { return entries } - private static func parseSingle(_ reader: inout ByteReader, end: Int) -> ParsedSCT? { - guard let _ = try? reader.readUInt8() else { return nil } + private static func parseSingle(_ reader: inout ByteReader, end _: Int) -> ParsedSCT? { + guard (try? reader.readUInt8()) != nil else { return nil } guard let logIDBytes = try? reader.readBytes(32) else { return nil } let logName = KnownCTLogs.name(forLogID: logIDBytes) @@ -61,7 +62,7 @@ enum SCTDecoder { let timestamp = Date(timeIntervalSince1970: Double(timestampMs) / 1000.0).inspectDisplayString guard let extensionsLength = try? reader.readUInt16(), - let _ = try? reader.skip(Int(extensionsLength)) else { return nil } + (try? reader.skip(Int(extensionsLength))) != nil else { return nil } guard let hashAlgo = try? reader.readUInt8(), let sigAlgo = try? reader.readUInt8() else { return nil } diff --git a/Packages/InspectCore/Sources/Core/SecurityAnalyzer.swift b/Packages/InspectCore/Sources/Core/SecurityAnalyzer.swift index bf33383..e6cb655 100644 --- a/Packages/InspectCore/Sources/Core/SecurityAnalyzer.swift +++ b/Packages/InspectCore/Sources/Core/SecurityAnalyzer.swift @@ -10,10 +10,19 @@ public struct SecurityAnalyzer: Sendable { severity: .critical, title: "No Certificate Chain", message: "The handshake completed without a parseable certificate chain." - ) + ), ]) } + var findings: [SecurityFinding] = [] + findings.append(contentsOf: trustAndIdentityFindings(requestedURL: requestedURL, trust: trust, leaf: leaf)) + findings.append(contentsOf: leafProfileFindings(leaf: leaf, trust: trust)) + findings.append(contentsOf: keyAndSignatureFindings(leaf: leaf)) + findings.append(contentsOf: chainFindings(leaf: leaf, certificates: certificates)) + return SecurityAssessment(findings: findings) + } + + private func trustAndIdentityFindings(requestedURL: URL, trust: TrustSummary, leaf: CertificateDetails) -> [SecurityFinding] { var findings: [SecurityFinding] = [] if trust.isTrusted { @@ -63,6 +72,12 @@ public struct SecurityAnalyzer: Sendable { )) } + return findings + } + + private func leafProfileFindings(leaf: CertificateDetails, trust: TrustSummary) -> [SecurityFinding] { + var findings: [SecurityFinding] = [] + if leaf.isSelfIssued { findings.append(SecurityFinding( severity: trust.isTrusted ? .warning : .critical, @@ -71,7 +86,7 @@ public struct SecurityAnalyzer: Sendable { )) } - if leaf.dnsNames.isEmpty && leaf.ipAddresses.isEmpty { + if leaf.dnsNames.isEmpty, leaf.ipAddresses.isEmpty { findings.append(SecurityFinding( severity: .warning, title: "No Subject Alternative Name", @@ -95,6 +110,12 @@ public struct SecurityAnalyzer: Sendable { )) } + return findings + } + + private func keyAndSignatureFindings(leaf: CertificateDetails) -> [SecurityFinding] { + var findings: [SecurityFinding] = [] + if let bitSize = leaf.publicKey.bitSize, leaf.publicKey.algorithm == "RSA" { if bitSize < 1024 { findings.append(SecurityFinding( @@ -120,8 +141,15 @@ public struct SecurityAnalyzer: Sendable { )) } + return findings + } + + private func chainFindings(leaf: CertificateDetails, certificates: [CertificateDetails]) -> [SecurityFinding] { + var findings: [SecurityFinding] = [] + if leaf.extendedKeyUsage.isEmpty == false, - leaf.extendedKeyUsage.contains(where: isServerAuthUsage) == false { + leaf.extendedKeyUsage.contains(where: isServerAuthUsage) == false + { findings.append(SecurityFinding( severity: .warning, title: "Missing Server Auth EKU", @@ -138,7 +166,7 @@ public struct SecurityAnalyzer: Sendable { } let interceptionProducts = detectedInterceptionProducts(in: certificates) - if interceptionProducts.isEmpty == false { + if !interceptionProducts.isEmpty { findings.append(SecurityFinding( severity: .warning, title: "Possible TLS Interception Product", @@ -148,7 +176,7 @@ public struct SecurityAnalyzer: Sendable { findings.append(contentsOf: chainLinkageFindings(certificates: certificates)) - return SecurityAssessment(findings: findings) + return findings } private func chainLinkageFindings(certificates: [CertificateDetails]) -> [SecurityFinding] { @@ -160,7 +188,8 @@ public struct SecurityAnalyzer: Sendable { for (child, issuer) in zip(certificates, certificates.dropFirst()) { guard let authorityKeyID = child.authorityKeyIdentifier.first(where: { $0.label == "Key Identifier" })?.value, - let subjectKeyID = issuer.subjectKeyIdentifier else { + let subjectKeyID = issuer.subjectKeyIdentifier + else { continue } @@ -241,7 +270,7 @@ public struct SecurityAnalyzer: Sendable { ("burp", "Burp"), ("charles", "Charles"), ("fiddler", "Fiddler"), - ("proxyman", "Proxyman") + ("proxyman", "Proxyman"), ] var matches = Set() diff --git a/Packages/InspectCore/Sources/Core/TLSClientHelloSNIExtractor.swift b/Packages/InspectCore/Sources/Core/TLSClientHelloSNIExtractor.swift index 2bb7bbe..e0bb7c8 100644 --- a/Packages/InspectCore/Sources/Core/TLSClientHelloSNIExtractor.swift +++ b/Packages/InspectCore/Sources/Core/TLSClientHelloSNIExtractor.swift @@ -2,12 +2,10 @@ import Foundation enum TLSClientHelloSNIExtractor { static func serverName(from payload: [UInt8]) -> String? { - guard payload.count >= 5 else { - return nil - } - - guard payload[0] == TLSRecordContentType.handshake, - payload[1] == TLSVersion.major else { + guard payload.count >= 5, + payload[0] == TLSRecordContentType.handshake, + payload[1] == TLSVersion.major + else { return nil } @@ -18,12 +16,17 @@ enum TLSClientHelloSNIExtractor { } var cursor = 5 - - guard payload[cursor] == TLSHandshakeType.clientHello else { + guard let extensionsStart = skipToExtensions(payload: payload, cursor: &cursor, recordEnd: recordEnd) else { return nil } - guard cursor + 4 <= recordEnd else { + return findSNIExtension(payload: payload, cursor: extensionsStart, extensionsEnd: cursor) + } + + private static func skipToExtensions(payload: [UInt8], cursor: inout Int, recordEnd: Int) -> Int? { + guard payload[cursor] == TLSHandshakeType.clientHello, + cursor + 4 <= recordEnd + else { return nil } @@ -34,57 +37,47 @@ enum TLSClientHelloSNIExtractor { cursor += 4 let handshakeEnd = min(recordEnd, cursor + handshakeLength) - guard handshakeEnd > cursor else { + guard handshakeEnd > cursor, + cursor + 34 <= handshakeEnd + else { return nil } // client_version(2) + random(32) - guard cursor + 34 <= handshakeEnd else { - return nil - } cursor += 34 - // session_id (1-byte length prefix) - guard cursor + 1 <= handshakeEnd else { - return nil - } - let sessionIDLength = Int(payload[cursor]) - cursor += 1 + sessionIDLength - guard cursor <= handshakeEnd else { + guard skipField(payload: payload, cursor: &cursor, end: handshakeEnd, lengthBytes: 1), + skipField(payload: payload, cursor: &cursor, end: handshakeEnd, lengthBytes: 2), + skipField(payload: payload, cursor: &cursor, end: handshakeEnd, lengthBytes: 1), + cursor + 2 <= handshakeEnd + else { return nil } - // cipher_suites (2-byte length prefix) - guard cursor + 2 <= handshakeEnd else { - return nil - } - let cipherSuitesLength = (Int(payload[cursor]) << 8) | Int(payload[cursor + 1]) - cursor += 2 + cipherSuitesLength - guard cursor <= handshakeEnd else { - return nil - } - - // compression_methods (1-byte length prefix) - guard cursor + 1 <= handshakeEnd else { - return nil - } - let compressionMethodsLength = Int(payload[cursor]) - cursor += 1 + compressionMethodsLength - guard cursor <= handshakeEnd else { - return nil - } - - // extensions (2-byte length prefix) - guard cursor + 2 <= handshakeEnd else { - return nil - } let extensionsLength = (Int(payload[cursor]) << 8) | Int(payload[cursor + 1]) cursor += 2 let extensionsEnd = min(handshakeEnd, cursor + extensionsLength) - guard extensionsEnd > cursor else { - return nil + guard extensionsEnd > cursor else { return nil } + + let extensionsStart = cursor + cursor = extensionsEnd + return extensionsStart + } + + private static func skipField(payload: [UInt8], cursor: inout Int, end: Int, lengthBytes: Int) -> Bool { + guard cursor + lengthBytes <= end else { return false } + let length: Int + if lengthBytes == 1 { + length = Int(payload[cursor]) + } else { + length = (Int(payload[cursor]) << 8) | Int(payload[cursor + 1]) } + cursor += lengthBytes + length + return cursor <= end + } + private static func findSNIExtension(payload: [UInt8], cursor: Int, extensionsEnd: Int) -> String? { + var cursor = cursor while cursor + 4 <= extensionsEnd { let extensionType = (UInt16(payload[cursor]) << 8) | UInt16(payload[cursor + 1]) let extensionLength = (Int(payload[cursor + 2]) << 8) | Int(payload[cursor + 3]) @@ -130,10 +123,11 @@ enum TLSClientHelloSNIExtractor { } if nameType == TLSServerNameType.hostName { - let nameBytes = Array(bytes[cursor..<(cursor + nameLength)]) + let nameBytes = Array(bytes[cursor ..< (cursor + nameLength)]) guard let name = String(bytes: nameBytes, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines), - name.isEmpty == false else { + name.isEmpty == false + else { return nil } diff --git a/Packages/InspectCore/Sources/Core/TLSFlowObservationFeed.swift b/Packages/InspectCore/Sources/Core/TLSFlowObservationFeed.swift index 110a022..4697256 100644 --- a/Packages/InspectCore/Sources/Core/TLSFlowObservationFeed.swift +++ b/Packages/InspectCore/Sources/Core/TLSFlowObservationFeed.swift @@ -17,7 +17,7 @@ public actor TLSFlowObservationFeed { if let containerURL = InspectSharedContainer.containerURL(appGroupIdentifier: appGroupIdentifier) { let fileURL = containerURL.appendingPathComponent(Self.fileName(for: key)) - self.storage = TLSFlowObservationFeedStorage( + storage = TLSFlowObservationFeedStorage( fileURL: fileURL, maximumPendingItems: maximumPendingItems ) @@ -25,7 +25,7 @@ public actor TLSFlowObservationFeed { "init appGroup=\(appGroupIdentifier) key=\(key) path=\(fileURL.path) bundle=\(bundleIdentifier)" ) } else { - self.storage = nil + storage = nil logger.critical( "init failed appGroup=\(appGroupIdentifier) key=\(key) because shared container URL is unavailable" ) @@ -39,7 +39,7 @@ public actor TLSFlowObservationFeed { ) { self.key = key let bundleIdentifier = Bundle.main.bundleIdentifier ?? "nil" - self.storage = TLSFlowObservationFeedStorage( + storage = TLSFlowObservationFeedStorage( fileURL: fileURL, maximumPendingItems: maximumPendingItems ) diff --git a/Packages/InspectCore/Sources/Core/TLSInspector.swift b/Packages/InspectCore/Sources/Core/TLSInspector.swift index 7feddd8..656d7c9 100644 --- a/Packages/InspectCore/Sources/Core/TLSInspector.swift +++ b/Packages/InspectCore/Sources/Core/TLSInspector.swift @@ -106,21 +106,22 @@ private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionT } } + private static let cipherSuiteNames: [tls_ciphersuite_t: String] = [ + .RSA_WITH_AES_128_GCM_SHA256: "RSA AES-128-GCM SHA256", + .RSA_WITH_AES_256_GCM_SHA384: "RSA AES-256-GCM SHA384", + .ECDHE_RSA_WITH_AES_128_GCM_SHA256: "ECDHE-RSA AES-128-GCM SHA256", + .ECDHE_RSA_WITH_AES_256_GCM_SHA384: "ECDHE-RSA AES-256-GCM SHA384", + .ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "ECDHE-ECDSA AES-128-GCM SHA256", + .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "ECDHE-ECDSA AES-256-GCM SHA384", + .ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: "ECDHE-RSA ChaCha20-Poly1305", + .ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: "ECDHE-ECDSA ChaCha20-Poly1305", + .AES_128_GCM_SHA256: "AES-128-GCM SHA256", + .AES_256_GCM_SHA384: "AES-256-GCM SHA384", + .CHACHA20_POLY1305_SHA256: "ChaCha20-Poly1305 SHA256", + ] + static func cipherSuiteName(_ suite: tls_ciphersuite_t) -> String { - switch suite { - case .RSA_WITH_AES_128_GCM_SHA256: return "RSA AES-128-GCM SHA256" - case .RSA_WITH_AES_256_GCM_SHA384: return "RSA AES-256-GCM SHA384" - case .ECDHE_RSA_WITH_AES_128_GCM_SHA256: return "ECDHE-RSA AES-128-GCM SHA256" - case .ECDHE_RSA_WITH_AES_256_GCM_SHA384: return "ECDHE-RSA AES-256-GCM SHA384" - case .ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: return "ECDHE-ECDSA AES-128-GCM SHA256" - case .ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: return "ECDHE-ECDSA AES-256-GCM SHA384" - case .ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: return "ECDHE-RSA ChaCha20-Poly1305" - case .ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: return "ECDHE-ECDSA ChaCha20-Poly1305" - case .AES_128_GCM_SHA256: return "AES-128-GCM SHA256" - case .AES_256_GCM_SHA384: return "AES-256-GCM SHA384" - case .CHACHA20_POLY1305_SHA256: return "ChaCha20-Poly1305 SHA256" - default: return "0x\(String(suite.rawValue, radix: 16, uppercase: true))" - } + cipherSuiteNames[suite] ?? "0x\(String(suite.rawValue, radix: 16, uppercase: true))" } func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive _: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { @@ -140,7 +141,7 @@ private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionT } let reports = buildReports() - guard reports.isEmpty == false else { + guard !reports.isEmpty else { continuation?.resume(throwing: error ?? InspectionError.missingServerTrust) continuation = nil return @@ -156,8 +157,22 @@ private final class RequestRunner: NSObject, URLSessionDataDelegate, URLSessionT } private func buildReports() -> [TLSInspectionReport] { - capturedTrustEvents.enumerated().compactMap { index, event in - let transaction = transactionMetrics[safe: index] + // Match trust events to transactions by host rather than index. + // URLSession may skip TLS challenges on connection reuse, so + // index-based pairing can pair the wrong metadata with a host. + var metricsByHost: [String: [URLSessionTaskTransactionMetrics]] = [:] + for metric in transactionMetrics { + if let host = metric.request.url?.host?.lowercased() { + metricsByHost[host, default: []].append(metric) + } + } + + return capturedTrustEvents.compactMap { event in + let hostKey = event.host.lowercased() + let transaction = metricsByHost[hostKey]?.first + if transaction != nil { + metricsByHost[hostKey]?.removeFirst() + } let requestURL = transaction?.request.url ?? event.requestURL ?? makeFallbackURL(host: event.host) guard let requestURL else { return nil diff --git a/Packages/InspectCore/Sources/Core/TLSPassiveHandshakeCapture.swift b/Packages/InspectCore/Sources/Core/TLSPassiveHandshakeCapture.swift index 7d088c5..1a3d0f7 100644 --- a/Packages/InspectCore/Sources/Core/TLSPassiveHandshakeCapture.swift +++ b/Packages/InspectCore/Sources/Core/TLSPassiveHandshakeCapture.swift @@ -56,7 +56,7 @@ struct TLSServerCertificateCapture { return nil } - let payload = Data(recordBuffer[5.. Int { (Int(data[offset]) << 16) | - (Int(data[offset + 1]) << 8) | - Int(data[offset + 2]) + (Int(data[offset + 1]) << 8) | + Int(data[offset + 2]) } } diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailModels.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailModels.swift index b2dabbf..76ba1ac 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailModels.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailModels.swift @@ -70,7 +70,7 @@ struct CertificateDetailContent { titled: "Validity", rows: [ DetailLine(label: "Not Valid Before", value: certificate.validity.notBefore?.inspectDisplayString ?? "Unavailable"), - DetailLine(label: "Not Valid After", value: certificate.validity.notAfter?.inspectDisplayString ?? "Unavailable") + DetailLine(label: "Not Valid After", value: certificate.validity.notAfter?.inspectDisplayString ?? "Unavailable"), ], into: §ions ) @@ -81,19 +81,19 @@ struct CertificateDetailContent { DetailLine(label: "Version", value: certificate.version), DetailLine(label: "Signature Algorithm", value: certificate.signatureAlgorithm), DetailLine(label: "Serial Number", value: certificate.serialNumber, style: .stacked, monospaced: true), - DetailLine(label: "Raw Signature", value: certificate.signature, style: .stacked, monospaced: true) + DetailLine(label: "Raw Signature", value: certificate.signature, style: .stacked, monospaced: true), ], into: §ions ) let usageRows = certificate.keyUsage.map { DetailLine(label: "Key Usage", value: $0) } - + certificate.extendedKeyUsage.map { DetailLine(label: "Extended Key Usage", value: $0) } + + certificate.extendedKeyUsage.map { DetailLine(label: "Extended Key Usage", value: $0) } appendSection(titled: "Usage", rows: usageRows, into: §ions) let namesAndAccessRows = certificate.subjectAlternativeNames.map(DetailLine.init) - + certificate.authorityInfoAccess.map(DetailLine.init) + + certificate.authorityInfoAccess.map(DetailLine.init) appendSection(titled: "Names & Access", rows: namesAndAccessRows, into: §ions) appendSection( @@ -102,7 +102,7 @@ struct CertificateDetailContent { DetailLine(label: "Algorithm", value: certificate.publicKey.algorithm), DetailLine(label: "Bit Size", value: certificate.publicKey.bitSize.map(String.init) ?? "Unavailable"), DetailLine(label: "SPKI SHA-256", value: certificate.publicKey.spkiSHA256Fingerprint, style: .stacked, monospaced: true), - DetailLine(label: "Key Data", value: certificate.publicKey.hexRepresentation, style: .stacked, monospaced: true) + DetailLine(label: "Key Data", value: certificate.publicKey.hexRepresentation, style: .stacked, monospaced: true), ], into: §ions ) diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailTheme.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailTheme.swift index 0009770..f744a6a 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailTheme.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailTheme.swift @@ -11,25 +11,25 @@ extension Color { static var certificateChainSelectionBackground: Color { #if os(macOS) - return .inspectAccent.opacity(0.16) + return .inspectAccent.opacity(0.16) #else - return .accentColor + return .accentColor #endif } static func certificateChainPrimaryText(isSelected: Bool) -> Color { #if os(macOS) - return .primary + return .primary #else - return isSelected ? .white : .primary + return isSelected ? .white : .primary #endif } static func certificateChainSecondaryText(isSelected: Bool) -> Color { #if os(macOS) - return .secondary + return .secondary #else - return isSelected ? .white.opacity(0.82) : .secondary + return isSelected ? .white.opacity(0.82) : .secondary #endif } } diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift index 56ddf6d..f612550 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+iOS.swift @@ -4,10 +4,6 @@ extension CertificateDetailView { var platformContent: some View { List { - if inspection.didRedirect { - reportSection - } - chainSection Section { @@ -34,22 +30,6 @@ } } - var reportSection: some View { - Section { - Picker("TLS Hop", selection: $selectedReportIndex) { - ForEach(Array(inspection.reports.enumerated()), id: \.element.id) { index, report in - Text("Hop \(index + 1) • \(report.host)").tag(index) - } - } - .pickerStyle(.navigationLink) - .onChange(of: selectedReportIndex) { _, newValue in - updateReportSelection(to: newValue) - } - } header: { - sectionHeader("TLS Hop") - } - } - var chainSection: some View { Section { CompactCertificateChainPanel( @@ -60,7 +40,7 @@ ) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } header: { - sectionHeader(inspection.didRedirect ? "Certificate Chain For Selected Hop" : "Certificate Chain") + sectionHeader("Certificate Chain") } } diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift index 3a6a7df..fcbf1fd 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateDetailView+macOS.swift @@ -11,23 +11,6 @@ InspectCard { VStack(alignment: .leading, spacing: 14) { - if inspection.didRedirect { - VStack(alignment: .leading, spacing: 10) { - Text("TLS Hop") - .font(.inspectRootHeadline) - - Picker("TLS Hop", selection: $selectedReportIndex) { - ForEach(Array(inspection.reports.enumerated()), id: \.element.id) { index, report in - Text("Hop \(index + 1) • \(report.host)").tag(index) - } - } - .labelsHidden() - .onChange(of: selectedReportIndex) { _, newValue in - updateReportSelection(to: newValue) - } - } - } - Text("Certificate Chain") .font(.inspectRootHeadline) @@ -42,7 +25,7 @@ } .padding(EdgeInsets(top: 52, leading: 16, bottom: 16, trailing: 16)) } - .frame(minWidth: 260, idealWidth: 290, maxWidth: 320) + .frame(minWidth: 280, idealWidth: 340, maxWidth: 380) ScrollView { VStack(alignment: .leading, spacing: 16) { @@ -134,9 +117,6 @@ title: "Chain Position", value: "\(selectedIndex + 1) of \((selectedReport?.certificates.count ?? 0))" ) - if inspection.didRedirect { - MacCertificateQuickFact(title: "TLS Hop", value: "Hop \(selectedReportIndex + 1) of \(inspection.reports.count)") - } } } diff --git a/Packages/InspectCore/Sources/Feature/Certificate/CertificateExportWriter.swift b/Packages/InspectCore/Sources/Feature/Certificate/CertificateExportWriter.swift index 536db2c..85a395a 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/CertificateExportWriter.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/CertificateExportWriter.swift @@ -1,5 +1,5 @@ -import InspectCore import Foundation +import InspectCore enum CertificateExportWriter { static func writeTemporaryCertificate(_ certificate: CertificateDetails, host: String, indexInChain: Int) -> URL? { diff --git a/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSectionViews.swift b/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSectionViews.swift index a916c70..6b21702 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSectionViews.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSectionViews.swift @@ -1,118 +1,118 @@ #if os(macOS) -import SwiftUI + import SwiftUI -struct MacCertificateSectionCard: View { - let section: CertificateDetailSection - let onCopy: (DetailLine) -> Void + struct MacCertificateSectionCard: View { + let section: CertificateDetailSection + let onCopy: (DetailLine) -> Void - var body: some View { - InspectCard { - VStack(alignment: .leading, spacing: 0) { - Text(section.title) - .font(.system(size: 17, weight: .semibold)) - .padding(.bottom, 6) + var body: some View { + InspectCard { + VStack(alignment: .leading, spacing: 0) { + Text(section.title) + .font(.system(size: 17, weight: .semibold)) + .padding(.bottom, 6) - ForEach(Array(section.rows.enumerated()), id: \.element.id) { index, row in - Group { - switch row.style { - case .inline: - MacInlineDetailRow(row: row) { - onCopy(row) - } - case .stacked: - MacStackedDetailRow(row: row) { - onCopy(row) + ForEach(Array(section.rows.enumerated()), id: \.element.id) { index, row in + Group { + switch row.style { + case .inline: + MacInlineDetailRow(row: row) { + onCopy(row) + } + case .stacked: + MacStackedDetailRow(row: row) { + onCopy(row) + } } } - } - if index != section.rows.count - 1 { - Divider() - .padding(.vertical, 2) + if index != section.rows.count - 1 { + Divider() + .padding(.vertical, 2) + } } } } } } -} -private struct MacInlineDetailRow: View { - let row: DetailLine - let onCopy: () -> Void + private struct MacInlineDetailRow: View { + let row: DetailLine + let onCopy: () -> Void - var body: some View { - HStack(alignment: .top, spacing: 16) { - Text(row.label) - .font(.inspectDetailCompactCaptionSemibold) - .foregroundStyle(.secondary) - .frame(width: 150, alignment: .leading) + var body: some View { + HStack(alignment: .top, spacing: 16) { + Text(row.label) + .font(.inspectDetailCompactCaptionSemibold) + .foregroundStyle(.secondary) + .frame(width: 150, alignment: .leading) - Text(row.value) - .font(.inspectDetailCompactBody) - .foregroundStyle(.primary) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(nil) + Text(row.value) + .font(.inspectDetailCompactBody) + .foregroundStyle(.primary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(nil) - copyButton - } - .padding(.vertical, 8) - .contextMenu { - Button("Copy Value", systemImage: "doc.on.doc", action: onCopy) + copyButton + } + .padding(.vertical, 8) + .contextMenu { + Button("Copy Value", systemImage: "doc.on.doc", action: onCopy) + } } - } - private var copyButton: some View { - Button(action: onCopy) { - Image(systemName: "doc.on.doc") - .font(.system(size: 13, weight: .semibold)) - .foregroundStyle(.secondary) + private var copyButton: some View { + Button(action: onCopy) { + Image(systemName: "doc.on.doc") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .help("Copy \(row.label)") } - .buttonStyle(.plain) - .help("Copy \(row.label)") } -} -private struct MacStackedDetailRow: View { - let row: DetailLine - let onCopy: () -> Void + private struct MacStackedDetailRow: View { + let row: DetailLine + let onCopy: () -> Void - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .center) { - Text(row.label) - .font(.inspectDetailCompactCaptionSemibold) - .foregroundStyle(.secondary) - - Spacer(minLength: 12) - - Button(action: onCopy) { - Image(systemName: "doc.on.doc") + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .center) { + Text(row.label) .font(.inspectDetailCompactCaptionSemibold) .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - } - .frame(maxWidth: .infinity, alignment: .leading) - Text(row.value) - .font(row.monospaced ? .inspectDetailCompactMonospaced : .inspectDetailCompactBody) - .foregroundStyle(.primary) + Spacer(minLength: 12) + + Button(action: onCopy) { + Image(systemName: "doc.on.doc") + .font(.inspectDetailCompactCaptionSemibold) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + } .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(nil) - .multilineTextAlignment(.leading) - .textSelection(.enabled) - .padding(.horizontal, 10) - .padding(.vertical, 9) - .background( - RoundedRectangle(cornerRadius: 14, style: .continuous) - .fill(Color.inspectChromeFill) - ) - } - .padding(.vertical, 8) - .contextMenu { - Button("Copy Value", systemImage: "doc.on.doc", action: onCopy) + + Text(row.value) + .font(row.monospaced ? .inspectDetailCompactMonospaced : .inspectDetailCompactBody) + .foregroundStyle(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(nil) + .multilineTextAlignment(.leading) + .textSelection(.enabled) + .padding(.horizontal, 10) + .padding(.vertical, 9) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.inspectChromeFill) + ) + } + .padding(.vertical, 8) + .contextMenu { + Button("Copy Value", systemImage: "doc.on.doc", action: onCopy) + } } } -} #endif diff --git a/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSummaryViews.swift b/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSummaryViews.swift index ef2b2ca..18b559e 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSummaryViews.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/MacCertificateSummaryViews.swift @@ -1,41 +1,41 @@ #if os(macOS) -import SwiftUI + import SwiftUI -struct MacCertificateQuickFact: View { - let title: String - let value: String + struct MacCertificateQuickFact: View { + let title: String + let value: String - var body: some View { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.inspectDetailCompactCaptionSemibold) - .foregroundStyle(.secondary) + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.inspectDetailCompactCaptionSemibold) + .foregroundStyle(.secondary) - Text(value) - .font(.inspectDetailCompactBody) - .foregroundStyle(.primary) - .fixedSize(horizontal: false, vertical: true) + Text(value) + .font(.inspectDetailCompactBody) + .foregroundStyle(.primary) + .fixedSize(horizontal: false, vertical: true) + } } } -} -struct MacCertificateStatPill: View { - let title: String - let value: String + struct MacCertificateStatPill: View { + let title: String + let value: String - var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.inspectDetailCompactCaption) - .foregroundStyle(.secondary) + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.inspectDetailCompactCaption) + .foregroundStyle(.secondary) - Text(value) - .font(.inspectDetailCompactBodySemibold) - .lineLimit(1) + Text(value) + .font(.inspectDetailCompactBodySemibold) + .lineLimit(1) + } + .padding(.horizontal, 10) + .padding(.vertical, 8) + .background(Color.inspectChromeFill, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) } - .padding(.horizontal, 10) - .padding(.vertical, 8) - .background(Color.inspectChromeFill, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) } -} #endif diff --git a/Packages/InspectCore/Sources/Feature/Certificate/RevocationStatusView.swift b/Packages/InspectCore/Sources/Feature/Certificate/RevocationStatusView.swift index f6db7ef..957d741 100644 --- a/Packages/InspectCore/Sources/Feature/Certificate/RevocationStatusView.swift +++ b/Packages/InspectCore/Sources/Feature/Certificate/RevocationStatusView.swift @@ -74,8 +74,8 @@ struct RevocationStatusBadge: View { case .unchecked: return "OCSP and CRL endpoints" case .checking: return nil case .good: return "OCSP/CRL verification passed" - case .revoked(let reason): return reason - case .unreachable(let reason): return reason + case let .revoked(reason): return reason + case let .unreachable(reason): return reason } } } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionAppStoreScreenshotView.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionAppStoreScreenshotView.swift index de46b36..d219b81 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionAppStoreScreenshotView.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionAppStoreScreenshotView.swift @@ -14,24 +14,24 @@ public struct InspectionAppStoreScreenshotView: View { public var body: some View { #if os(macOS) - macShell + macShell #else - switch scenario { - case .inspectTab, .monitorTab: - tabShell - case .hostDetail: - if let host = featuredHost { + switch scenario { + case .inspectTab, .monitorTab: + tabShell + case .hostDetail: + if let host = featuredHost { + NavigationStack { + InspectionMonitorHostDetailView(store: monitorStore, host: host) + } + .tint(.inspectAccent) + } + case .certificateChain: NavigationStack { - InspectionMonitorHostDetailView(store: monitorStore, host: host) + CertificateDetailView(report: InspectionScreenshotFixtures.featuredReport, initialSelectionIndex: 0) } .tint(.inspectAccent) } - case .certificateChain: - NavigationStack { - CertificateDetailView(report: InspectionScreenshotFixtures.featuredReport, initialSelectionIndex: 0) - } - .tint(.inspectAccent) - } #endif } @@ -42,10 +42,10 @@ public struct InspectionAppStoreScreenshotView: View { showsMonitorCard: false, showsAboutCard: false ) - .tabItem { - Label(InspectSection.inspect.title, systemImage: InspectSection.inspect.systemImage) - } - .tag(InspectSection.inspect) + .tabItem { + Label(InspectSection.inspect.title, systemImage: InspectSection.inspect.systemImage) + } + .tag(InspectSection.inspect) InspectionMonitorView(monitorStore: monitorStore) .tabItem { @@ -63,59 +63,59 @@ public struct InspectionAppStoreScreenshotView: View { } #if os(macOS) - private var macShell: some View { - NavigationSplitView { - List(selection: .constant(selectedSidebarTab)) { - Label(InspectSection.inspect.title, systemImage: InspectSection.inspect.systemImage) - .tag(InspectSection.inspect) - Label(InspectSection.monitor.title, systemImage: InspectSection.monitor.systemImage) - .tag(InspectSection.monitor) - Label(InspectSection.settings.title, systemImage: InspectSection.settings.systemImage) - .tag(InspectSection.settings) + private var macShell: some View { + NavigationSplitView { + List(selection: .constant(selectedSidebarTab)) { + Label(InspectSection.inspect.title, systemImage: InspectSection.inspect.systemImage) + .tag(InspectSection.inspect) + Label(InspectSection.monitor.title, systemImage: InspectSection.monitor.systemImage) + .tag(InspectSection.monitor) + Label(InspectSection.settings.title, systemImage: InspectSection.settings.systemImage) + .tag(InspectSection.settings) + } + .navigationSplitViewColumnWidth(min: 164, ideal: 188, max: 220) + } detail: { + macDetail } - .navigationSplitViewColumnWidth(min: 164, ideal: 188, max: 220) - } detail: { - macDetail + .navigationSplitViewStyle(.balanced) + .frame(minWidth: 1280, minHeight: 800) + .tint(.inspectAccent) } - .navigationSplitViewStyle(.balanced) - .frame(minWidth: 1280, minHeight: 800) - .tint(.inspectAccent) - } - @ViewBuilder - private var macDetail: some View { - switch scenario { - case .inspectTab: - InspectionRootView( - screenshotScenario: .inspectTab, - showsMonitorCard: false, - showsAboutCard: false - ) - case .monitorTab: - InspectionMonitorView(monitorStore: monitorStore) - case .hostDetail: - if let host = featuredHost { + @ViewBuilder + private var macDetail: some View { + switch scenario { + case .inspectTab: + InspectionRootView( + screenshotScenario: .inspectTab, + showsMonitorCard: false, + showsAboutCard: false + ) + case .monitorTab: + InspectionMonitorView(monitorStore: monitorStore) + case .hostDetail: + if let host = featuredHost { + NavigationStack { + InspectionMonitorHostDetailView(store: monitorStore, host: host) + } + } else { + screenshotPlaceholder(title: "Monitor") + } + case .certificateChain: NavigationStack { - InspectionMonitorHostDetailView(store: monitorStore, host: host) + CertificateDetailView(report: InspectionScreenshotFixtures.featuredReport, initialSelectionIndex: 0) } - } else { - screenshotPlaceholder(title: "Monitor") - } - case .certificateChain: - NavigationStack { - CertificateDetailView(report: InspectionScreenshotFixtures.featuredReport, initialSelectionIndex: 0) } } - } - private var selectedSidebarTab: InspectSection { - switch scenario { - case .inspectTab, .certificateChain: - return .inspect - case .monitorTab, .hostDetail: - return .monitor + private var selectedSidebarTab: InspectSection { + switch scenario { + case .inspectTab, .certificateChain: + return .inspect + case .monitorTab, .hostDetail: + return .monitor + } } - } #endif private var featuredHost: InspectionMonitoredHost? { diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionExternalInputCenter.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionExternalInputCenter.swift index 2d9da27..65846c9 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionExternalInputCenter.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionExternalInputCenter.swift @@ -47,7 +47,8 @@ public enum InspectionExternalInputCenter { public static func consumePendingSharedReportRequest() -> InspectionExternalRequest? { guard let token = InspectionSharedPendingReportStore.consumeToken(), - let report = InspectionSharedReportStore.consume(token: token) else { + let report = InspectionSharedReportStore.consume(token: token) + else { return nil } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift index c824dab..f0f32e1 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionNavigation.swift @@ -54,7 +54,7 @@ extension InspectionCertificateRoute: Hashable { } } } - .frame(width: 960, height: 720) + .frame(width: 1080, height: 720) } } #endif diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRecentItems.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRecentItems.swift index daa8d63..0b9b95d 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRecentItems.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRecentItems.swift @@ -1,5 +1,5 @@ -import InspectCore import Foundation +import InspectCore enum RecentInputFormatter { static func host(for recent: String) -> String? { diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift index c2ce89b..a343937 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRedirectsCard.swift @@ -7,17 +7,22 @@ struct InspectionRedirectsCard: View { var body: some View { InspectCard { - VStack(alignment: .leading, spacing: 14) { + VStack(alignment: .leading, spacing: 0) { Text("Redirects") .font(.inspectRootHeadline) + .padding(.bottom, 14) ForEach(Array(inspection.reports.enumerated()), id: \.element.id) { index, report in + if index > 0 { + Divider() + } + Button { selectedReportIndex = index } label: { - HStack(spacing: 12) { - Text("Hop \(index + 1)") - .font(.inspectRootCaption) + HStack(alignment: .center, spacing: 12) { + Text("\(index + 1).") + .font(.inspectRootSubheadlineSemibold) .foregroundStyle(.secondary) Text(report.host) @@ -41,14 +46,10 @@ struct InspectionRedirectsCard: View { .foregroundStyle(.tertiary) } } - .padding(.vertical, 8) + .padding(.vertical, 12) .contentShape(Rectangle()) } .buttonStyle(.plain) - - if inspection.reports.indices.contains(index + 1) { - Divider() - } } } } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift index f9b6a2a..65175ea 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionResultsContent.swift @@ -67,6 +67,6 @@ struct InspectionResultsContent: View { } private var selectedReport: TLSInspectionReport? { - inspection?.reports[safe: selectedReportIndex] + inspection?.reports[safe: min(selectedReportIndex, (inspection?.reports.count ?? 1) - 1)] } } diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift index 5803d06..63b7bde 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionRootView.swift @@ -73,6 +73,11 @@ public struct InspectionRootView: View { handleExternalRequest(request) } + .onChange(of: store.isLoading) { _, isLoading in + if isLoading { + selectedReportIndex = 0 + } + } .onChange(of: store.inspection?.id) { _, _ in guard screenshotScenario == nil, let report = store.inspection?.primaryReport else { return @@ -114,7 +119,8 @@ public struct InspectionRootView: View { private var rootContent: some View { let inspection = store.inspection - let report = inspection?.primaryReport + let clampedIndex = min(selectedReportIndex, max((inspection?.reports.count ?? 1) - 1, 0)) + let report = inspection?.reports[safe: clampedIndex] let recentItems = screenshotScenario?.showsRecents == false ? [] : store.recentInputs.map(RecentLookupItem.init) diff --git a/Packages/InspectCore/Sources/Feature/Inspection/InspectionScreenshotFixtures.swift b/Packages/InspectCore/Sources/Feature/Inspection/InspectionScreenshotFixtures.swift index c2a7028..226c5e8 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/InspectionScreenshotFixtures.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/InspectionScreenshotFixtures.swift @@ -13,7 +13,7 @@ enum InspectionScreenshotFixtures { severity: .good, title: "Trusted Chain", message: "The captured chain validated successfully." - ) + ), ], certificates: [ makeCertificate( @@ -39,7 +39,7 @@ enum InspectionScreenshotFixtures { fingerprint: "55:66:77:88", isLeaf: false, isRoot: true - ) + ), ] ) @@ -56,7 +56,7 @@ enum InspectionScreenshotFixtures { fingerprint: "99:AA:BB:CC", isLeaf: true, isRoot: false - ) + ), ] ) @@ -98,8 +98,8 @@ enum InspectionScreenshotFixtures { title: String, issuer: String, fingerprint: String, - isLeaf: Bool, - isRoot: Bool + isLeaf: Bool = false, + isRoot: Bool = false ) -> CertificateDetails { CertificateDetails( id: "\(host)-\(title)", diff --git a/Packages/InspectCore/Sources/Feature/Inspection/RecentLookupIcon.swift b/Packages/InspectCore/Sources/Feature/Inspection/RecentLookupIcon.swift index 0884b31..278b514 100644 --- a/Packages/InspectCore/Sources/Feature/Inspection/RecentLookupIcon.swift +++ b/Packages/InspectCore/Sources/Feature/Inspection/RecentLookupIcon.swift @@ -101,7 +101,7 @@ private enum FaviconCache { do { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, - (200..<300).contains(httpResponse.statusCode), + (200 ..< 300).contains(httpResponse.statusCode), data.isEmpty == false else { return nil diff --git a/Packages/InspectCore/Sources/Feature/Monitor/InspectionLiveMonitorPreferenceStore.swift b/Packages/InspectCore/Sources/Feature/Monitor/InspectionLiveMonitorPreferenceStore.swift index 04badbb..7067c83 100644 --- a/Packages/InspectCore/Sources/Feature/Monitor/InspectionLiveMonitorPreferenceStore.swift +++ b/Packages/InspectCore/Sources/Feature/Monitor/InspectionLiveMonitorPreferenceStore.swift @@ -3,7 +3,7 @@ import InspectCore public enum InspectionLiveMonitorPreferenceStore { private static let enabledKey = "inspect.monitor.enabled.v1" - nonisolated(unsafe) private static let sharedDefaults = UserDefaults(suiteName: InspectSharedContainer.appGroupIdentifier) ?? .standard + private nonisolated(unsafe) static let sharedDefaults = UserDefaults(suiteName: InspectSharedContainer.appGroupIdentifier) ?? .standard public static var isEnabled: Bool { defaults.bool(forKey: enabledKey) diff --git a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorCard.swift b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorCard.swift index 5cab2d0..3c904ee 100644 --- a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorCard.swift +++ b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorCard.swift @@ -10,7 +10,7 @@ struct InspectionMonitorCard: View { isRefreshing: Bool = false, refreshAction: (() -> Void)? = nil ) { - self._store = Bindable(store) + _store = Bindable(store) self.isRefreshing = isRefreshing self.refreshAction = refreshAction } diff --git a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorModels.swift b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorModels.swift index 0693f37..671857b 100644 --- a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorModels.swift +++ b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorModels.swift @@ -7,7 +7,7 @@ struct InspectionMonitorEntry: Identifiable, Equatable { let note: String? init(event: TLSProbeEvent, note: String?) { - self.id = event.id + id = event.id self.event = event self.note = note } @@ -33,7 +33,7 @@ enum InspectionMonitoredHostState: Equatable { } } -enum InspectionMonitoredHostCertificateAvailability: Equatable { +enum MonitoredHostCertAvailability: Equatable { case captured case pending @@ -54,9 +54,11 @@ struct InspectionMonitoredHost: Identifiable, Equatable { let lastSeenAt: Date let latestReport: TLSInspectionReport? let state: InspectionMonitoredHostState - let certificateAvailability: InspectionMonitoredHostCertificateAvailability + let certificateAvailability: MonitoredHostCertAvailability - var id: String { host } + var id: String { + host + } var statusTitle: String { state.title @@ -102,7 +104,9 @@ enum InspectionMonitorHostFilter: String, CaseIterable, Identifiable { case review case pending - var id: String { rawValue } + var id: String { + rawValue + } var title: String { switch self { diff --git a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorStore.swift b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorStore.swift index dd19600..6f4d310 100644 --- a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorStore.swift +++ b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorStore.swift @@ -33,9 +33,9 @@ final class InspectionMonitorStore { self.flowObservationFeed = flowObservationFeed self.enableNetworkFeedPolling = enableNetworkFeedPolling self.userDefaults = userDefaults - self.isEnabled = userDefaults.bool(forKey: Self.enabledKey) - self.entries = Self.loadPersistedEntries(from: userDefaults) - self.lastLeafFingerprintByHost = Self.makeFingerprintIndex(from: entries) + isEnabled = userDefaults.bool(forKey: Self.enabledKey) + entries = Self.loadPersistedEntries(from: userDefaults) + lastLeafFingerprintByHost = Self.makeFingerprintIndex(from: entries) let enabledKey = Self.enabledKey defaultsObserver = NotificationCenter.default.addObserver( forName: UserDefaults.didChangeNotification, @@ -160,7 +160,8 @@ final class InspectionMonitorStore { } guard case let .captured(report) = entry.event.result, - latestCapturedReportByHost[host] == nil else { + latestCapturedReportByHost[host] == nil + else { continue } @@ -181,7 +182,8 @@ final class InspectionMonitorStore { for entry in entries { guard case let .captured(report) = entry.event.result, - report.host.lowercased() == normalizedHost else { + report.host.lowercased() == normalizedHost + else { continue } @@ -242,7 +244,8 @@ final class InspectionMonitorStore { private func pollFeedIfNeeded() async { guard isEnabled, - let flowObservationFeed else { + let flowObservationFeed + else { return } @@ -290,7 +293,7 @@ final class InspectionMonitorStore { let supportsActiveProbe = MonitorHostClassifier.isIPAddressLiteral(host) == false let firstSeenAt = entries.last?.event.occurredAt ?? lastEntry.event.occurredAt let state: InspectionMonitoredHostState - let certificateAvailability: InspectionMonitoredHostCertificateAvailability + let certificateAvailability: MonitoredHostCertAvailability if let latestReport { certificateAvailability = .captured @@ -345,7 +348,8 @@ final class InspectionMonitorStore { private static func loadPersistedEntries(from userDefaults: UserDefaults) -> [InspectionMonitorEntry] { guard let data = userDefaults.data(forKey: entriesKey), - let snapshots = try? JSONDecoder().decode([InspectionMonitorEntrySnapshot].self, from: data) else { + let snapshots = try? JSONDecoder().decode([InspectionMonitorEntrySnapshot].self, from: data) + else { return [] } @@ -359,7 +363,8 @@ final class InspectionMonitorStore { guard case let .captured(report) = entry.event.result, let leafFingerprint = report.leafCertificate?.fingerprints.first(where: { $0.label.caseInsensitiveCompare("SHA-256") == .orderedSame - })?.value else { + })?.value + else { continue } @@ -375,8 +380,8 @@ private struct InspectionMonitorEntrySnapshot: Codable { let note: String? init(entry: InspectionMonitorEntry) { - self.event = entry.event - self.note = entry.note + event = entry.event + note = entry.note } var entry: InspectionMonitorEntry { diff --git a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorView.swift b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorView.swift index 8eb1fd8..5c062ff 100644 --- a/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorView.swift +++ b/Packages/InspectCore/Sources/Feature/Monitor/InspectionMonitorView.swift @@ -64,7 +64,7 @@ public struct InspectionMonitorView: View { isRefreshing: isRefreshing, refreshAction: cardRefreshAction ) - .id("monitor") + .id("monitor") InspectionMonitorHostListCard( store: monitorStore, @@ -73,7 +73,7 @@ public struct InspectionMonitorView: View { isSearchExpanded: $isHostSearchExpanded, isSearchFocused: $isHostSearchFocused ) - .id("monitor.hosts") + .id("monitor.hosts") } .padding(.horizontal, 20) .padding(.top, 20) diff --git a/Packages/InspectCore/Sources/Feature/Settings/InspectReviewRequester.swift b/Packages/InspectCore/Sources/Feature/Settings/InspectReviewRequester.swift index c969c19..8857924 100644 --- a/Packages/InspectCore/Sources/Feature/Settings/InspectReviewRequester.swift +++ b/Packages/InspectCore/Sources/Feature/Settings/InspectReviewRequester.swift @@ -2,30 +2,32 @@ import Foundation import StoreKit #if canImport(UIKit) -import UIKit + import UIKit #elseif canImport(AppKit) -import AppKit + import AppKit #endif public enum InspectReviewRequester { @MainActor public static func requestReview() { #if canImport(UIKit) - guard let scene = UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .first(where: { $0.activationState == .foregroundActive }) - ?? UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first else { - return - } + guard let scene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }) + ?? UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first + else { + return + } - AppStore.requestReview(in: scene) + AppStore.requestReview(in: scene) #elseif os(macOS) - guard let controller = NSApp.keyWindow?.contentViewController - ?? NSApp.mainWindow?.contentViewController else { - return - } + guard let controller = NSApp.keyWindow?.contentViewController + ?? NSApp.mainWindow?.contentViewController + else { + return + } - AppStore.requestReview(in: controller) + AppStore.requestReview(in: controller) #endif } } diff --git a/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsIconLabel.swift b/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsIconLabel.swift index 8a9c127..bffc851 100644 --- a/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsIconLabel.swift +++ b/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsIconLabel.swift @@ -27,17 +27,17 @@ public struct InspectSettingsIconLabel: View { private var settingsIconCornerRadius: CGFloat { #if os(macOS) - 8 + 8 #else - 9 + 9 #endif } private var settingsIconFontSize: CGFloat { #if os(macOS) - 13 + 13 #else - 14 + 14 #endif } } diff --git a/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsRows.swift b/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsRows.swift index 8ff15f7..b0c9c6c 100644 --- a/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsRows.swift +++ b/Packages/InspectCore/Sources/Feature/Settings/InspectSettingsRows.swift @@ -104,13 +104,13 @@ public struct InspectSettingsMessageRow: View { public init(message: String, systemImage: String, color: Color) { self.message = message self.systemImage = systemImage - self.foregroundStyle = AnyShapeStyle(color) + foregroundStyle = AnyShapeStyle(color) } public init(message: String, systemImage: String, hierarchicalStyle: HierarchicalShapeStyle) { self.message = message self.systemImage = systemImage - self.foregroundStyle = AnyShapeStyle(hierarchicalStyle) + foregroundStyle = AnyShapeStyle(hierarchicalStyle) } public var body: some View { diff --git a/Packages/InspectCore/Sources/Feature/Settings/InspectSharedSettingsSections.swift b/Packages/InspectCore/Sources/Feature/Settings/InspectSharedSettingsSections.swift index f699d25..514ae35 100644 --- a/Packages/InspectCore/Sources/Feature/Settings/InspectSharedSettingsSections.swift +++ b/Packages/InspectCore/Sources/Feature/Settings/InspectSharedSettingsSections.swift @@ -95,9 +95,9 @@ public struct InspectAboutSettingsSection: View { private var versionFont: Font { #if os(macOS) - .body.weight(.medium) + .body.weight(.medium) #else - .subheadline.weight(.medium) + .subheadline.weight(.medium) #endif } } diff --git a/Packages/InspectCore/Sources/Feature/Settings/InspectionReviewPromptStore.swift b/Packages/InspectCore/Sources/Feature/Settings/InspectionReviewPromptStore.swift index 87a7b54..d90f517 100644 --- a/Packages/InspectCore/Sources/Feature/Settings/InspectionReviewPromptStore.swift +++ b/Packages/InspectCore/Sources/Feature/Settings/InspectionReviewPromptStore.swift @@ -1,5 +1,5 @@ -import InspectCore import Foundation +import InspectCore enum InspectionReviewPromptStore { private static let promptedVersionKey = "inspect.review-prompted-version.v1" diff --git a/Packages/InspectCore/Sources/Feature/Shared/InspectAppRoute.swift b/Packages/InspectCore/Sources/Feature/Shared/InspectAppRoute.swift index e58a9a2..bb009ff 100644 --- a/Packages/InspectCore/Sources/Feature/Shared/InspectAppRoute.swift +++ b/Packages/InspectCore/Sources/Feature/Shared/InspectAppRoute.swift @@ -13,7 +13,7 @@ public enum InspectAppRoute: Sendable, Equatable { case let .section(section): components.host = InspectScheme.sectionHost components.queryItems = [ - URLQueryItem(name: InspectScheme.sectionQueryItemName, value: section.rawValue.lowercased()) + URLQueryItem(name: InspectScheme.sectionQueryItemName, value: section.rawValue.lowercased()), ] case .toggleLiveMonitor: components.host = InspectScheme.toggleLiveMonitorHost @@ -32,7 +32,8 @@ public enum InspectAppRoute: Sendable, Equatable { guard let rawSection = URLComponents(url: url, resolvingAgainstBaseURL: false)? .queryItems? .first(where: { $0.name == InspectScheme.sectionQueryItemName })? - .value else { + .value + else { return nil } diff --git a/Packages/InspectCore/Sources/Feature/Shared/InspectPlatformSupport.swift b/Packages/InspectCore/Sources/Feature/Shared/InspectPlatformSupport.swift index ed60c6d..27ef984 100644 --- a/Packages/InspectCore/Sources/Feature/Shared/InspectPlatformSupport.swift +++ b/Packages/InspectCore/Sources/Feature/Shared/InspectPlatformSupport.swift @@ -2,29 +2,31 @@ import Foundation import SwiftUI #if os(iOS) -import UIKit -typealias InspectPlatformColor = UIColor -typealias InspectPlatformImage = UIImage + import UIKit + + typealias InspectPlatformColor = UIColor + typealias InspectPlatformImage = UIImage #elseif os(macOS) -import AppKit -typealias InspectPlatformColor = NSColor -typealias InspectPlatformImage = NSImage + import AppKit + + typealias InspectPlatformColor = NSColor + typealias InspectPlatformImage = NSImage #endif enum InspectPlatform { static func dynamicColor(light: InspectPlatformColor, dark: InspectPlatformColor) -> Color { #if os(iOS) - Color( - uiColor: UIColor { traits in - traits.userInterfaceStyle == .dark ? dark : light - } - ) + Color( + uiColor: UIColor { traits in + traits.userInterfaceStyle == .dark ? dark : light + } + ) #elseif os(macOS) - Color( - nsColor: NSColor(name: nil) { appearance in - appearance.bestMatch(from: [.darkAqua, .vibrantDark]) != nil ? dark : light - } - ) + Color( + nsColor: NSColor(name: nil) { appearance in + appearance.bestMatch(from: [.darkAqua, .vibrantDark]) != nil ? dark : light + } + ) #endif } @@ -42,75 +44,75 @@ enum InspectPlatform { static var cardFillDark: InspectPlatformColor { #if os(iOS) - UIColor.secondarySystemGroupedBackground.withAlphaComponent(0.92) + UIColor.secondarySystemGroupedBackground.withAlphaComponent(0.92) #elseif os(macOS) - NSColor( - red: 0.11, - green: 0.13, - blue: 0.18, - alpha: 0.94 - ) + NSColor( + red: 0.11, + green: 0.13, + blue: 0.18, + alpha: 0.94 + ) #endif } static var chromeFillDark: InspectPlatformColor { #if os(iOS) - UIColor.tertiarySystemGroupedBackground.withAlphaComponent(0.98) + UIColor.tertiarySystemGroupedBackground.withAlphaComponent(0.98) #elseif os(macOS) - NSColor( - red: 0.16, - green: 0.18, - blue: 0.24, - alpha: 0.98 - ) + NSColor( + red: 0.16, + green: 0.18, + blue: 0.24, + alpha: 0.98 + ) #endif } static var chromeMutedFillDark: InspectPlatformColor { #if os(iOS) - UIColor.secondarySystemGroupedBackground.withAlphaComponent(0.98) + UIColor.secondarySystemGroupedBackground.withAlphaComponent(0.98) #elseif os(macOS) - NSColor( - red: 0.20, - green: 0.22, - blue: 0.29, - alpha: 0.96 - ) + NSColor( + red: 0.20, + green: 0.22, + blue: 0.29, + alpha: 0.96 + ) #endif } static var groupedBackground: Color { #if os(iOS) - Color(uiColor: .systemGroupedBackground) + Color(uiColor: .systemGroupedBackground) #elseif os(macOS) - Color(nsColor: .windowBackgroundColor) + Color(nsColor: .windowBackgroundColor) #endif } static var secondaryGroupedBackground: Color { #if os(iOS) - Color(uiColor: .secondarySystemGroupedBackground) + Color(uiColor: .secondarySystemGroupedBackground) #elseif os(macOS) - Color(nsColor: .controlBackgroundColor) + Color(nsColor: .controlBackgroundColor) #endif } static func pasteboardString() -> String? { #if os(iOS) - UIPasteboard.general.string + UIPasteboard.general.string #elseif os(macOS) - NSPasteboard.general.string(forType: .string) + NSPasteboard.general.string(forType: .string) #endif } @MainActor static func copyToPasteboard(_ value: String) { #if os(iOS) - UIPasteboard.general.string = value - UINotificationFeedbackGenerator().notificationOccurred(.success) + UIPasteboard.general.string = value + UINotificationFeedbackGenerator().notificationOccurred(.success) #elseif os(macOS) - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(value, forType: .string) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(value, forType: .string) #endif } @@ -120,17 +122,17 @@ enum InspectPlatform { static var topBarLeadingPlacement: ToolbarItemPlacement { #if os(iOS) - .topBarLeading + .topBarLeading #elseif os(macOS) - .automatic + .automatic #endif } static var topBarTrailingPlacement: ToolbarItemPlacement { #if os(iOS) - .topBarTrailing + .topBarTrailing #elseif os(macOS) - .automatic + .automatic #endif } } @@ -138,9 +140,9 @@ enum InspectPlatform { extension Image { init(platformImage: InspectPlatformImage) { #if os(iOS) - self.init(uiImage: platformImage) + self.init(uiImage: platformImage) #elseif os(macOS) - self.init(nsImage: platformImage) + self.init(nsImage: platformImage) #endif } } @@ -148,61 +150,57 @@ extension Image { extension View { func inspectPlatformURLField() -> some View { #if os(iOS) - self - .textInputAutocapitalization(.never) - .keyboardType(.URL) - .autocorrectionDisabled() - .submitLabel(.go) + textInputAutocapitalization(.never) + .keyboardType(.URL) + .autocorrectionDisabled() + .submitLabel(.go) #elseif os(macOS) - self - .autocorrectionDisabled() - .submitLabel(.go) + autocorrectionDisabled() + .submitLabel(.go) #endif } func inspectInlineNavigationTitle() -> some View { #if os(iOS) - self.navigationBarTitleDisplayMode(.inline) + navigationBarTitleDisplayMode(.inline) #elseif os(macOS) - self + self #endif } func inspectNavigationBarVisible() -> some View { #if os(iOS) - self.toolbar(.visible, for: .navigationBar) + toolbar(.visible, for: .navigationBar) #elseif os(macOS) - self + self #endif } func inspectNavigationBarHidden() -> some View { #if os(iOS) - self.toolbar(.hidden, for: .navigationBar) + toolbar(.hidden, for: .navigationBar) #elseif os(macOS) - self + self #endif } func inspectGroupedListStyle(background: Color) -> some View { #if os(iOS) - self - .listStyle(.insetGrouped) - .scrollContentBackground(.hidden) - .background(background) + listStyle(.insetGrouped) + .scrollContentBackground(.hidden) + .background(background) #elseif os(macOS) - self - .listStyle(.inset) - .scrollContentBackground(.hidden) - .background(background) + listStyle(.inset) + .scrollContentBackground(.hidden) + .background(background) #endif } func inspectScrollDismissesKeyboard() -> some View { #if os(iOS) - self.scrollDismissesKeyboard(.immediately) + scrollDismissesKeyboard(.immediately) #elseif os(macOS) - self + self #endif } } diff --git a/Packages/InspectCore/Sources/Feature/Shared/InspectSafariController.swift b/Packages/InspectCore/Sources/Feature/Shared/InspectSafariController.swift index cee432a..4c87378 100644 --- a/Packages/InspectCore/Sources/Feature/Shared/InspectSafariController.swift +++ b/Packages/InspectCore/Sources/Feature/Shared/InspectSafariController.swift @@ -1,34 +1,34 @@ #if !os(macOS) -import SafariServices -import SwiftUI + import SafariServices + import SwiftUI -struct InspectSafariController: UIViewControllerRepresentable { - let url: URL + struct InspectSafariController: UIViewControllerRepresentable { + let url: URL - func makeUIViewController(context: Context) -> SFSafariViewController { - let controller = SFSafariViewController(url: url) - controller.preferredControlTintColor = UIColor(Color.inspectAccent) - return controller - } + func makeUIViewController(context _: Context) -> SFSafariViewController { + let controller = SFSafariViewController(url: url) + controller.preferredControlTintColor = UIColor(Color.inspectAccent) + return controller + } - func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {} -} + func updateUIViewController(_: SFSafariViewController, context _: Context) {} + } -extension View { - func inspectSafariSheet(url: URL?, isPresented: Binding) -> some View { - sheet(isPresented: isPresented) { - if let url { - InspectSafariController(url: url) + extension View { + func inspectSafariSheet(url: URL?, isPresented: Binding) -> some View { + sheet(isPresented: isPresented) { + if let url { + InspectSafariController(url: url) + } } } } -} #else -import SwiftUI + import SwiftUI -extension View { - func inspectSafariSheet(url: URL?, isPresented: Binding) -> some View { - self + extension View { + func inspectSafariSheet(url _: URL?, isPresented _: Binding) -> some View { + self + } } -} #endif diff --git a/Packages/InspectCore/Sources/Feature/Shared/InspectSection.swift b/Packages/InspectCore/Sources/Feature/Shared/InspectSection.swift index c18a221..bef5298 100644 --- a/Packages/InspectCore/Sources/Feature/Shared/InspectSection.swift +++ b/Packages/InspectCore/Sources/Feature/Shared/InspectSection.swift @@ -5,9 +5,13 @@ public enum InspectSection: String, CaseIterable, Hashable, Identifiable, Sendab case monitor = "Monitor" case settings = "Settings" - public var id: String { rawValue } + public var id: String { + rawValue + } - public var title: String { rawValue } + public var title: String { + rawValue + } public var systemImage: String { switch self { diff --git a/Packages/InspectCore/Sources/Feature/Shared/InspectionCommonStrings.swift b/Packages/InspectCore/Sources/Feature/Shared/InspectionCommonStrings.swift index 05e9851..67f981a 100644 --- a/Packages/InspectCore/Sources/Feature/Shared/InspectionCommonStrings.swift +++ b/Packages/InspectCore/Sources/Feature/Shared/InspectionCommonStrings.swift @@ -1,6 +1,6 @@ public enum InspectionCommonStrings { - public static let yes = "Yes" - public static let no = "No" + public static let yesLabel = "Yes" + public static let noLabel = "No" public enum VPNStatus { public static let invalid = "Invalid" diff --git a/Packages/InspectCore/Sources/Feature/Theme/InspectSurfaceViews.swift b/Packages/InspectCore/Sources/Feature/Theme/InspectSurfaceViews.swift index b9ccd14..e2946c9 100644 --- a/Packages/InspectCore/Sources/Feature/Theme/InspectSurfaceViews.swift +++ b/Packages/InspectCore/Sources/Feature/Theme/InspectSurfaceViews.swift @@ -6,7 +6,7 @@ struct InspectBackground: View { colors: [ .inspectBackgroundStart, .inspectBackgroundMiddle, - .inspectBackgroundEnd + .inspectBackgroundEnd, ], startPoint: .topLeading, endPoint: .bottomTrailing diff --git a/Packages/InspectCore/Sources/Feature/Theme/InspectionViewModifiers.swift b/Packages/InspectCore/Sources/Feature/Theme/InspectionViewModifiers.swift index 000433e..9df0f72 100644 --- a/Packages/InspectCore/Sources/Feature/Theme/InspectionViewModifiers.swift +++ b/Packages/InspectCore/Sources/Feature/Theme/InspectionViewModifiers.swift @@ -8,7 +8,7 @@ extension View { @ViewBuilder func applyExtensionScrollMargins(_ presentation: InspectionPresentation) -> some View { if presentation == .actionExtension { - self.contentMargins(.top, 0, for: .scrollContent) + contentMargins(.top, 0, for: .scrollContent) } else { self } diff --git a/Packages/InspectCore/Sources/Feature/Theme/Layout.swift b/Packages/InspectCore/Sources/Feature/Theme/Layout.swift index 1f2debc..accbdcc 100644 --- a/Packages/InspectCore/Sources/Feature/Theme/Layout.swift +++ b/Packages/InspectCore/Sources/Feature/Theme/Layout.swift @@ -17,37 +17,37 @@ private struct PlatformValues { static let current: PlatformValues = { #if os(macOS) - PlatformValues( - rootStackSpacing: 16, - rootSideRailWidth: 380, - rootCompactContentMaxWidth: 1140, - rootCompactHorizontalPadding: 28, - inputCardSpacing: 12, - inputSampleColumnWidth: 320, - inputDemoTargetVerticalPadding: 6, - inputDemoTargetControlSize: .small, - inputPrompt: "Inspect a host name or HTTPS URL.", - inputPromptFont: .inspectRootCaptionSemibold, - inputUsesHorizontalCompactDemoTargets: true, - inputUsesInlineCompactInputControls: true, - diagnosticsContentMaxWidth: 920 - ) + PlatformValues( + rootStackSpacing: 16, + rootSideRailWidth: 380, + rootCompactContentMaxWidth: 1140, + rootCompactHorizontalPadding: 28, + inputCardSpacing: 12, + inputSampleColumnWidth: 320, + inputDemoTargetVerticalPadding: 6, + inputDemoTargetControlSize: .small, + inputPrompt: "Inspect a host name or HTTPS URL.", + inputPromptFont: .inspectRootCaptionSemibold, + inputUsesHorizontalCompactDemoTargets: true, + inputUsesInlineCompactInputControls: true, + diagnosticsContentMaxWidth: 920 + ) #else - PlatformValues( - rootStackSpacing: 18, - rootSideRailWidth: 360, - rootCompactContentMaxWidth: nil, - rootCompactHorizontalPadding: 20, - inputCardSpacing: 14, - inputSampleColumnWidth: 300, - inputDemoTargetVerticalPadding: 8, - inputDemoTargetControlSize: .regular, - inputPrompt: "Enter a host name or HTTPS URL.", - inputPromptFont: .inspectRootSubheadline, - inputUsesHorizontalCompactDemoTargets: false, - inputUsesInlineCompactInputControls: false, - diagnosticsContentMaxWidth: 760 - ) + PlatformValues( + rootStackSpacing: 18, + rootSideRailWidth: 360, + rootCompactContentMaxWidth: nil, + rootCompactHorizontalPadding: 20, + inputCardSpacing: 14, + inputSampleColumnWidth: 300, + inputDemoTargetVerticalPadding: 8, + inputDemoTargetControlSize: .regular, + inputPrompt: "Enter a host name or HTTPS URL.", + inputPromptFont: .inspectRootSubheadline, + inputUsesHorizontalCompactDemoTargets: false, + inputUsesInlineCompactInputControls: false, + diagnosticsContentMaxWidth: 760 + ) #endif }() } @@ -63,9 +63,9 @@ enum InspectLayout { } #if os(macOS) - return true + return true #else - return horizontalSizeClass == .regular + return horizontalSizeClass == .regular #endif } @@ -77,48 +77,88 @@ enum InspectLayout { usesRegularDashboardLayout ? 32 : PlatformValues.current.rootCompactHorizontalPadding } - static var stackSpacing: CGFloat { PlatformValues.current.rootStackSpacing } - static var sideRailWidth: CGFloat { PlatformValues.current.rootSideRailWidth } + static var stackSpacing: CGFloat { + PlatformValues.current.rootStackSpacing + } + + static var sideRailWidth: CGFloat { + PlatformValues.current.rootSideRailWidth + } } enum Input { static func usesRegularWidthLayout(horizontalSizeClass: UserInterfaceSizeClass?) -> Bool { #if os(macOS) - return true + return true #else - return horizontalSizeClass == .regular + return horizontalSizeClass == .regular #endif } - static var usesHorizontalCompactDemoTargets: Bool { PlatformValues.current.inputUsesHorizontalCompactDemoTargets } - static var usesInlineCompactInputControls: Bool { PlatformValues.current.inputUsesInlineCompactInputControls } - static var inputPrompt: String { PlatformValues.current.inputPrompt } - static var promptFont: Font { PlatformValues.current.inputPromptFont } - static var cardSpacing: CGFloat { PlatformValues.current.inputCardSpacing } - static var sampleColumnWidth: CGFloat { PlatformValues.current.inputSampleColumnWidth } - static var demoTargetVerticalPadding: CGFloat { PlatformValues.current.inputDemoTargetVerticalPadding } - static var demoTargetControlSize: ControlSize { PlatformValues.current.inputDemoTargetControlSize } + static var usesHorizontalCompactDemoTargets: Bool { + PlatformValues.current.inputUsesHorizontalCompactDemoTargets + } + + static var usesInlineCompactInputControls: Bool { + PlatformValues.current.inputUsesInlineCompactInputControls + } + + static var inputPrompt: String { + PlatformValues.current.inputPrompt + } + + static var promptFont: Font { + PlatformValues.current.inputPromptFont + } + + static var cardSpacing: CGFloat { + PlatformValues.current.inputCardSpacing + } + + static var sampleColumnWidth: CGFloat { + PlatformValues.current.inputSampleColumnWidth + } + + static var demoTargetVerticalPadding: CGFloat { + PlatformValues.current.inputDemoTargetVerticalPadding + } + + static var demoTargetControlSize: ControlSize { + PlatformValues.current.inputDemoTargetControlSize + } } enum Monitor { static var usesInlineCardSearch: Bool { #if os(iOS) || os(macOS) - true + true #else - false + false #endif } - static var inlineSearchButtonSize: CGFloat { 32 } - static var scrollBottomContentPadding: CGFloat { 24 } + static var inlineSearchButtonSize: CGFloat { + 32 + } + + static var scrollBottomContentPadding: CGFloat { + 24 + } } enum Diagnostics { - static var contentMaxWidth: CGFloat { PlatformValues.current.diagnosticsContentMaxWidth } + static var contentMaxWidth: CGFloat { + PlatformValues.current.diagnosticsContentMaxWidth + } } enum Summary { - static var badgeSpacing: CGFloat { 10 } - static var maxBadgesPerRow: Int { 4 } + static var badgeSpacing: CGFloat { + 10 + } + + static var maxBadgesPerRow: Int { + 4 + } } } diff --git a/justfile b/justfile index cc76522..52c025b 100644 --- a/justfile +++ b/justfile @@ -4,6 +4,15 @@ mod rust default: @just --list --list-submodules +lint: + swiftlint lint --quiet Apps Packages/InspectCore/Sources + +format: + swiftformat Apps Packages/InspectCore/Sources + +update-ct-logs: + python3 scripts/update_ct_logs.py + generate: ./scripts/xcodegen_generate.sh diff --git a/project.local.yml.example b/project.local.yml.example index ccab7a2..806e0e7 100644 --- a/project.local.yml.example +++ b/project.local.yml.example @@ -4,4 +4,4 @@ settings: base: DEVELOPMENT_TEAM: V28VJH6B6S CODE_SIGN_STYLE: Automatic - + INSPECT_APP_GROUP_IDENTIFIER: group.v28vjh6b6s.inspect.monitor diff --git a/project.yml b/project.yml index 72e3a6a..1ebea0b 100644 --- a/project.yml +++ b/project.yml @@ -11,14 +11,10 @@ options: iOS: "18.0" macOS: "15.0" -configFiles: - Debug: Configs/Project.xcconfig - Release: Configs/Project.xcconfig - settings: base: - CURRENT_PROJECT_VERSION: 84 - MARKETING_VERSION: 2.5.1 + CURRENT_PROJECT_VERSION: 86 + MARKETING_VERSION: 2.5.2 SWIFT_VERSION: 6.0 TARGETED_DEVICE_FAMILY: "1,2" INSPECT_APP_GROUP_IDENTIFIER: group.in.fourplex.inspect.monitor diff --git a/scripts/update_ct_logs.py b/scripts/update_ct_logs.py index 41bd75f..7fbbb99 100755 --- a/scripts/update_ct_logs.py +++ b/scripts/update_ct_logs.py @@ -10,6 +10,8 @@ LOG_LIST_URL = "https://www.gstatic.com/ct/log_list/v3/log_list.json" OUTPUT = Path(__file__).resolve().parent.parent / "Packages/InspectCore/Sources/Core/KnownCTLogs.swift" +USABLE_STATES = {"usable", "qualified", "pending"} + def fetch_log_list(): with urllib.request.urlopen(LOG_LIST_URL) as resp: @@ -20,48 +22,61 @@ def build_entries(data): entries = [] for op in data.get("operators", []): op_name = op["name"] - for log in op.get("logs", []): - log_id_b64 = log["log_id"] - log_id_hex = base64.b64decode(log_id_b64).hex().upper() + all_logs = op.get("logs", []) + op.get("tiled_logs", []) + for log in all_logs: + state = log.get("state", {}) + if state and not (set(state.keys()) & USABLE_STATES): + continue + + log_id_bytes = base64.b64decode(log["log_id"]) description = log.get("description", "") display = description.removesuffix(" log").strip() if not display: display = f"{op_name} (unknown)" - entries.append((op_name, log_id_hex, display)) + entries.append((op_name, log_id_bytes, display)) return entries +def format_byte_literal(raw_bytes): + return ", ".join(f"0x{b:02X}" for b in raw_bytes) + + def render_swift(entries, today): - lines = [] - lines.append("// This file is auto-generated by scripts/update_ct_logs.py") - lines.append(f"// Source: {LOG_LIST_URL}") - lines.append("// Do not edit manually.") - lines.append("") - lines.append("enum KnownCTLogs {") - lines.append(" static func name(forLogID logID: [UInt8]) -> String {") - lines.append(" let hex = logID.inspectHexString(grouped: false)") - lines.append(" if let name = knownLogs[hex] {") - lines.append(" return name") - lines.append(" }") - lines.append(" let truncated = Array(logID.prefix(8)).inspectHexString(grouped: true)") - lines.append(' return "Unknown (\\(truncated)…)"') - lines.append(" }") - lines.append("") - lines.append(" // Log ID (SHA-256 of log's public key) → human-readable name") - lines.append(f" // Last updated: {today}") - lines.append(" static let knownLogs: [String: String] = [") + lines = [ + "// This file is auto-generated by scripts/update_ct_logs.py", + f"// Source: {LOG_LIST_URL}", + "// Do not edit manually.", + "", + "import Foundation", + "", + "enum KnownCTLogs {", + " static func name(forLogID logID: [UInt8]) -> String {", + " if let name = knownLogs[Data(logID)] {", + " return name", + " }", + " let truncated = Array(logID.prefix(8)).inspectHexString(grouped: true)", + ' return "Unknown (\\(truncated)…)"', + " }", + "", + " // Log ID (SHA-256 of log's public key) → human-readable name", + f" // Last updated: {today}", + " private static let knownLogs: [Data: String] = [", + ] current_op = None - for op_name, log_id_hex, display in entries: + for op_name, log_id_bytes, display in entries: if op_name != current_op: if current_op is not None: lines.append("") lines.append(f" // {op_name}") current_op = op_name - lines.append(f' "{log_id_hex}": "{display}",') + hex_id = log_id_bytes.hex().upper() + byte_lit = format_byte_literal(log_id_bytes) + lines.append(f" // {hex_id}") + lines.append(f' Data([{byte_lit}]): "{display}",') lines.append(" ]") lines.append("}")