Skip to content

Commit 53defb4

Browse files
committed
Initial release v1.0 - macOS persistence mechanism scanner
Features: - Enumerate all persistence mechanisms (LaunchDaemons, LaunchAgents, Login Items, Kexts, System Extensions, Privileged Helpers, Cron Jobs, MDM Profiles, Application Support) - Code signature verification with color-coded trust levels - Snapshot and timeline comparison - Disable/Enable items with admin privileges - Native SwiftUI interface
0 parents  commit 53defb4

36 files changed

Lines changed: 7109 additions & 0 deletions

.gitignore

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Build
2+
.build/
3+
DerivedData/
4+
*.xcodeproj/xcuserdata/
5+
*.xcworkspace/xcuserdata/
6+
7+
# macOS
8+
.DS_Store
9+
*.swp
10+
*~
11+
12+
# Temporary files
13+
GenerateIcon.swift
14+
icona.png
15+
AppIcon.icns
16+
17+
# App bundle (built separately)
18+
*.app/
19+
20+
# Package manager
21+
.swiftpm/
22+
Packages/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 pinperepette
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import Foundation
2+
import SwiftUI
3+
import Combine
4+
5+
/// Global application state
6+
@MainActor
7+
final class AppState: ObservableObject {
8+
/// Shared instance
9+
static let shared = AppState()
10+
11+
// MARK: - Scanning State
12+
13+
/// All discovered persistence items
14+
@Published var items: [PersistenceItem] = []
15+
16+
/// Items filtered by current selection
17+
@Published var filteredItems: [PersistenceItem] = []
18+
19+
/// Currently selected category
20+
@Published var selectedCategory: PersistenceCategory? = nil
21+
22+
/// Currently selected item
23+
@Published var selectedItem: PersistenceItem? = nil
24+
25+
/// Whether a scan is in progress
26+
@Published var isScanning: Bool = false
27+
28+
/// Current scan progress
29+
@Published var scanProgress: Double = 0
30+
31+
/// Currently scanning category
32+
@Published var currentScanCategory: PersistenceCategory? = nil
33+
34+
/// Last scan date
35+
@Published var lastScanDate: Date? = nil
36+
37+
// MARK: - Search & Filter
38+
39+
/// Search query
40+
@Published var searchQuery: String = ""
41+
42+
/// Current sort order
43+
@Published var sortOrder: SortOrder = .trustLevel
44+
45+
/// Current filter
46+
@Published var trustFilter: TrustLevel? = nil
47+
48+
/// Show only enabled items
49+
@Published var showOnlyEnabled: Bool = false
50+
51+
// MARK: - UI State
52+
53+
/// Whether to show snapshots sheet
54+
@Published var showSnapshotsSheet: Bool = false
55+
56+
/// Whether to skip FDA check (temporary)
57+
@Published var skipFDACheck: Bool = false
58+
59+
/// Sidebar collapsed state
60+
@Published var sidebarCollapsed: Bool = false
61+
62+
/// Detail panel collapsed state
63+
@Published var detailCollapsed: Bool = false
64+
65+
// MARK: - Snapshots
66+
67+
/// Available snapshots
68+
@Published var snapshots: [Snapshot] = []
69+
70+
/// Current snapshot being viewed
71+
@Published var currentSnapshot: Snapshot? = nil
72+
73+
// MARK: - Private
74+
75+
private let scanner = ScannerOrchestrator()
76+
private var cancellables = Set<AnyCancellable>()
77+
78+
private init() {
79+
setupBindings()
80+
loadSnapshots()
81+
}
82+
83+
private func setupBindings() {
84+
// Update filtered items when selection, search, or filter changes
85+
Publishers.CombineLatest4(
86+
$items,
87+
$selectedCategory,
88+
$searchQuery,
89+
$trustFilter
90+
)
91+
.combineLatest($showOnlyEnabled, $sortOrder)
92+
.debounce(for: .milliseconds(100), scheduler: RunLoop.main)
93+
.sink { [weak self] combined in
94+
let ((items, category, query, trustFilter), showOnlyEnabled, sortOrder) = combined
95+
self?.updateFilteredItems(
96+
items: items,
97+
category: category,
98+
query: query,
99+
trustFilter: trustFilter,
100+
showOnlyEnabled: showOnlyEnabled,
101+
sortOrder: sortOrder
102+
)
103+
}
104+
.store(in: &cancellables)
105+
106+
// Observe scanner state
107+
scanner.$isScanning
108+
.assign(to: &$isScanning)
109+
110+
scanner.$progress
111+
.assign(to: &$scanProgress)
112+
113+
scanner.$currentCategory
114+
.assign(to: &$currentScanCategory)
115+
}
116+
117+
private func updateFilteredItems(
118+
items: [PersistenceItem],
119+
category: PersistenceCategory?,
120+
query: String,
121+
trustFilter: TrustLevel?,
122+
showOnlyEnabled: Bool,
123+
sortOrder: SortOrder
124+
) {
125+
var filtered = items
126+
127+
// Filter by category
128+
if let category = category {
129+
filtered = filtered.filter { $0.category == category }
130+
}
131+
132+
// Filter by search query
133+
if !query.isEmpty {
134+
let lowercaseQuery = query.lowercased()
135+
filtered = filtered.filter { item in
136+
item.name.lowercased().contains(lowercaseQuery) ||
137+
item.identifier.lowercased().contains(lowercaseQuery) ||
138+
(item.signatureInfo?.organizationName?.lowercased().contains(lowercaseQuery) ?? false) ||
139+
(item.signatureInfo?.teamIdentifier?.lowercased().contains(lowercaseQuery) ?? false)
140+
}
141+
}
142+
143+
// Filter by trust level
144+
if let trustFilter = trustFilter {
145+
filtered = filtered.filter { $0.trustLevel == trustFilter }
146+
}
147+
148+
// Filter by enabled state
149+
if showOnlyEnabled {
150+
filtered = filtered.filter { $0.isEnabled }
151+
}
152+
153+
// Sort
154+
filtered = sortItems(filtered, by: sortOrder)
155+
156+
filteredItems = filtered
157+
}
158+
159+
private func sortItems(_ items: [PersistenceItem], by order: SortOrder) -> [PersistenceItem] {
160+
switch order {
161+
case .trustLevel:
162+
return items.sorted { $0.trustLevel < $1.trustLevel }
163+
case .name:
164+
return items.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
165+
case .category:
166+
return items.sorted { $0.category.displayName < $1.category.displayName }
167+
case .dateModified:
168+
return items.sorted {
169+
($0.plistModifiedAt ?? .distantPast) > ($1.plistModifiedAt ?? .distantPast)
170+
}
171+
case .vendor:
172+
return items.sorted {
173+
($0.signatureInfo?.organizationName ?? "zzz").localizedCaseInsensitiveCompare(
174+
$1.signatureInfo?.organizationName ?? "zzz"
175+
) == .orderedAscending
176+
}
177+
}
178+
}
179+
180+
// MARK: - Public Methods
181+
182+
/// Scan all categories
183+
func scanAll() async {
184+
let startTime = CFAbsoluteTimeGetCurrent()
185+
items = await scanner.scanAll()
186+
let elapsed = CFAbsoluteTimeGetCurrent() - startTime
187+
print("⚡️ Scan completed in \(String(format: "%.2f", elapsed)) seconds - Found \(items.count) items")
188+
lastScanDate = Date()
189+
190+
// Create automatic snapshot if first scan
191+
if snapshots.isEmpty {
192+
await createSnapshot(trigger: .firstLaunch)
193+
}
194+
}
195+
196+
/// Scan a specific category
197+
func scan(category: PersistenceCategory) async {
198+
let newItems = await scanner.scan(category: category)
199+
200+
// Update items for this category
201+
items.removeAll { $0.category == category }
202+
items.append(contentsOf: newItems)
203+
}
204+
205+
/// Create a manual snapshot
206+
func createManualSnapshot() async {
207+
await createSnapshot(trigger: .manual)
208+
}
209+
210+
/// Create a snapshot
211+
func createSnapshot(trigger: SnapshotTrigger, note: String? = nil) async {
212+
print("📸 Creating snapshot with \(items.count) items...")
213+
214+
let snapshot = Snapshot(
215+
trigger: trigger,
216+
note: note,
217+
itemCount: items.count
218+
)
219+
220+
do {
221+
try DatabaseManager.shared.saveSnapshot(snapshot, items: items)
222+
loadSnapshots()
223+
print("✅ Snapshot saved! Total snapshots: \(snapshots.count)")
224+
} catch {
225+
print("❌ Failed to save snapshot: \(error)")
226+
}
227+
}
228+
229+
/// Load snapshots from database
230+
func loadSnapshots() {
231+
do {
232+
let loaded = try DatabaseManager.shared.getAllSnapshots()
233+
let msg = "📂 Loaded \(loaded.count) snapshots from database\n"
234+
try? msg.write(toFile: "/tmp/mpc_debug.log", atomically: false, encoding: .utf8)
235+
snapshots = loaded
236+
} catch {
237+
let msg = "❌ Failed to load snapshots: \(error)\n"
238+
try? msg.write(toFile: "/tmp/mpc_debug.log", atomically: false, encoding: .utf8)
239+
}
240+
}
241+
242+
/// Get item counts by category
243+
func itemCount(for category: PersistenceCategory) -> Int {
244+
items.filter { $0.category == category }.count
245+
}
246+
247+
/// Get suspicious item count
248+
var suspiciousCount: Int {
249+
items.filter { $0.trustLevel == .unsigned || $0.trustLevel == .suspicious }.count
250+
}
251+
252+
/// Get total item count
253+
var totalCount: Int {
254+
items.count
255+
}
256+
}
257+
258+
// MARK: - Sort Order
259+
260+
enum SortOrder: String, CaseIterable, Identifiable {
261+
case trustLevel = "trust_level"
262+
case name = "name"
263+
case category = "category"
264+
case dateModified = "date_modified"
265+
case vendor = "vendor"
266+
267+
var id: String { rawValue }
268+
269+
var displayName: String {
270+
switch self {
271+
case .trustLevel: return "Trust Level"
272+
case .name: return "Name"
273+
case .category: return "Category"
274+
case .dateModified: return "Date Modified"
275+
case .vendor: return "Vendor"
276+
}
277+
}
278+
279+
var symbolName: String {
280+
switch self {
281+
case .trustLevel: return "shield"
282+
case .name: return "textformat"
283+
case .category: return "folder"
284+
case .dateModified: return "calendar"
285+
case .vendor: return "building.2"
286+
}
287+
}
288+
}

0 commit comments

Comments
 (0)