Skip to content

Commit acfee9b

Browse files
feat(mechanisms): add standard user support and network check (#12)
## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix / Chore (PATCH) (non-breaking change which fixes an issue or does not add functionality) - [x] New feature (MINOR) (non-breaking change which adds functionality) - [ ] Breaking change (MAJOR) (fix or feature that would cause existing functionality to not work as expected) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Released version 1.1.0 with enhanced security settings and streamlined login flows. - Introduced proactive server connectivity checks and automatic management of user privileges for smoother operation. - Added automated cleanup tasks that run on system startup. - New methods for retrieving MDM server details and managing user privileges. - Added a launch agent for executing cleanup scripts. - **Chores** - Updated system configurations to support the latest macOS compatibility improvements. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 9cfa2aa commit acfee9b

5 files changed

Lines changed: 241 additions & 12 deletions

File tree

Bootstrap Buddy/Bootstrap Buddy.xcodeproj/project.pbxproj

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
ENABLE_HARDENED_RUNTIME = NO;
192192
ENABLE_STRICT_OBJC_MSGSEND = YES;
193193
ENABLE_TESTABILITY = YES;
194+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
194195
GCC_C_LANGUAGE_STANDARD = gnu11;
195196
GCC_DYNAMIC_NO_PIC = NO;
196197
GCC_NO_COMMON_BLOCKS = YES;
@@ -205,7 +206,7 @@
205206
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
206207
GCC_WARN_UNUSED_FUNCTION = YES;
207208
GCC_WARN_UNUSED_VARIABLE = YES;
208-
MACOSX_DEPLOYMENT_TARGET = 10.14;
209+
MACOSX_DEPLOYMENT_TARGET = 13.5;
209210
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
210211
MTL_FAST_MATH = YES;
211212
ONLY_ACTIVE_ARCH = YES;
@@ -252,6 +253,7 @@
252253
ENABLE_HARDENED_RUNTIME = NO;
253254
ENABLE_NS_ASSERTIONS = NO;
254255
ENABLE_STRICT_OBJC_MSGSEND = YES;
256+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
255257
GCC_C_LANGUAGE_STANDARD = gnu11;
256258
GCC_NO_COMMON_BLOCKS = YES;
257259
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -260,7 +262,7 @@
260262
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
261263
GCC_WARN_UNUSED_FUNCTION = YES;
262264
GCC_WARN_UNUSED_VARIABLE = YES;
263-
MACOSX_DEPLOYMENT_TARGET = 10.14;
265+
MACOSX_DEPLOYMENT_TARGET = 13.5;
264266
MTL_ENABLE_DEBUG_INFO = NO;
265267
MTL_FAST_MATH = YES;
266268
"OTHER_CODE_SIGN_FLAGS[sdk=*]" = "--timestamp";
@@ -285,8 +287,8 @@
285287
INFOPLIST_KEY_NSHumanReadableCopyright = "@ 2024 Inetum Polska Sp. z o.o";
286288
INFOPLIST_KEY_NSPrincipalClass = "";
287289
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Security/SecurityAgentPlugins";
288-
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
289-
MARKETING_VERSION = 1.0.0;
290+
MACOSX_DEPLOYMENT_TARGET = 13.5;
291+
MARKETING_VERSION = 1.1.0;
290292
OTHER_CODE_SIGN_FLAGS = "--timestamp";
291293
PRODUCT_BUNDLE_IDENTIFIER = "com.inetum.Bootstrap-Buddy";
292294
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -316,8 +318,8 @@
316318
INFOPLIST_KEY_NSHumanReadableCopyright = "@ 2024 Inetum Polska Sp. z o.o";
317319
INFOPLIST_KEY_NSPrincipalClass = "";
318320
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Security/SecurityAgentPlugins";
319-
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
320-
MARKETING_VERSION = 1.0.0;
321+
MACOSX_DEPLOYMENT_TARGET = 13.5;
322+
MARKETING_VERSION = 1.1.0;
321323
OTHER_CODE_SIGN_FLAGS = "--timestamp";
322324
PRODUCT_BUNDLE_IDENTIFIER = "com.inetum.Bootstrap-Buddy";
323325
PRODUCT_NAME = "$(TARGET_NAME)";

Bootstrap Buddy/Mechanisms/BBMechanism.swift

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
// https://github.com/grahamgilbert/crypt/
2525

2626
import Foundation
27+
import Network
2728
import Security
2829
import os.log
2930

@@ -105,11 +106,104 @@ class BBMechanism: NSObject {
105106
return s.replacingOccurrences(of: "\0", with: "") as NSString
106107
}
107108

108-
// profiles Errors
109-
private enum ProfilesError: Error {
110-
case profilesFailed(retCode: Int32)
111-
case outputPlistNull
112-
case outputPlistMalformed
109+
// Get MDM Server FQDN and port:
110+
func getMDMServerDetails() -> (fqdn: String, port: UInt16)? {
111+
os_log("Getting MDM server details…", log: BBMechanism.log, type: .default)
112+
let task = Process()
113+
task.launchPath = "/usr/libexec/mdmclient"
114+
task.arguments = ["DumpManagementStatus"]
115+
let pipe = Pipe()
116+
task.standardOutput = pipe
117+
task.standardError = Pipe()
118+
do {
119+
try task.run()
120+
task.waitUntilExit()
121+
122+
let exitStatus = task.terminationStatus
123+
if exitStatus != 0 {
124+
os_log("ERROR: mdmclient failed with non-zero exit status: %d", log: BBMechanism.log, type: .error, exitStatus)
125+
return nil
126+
}
127+
128+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
129+
130+
guard let output: String = String(data: data, encoding: String.Encoding.utf8) else {
131+
os_log("Unable to decode output.", log: BBMechanism.log, type: .error)
132+
return nil
133+
}
134+
135+
let regex = try NSRegularExpression(pattern: #"ServerURL = "(https?://[^"]+)""#, options: [])
136+
guard let match = regex.firstMatch(in: output, options: [], range: NSRange(output.startIndex..., in: output)) else {
137+
os_log("ServerURL not found.", log: BBMechanism.log, type: .error)
138+
return nil
139+
}
140+
141+
guard let matchRange = Range(match.range(at: 1), in: output) else {
142+
os_log("Invalid regex match, crashing prevented.", log: BBMechanism.log, type: .error)
143+
return nil
144+
}
145+
146+
let urlString = String(output[matchRange])
147+
os_log("Extracted ServerURL: %{public}@", log: BBMechanism.log, type: .debug, urlString)
148+
149+
if let urlComponents = URLComponents(string: urlString), let host = urlComponents.host {
150+
let port = urlComponents.port ?? 443
151+
os_log("Parsed FQDN: %{public}@, Port: %d", log: BBMechanism.log, type: .debug, host, port)
152+
return (fqdn: host, port: UInt16(port))
153+
} else {
154+
os_log("Failed to parse URL components.", log: BBMechanism.log, type: .error)
155+
}
156+
} catch {
157+
os_log("ERROR: %{public}@", log: BBMechanism.log, type: .error, error.localizedDescription)
158+
}
159+
return nil
160+
}
161+
162+
// Function to check if the FQDN is reachable on the given port with detailed logging
163+
func checkMDMReachability(fqdn: String, port: UInt16) -> (Bool) {
164+
os_log("Checking reachability of %{public}@ on port %{public}@…", log: BBMechanism.log,
165+
type: .default, String(fqdn), String(port))
166+
guard let nwPort = NWEndpoint.Port(rawValue: port) else {
167+
os_log("Invalid port number: %{public}@", log: BBMechanism.log, type: .error, String(port))
168+
return false
169+
}
170+
let connection = NWConnection(host: NWEndpoint.Host(fqdn), port: nwPort, using: .tcp)
171+
let semaphore = DispatchSemaphore(value: 0)
172+
let queue = DispatchQueue(label: "com.inetum.Bootstrap-Buddy.checkMDM")
173+
var isReachable = false
174+
connection.stateUpdateHandler = { state in
175+
queue.sync {
176+
switch state {
177+
case .setup:
178+
os_log("Connection setup initiated…", log: BBMechanism.log, type: .debug)
179+
case .waiting(let reason):
180+
os_log("Connection waiting: %{public}@", log: BBMechanism.log, type: .debug, reason.localizedDescription)
181+
case .preparing:
182+
os_log("Preparing connection…", log: BBMechanism.log, type: .debug)
183+
case .ready:
184+
os_log("Connection successful!", log: BBMechanism.log, type: .default)
185+
isReachable = true
186+
semaphore.signal()
187+
case .failed(let error):
188+
os_log("Connection failed: %{public}@", log: BBMechanism.log, type: .error, error.localizedDescription)
189+
semaphore.signal()
190+
case .cancelled:
191+
os_log("Connection was cancelled.", log: BBMechanism.log, type: .debug)
192+
default:
193+
os_log("Unexpected connection state: %{public}@", log: BBMechanism.log, type: .error, String(describing: state))
194+
}
195+
}
196+
}
197+
connection.start(queue: .global())
198+
// Wait up to 5 seconds for a response:
199+
let timeout = DispatchTime.now() + 5
200+
if semaphore.wait(timeout: timeout) == .timedOut {
201+
os_log("Timeout: No response received in 5 seconds.", log: BBMechanism.log, type: .error)
202+
connection.cancel()
203+
return false
204+
}
205+
connection.cancel()
206+
return isReachable
113207
}
114208

115209
// Check Bootstrap Token status, whether it's supported and escrowed:
@@ -121,6 +215,7 @@ class BBMechanism: NSObject {
121215
let pipe = Pipe()
122216
task.standardOutput = pipe
123217
task.launch()
218+
task.waitUntilExit()
124219
let data = pipe.fileHandleForReading.readDataToEndOfFile()
125220
guard let output: String = String(data: data, encoding: String.Encoding.utf8)
126221
else { return (false, false) }
@@ -147,6 +242,7 @@ class BBMechanism: NSObject {
147242
let pipe = Pipe()
148243
task.standardOutput = pipe
149244
task.launch()
245+
task.waitUntilExit()
150246
let data = pipe.fileHandleForReading.readDataToEndOfFile()
151247
guard let output: String = String(data: data, encoding: String.Encoding.utf8)
152248
else { return false }
@@ -158,4 +254,57 @@ class BBMechanism: NSObject {
158254
return false
159255
}
160256
}
257+
258+
// Check if user at login window is and admin:
259+
func checkAdminStatus(username: String) -> (Bool) {
260+
os_log("Checking if user \"%{public}@\" is an admin…", log: BBMechanism.log, type: .default, username)
261+
let task = Process()
262+
task.launchPath = "/usr/bin/dscl"
263+
task.arguments = [".", "read", "/Groups/admin", "GroupMembership"]
264+
let pipe = Pipe()
265+
task.standardOutput = pipe
266+
task.launch()
267+
task.waitUntilExit()
268+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
269+
guard let output: String = String(data: data, encoding: String.Encoding.utf8)
270+
else { return false }
271+
// Check if the username is in the output:
272+
os_log("dscl output: %{public}@", log: BBMechanism.log, type: .debug, output)
273+
os_log("Admin Status: %{public}@", log: BBMechanism.log, type: .debug, output.contains(username).description)
274+
return output.contains(username)
275+
}
276+
277+
// Elevate user to admin
278+
func elevateUser(username: String) throws {
279+
os_log("Temporarily adding user \"%{public}@\" to admin group…", log: BBMechanism.log, type: .default, username)
280+
let task = Process()
281+
task.launchPath = "/usr/bin/dscl"
282+
task.arguments = [".", "append", "/Groups/admin", "GroupMembership", username]
283+
task.launch()
284+
task.waitUntilExit()
285+
if task.terminationStatus != 0 {
286+
let termstatus = String(describing: task.terminationStatus)
287+
os_log(
288+
"User elevation failed with a non-zero exit status: %{public}@",
289+
log: BBMechanism.log, type: .error, termstatus)
290+
}
291+
os_log("User \"%{public}@\" elevated to admin.", log: BBMechanism.log, type: .default, username)
292+
}
293+
294+
// Demote user to standard
295+
func demoteUser(username: String) throws {
296+
os_log("Removing user \"%{public}@\" from admin group…", log: BBMechanism.log, type: .default, username)
297+
let task = Process()
298+
task.launchPath = "/usr/bin/dscl"
299+
task.arguments = [".", "delete", "/Groups/admin", "GroupMembership", username]
300+
task.launch()
301+
task.waitUntilExit()
302+
if task.terminationStatus != 0 {
303+
let termstatus = String(describing: task.terminationStatus)
304+
os_log(
305+
"User demotion failed with a non-zero exit status: %{public}@",
306+
log: BBMechanism.log, type: .error, termstatus)
307+
}
308+
os_log("User \"%{public}@\" demoted to standard.", log: BBMechanism.log, type: .default, username)
309+
}
161310
}

Bootstrap Buddy/Mechanisms/Invoke.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ class Invoke: BBMechanism {
3838
@objc func run() {
3939
os_log("Starting Bootstrap Buddy:Invoke", log: Invoke.log, type: .default)
4040

41+
// Check MDM reachability
42+
if let mdm = getMDMServerDetails() {
43+
let fqdn: String = mdm.fqdn
44+
let port: UInt16 = mdm.port
45+
46+
let reachable: Bool = checkMDMReachability(fqdn: fqdn, port: port)
47+
if !reachable {
48+
os_log("MDM server unreachable.", log: Invoke.log, type: .error)
49+
allowLogin()
50+
return
51+
}
52+
} else {
53+
os_log("Unable to retrieve MDM server details.", log: Invoke.log, type: .error)
54+
allowLogin()
55+
return
56+
}
57+
4158
// Get Bootstrap Token status
4259
let bootstrapstatus = getBootstrapStatus()
4360
let btSupported: Bool = bootstrapstatus.supported
@@ -75,6 +92,22 @@ class Invoke: BBMechanism {
7592
return
7693
}
7794

95+
// Check if the user at login window is an admin
96+
let adminUser: Bool = checkAdminStatus(username: username as String)
97+
98+
// Elevate if not an admin
99+
if !adminUser {
100+
do {
101+
try elevateUser(username: username as String)
102+
} catch {
103+
os_log(
104+
"Failed to add user to admin group: %{public}@", log: Invoke.log,
105+
type: .error, error.localizedDescription)
106+
allowLogin()
107+
return
108+
}
109+
}
110+
78111
// EscrowBootstrapToken is True, call profiles to escrow Bootstrap Token
79112
os_log("Escrowing Bootstrap Token…", log: Invoke.log, type: .default)
80113
do {
@@ -86,6 +119,17 @@ class Invoke: BBMechanism {
86119
error.localizedDescription)
87120
}
88121

122+
// Demote previously elevated user
123+
if !adminUser {
124+
do {
125+
try demoteUser(username: username as String)
126+
} catch {
127+
os_log(
128+
"ERROR: Failed to remove user from admin group: %{public}@", log: Invoke.log,
129+
type: .error, error.localizedDescription)
130+
}
131+
}
132+
89133
allowLogin()
90134
return
91135
}

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ Nothing yet.
3030
- First removal.
3131
- Second removal. -->
3232

33+
## [1.1.0] - 2025-03-20
34+
35+
### Added
36+
37+
- Temporary user account privileges elevation for successful Bootstrap Token escrow on standard accounts.
38+
- Vendor–agnostic MDM server reachability check.
39+
- Additional LaunchDaemon in scripts to facilitate frequent plugin testing.
40+
41+
### Changed
42+
43+
- Refactored code and updated project settings.
44+
3345
## [1.0.0] - 2024-10-19
3446

3547
### Added
@@ -57,7 +69,8 @@ Nothing yet.
5769

5870
- Initial DevOps setup.
5971

60-
[unreleased]: https://github.com/Inetum-Poland/bootstrap-buddy/compare/v1.0.0...HEAD
72+
[unreleased]: https://github.com/Inetum-Poland/bootstrap-buddy/compare/v1.1.0...HEAD
73+
[1.1.0]: https://github.com/Inetum-Poland/bootstrap-buddy/compare/v1.0.0...v1.1.0
6174
[1.0.0]: https://github.com/Inetum-Poland/bootstrap-buddy/compare/v0.0.3...v1.0.0
6275
[0.0.3]: https://github.com/Inetum-Poland/bootstrap-buddy/compare/v0.0.1...v0.0.3
6376
[0.0.1]: https://github.com/Inetum-Poland/bootstrap-buddy/releases/tag/v0.0.1
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>KeepAlive</key>
6+
<dict>
7+
<key>SuccessfulExit</key>
8+
<false/>
9+
</dict>
10+
<key>Label</key>
11+
<string>com.inetum.Bootstrap-Buddy.AuthDBTeardown</string>
12+
<key>ProgramArguments</key>
13+
<array>
14+
<string>/bin/bash</string>
15+
<string>-c</string>
16+
<string>/Library/Security/SecurityAgentPlugins/Bootstrap\ Buddy.bundle/Contents/Resources/AuthDBTeardown.sh</string>
17+
</array>
18+
<key>RunAtLoad</key>
19+
<true/>
20+
</dict>
21+
</plist>

0 commit comments

Comments
 (0)