Last active
August 8, 2025 23:48
-
-
Save andriitishchenko/c0442ed3b8ae8ed5570534c846a83b1c to your computer and use it in GitHub Desktop.
Simple Endpoint Security system extension to receive events
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // hostApp.swift | |
| // Main app (host app) | |
| /* | |
| // entitlements: | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>com.apple.developer.system-extension.install</key> | |
| <true/> | |
| <key>com.apple.security.app-sandbox</key> | |
| <true/> | |
| <key>com.apple.security.files.user-selected.read-only</key> | |
| <true/> | |
| </dict> | |
| </plist> | |
| */ | |
| /* | |
| CHANGE TO YOUR ID: | |
| 1. com.example.esextension | |
| 2. esextension.appex | |
| */ | |
| import SwiftUI | |
| import SystemExtensions | |
| import os | |
| @main | |
| struct esseApp: App { | |
| @StateObject private var installer = SystemExtensionInstaller.shared | |
| var body: some Scene { | |
| WindowGroup { | |
| ContentView() | |
| .onAppear { | |
| installer.installExtension() | |
| } | |
| } | |
| } | |
| } | |
| final class SystemExtensionInstaller: NSObject, OSSystemExtensionRequestDelegate, ObservableObject { | |
| static let shared = SystemExtensionInstaller() | |
| let log = OSLog(subsystem: "com.example", category: "hostApp") | |
| func installExtension() { | |
| guard let _ = Bundle.main.builtInPlugInsURL?.appendingPathComponent("esextension.appex") else { | |
| print("Extension not found") | |
| return | |
| } | |
| let request = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: "com.example.esextension", queue: .main) | |
| request.delegate = self | |
| OSSystemExtensionManager.shared.submitRequest(request) | |
| } | |
| func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) { | |
| print("Extension installed: \(result)") | |
| os_log("Extension installed: %{public}@", log: log, type: .info, String(describing: result)) | |
| } | |
| func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) { | |
| print("Extension installation failed: \(error)") | |
| os_log("Extension installation failed: %@", log: log, type: .error, error.localizedDescription) | |
| } | |
| func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) { | |
| // pop "bla-bla would like to install a new endpoint" to Login & Extensions [OK] [Open System Settings] | |
| // manually approve it Settings -> search "extensions" -> "Endpoint Security Extensions" -> Enable | |
| print("User approval needed to install extension") | |
| os_log("User approval needed to install extension", log: log, type: .info) | |
| } | |
| func request(_ request: OSSystemExtensionRequest, | |
| actionForReplacingExtension existing: OSSystemExtensionProperties, | |
| withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction { | |
| return .replace | |
| } | |
| } | |
| struct ContentView: View { | |
| var body: some View { | |
| VStack { | |
| Text("Hello") | |
| .font(.headline) | |
| .padding() | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // main.swift | |
| // Endpoint Security Extension Code | |
| // This is an Endpoint Security extension to demonstrate basic functionality. | |
| // | |
| // https://developer.apple.com/documentation/endpointsecurity | |
| // Instructions: | |
| // https://developer.apple.com/documentation/endpointsecurity/monitoring-system-events-with-endpoint-security | |
| // It requires Apple approval and is associated with the host application "com.example.esextension". | |
| // To create a client, your app must have the com.apple.developer.endpoint-security.client entitlement. | |
| // The user also needs to approve your app with Transparency, Consent, and Control (TCC) mechanisms. | |
| // The user does this in the Security and Privacy pane of System Preferences, by adding the app to Full Disk Access. | |
| /* | |
| Requaried frameworks: | |
| - libbsm | |
| - libEndpointSecurity | |
| - SystemExtension | |
| - UniformTypeIdentifiers | |
| ### Expected Behavior | |
| 1. **Block TextEdit:** The extension is expected to block the launch of the TextEdit application. | |
| 2. **EICAR Signature Block:** It will deny access to files containing the EICAR antivirus test signature. | |
| **Test Command:** | |
| `echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/test_virus_signature.txt` | |
| 3. **System Directory Protection:** Writing to the `/usr/local/bin/` directory will be prevented. | |
| **Test Command:** | |
| `echo 'test' > /usr/local/bin/test_virus_signature.txt` | |
| To debug, use the **Console.app** and filter by the process name "com.example.esextension". | |
| */ | |
| /* | |
| // entitlements: | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>com.apple.security.app-sandbox</key> | |
| <false/> | |
| <key>com.apple.developer.endpoint-security.client</key> | |
| <true/> | |
| </dict> | |
| </plist> | |
| */ | |
| import Foundation | |
| import EndpointSecurity | |
| import os.log | |
| // OSLog Logger | |
| let log = OSLog(subsystem: "com.example.esextension", category: "esextension") | |
| let gEventQueue = DispatchQueue(label: "com.example.esextension.eventQueue", qos: .userInitiated, attributes: .concurrent) | |
| // MARK: - Helpers | |
| func signingID(_ token: es_string_token_t) -> String? { | |
| let data = Data(bytes: token.data, count: Int(token.length)) | |
| return String(data: data, encoding: .utf8) | |
| } | |
| func filePath(_ file: UnsafePointer<es_file_t>) -> String { | |
| String(cString: file.pointee.path.data) | |
| } | |
| // MARK: - EICAR Check | |
| func isEicarFile(_ file: UnsafePointer<es_file_t>) -> Bool { | |
| let eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" | |
| let eicarData = Data(eicar.utf8) | |
| let eicarLength = eicarData.count | |
| let eicarMaxLength = 128 | |
| let path = filePath(file) | |
| guard file.pointee.stat.st_size >= eicarLength, | |
| file.pointee.stat.st_size <= eicarMaxLength | |
| else { return false } | |
| let fileURL = URL(filePath: path) | |
| guard let fileData = try? Data(contentsOf: fileURL, options: .mappedIfSafe) else { return false } | |
| return fileData.prefix(eicarLength) == eicarData | |
| } | |
| // MARK: - Event Handlers | |
| func handleExec(_ client: OpaquePointer, _ msg: UnsafePointer<es_message_t>) { | |
| guard let id = signingID(msg.pointee.event.exec.target.pointee.signing_id) else { | |
| es_respond_auth_result(client, msg, ES_AUTH_RESULT_ALLOW, true) | |
| return | |
| } | |
| if id == "com.apple.TextEdit" { | |
| os_log("Blocked execution: %{public}@", log: log, type: .info, id) | |
| es_respond_auth_result(client, msg, ES_AUTH_RESULT_DENY, true) | |
| } else { | |
| es_respond_auth_result(client, msg, ES_AUTH_RESULT_ALLOW, true) | |
| } | |
| } | |
| func handleOpenWorker(_ client: OpaquePointer, _ msg: UnsafePointer<es_message_t>) { | |
| let file = msg.pointee.event.open.file | |
| let path = filePath(file) | |
| if isEicarFile(file) { | |
| os_log("Denied open (EICAR): %{public}@", log: log, type: .info, path) | |
| es_respond_flags_result(client, msg, 0, true) | |
| } else if path.hasPrefix("/usr/local/bin/") { | |
| os_log("Restricted write: %{public}@", log: log, type: .info, path) | |
| es_respond_flags_result(client, msg, UInt32(O_RDONLY), true) | |
| } else { | |
| es_respond_flags_result(client, msg, UInt32.max, true) | |
| } | |
| } | |
| func handleOpen(_ client: OpaquePointer, _ msg: UnsafePointer<es_message_t>) { | |
| es_retain_message(msg) | |
| gEventQueue.async { | |
| handleOpenWorker(client, msg) | |
| es_release_message(msg) | |
| } | |
| } | |
| // MARK: - Main Handler Block | |
| let handleEvent: es_handler_block_t = { (client, msg) in | |
| switch msg.pointee.event_type { | |
| case ES_EVENT_TYPE_AUTH_EXEC: | |
| handleExec(client, msg) | |
| case ES_EVENT_TYPE_AUTH_OPEN: | |
| handleOpen(client, msg) | |
| default: | |
| if msg.pointee.action_type == ES_ACTION_TYPE_AUTH { | |
| es_respond_auth_result(client, msg, ES_AUTH_RESULT_ALLOW, true) | |
| } else { | |
| os_log("Unexpected event type: %d", log: log, type: .error, msg.pointee.event_type.rawValue) | |
| } | |
| } | |
| } | |
| // MARK: - Client creation result enum | |
| enum ESClientCreationResult: CustomStringConvertible { | |
| case success | |
| case notEntitled | |
| case notPrivileged | |
| case notPermitted | |
| case invalidArgument | |
| case tooManyClients | |
| case internalError | |
| case unknown(Int32) | |
| init(esResult: es_new_client_result_t) { | |
| switch esResult { | |
| case ES_NEW_CLIENT_RESULT_SUCCESS: | |
| self = .success | |
| case ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED: | |
| self = .notEntitled | |
| case ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED: | |
| self = .notPrivileged | |
| case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: | |
| self = .notPermitted | |
| case ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT: | |
| self = .invalidArgument | |
| case ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS: | |
| self = .tooManyClients | |
| case ES_NEW_CLIENT_RESULT_ERR_INTERNAL: | |
| self = .internalError | |
| default: | |
| self = .unknown(Int32(esResult.rawValue)) | |
| } | |
| } | |
| var description: String { | |
| switch self { | |
| case .success: return "Success" | |
| case .notEntitled: return "Missing entitlement" | |
| case .notPrivileged: return "Not running as root" | |
| case .notPermitted: return "TCC permission denied" | |
| case .invalidArgument: return "Invalid argument" | |
| case .tooManyClients: return "Too many clients" | |
| case .internalError: return "Internal error" | |
| case .unknown(let code): return "Unknown error code: \(code)" | |
| } | |
| } | |
| } | |
| func start() { | |
| var clientPtr: OpaquePointer? | |
| let newClientRaw = es_new_client(&clientPtr, handleEvent) | |
| let newClientResult = ESClientCreationResult(esResult: newClientRaw) | |
| guard case .success = newClientResult else { | |
| os_log("%{public}@", type: .fault, newClientResult.description) | |
| if case .notPermitted = newClientResult { | |
| try? Process.run( | |
| URL(fileURLWithPath: "/usr/bin/open"), | |
| arguments: ["x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"]) | |
| } | |
| exit(EXIT_FAILURE) | |
| } | |
| guard let client = clientPtr else { | |
| os_log("Client pointer is nil after success.", log: log, type: .fault) | |
| exit(EXIT_FAILURE) | |
| } | |
| defer { | |
| es_delete_client(client) | |
| } | |
| var events: [es_event_type_t] = [ES_EVENT_TYPE_AUTH_EXEC, ES_EVENT_TYPE_AUTH_OPEN] | |
| let subscribeResult = es_subscribe(client, &events, UInt32(events.count)) | |
| if subscribeResult != ES_RETURN_SUCCESS { | |
| os_log("Client failed to subscribe to events. Code: %d", log: log, type: .error, subscribeResult.rawValue) | |
| exit(EXIT_FAILURE) | |
| } | |
| os_log("Client initialized and subscribed successfully.", log: log, type: .info) | |
| dispatchMain() | |
| } | |
| start() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment