Skip to content

Commit 41bf63f

Browse files
committed
Add LOB (Managed PKG) app deployment visibility via native MDM channel
Intune LOB apps deployed via Apple's native MDM InstallApplication command are invisible in IntuneMDMDaemon logs. This adds monitoring by reading from the macOS unified log, /var/log/install.log, and InstallHistory.plist. New data pipeline: - UnifiedLogReader: queries unified log via `log show --style json` with InstallApplication category predicate - InstallLogParser: parses /var/log/install.log for install records - InstallHistoryParser: parses InstallHistory.plist, detecting MDM installs via appstored process, mdmclient, and GUID-named installer entries - LOBCorrelationEngine: merges all three sources into LOBAppEvent objects, grouping by MDM command UUID New UI: - Three-tab source selector: Agent Apps | LOB Apps | All Apps - LOBSidebarView: event list with status/search filtering - LOBDetailView: lifecycle timeline, receipt info, combined log view - DeploymentLifecycleView: visual MDM Command > Download > Install > Verify - AllAppsOverviewView: combined timeline of both channels - Channel badges on all policy rows (Agent vs Managed LOB) Existing changes: - Models.swift: DeploymentChannel enum, deploymentChannel on PolicyExecution - LogParser.swift: removed AppPolicyResultsReporter filter, added channel - ClipLibrary.swift: deploymentChannel in snapshot persistence - ViewController.swift: source selector, LOB engine, conditional rendering Resolves #34
1 parent 11e7bb2 commit 41bf63f

18 files changed

Lines changed: 2585 additions & 15 deletions

