Skip to content

Commit 28be561

Browse files
committed
Release v1.3.0 - Risk Assessment, Timeline & Forensics
New Features: - Risk Assessment System: Automatic security risk scoring (0-100) for every persistence item - Signed-but-Dangerous Detection: Advanced analysis for signed but potentially malicious software - Timeline & Forensics: Complete forensic timeline with file creation, modification, and execution timestamps - Timestamp Anomaly Detection: Automatic detection of timestomping, file replacement, and binary swap attacks - MITRE ATT&CK Mapping: Every persistence mechanism linked to ATT&CK tactics and techniques - Interactive Graph Visualization: Full system graph and per-item focused graphs Improvements: - Faster, more responsive search functionality - Items sorted by risk score for quick threat identification - Enhanced detail view with comprehensive forensic information - Improved UI layout and organization Technical: - Added FileTimestampExtractor for forensic timestamp extraction - Added SignedButDangerousAnalyzer for advanced threat detection - Enhanced PersistenceItem model with forensic fields - Canvas-based graph rendering for better performance
0 parents  commit 28be561

62 files changed

Lines changed: 14429 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
*.iconset/
16+
.claude/
17+
18+
# App bundle (built separately)
19+
*.app/
20+
21+
# Package manager
22+
.swiftpm/
23+
Packages/

AppIcon.icns

