diff options
| author | Christian Cleberg <[email protected]> | 2026-04-26 00:39:28 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-26 00:39:28 -0500 |
| commit | 237a72a03102319638c5b0572ca3ab543238b821 (patch) | |
| tree | 21b34e8b69994ac58ef1c8c49c05bc4a4b65c3b7 /DomainDig/ContentView.swift | |
| parent | cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5 (diff) | |
| download | domain-dig-237a72a03102319638c5b0572ca3ab543238b821.tar.gz domain-dig-237a72a03102319638c5b0572ca3ab543238b821.tar.bz2 domain-dig-237a72a03102319638c5b0572ca3ab543238b821.zip | |
DomainDig v3.5.0: Expand the Pro+ Data+ intelligence layer with deeper local historical context
and inferred enrichment.
- add derived intelligence fields for provider fingerprinting, classification,
ownership transitions, hosting transitions, subdomain history, risk signals,
and inferred timeline events
- expand DNS history beyond A/NS snapshots to retain A, AAAA, MX, NS, TXT, and
CNAME change state
- persist enriched intelligence in snapshots and history entries so analysis is
local-first and incremental
- add a dedicated Data+ Intelligence panel to current and historical domain
detail views
- surface intelligence events in timeline rows and include Data+ changes in diff
output
- preserve non-blocking inspection behavior by keeping enrichment additive to
the main lookup path
This makes Pro+ materially deeper for investigative workflows by improving
historical ownership visibility, infrastructure context, hosting change
detection, subdomain intelligence, and explainable risk signals.
Diffstat (limited to 'DomainDig/ContentView.swift')
| -rw-r--r-- | DomainDig/ContentView.swift | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index 3716f88..5a469e1 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -11,6 +11,7 @@ enum LookupInputMode: String, CaseIterable, Identifiable { enum ResultSection: String, Hashable { case domain + case intelligence case ownership case dns case web @@ -78,6 +79,10 @@ struct ContentView: View { .padding(.top, appDensity.metrics.cardSpacing) } } + if let report = viewModel.currentReport { + intelligenceSection(report: report) + .padding(.top, appDensity.metrics.sectionSpacing) + } domainOverviewSection .padding(.top, appDensity.metrics.sectionSpacing) ownershipSection @@ -415,6 +420,14 @@ struct ContentView: View { ) } + private func intelligenceSection(report: DomainReport) -> some View { + IntelligenceSectionView( + isCollapsed: sectionCollapsedBinding(.intelligence), + report: report, + showsPlaceholder: FeatureAccessService.currentTier != .proPlus + ) + } + private var ownershipSection: some View { OwnershipSectionView( isCollapsed: sectionCollapsedBinding(.ownership), @@ -1558,6 +1571,126 @@ struct OwnershipSectionView: View { } } +struct IntelligenceSectionView: View { + @Environment(\.appDensity) private var appDensity + @Binding var isCollapsed: Bool + let report: DomainReport + let showsPlaceholder: Bool + + var body: some View { + CollapsibleSectionView(title: "Data+ Intelligence", isCollapsed: $isCollapsed) { + CardView(allowsHorizontalScroll: false) { + if showsPlaceholder { + MessageRowView(text: "Richer intelligence history, hosting analysis, and risk signals are available in Pro+", isError: false) + } else { + if let provider = report.inferredProvider { + intelligenceBlock(title: "Infrastructure") { + LabeledValueRow(row: .init(label: "Provider", value: provider.name, tone: .primary)) + if !provider.evidence.isEmpty { + MessageRowView(text: provider.evidence.joined(separator: " • "), isError: false) + } + if !report.priorProviders.isEmpty { + LabeledValueRow(row: .init(label: "Prior", value: report.priorProviders.joined(separator: ", "), tone: .secondary)) + } + } + } + if let classification = report.domainClassification { + intelligenceBlock(title: "Classification") { + LabeledValueRow(row: .init(label: "Purpose", value: classification.kind.title, tone: .primary)) + MessageRowView(text: classification.reasons.joined(separator: " • "), isError: false) + } + } + intelligenceBlock(title: "Risk Signals") { + if report.riskSignals.isEmpty { + MessageRowView(text: "No material historical risk signals detected", isError: false) + } else { + ForEach(report.riskSignals.prefix(4)) { signal in + VStack(alignment: .leading, spacing: 3) { + Text(signal.title) + .font(appDensity.font(.caption, weight: .semibold)) + Text(signal.detail) + .font(appDensity.font(.caption2)) + .foregroundStyle(.secondary) + } + } + } + } + intelligenceBlock(title: "Ownership History") { + if report.ownershipTransitions.isEmpty { + MessageRowView(text: "No ownership transitions observed locally", isError: false) + } else { + ForEach(report.ownershipTransitions.prefix(4)) { event in + intelligenceEventRow(date: event.date, title: event.summary) + } + } + } + intelligenceBlock(title: "Hosting History") { + if report.hostingTransitions.isEmpty { + MessageRowView(text: "No hosting transitions observed locally", isError: false) + } else { + ForEach(report.hostingTransitions.prefix(4)) { event in + intelligenceEventRow(date: event.date, title: event.summary) + } + } + } + intelligenceBlock(title: "Subdomain Intelligence") { + if report.subdomainHistory.isEmpty { + MessageRowView(text: "No subdomain history available", isError: false) + } else { + ForEach(report.subdomainHistory.prefix(5)) { item in + VStack(alignment: .leading, spacing: 3) { + HStack { + Text(item.hostname) + .font(appDensity.font(.caption)) + Spacer() + if item.isEphemeral { + Text("Ephemeral") + .font(appDensity.font(.caption2)) + .foregroundStyle(.yellow) + } + } + Text("First \(item.firstSeen.formatted(date: .abbreviated, time: .omitted)) • Last \(item.lastSeen.formatted(date: .abbreviated, time: .omitted)) • Seen \(item.recurrenceCount)x") + .font(appDensity.font(.caption2)) + .foregroundStyle(.secondary) + } + } + } + } + intelligenceBlock(title: "Timeline") { + if report.intelligenceTimeline.isEmpty { + MessageRowView(text: "No inferred intelligence events yet", isError: false) + } else { + ForEach(report.intelligenceTimeline.prefix(5)) { event in + intelligenceEventRow(date: event.date, title: "\(event.title): \(event.detail)") + } + } + } + } + } + } + } + + @ViewBuilder + private func intelligenceBlock<Content: View>(title: String, @ViewBuilder content: () -> Content) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(appDensity.font(.subheadline, weight: .semibold)) + .foregroundStyle(.cyan) + content() + } + } + + private func intelligenceEventRow(date: Date, title: String) -> some View { + VStack(alignment: .leading, spacing: 3) { + Text(date.formatted(date: .abbreviated, time: .omitted)) + .font(appDensity.font(.caption2)) + .foregroundStyle(.secondary) + Text(title) + .font(appDensity.font(.caption)) + } + } +} + struct SubdomainsSectionView: View { @Environment(\.appDensity) private var appDensity @Binding var isCollapsed: Bool |
