Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,31 @@ For getting started with Support Companion, please refer to the [Wiki](https://g
## Overview

### Tray Menu
<img width="1920" alt="sctray2 1" src="https://github.com/user-attachments/assets/b8a0ab92-ba5d-4270-9b88-3e58b64f94a8">
<img width="1513" alt="Screenshot 2025-11-04 at 14 35 40" src="https://github.com/user-attachments/assets/4f3e9d46-8498-4116-a37b-5b972ec8bfc8" />

### Home
<img width="1513" alt="1_home" src="https://github.com/user-attachments/assets/ddf7b35d-5921-47b7-ac72-b17d2ead8211">
<img width="1513" alt="Screenshot_2025-11-04_at_14_36_26" src="https://github.com/user-attachments/assets/d0caf681-9069-4cbb-b206-434a23dd0822" />

### Identity
<img width="1513" alt="2_Identity" src="https://github.com/user-attachments/assets/557e14cf-ba13-4754-ac27-3eec2e37dc53">
<img width="1513" alt="Screenshot 2025-11-04 at 14 36 49" src="https://github.com/user-attachments/assets/712eb394-34a1-4d2a-8522-53901bce153b" />

### Apps
<img width="1513" alt="3_Apps" src="https://github.com/user-attachments/assets/355c0624-43f0-4516-9177-def489203a35">
<img width="1513" alt="Screenshot_2025-11-04_at_14_37_08" src="https://github.com/user-attachments/assets/edee1e90-df0f-4403-8d0e-dc03f9cccd0b" />

### Self Service
<img width="1513" alt="4_SelfService" src="https://github.com/user-attachments/assets/ca0e520a-d941-41c1-bbd5-93db3e93f236">

### Company Portal
<img width="1513" alt="5_CompPortal" src="https://github.com/user-attachments/assets/2cdcb2af-7cf7-47f4-a044-8d722dd85232">
<img width="1513" alt="Screenshot 2025-11-04 at 14 37 23" src="https://github.com/user-attachments/assets/78d38dcf-38ad-4f09-9f66-ac3a4dfe7102" />

### KB
<img width="1513" alt="6_KB" src="https://github.com/user-attachments/assets/bdaaf50d-84ff-4505-983f-334b29498fc8">
<img width="1513" alt="Screenshot 2025-11-04 at 14 37 42" src="https://github.com/user-attachments/assets/29042dfe-22ce-4470-b882-94acbfaf41c0" />

### Desktop Info
<img width="1800" alt="7_Desktop" src="https://github.com/user-attachments/assets/9f104a8e-2298-4582-acb7-3ef68455ab00">
<img width="1513" alt="Screenshot_2025-11-04_at_14_38_25" src="https://github.com/user-attachments/assets/7eabd11c-400e-4804-8607-b5ec88c1155d" />

### Notifications
<img width="365" alt="8_Notifications" src="https://github.com/user-attachments/assets/d4faef1c-624c-444e-8020-deae6d4f074d">

### Company Portal
<img width="1513" alt="5_CompPortal" src="https://github.com/user-attachments/assets/2cdcb2af-7cf7-47f4-a044-8d722dd85232">

## Credits
[woodys-findings](https://www.woodys-findings.com/posts/cocoa-implement-privileged-helper) for privileged helper code
166 changes: 98 additions & 68 deletions SupportCompanion/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,61 @@ class Preferences: ObservableObject {
case generic = "LastGenericNotificationTime"
case appUpdate = "LastAppUpdateNotificationTime"
}

// MARK: - Notifications

@AppStorage(NotificationType.softwareUpdate.rawValue) var lastSoftwareUpdateNotificationTime: String = ""

@AppStorage(NotificationType.rebootReminder.rawValue) var lastRebootReminderNotificationTime: String = ""

@AppStorage(NotificationType.generic.rawValue) var lastGenericNotificationTime: String = ""

@AppStorage(NotificationType.appUpdate.rawValue) var lastAppUpdateNotificationTime: String = ""

@AppStorage("NotificationTitle") var notificationTitle: String = "Support Companion"

@AppStorage("NotificationInterval") var notificationInterval: Int = 4

@AppStorage("NotifcationImage") var notificationImage: String = ""

@AppStorage("SoftwareUpdateNotificationButtonText") var softwareUpdateNotificationButtonText: String = Constants.Notifications.SoftwareUpdate.UpdateNotificationButtonText

@AppStorage("SoftwareUpdateNotificationCommand") var softwareUpdateNotificationCommand: String = "open \(Constants.Panels.softwareUpdates)"

@AppStorage("SoftwareUpdateNotificationMessage") var softwareUpdateNotificationMessage: String = Constants.Notifications.SoftwareUpdate.UpdateNotificationMessage

@AppStorage("AppUpdateNotificationMessage") var appUpdateNotificationMessage: String = Constants.Notifications.AppUpdate.UpdateNotificationMessage

@AppStorage("AppUpdateNotificationButtonText") var appUpdateNotificationButtonText: String = Constants.Notifications.AppUpdate.UpdateNotificationButtonText

@AppStorage("AppUpdateNotificationCommand") var appUpdateNotificationCommand: String = ""

@AppStorage("RebootReminderDays") var rebootReminderDays: Int = 0

// MARK: - branding

@AppStorage("BrandName") var brandName: String = "Support Companion"

@AppStorage("BrandLogo") var brandLogo: String = ""

@AppStorage("BrandLogoLight") var brandLogoLight: String = ""

@AppStorage("AccentColor") var accentColor: String?

// MARK: - Menu

@AppStorage("MenuShowIdentity") var menuShowIdentity: Bool = true

@AppStorage("MenuShowApps") var menuShowApps: Bool = true

@AppStorage("MenuShowSelfService") var menuShowSelfService: Bool = true

@AppStorage("CompanyPortalUrl") var companyPortalUrl: String = ""

@AppStorage("MenuShowCompanyPortal") var menuShowCompanyPortal: Bool = true

@AppStorage("MenuShowKnowledgeBase") var menuShowKnowledgeBase: Bool = true

@AppStorage("KnowledgeBaseUrl") var knowledgeBaseUrl: String = ""

@AppStorage("ShowLogoInTrayMenu") var showLogoInTrayMenu: Bool = true
Expand All @@ -78,52 +80,52 @@ class Preferences: ObservableObject {
@AppStorage("MarkdownMenuLabel") var markdownMenuLabel: String = ""

@AppStorage("MarkdownMenuIcon") var markdownMenuIcon: String = ""

@AppStorage("CustomCardsMenuLabel") var customCardsMenuLabel: String = ""

@AppStorage("CustomCardsMenuIcon") var customCardsMenuIcon: String = ""

@AppStorage("TrayMenuBrandingIcon") var trayMenuBrandingIcon: String = ""

@AppStorage("TrayMenuShowIcon") var trayMenuShowIcon: Bool = true

// MARK: - Actions

@AppStorage("SupportPageUrl") var supportPageURL: String = ""

@AppStorage("ChangePasswordMode") var changePasswordMode: String = ""

@AppStorage("ChangePasswordUrl") var changePasswordUrl: String = ""

@AppStorage("Mode") var mode: String = ""

@Published var actions: [Action] = []

@Published var hiddenActions: [String] = UserDefaults.standard.array(forKey: "HiddenActions") as? [String] ?? []

@Published var logFolders: [String] = UserDefaults.standard.array(forKey: "LogFolders") as? [String] ?? []

@Published var excludedLogFolders: [String] = UserDefaults.standard.array(forKey: "ExcludedLogFolders") as? [String] ?? []

@AppStorage("RequirePrivilegedActionAuthentication") var requirePrivilegedActionAuthentication: Bool = true

// MARK: - Desktop Info

@AppStorage("DesktopInfoBackgroundOpacity") var desktopInfoBackgroundOpacity: Double = 0.001

@AppStorage("DesktopInfoBackgroundFrosted") var desktopInfoBackgroundFrosted: Bool = false

@AppStorage("DesktopInfoWindowPosition") var desktopInfoWindowPosition: String = "LowerRight"
@Published var currentWindowPosition: String = "LowerRight"

@AppStorage("ShowDesktopInfo") var showDesktopInfo: Bool = false

@AppStorage("DesktopInfoFontSize") var desktopInfoFontSize: Int = 14

@AppStorage("DesktopInfoLevel") var desktopInfoLevel: Int = 4

@Published var desktopInfoHideItems: [String] = UserDefaults.standard.array(forKey: "DesktopInfoHideItems") as? [String] ?? []

// MARK: - Home

@AppStorage("CustomCardPath") var customCardPath: String = "" {
Expand All @@ -147,18 +149,18 @@ class Preferences: ObservableObject {
@Published var customCardPathPublished: String = ""

@Published var hiddenCards: [String] = UserDefaults.standard.array(forKey: "HiddenCards") as? [String] ?? []

private var cancellable: AnyCancellable?

private var cancellables = Set<AnyCancellable>()
// Watcher for ~/Library/Preferences to detect external defaults writes
private var prefsDirSource: DispatchSourceFileSystemObject?
private var prefsDirFD: Int32 = -1

// MARK: - Support info

@AppStorage("SupportEmail") var supportEmail: String = ""

@AppStorage("SupportPhone") var supportPhone: String = ""

// MARK: - Elevate privileges
Expand All @@ -184,7 +186,7 @@ class Preferences: ObservableObject {
@AppStorage("JamfLogPollHours") var jamfLogPollHours: Int = 36

var mdm: String = "Unknown"

init() {
ensureDefaultsInitialized()
startWatchingCustomCardPath()
Expand All @@ -200,7 +202,7 @@ class Preferences: ObservableObject {
}
}
.store(in: &cancellables)

// Observe changes to UserDefaults specifically for complex types
cancellable = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)
.receive(on: RunLoop.main)
Expand Down Expand Up @@ -291,18 +293,46 @@ class Preferences: ObservableObject {
let selfServiceExists = fileManager.fileExists(atPath: Constants.AppPaths.selfService)
let mscExists = fileManager.fileExists(atPath: Constants.AppPaths.MSC)
let mdmUrl = await getMDMUrl()

if mdmUrl != "Unknown" {
Logger.shared.logDebug("MDM URL detected: \(mdmUrl)")
if mdmUrl.contains("i.manage.microsoft.com") {
Logger.shared.logDebug("MDM URL contains i.manage.microsoft.com, setting mdm to Intune.")
mdm = "Intune"
} else if mdmUrl.contains("jamf") {
Logger.shared.logDebug("MDM URL contains jamf, setting mdm to Jamf.")
mdm = "Jamf"
}
}


if mdmUrl != "Unknown" {
Logger.shared.logDebug("MDM URL detected: \(mdmUrl)")

// Try to parse the URL and inspect the host
if let url = URL(string: mdmUrl),
let host = url.host?.lowercased() {

// Detect Intune via manage.microsoft.* host
let pattern = #"(^|\.)manage\.microsoft\.[a-z0-9-]{2,63}$"#
if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
let range = NSRange(host.startIndex..<host.endIndex, in: host)
if regex.firstMatch(in: host, options: [], range: range) != nil {
Logger.shared.logDebug("MDM host '\(host)' is a manage.microsoft.* endpoint, setting MDM to Intune.")
mdm = "Intune"
return
}
}

// Detect Jamf via host substring
if host.contains("jamf") {
Logger.shared.logDebug("MDM host '\(host)' contains 'jamf', setting MDM to Jamf.")
mdm = "Jamf"
return
}

} else {
// Fallback: work directly on the raw URL string if parsing fails
let lower = mdmUrl.lowercased()

if lower.contains("i.manage.microsoft.com") {
Logger.shared.logDebug("MDM URL contains i.manage.microsoft.com, setting MDM to Intune.")
mdm = "Intune"
} else if lower.contains("jamf") {
Logger.shared.logDebug("MDM URL contains jamf, setting MDM to Jamf.")
mdm = "Jamf"
}
}
}

if companyPortalExists && mscExists {
Logger.shared.logDebug("Both Munki and Company Portal paths exist, defaulting to Munki mode.")
mode = Constants.modes.munki
Expand Down Expand Up @@ -339,20 +369,20 @@ class Preferences: ObservableObject {
UserDefaults.standard.set(logFolders, forKey: "LogFolders")
Logger.shared.logDebug("Log folders saved to UserDefaults: \(logFolders)")
}

private func loadHiddenCards() {
// Fetch hidden cards asynchronously to avoid modifying `@Published` directly during a view update
DispatchQueue.main.async { [weak self] in
self?.hiddenCards = UserDefaults.standard.array(forKey: "HiddenCards") as? [String] ?? []
}
}

private func loadLogFolders() {
DispatchQueue.main.async { [weak self] in
self?.logFolders = UserDefaults.standard.array(forKey: "LogFolders") as? [String] ?? []
}
}

private func loadHiddenActions() {
DispatchQueue.main.async { [weak self] in
self?.hiddenActions = UserDefaults.standard.array(forKey: "HiddenActions") as? [String] ?? []
Expand All @@ -365,13 +395,13 @@ class Preferences: ObservableObject {
}
}


private func loadDesktopInfoHideItems() {
DispatchQueue.main.async { [weak self] in
self?.desktopInfoHideItems = UserDefaults.standard.array(forKey: "DesktopInfoHideItems") as? [String] ?? []
}
}

private func loadActions() {
DispatchQueue.main.async { [weak self] in
let actions = UserDefaults.standard.array(forKey: "Actions") as? [[String: Any]] ?? []
Expand All @@ -389,7 +419,7 @@ class Preferences: ObservableObject {
self?.actions = newActions
}
}

struct DefaultValues {
static let values: [String: Any] = [
"LastSoftwareUpdateNotificationTime": "",
Expand Down Expand Up @@ -437,7 +467,7 @@ class Preferences: ObservableObject {
}
}
}


func resetUserDefaults() {
let bundleIdentifier = "com.github.macadmins.SupportCompanion"
Expand Down Expand Up @@ -466,11 +496,11 @@ class Preferences: ObservableObject {
// Execute the write command
executeShellCommand(command: writeCommand)
}

Task {
await detectModeAndSetLogFolders()
}

Logger.shared.logDebug("Defaults have been reset using defaults write.")
}

Expand Down
Loading