1.84 MB
Binary file not shown.

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: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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 = .riskScore
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+
/// Item to show in focused graph view (nil = show full graph)
66+
@Published var focusedGraphItem: PersistenceItem? = nil
67+
68+
// MARK: - Snapshots
69+
70+
/// Available snapshots
71+
@Published var snapshots: [Snapshot] = []
72+
73+
/// Current snapshot being viewed
74+
@Published var currentSnapshot: Snapshot? = nil
75+
76+
// MARK: - Private
77+
78+
private let scanner = ScannerOrchestrator()
79+
private var cancellables = Set<AnyCancellable>()
80+
81+
private init() {
82+
setupBindings()
83+
loadSnapshots()
84+
}
85+
86+
private func setupBindings() {
87+
// Update filtered items when selection, search, or filter changes
88+
Publishers.CombineLatest4(
89+
$items,
90+
$selectedCategory,
91+
$searchQuery,
92+
$trustFilter
93+
)
94+
.combineLatest($showOnlyEnabled, $sortOrder)
95+
.debounce(for: .milliseconds(50), scheduler: RunLoop.main)
96+
.receive(on: RunLoop.main)
97+
.sink { [weak self] combined in
98+
let ((items, category, query, trustFilter), showOnlyEnabled, sortOrder) = combined
99+
self?.updateFilteredItems(
100+
items: items,
101+
category: category,
102+
query: query,
103+
trustFilter: trustFilter,
104+
showOnlyEnabled: showOnlyEnabled,
105+
sortOrder: sortOrder
106+
)
107+
}
108+
.store(in: &cancellables)
109+
110+
// Observe scanner state
111+
scanner.$isScanning
112+
.assign(to: &$isScanning)
113+
114+
scanner.$progress
115+
.assign(to: &$scanProgress)
116+
117+
scanner.$currentCategory
118+
.assign(to: &$currentScanCategory)
119+
}
120+
121+
private func updateFilteredItems(
122+
items: [PersistenceItem],
123+
category: PersistenceCategory?,
124+
query: String,
125+
trustFilter: TrustLevel?,
126+
showOnlyEnabled: Bool,
127+
sortOrder: SortOrder
128+
) {
129+
var filtered = items
130+
131+
// Filter by category
132+
if let category = category {
133+
filtered = filtered.filter { $0.category == category }
134+
}
135+
136+
// Filter by search query
137+
if !query.isEmpty {
138+
let lowercaseQuery = query.lowercased()
139+
filtered = filtered.filter { item in
140+
item.name.lowercased().contains(lowercaseQuery) ||
141+
item.identifier.lowercased().contains(lowercaseQuery) ||
142+
(item.signatureInfo?.organizationName?.lowercased().contains(lowercaseQuery) ?? false) ||
143+
(item.signatureInfo?.teamIdentifier?.lowercased().contains(lowercaseQuery) ?? false)
144+
}
145+
}
146+
147+
// Filter by trust level
148+
if let trustFilter = trustFilter {
149+
filtered = filtered.filter { $0.trustLevel == trustFilter }
150+
}
151+
152+
// Filter by enabled state
153+
if showOnlyEnabled {
154+
filtered = filtered.filter { $0.isEnabled }
155+
}
156+
157+
// Sort
158+
filtered = sortItems(filtered, by: sortOrder)
159+
160+
filteredItems = filtered
161+
}
162+
163+
private func sortItems(_ items: [PersistenceItem], by order: SortOrder) -> [PersistenceItem] {
164+
switch order {
165+
case .riskScore:
166+
return items.sorted { ($0.riskScore ?? 0) > ($1.riskScore ?? 0) }
167+
case .trustLevel:
168+
return items.sorted { $0.trustLevel < $1.trustLevel }
169+
case .name:
170+
return items.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
171+
case .category:
172+
return items.sorted { $0.category.displayName < $1.category.displayName }
173+
case .dateModified:
174+
return items.sorted {
175+
($0.plistModifiedAt ?? .distantPast) > ($1.plistModifiedAt ?? .distantPast)
176+
}
177+
case .vendor:
178+
return items.sorted {
179+
($0.signatureInfo?.organizationName ?? "zzz").localizedCaseInsensitiveCompare(
180+
$1.signatureInfo?.organizationName ?? "zzz"
181+
) == .orderedAscending
182+
}
183+
}
184+
}
185+
186+
// MARK: - Public Methods
187+
188+
/// Scan all categories
189+
func scanAll() async {
190+
let startTime = CFAbsoluteTimeGetCurrent()
191+
items = await scanner.scanAll()
192+
let elapsed = CFAbsoluteTimeGetCurrent() - startTime
193+
print("⚡️ Scan completed in \(String(format: "%.2f", elapsed)) seconds - Found \(items.count) items")
194+
lastScanDate = Date()
195+
196+
// Create automatic snapshot if first scan
197+
if snapshots.isEmpty {
198+
await createSnapshot(trigger: .firstLaunch)
199+
}
200+
}
201+
202+
/// Scan a specific category
203+
func scan(category: PersistenceCategory) async {
204+
let newItems = await scanner.scan(category: category)
205+
206+
// Update items for this category
207+
items.removeAll { $0.category == category }
208+
items.append(contentsOf: newItems)
209+
}
210+
211+
/// Create a manual snapshot
212+
func createManualSnapshot() async {
213+
await createSnapshot(trigger: .manual)
214+
}
215+
216+
/// Create a snapshot
217+
func createSnapshot(trigger: SnapshotTrigger, note: String? = nil) async {
218+
print("📸 Creating snapshot with \(items.count) items...")
219+
220+
let snapshot = Snapshot(
221+
trigger: trigger,
222+
note: note,
223+
itemCount: items.count
224+
)
225+
226+
do {
227+
try DatabaseManager.shared.saveSnapshot(snapshot, items: items)
228+
loadSnapshots()
229+
print("✅ Snapshot saved! Total snapshots: \(snapshots.count)")
230+
} catch {
231+
print("❌ Failed to save snapshot: \(error)")
232+
}
233+
}
234+
235+
/// Load snapshots from database
236+
func loadSnapshots() {
237+
do {
238+
let loaded = try DatabaseManager.shared.getAllSnapshots()
239+
let msg = "📂 Loaded \(loaded.count) snapshots from database\n"
240+
try? msg.write(toFile: "/tmp/mpc_debug.log", atomically: false, encoding: .utf8)
241+
snapshots = loaded
242+
} catch {
243+
let msg = "❌ Failed to load snapshots: \(error)\n"
244+
try? msg.write(toFile: "/tmp/mpc_debug.log", atomically: false, encoding: .utf8)
245+
}
246+
}
247+
248+
/// Get item counts by category
249+
func itemCount(for category: PersistenceCategory) -> Int {
250+
items.filter { $0.category == category }.count
251+
}
252+
253+
/// Get suspicious item count
254+
var suspiciousCount: Int {
255+
items.filter { $0.trustLevel == .unsigned || $0.trustLevel == .suspicious }.count
256+
}
257+
258+
/// Get total item count
259+
var totalCount: Int {
260+
items.count
261+
}
262+
}
263+
264+
// MARK: - Sort Order
265+
266+
enum SortOrder: String, CaseIterable, Identifiable {
267+
case riskScore = "risk_score"
268+
case trustLevel = "trust_level"
269+
case name = "name"
270+
case category = "category"
271+
case dateModified = "date_modified"
272+
case vendor = "vendor"
273+
274+
var id: String { rawValue }
275+
276+
var displayName: String {
277+
switch self {
278+
case .riskScore: return "Risk Score"
279+
case .trustLevel: return "Trust Level"
280+
case .name: return "Name"
281+
case .category: return "Category"
282+
case .dateModified: return "Date Modified"
283+
case .vendor: return "Vendor"
284+
}
285+
}
286+
287+
var symbolName: String {
288+
switch self {
289+
case .riskScore: return "exclamationmark.triangle"
290+
case .trustLevel: return "shield"
291+
case .name: return "textformat"
292+
case .category: return "folder"
293+
case .dateModified: return "calendar"
294+
case .vendor: return "building.2"
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)