IntuneLogWatch.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
1B35FC812EDD379500F4EE46 /* Exceptions for "IntuneLogWatch" folder in "IntuneLogWatchQuickLook" target */ = {
107107
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
108108
membershipExceptions = (
109+
AllAppsOverviewView.swift,
109110
AllLogEntriesView.swift,
110111
AppIconHelper.swift,
111112
AppIconProvider.swift,
@@ -115,18 +116,27 @@
115116
CertificateInspector.swift,
116117
ClipLibrary.swift,
117118
ClipLibraryView.swift,
119+
DeploymentLifecycleView.swift,
118120
ErrorCodeDetailView.swift,
119121
ErrorCodesReferenceViewSimple.swift,
122+
InstallHistoryParser.swift,
123+
InstallLogParser.swift,
120124
IntuneErrorCodes.swift,
121125
IntuneLogWatchApp.swift,
126+
LOBCorrelationEngine.swift,
127+
LOBDetailView.swift,
128+
LOBModels.swift,
129+
LOBSidebarView.swift,
122130
LogEntryDetailView.swift,
123131
LogParser.swift,
132+
LogSourceSelector.swift,
124133
Models.swift,
125134
PackageInspectorHelper.swift,
126135
PolicyDetailView.swift,
127136
PolicyExportHelper.swift,
128137
SyncEventDetailView.swift,
129138
TooltipView.swift,
139+
UnifiedLogReader.swift,
130140
ViewController.swift,
131141
);
132142
target = 1B35FC5B2EDD0A0200F4EE46 /* IntuneLogWatchQuickLook */;
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
//
2+
// AllAppsOverviewView.swift
3+
// IntuneLogWatch
4+
//
5+
// Combined view showing both LOB and Agent-deployed apps in a unified timeline.
6+
//
7+
8+
import SwiftUI
9+
10+
struct AllAppsOverviewView: View {
11+
let agentAnalysis: LogAnalysis?
12+
let lobAnalysis: LOBAnalysis?
13+
@State private var searchText = ""
14+
15+
var body: some View {
16+
VStack(spacing: 0) {
17+
overviewHeader
18+
Divider()
19+
20+
if combinedEntries.isEmpty {
21+
emptyState
22+
} else {
23+
searchBar
24+
entryList
25+
}
26+
}
27+
}
28+
29+
// MARK: - Header
30+
31+
private var overviewHeader: some View {
32+
VStack(alignment: .leading, spacing: 8) {
33+
HStack {
34+
Image(systemName: "apps.iphone.badge.plus")
35+
.foregroundColor(.blue)
36+
Text("All App Deployments")
37+
.font(.title3)
38+
.fontWeight(.semibold)
39+
Spacer()
40+
}
41+
42+
HStack(spacing: 24) {
43+
if let agent = agentAnalysis {
44+
let appPolicies = agent.syncEvents.flatMap { $0.policies }.filter { $0.type == .app }
45+
VStack(spacing: 2) {
46+
HStack(spacing: 4) {
47+
ChannelBadge(channel: .agent)
48+
Text("\(appPolicies.count)")
49+
.fontWeight(.semibold)
50+
}
51+
Text("Agent Apps")
52+
.font(.caption2)
53+
.foregroundColor(.secondary)
54+
}
55+
}
56+
57+
if let lob = lobAnalysis {
58+
VStack(spacing: 2) {
59+
HStack(spacing: 4) {
60+
ChannelBadge(channel: .managedLOB)
61+
Text("\(lob.totalEvents)")
62+
.fontWeight(.semibold)
63+
}
64+
Text("LOB Apps")
65+
.font(.caption2)
66+
.foregroundColor(.secondary)
67+
}
68+
}
69+
70+
Spacer()
71+
}
72+
}
73+
.padding()
74+
.background(Color(NSColor.controlBackgroundColor))
75+
}
76+
77+
// MARK: - Search
78+
79+
private var searchBar: some View {
80+
TextField("Search all apps...", text: $searchText)
81+
.textFieldStyle(RoundedBorderTextFieldStyle())
82+
.padding(.horizontal)
83+
.padding(.vertical, 8)
84+
}
85+
86+
// MARK: - Entry List
87+
88+
private var combinedEntries: [AllAppEntry] {
89+
var entries: [AllAppEntry] = []
90+
91+
// Add agent app policies
92+
if let agent = agentAnalysis {
93+
for syncEvent in agent.syncEvents {
94+
for policy in syncEvent.policies where policy.type == .app {
95+
entries.append(AllAppEntry(
96+
id: policy.id,
97+
name: policy.displayName,
98+
channel: .agent,
99+
status: policy.status.displayName,
100+
statusColor: agentStatusColor(policy.status),
101+
timestamp: policy.startTime ?? syncEvent.startTime,
102+
detail: [policy.appType, policy.appIntent].compactMap { $0 }.joined(separator: " / "),
103+
bundleId: policy.bundleId
104+
))
105+
}
106+
}
107+
}
108+
109+
// Add LOB events
110+
if let lob = lobAnalysis {
111+
for event in lob.events {
112+
entries.append(AllAppEntry(
113+
id: event.id,
114+
name: event.displayName,
115+
channel: .managedLOB,
116+
status: event.status.displayName,
117+
statusColor: lobStatusColor(event.status),
118+
timestamp: event.timestamp,
119+
detail: event.packageVersion.map { "v\($0)" },
120+
bundleId: event.packageId
121+
))
122+
}
123+
}
124+
125+
// Sort by timestamp (newest first)
126+
entries.sort { $0.timestamp > $1.timestamp }
127+
128+
// Filter
129+
if !searchText.isEmpty {
130+
entries = entries.filter {
131+
$0.name.localizedCaseInsensitiveContains(searchText) ||
132+
($0.bundleId?.localizedCaseInsensitiveContains(searchText) ?? false) ||
133+
($0.detail?.localizedCaseInsensitiveContains(searchText) ?? false)
134+
}
135+
}
136+
137+
return entries
138+
}
139+
140+
private var entryList: some View {
141+
List(combinedEntries) { entry in
142+
HStack {
143+
ChannelBadge(channel: entry.channel)
144+
145+
VStack(alignment: .leading, spacing: 2) {
146+
Text(entry.name)
147+
.font(.headline)
148+
.lineLimit(1)
149+
if let bundleId = entry.bundleId {
150+
Text(bundleId)
151+
.font(.caption)
152+
.foregroundColor(.secondary)
153+
.lineLimit(1)
154+
}
155+
}
156+
157+
Spacer()
158+
159+
if let detail = entry.detail {
160+
Text(detail)
161+
.font(.caption)
162+
.foregroundColor(.secondary)
163+
}
164+
165+
Text(entry.status)
166+
.font(.caption2)
167+
.padding(.horizontal, 6)
168+
.padding(.vertical, 2)
169+
.background(entry.statusColor.opacity(0.2))
170+
.foregroundColor(entry.statusColor)
171+
.cornerRadius(4)
172+
173+
Text(formatDateTime(entry.timestamp))
174+
.font(.caption)
175+
.foregroundColor(.secondary)
176+
}
177+
.padding(.vertical, 2)
178+
}
179+
}
180+
181+
// MARK: - Empty State
182+
183+
private var emptyState: some View {
184+
VStack(spacing: 12) {
185+
Spacer()
186+
Image(systemName: "apps.iphone")
187+
.font(.system(size: 40))
188+
.foregroundColor(.secondary)
189+
Text("No App Deployments Found")
190+
.font(.headline)
191+
.foregroundColor(.secondary)
192+
Text("Load Agent logs and/or LOB data to see a combined view of all app deployments.")
193+
.font(.caption)
194+
.foregroundColor(.secondary)
195+
.multilineTextAlignment(.center)
196+
Spacer()
197+
}
198+
.padding()
199+
}
200+
201+
// MARK: - Helpers
202+
203+
private func agentStatusColor(_ status: PolicyStatus) -> Color {
204+
switch status {
205+
case .completed: return .green
206+
case .failed: return .red
207+
case .warning: return .orange
208+
case .running: return .blue
209+
case .pending: return .secondary
210+
}
211+
}
212+
213+
private func lobStatusColor(_ status: LOBDeploymentStatus) -> Color {
214+
switch status {
215+
case .completed: return .green
216+
case .failed: return .red
217+
case .installing, .downloading: return .blue
218+
case .pending: return .orange
219+
case .unknown: return .secondary
220+
}
221+
}
222+
223+
private func formatDateTime(_ date: Date) -> String {
224+
let formatter = DateFormatter()
225+
formatter.dateStyle = .short
226+
formatter.timeStyle = .short
227+
return formatter.string(from: date)
228+
}
229+
}
230+
231+
// MARK: - All App Entry Model
232+
233+
struct AllAppEntry: Identifiable {
234+
let id: UUID
235+
let name: String
236+
let channel: DeploymentChannel
237+
let status: String
238+
let statusColor: Color
239+
let timestamp: Date
240+
let detail: String?
241+
let bundleId: String?
242+
}

IntuneLogWatch/ClipLibrary.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ struct PolicyExecutionSnapshot: Codable, Hashable {
6262
let scriptType: String?
6363
let executionContext: String?
6464
let healthDomain: String?
65+
let deploymentChannel: String?
6566

6667
init(from policy: PolicyExecution) {
6768
self.policyId = policy.policyId
@@ -83,6 +84,7 @@ struct PolicyExecutionSnapshot: Codable, Hashable {
8384
self.scriptType = policy.scriptType
8485
self.executionContext = policy.executionContext
8586
self.healthDomain = policy.healthDomain
87+
self.deploymentChannel = policy.deploymentChannel.rawValue
8688
}
8789

8890
// Convert back to PolicyExecution for viewing
@@ -97,6 +99,7 @@ struct PolicyExecutionSnapshot: Codable, Hashable {
9799
scriptType: scriptType,
98100
executionContext: executionContext,
99101
healthDomain: healthDomain,
102+
deploymentChannel: DeploymentChannel(rawValue: deploymentChannel ?? "Agent") ?? .agent,
100103
status: status.toPolicyStatus(),
101104
startTime: startTime,
102105
endTime: endTime,

0 commit comments

Comments
 (0)