├── .github └── workflows │ └── main.yml ├── .gitignore ├── Example └── EndpointSecurityDemo │ ├── EndpointSecurityDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── EndpointSecurityConsole.xcscheme │ └── EndpointSecurityDemo │ ├── EndpointSecurityDemo-Bridging-Header.h │ ├── EndpointSecurityDemo.entitlements │ └── main.swift ├── Package.swift ├── README.md └── Sources ├── sEndpointSecurity ├── ESClient - Native │ ├── ESNativeClient.swift │ ├── ESNativeTypeDescriptions.swift │ └── ESNativeTypeUtils.swift ├── ESClient │ ├── ESClient.swift │ ├── ESClientProtocol.swift │ ├── ESClientTypes.swift │ ├── ESMessagePtr.swift │ ├── ESMutePath.swift │ └── ESMuteProcess.swift ├── ESMessage │ ├── ESConverter.swift │ └── ESTypes.swift ├── ESService │ ├── ESService.swift │ ├── ESServiceSubscriptionStore.swift │ └── ESSubscription.swift ├── Log.swift └── Utils.swift ├── sEndpointSecurityTests ├── ESClientTests.swift ├── ESClientTypesTests.swift ├── ESListener │ ├── ESServiceTests.swift │ └── MockESClient.swift ├── ESMutePathTests.swift ├── ESMuteProcessTests.swift ├── MockNativeClient.swift └── TestUtils.swift └── sEndpointSecurityXPC ├── ESXPCClient.swift ├── ESXPCConnection.swift ├── ESXPCInternals.swift └── ESXPCListener.swift /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: "*" 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | include: 14 | - xcode: "14.2" # Swift 5.7 15 | macOS: "13" 16 | iOS: "16.0" 17 | - xcode: "15.0" # Swift 5.9 18 | macOS: "13" 19 | iOS: "17.0" 20 | fail-fast: false 21 | 22 | runs-on: macos-${{ matrix.macOS }} 23 | name: Build with Xcode ${{ matrix.xcode }} on macOS ${{ matrix.macOS }} 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: Xcode Select Version 29 | uses: mobiledevops/xcode-select-version-action@v1 30 | with: 31 | xcode-select-version: ${{ matrix.xcode }} 32 | - run: xcodebuild -version 33 | 34 | - name: Test macOS with Xcode ${{ matrix.xcode }} 35 | run: | 36 | set -e 37 | set -o pipefail 38 | 39 | xcodebuild test -scheme sEndpointSecurity -destination "platform=macOS" SWIFT_ACTIVE_COMPILATION_CONDITIONS="SPELLBOOK_SLOW_CI_x20" | xcpretty 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## SPM 9 | Package.resolved 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /Example/EndpointSecurityDemo/EndpointSecurityDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3CA53E9A25BF37E200F02928 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA53E9925BF37E200F02928 /* main.swift */; }; 11 | A4304C9F28C47730009C1012 /* sEndpointSecurity in Frameworks */ = {isa = PBXBuildFile; productRef = A4304C9E28C47730009C1012 /* sEndpointSecurity */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 3CA53E8D25BF37E100F02928 /* EndpointSecurityDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EndpointSecurityDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 3CA53E9925BF37E200F02928 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 17 | A415BB912707561100608E5A /* EndpointSecurityDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EndpointSecurityDemo.entitlements; sourceTree = ""; }; 18 | A44A4FE92683395A008BF9F6 /* EndpointSecurityDemo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "EndpointSecurityDemo-Bridging-Header.h"; sourceTree = ""; }; 19 | A4F84BC928C476650036117D /* sEndpointSecurity */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = sEndpointSecurity; path = ../..; sourceTree = ""; }; 20 | /* End PBXFileReference section */ 21 | 22 | /* Begin PBXFrameworksBuildPhase section */ 23 | 3CA53E8A25BF37E100F02928 /* Frameworks */ = { 24 | isa = PBXFrameworksBuildPhase; 25 | buildActionMask = 2147483647; 26 | files = ( 27 | A4304C9F28C47730009C1012 /* sEndpointSecurity in Frameworks */, 28 | ); 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXFrameworksBuildPhase section */ 32 | 33 | /* Begin PBXGroup section */ 34 | 3CA53E8425BF37E100F02928 = { 35 | isa = PBXGroup; 36 | children = ( 37 | A4F84BC828C476650036117D /* Packages */, 38 | 3CA53E8F25BF37E100F02928 /* EndpointSecurityDemo */, 39 | 3CA53E8E25BF37E100F02928 /* Products */, 40 | 3CA53EA725BF386000F02928 /* Frameworks */, 41 | ); 42 | sourceTree = ""; 43 | }; 44 | 3CA53E8E25BF37E100F02928 /* Products */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | 3CA53E8D25BF37E100F02928 /* EndpointSecurityDemo.app */, 48 | ); 49 | name = Products; 50 | sourceTree = ""; 51 | }; 52 | 3CA53E8F25BF37E100F02928 /* EndpointSecurityDemo */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 3CA53E9925BF37E200F02928 /* main.swift */, 56 | A415BB912707561100608E5A /* EndpointSecurityDemo.entitlements */, 57 | A44A4FE92683395A008BF9F6 /* EndpointSecurityDemo-Bridging-Header.h */, 58 | ); 59 | path = EndpointSecurityDemo; 60 | sourceTree = ""; 61 | }; 62 | 3CA53EA725BF386000F02928 /* Frameworks */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | ); 66 | name = Frameworks; 67 | sourceTree = ""; 68 | }; 69 | A4F84BC828C476650036117D /* Packages */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | A4F84BC928C476650036117D /* sEndpointSecurity */, 73 | ); 74 | name = Packages; 75 | sourceTree = ""; 76 | }; 77 | /* End PBXGroup section */ 78 | 79 | /* Begin PBXNativeTarget section */ 80 | 3CA53E8C25BF37E100F02928 /* EndpointSecurityDemo */ = { 81 | isa = PBXNativeTarget; 82 | buildConfigurationList = 3CA53E9E25BF37E200F02928 /* Build configuration list for PBXNativeTarget "EndpointSecurityDemo" */; 83 | buildPhases = ( 84 | 3CA53E8925BF37E100F02928 /* Sources */, 85 | 3CA53E8A25BF37E100F02928 /* Frameworks */, 86 | 3CA53E8B25BF37E100F02928 /* Resources */, 87 | ); 88 | buildRules = ( 89 | ); 90 | dependencies = ( 91 | ); 92 | name = EndpointSecurityDemo; 93 | packageProductDependencies = ( 94 | A4304C9E28C47730009C1012 /* sEndpointSecurity */, 95 | ); 96 | productName = EndpointSecurityConsole; 97 | productReference = 3CA53E8D25BF37E100F02928 /* EndpointSecurityDemo.app */; 98 | productType = "com.apple.product-type.application"; 99 | }; 100 | /* End PBXNativeTarget section */ 101 | 102 | /* Begin PBXProject section */ 103 | 3CA53E8525BF37E100F02928 /* Project object */ = { 104 | isa = PBXProject; 105 | attributes = { 106 | LastSwiftUpdateCheck = 1320; 107 | LastUpgradeCheck = 1300; 108 | TargetAttributes = { 109 | 3CA53E8C25BF37E100F02928 = { 110 | CreatedOnToolsVersion = 12.3; 111 | LastSwiftMigration = 1250; 112 | }; 113 | }; 114 | }; 115 | buildConfigurationList = 3CA53E8825BF37E100F02928 /* Build configuration list for PBXProject "EndpointSecurityDemo" */; 116 | compatibilityVersion = "Xcode 9.3"; 117 | developmentRegion = en; 118 | hasScannedForEncodings = 0; 119 | knownRegions = ( 120 | en, 121 | Base, 122 | ); 123 | mainGroup = 3CA53E8425BF37E100F02928; 124 | packageReferences = ( 125 | ); 126 | productRefGroup = 3CA53E8E25BF37E100F02928 /* Products */; 127 | projectDirPath = ""; 128 | projectRoot = ""; 129 | targets = ( 130 | 3CA53E8C25BF37E100F02928 /* EndpointSecurityDemo */, 131 | ); 132 | }; 133 | /* End PBXProject section */ 134 | 135 | /* Begin PBXResourcesBuildPhase section */ 136 | 3CA53E8B25BF37E100F02928 /* Resources */ = { 137 | isa = PBXResourcesBuildPhase; 138 | buildActionMask = 2147483647; 139 | files = ( 140 | ); 141 | runOnlyForDeploymentPostprocessing = 0; 142 | }; 143 | /* End PBXResourcesBuildPhase section */ 144 | 145 | /* Begin PBXSourcesBuildPhase section */ 146 | 3CA53E8925BF37E100F02928 /* Sources */ = { 147 | isa = PBXSourcesBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | 3CA53E9A25BF37E200F02928 /* main.swift in Sources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXSourcesBuildPhase section */ 155 | 156 | /* Begin XCBuildConfiguration section */ 157 | 3CA53E9C25BF37E200F02928 /* Debug */ = { 158 | isa = XCBuildConfiguration; 159 | buildSettings = { 160 | ALWAYS_SEARCH_USER_PATHS = NO; 161 | CLANG_ANALYZER_NONNULL = YES; 162 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 163 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 164 | CLANG_CXX_LIBRARY = "libc++"; 165 | CLANG_ENABLE_MODULES = YES; 166 | CLANG_ENABLE_OBJC_ARC = YES; 167 | CLANG_ENABLE_OBJC_WEAK = YES; 168 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 169 | CLANG_WARN_BOOL_CONVERSION = YES; 170 | CLANG_WARN_COMMA = YES; 171 | CLANG_WARN_CONSTANT_CONVERSION = YES; 172 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 173 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 174 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 175 | CLANG_WARN_EMPTY_BODY = YES; 176 | CLANG_WARN_ENUM_CONVERSION = YES; 177 | CLANG_WARN_INFINITE_RECURSION = YES; 178 | CLANG_WARN_INT_CONVERSION = YES; 179 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 180 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 181 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 182 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 183 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 184 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 185 | CLANG_WARN_STRICT_PROTOTYPES = YES; 186 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 187 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 188 | CLANG_WARN_UNREACHABLE_CODE = YES; 189 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 190 | COPY_PHASE_STRIP = NO; 191 | DEBUG_INFORMATION_FORMAT = dwarf; 192 | ENABLE_HARDENED_RUNTIME = YES; 193 | ENABLE_STRICT_OBJC_MSGSEND = YES; 194 | ENABLE_TESTABILITY = YES; 195 | GCC_C_LANGUAGE_STANDARD = gnu11; 196 | GCC_DYNAMIC_NO_PIC = NO; 197 | GCC_NO_COMMON_BLOCKS = YES; 198 | GCC_OPTIMIZATION_LEVEL = 0; 199 | GCC_PREPROCESSOR_DEFINITIONS = ( 200 | "DEBUG=1", 201 | "$(inherited)", 202 | ); 203 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 204 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 205 | GCC_WARN_UNDECLARED_SELECTOR = YES; 206 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 207 | GCC_WARN_UNUSED_FUNCTION = YES; 208 | GCC_WARN_UNUSED_VARIABLE = YES; 209 | MACOSX_DEPLOYMENT_TARGET = 10.15; 210 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 211 | MTL_FAST_MATH = YES; 212 | ONLY_ACTIVE_ARCH = YES; 213 | PRODUCT_BUNDLE_IDENTIFIER = "com.alkenso.$(TARGET_NAME)"; 214 | SDKROOT = macosx; 215 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 216 | SWIFT_VERSION = 5.0; 217 | }; 218 | name = Debug; 219 | }; 220 | 3CA53E9D25BF37E200F02928 /* Release */ = { 221 | isa = XCBuildConfiguration; 222 | buildSettings = { 223 | ALWAYS_SEARCH_USER_PATHS = NO; 224 | CLANG_ANALYZER_NONNULL = YES; 225 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 227 | CLANG_CXX_LIBRARY = "libc++"; 228 | CLANG_ENABLE_MODULES = YES; 229 | CLANG_ENABLE_OBJC_ARC = YES; 230 | CLANG_ENABLE_OBJC_WEAK = YES; 231 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 232 | CLANG_WARN_BOOL_CONVERSION = YES; 233 | CLANG_WARN_COMMA = YES; 234 | CLANG_WARN_CONSTANT_CONVERSION = YES; 235 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 236 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 237 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 238 | CLANG_WARN_EMPTY_BODY = YES; 239 | CLANG_WARN_ENUM_CONVERSION = YES; 240 | CLANG_WARN_INFINITE_RECURSION = YES; 241 | CLANG_WARN_INT_CONVERSION = YES; 242 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 244 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 245 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 246 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 247 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 248 | CLANG_WARN_STRICT_PROTOTYPES = YES; 249 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 250 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 251 | CLANG_WARN_UNREACHABLE_CODE = YES; 252 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 253 | COPY_PHASE_STRIP = NO; 254 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 255 | ENABLE_HARDENED_RUNTIME = YES; 256 | ENABLE_NS_ASSERTIONS = NO; 257 | ENABLE_STRICT_OBJC_MSGSEND = YES; 258 | GCC_C_LANGUAGE_STANDARD = gnu11; 259 | GCC_NO_COMMON_BLOCKS = YES; 260 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 261 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 262 | GCC_WARN_UNDECLARED_SELECTOR = YES; 263 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 264 | GCC_WARN_UNUSED_FUNCTION = YES; 265 | GCC_WARN_UNUSED_VARIABLE = YES; 266 | MACOSX_DEPLOYMENT_TARGET = 10.15; 267 | MTL_ENABLE_DEBUG_INFO = NO; 268 | MTL_FAST_MATH = YES; 269 | PRODUCT_BUNDLE_IDENTIFIER = "com.alkenso.$(TARGET_NAME)"; 270 | SDKROOT = macosx; 271 | SWIFT_VERSION = 5.0; 272 | }; 273 | name = Release; 274 | }; 275 | 3CA53E9F25BF37E200F02928 /* Debug */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 279 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 280 | CLANG_ENABLE_MODULES = YES; 281 | CODE_SIGN_ENTITLEMENTS = "$(TARGET_NAME)/$(TARGET_NAME).entitlements"; 282 | CODE_SIGN_IDENTITY = "-"; 283 | COMBINE_HIDPI_IMAGES = YES; 284 | CREATE_INFOPLIST_SECTION_IN_BINARY = YES; 285 | GENERATE_INFOPLIST_FILE = YES; 286 | LD_RUNPATH_SEARCH_PATHS = ( 287 | "$(inherited)", 288 | "@executable_path/../Frameworks", 289 | ); 290 | PRODUCT_NAME = "$(TARGET_NAME)"; 291 | SWIFT_OBJC_BRIDGING_HEADER = "$(TARGET_NAME)/$(TARGET_NAME)-Bridging-Header.h"; 292 | }; 293 | name = Debug; 294 | }; 295 | 3CA53EA025BF37E200F02928 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | CLANG_ENABLE_MODULES = YES; 301 | CODE_SIGN_ENTITLEMENTS = "$(TARGET_NAME)/$(TARGET_NAME).entitlements"; 302 | CODE_SIGN_IDENTITY = "-"; 303 | COMBINE_HIDPI_IMAGES = YES; 304 | CREATE_INFOPLIST_SECTION_IN_BINARY = YES; 305 | GENERATE_INFOPLIST_FILE = YES; 306 | LD_RUNPATH_SEARCH_PATHS = ( 307 | "$(inherited)", 308 | "@executable_path/../Frameworks", 309 | ); 310 | PRODUCT_NAME = "$(TARGET_NAME)"; 311 | SWIFT_OBJC_BRIDGING_HEADER = "$(TARGET_NAME)/$(TARGET_NAME)-Bridging-Header.h"; 312 | }; 313 | name = Release; 314 | }; 315 | /* End XCBuildConfiguration section */ 316 | 317 | /* Begin XCConfigurationList section */ 318 | 3CA53E8825BF37E100F02928 /* Build configuration list for PBXProject "EndpointSecurityDemo" */ = { 319 | isa = XCConfigurationList; 320 | buildConfigurations = ( 321 | 3CA53E9C25BF37E200F02928 /* Debug */, 322 | 3CA53E9D25BF37E200F02928 /* Release */, 323 | ); 324 | defaultConfigurationIsVisible = 0; 325 | defaultConfigurationName = Release; 326 | }; 327 | 3CA53E9E25BF37E200F02928 /* Build configuration list for PBXNativeTarget "EndpointSecurityDemo" */ = { 328 | isa = XCConfigurationList; 329 | buildConfigurations = ( 330 | 3CA53E9F25BF37E200F02928 /* Debug */, 331 | 3CA53EA025BF37E200F02928 /* Release */, 332 | ); 333 | defaultConfigurationIsVisible = 0; 334 | defaultConfigurationName = Release; 335 | }; 336 | /* End XCConfigurationList section */ 337 | 338 | /* Begin XCSwiftPackageProductDependency section */ 339 | A4304C9E28C47730009C1012 /* sEndpointSecurity */ = { 340 | isa = XCSwiftPackageProductDependency; 341 | productName = sEndpointSecurity; 342 | }; 343 | /* End XCSwiftPackageProductDependency section */ 344 | }; 345 | rootObject = 3CA53E8525BF37E100F02928 /* Project object */; 346 | } 347 | -------------------------------------------------------------------------------- /Example/EndpointSecurityDemo/EndpointSecurityDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/EndpointSecurityDemo/EndpointSecurityDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/EndpointSecurityDemo/EndpointSecurityDemo.xcodeproj/xcshareddata/xcschemes/EndpointSecurityConsole.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 45 | 47 | 53 | 54 | 55 | 56 | 62 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Example/EndpointSecurityDemo/EndpointSecurityDemo/EndpointSecurityDemo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Example/EndpointSecurityDemo/EndpointSecurityDemo/EndpointSecurityDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.endpoint-security.client 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/EndpointSecurityDemo/EndpointSecurityDemo/main.swift: -------------------------------------------------------------------------------- 1 | import sEndpointSecurity 2 | 3 | import Combine 4 | import EndpointSecurity 5 | import Foundation 6 | import SpellbookFoundation 7 | 8 | class Main { 9 | init() { 10 | SCLogger.default.destinations.append { 11 | print($0) 12 | } 13 | } 14 | 15 | var client: ESClient! 16 | func start() throws { 17 | client = try ESClient() 18 | _ = try client.muteProcess(.token(.current()), events: .all) 19 | 20 | client.processMuteHandler = { 21 | // Mute out messages from 'mdworker_shared' 22 | $0.executable.path.contains("mdworker_shared") ? .muteAll(.all) : .allowAll 23 | } 24 | 25 | client.authMessageHandler = { raw, callback in 26 | let message = try! raw.converted() 27 | 28 | let process = message.process.executable.path.lastPathComponent 29 | switch message.event { 30 | case .rename(let rename): 31 | var filePath = rename.source.path + " -> " 32 | switch rename.destination { 33 | case .existingFile(let file): 34 | filePath += file.path 35 | case .newPath(let dir, let filename): 36 | filePath += dir.path.appendingPathComponent(filename) 37 | } 38 | print("AUTH-RENAME by \(process): \(filePath)") 39 | callback(.allowOnce) 40 | default: 41 | callback(.allow) 42 | print("AUTH-ERROR: unexpected event: \(message.eventType)") 43 | } 44 | } 45 | 46 | client.notifyMessageHandler = { 47 | let message = try! $0.converted(.full) 48 | 49 | let process = message.process.executable.path.lastPathComponent 50 | switch message.event { 51 | case .create(let create): 52 | let filePath: String 53 | switch create.destination { 54 | case .existingFile(let file): 55 | filePath = file.path 56 | case .newPath(let dir, let filename, _): 57 | filePath = dir.path.appendingPathComponent(filename) 58 | } 59 | print("NOTIFY-RENAME by \(process): \(filePath)") 60 | case .exec(let exec): 61 | let target = exec.target.executable.path.lastPathComponent 62 | print("NOTIFY-EXEC by \(process): \(target)") 63 | case .exit(let exit): 64 | print("NOTIFY-EXIT by \(process): exit status = \(exit.status)") 65 | default: 66 | print("NOTIFY-ERROR: unexpected event type = \(message.eventType)") 67 | } 68 | } 69 | 70 | let events = [ 71 | ES_EVENT_TYPE_AUTH_RENAME, 72 | 73 | ES_EVENT_TYPE_NOTIFY_CREATE, 74 | ES_EVENT_TYPE_NOTIFY_EXEC, 75 | ES_EVENT_TYPE_NOTIFY_EXIT, 76 | ] 77 | guard client.subscribe(events) else { 78 | throw CommonError.unexpected("Subscribe to ES events fails") 79 | } 80 | } 81 | } 82 | 83 | let main = Main() 84 | do { 85 | try main.start() 86 | print("Started") 87 | withExtendedLifetime(main) { RunLoop.main.run() } 88 | } catch { 89 | print("Failed to start demo. Error: \(error)") 90 | } 91 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "sEndpointSecurity", 8 | platforms: [ 9 | .macOS(.v11), 10 | ], 11 | products: [ 12 | .library(name: "sEndpointSecurity", targets: ["sEndpointSecurity"]), 13 | .library(name: "sEndpointSecurityXPC", targets: ["sEndpointSecurityXPC"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/Alkenso/SwiftSpellbook.git", from: "0.3.2"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "sEndpointSecurity", 21 | dependencies: [.product(name: "SpellbookFoundation", package: "SwiftSpellbook")], 22 | linkerSettings: [.linkedLibrary("EndpointSecurity")] 23 | ), 24 | .target( 25 | name: "sEndpointSecurityXPC", 26 | dependencies: ["sEndpointSecurity"] 27 | ), 28 | .testTarget( 29 | name: "sEndpointSecurityTests", 30 | dependencies: [ 31 | "sEndpointSecurity", 32 | .product(name: "SpellbookTestUtils", package: "SwiftSpellbook"), 33 | ] 34 | ), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The package now is part of [SwiftSpellbook_macOS](https://github.com/Alkenso/SwiftSpellbook_macOS) 2 | 3 | # sEndpointSecurity 4 | 5 |

6 | 7 | 8 | 9 | 10 |

11 | 12 | If you've found this or other my libraries helpful, please buy me some pizza 13 | 14 | 15 | 16 | # Intro 17 | With macOS 10.15 Catalina, Apple released beautiful framework EndpointSecurity. It is a usermode replacement for kAuth and MACF mechanisms previously available only from the Kernel. 18 | 19 | The framework provides lots functionality...but is plain C 20 | 21 | # Motivation 22 | 23 | sEndpointSecurity is Swift wrapper around ES C API and was written with three main goals is mind 24 | - provide convenient, Swift-style approach to EndpointSecurity API 25 | - keep in a single place all parsing and interoperations within C data types 26 | - solve the biggest problem dealing with file authentication events: debugging 27 | 28 | If first two are obvious, the third one requires a bit of additional explanation. 29 | When you subscribes to file authentication events, that means **nobody** can access the file before the client returns resolution for it. 30 | And what debugger does? On breakpoint, it suspends the application (the client) and prevent it from responding any events. 31 | This cause whole OS to hang until the client is killed by EndpontSecurity.kext from the Kernel. 32 | 33 | sEndpointSecurity provides approach to deal with debugging - XPC wrapper around ES client. 34 | So we move events receiving and responding to another process and deal with it over XPC. That allows us to debug the application. 35 | 36 | # API 37 | The rare one likes ReadMe without code samples. Here they are 38 | 39 | ## ESClient 40 | 41 | ESClient is a Swift wrapper around EndpointSecurity C API with a bit extended functional for convenience 42 | 43 | ``` 44 | import sEndpointSecurity 45 | 46 | // Create ESClient 47 | var status: es_new_client_result_t = ES_NEW_CLIENT_RESULT_ERR_INTERNAL 48 | guard let client = ESClient(status: &status) else { 49 | print("Failed to create ESClient. Status = \(status)") 50 | exit(1) 51 | } 52 | 53 | // Register message handlers 54 | client.authMessageHandler = { message, callback in 55 | print("Auth message: \(try! message.converted())") 56 | callback(.allowOnce) 57 | } 58 | 59 | client.notifyMessageHandler = { message in 60 | print("Notify message: \(try! message.converted())") 61 | } 62 | 63 | // Start receiving messages 64 | guard client.subscribe([ES_EVENT_TYPE_AUTH_EXEC, ES_EVENT_TYPE_NOTIFY_EXIT]) else { 65 | print("Failed to subscribe to ES messages") 66 | exit(2) 67 | } 68 | 69 | 70 | withExtendedLifetime(client) { RunLoop.main.run() } 71 | ``` 72 | 73 | ## ES over XPC 74 | ### ESXPCClient 75 | ESXPCClient is client counterpart of ES over XPC implementation. It looks very close to ESClient, but have some differences due to asynchronous XPC nature 76 | 77 | ``` 78 | import sEndpointSecurity 79 | 80 | // Create ESXPCClient 81 | let client = ESXPCClient(NSXPCConnection(serviceName: "com.alkenso.ESXPC")) 82 | 83 | // Register message handlers 84 | client.authMessageHandler = { message, callback in 85 | print("Auth message: \(try! message.converted())") 86 | callback(.allowOnce) 87 | } 88 | 89 | client.notifyMessageHandler = { message in 90 | print("Notify message: \(try! message.converted())") 91 | } 92 | 93 | let status = try! client.activate() 94 | guard status == ES_NEW_CLIENT_RESULT_SUCCESS else { 95 | print("Failed to activate ESXPCClient. Status = \(status)") 96 | exit(1) 97 | } 98 | 99 | // Start receiving messages 100 | client.subscribe([ES_EVENT_TYPE_AUTH_EXEC, ES_EVENT_TYPE_NOTIFY_EXIT]) { result in 101 | guard result.success == true else { 102 | print("Failed to subscribe to ES events") 103 | exit(2) 104 | } 105 | print("Successfully subscribed to ES events") 106 | } 107 | 108 | 109 | withExtendedLifetime(client) {} 110 | ``` 111 | 112 | ### ESXPCService 113 | ESXPCService is service counterpart of ES over XPC implementation. It is created in the process that actually works with ES framework. 114 | 115 | ``` 116 | import sEndpointSecurity 117 | 118 | let service = ESXPCService( 119 | listener: NSXPCListener.service(), 120 | createClient: ESClient.init 121 | ) 122 | service.activate() 123 | 124 | withExtendedLifetime(service) { RunLoop.main.run() } 125 | ``` 126 | 127 | # Dependencies 128 | The package is designed with the minimum dependencies. At the moment, it it the only one utility library SwiftSpellbook (no additional dependencies) 129 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESClient - Native/ESNativeClient.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import SpellbookFoundation 25 | 26 | public protocol ESNativeClient { 27 | var native: OpaquePointer { get } 28 | 29 | func esRespond(_ message: UnsafePointer, flags: UInt32, cache: Bool) -> es_respond_result_t 30 | 31 | func esSubscribe(_ events: [es_event_type_t]) -> es_return_t 32 | func esUnsubscribe(_ events: [es_event_type_t]) -> es_return_t 33 | func esUnsubscribeAll() -> es_return_t 34 | 35 | func esClearCache() -> es_clear_cache_result_t 36 | func esDeleteClient() -> es_return_t 37 | 38 | @available(macOS 13.0, *) 39 | func esInvertMuting(_ muteType: es_mute_inversion_type_t) -> es_return_t 40 | 41 | @available(macOS 13.0, *) 42 | func esMutingInverted(_ muteType: es_mute_inversion_type_t) -> es_mute_inverted_return_t 43 | 44 | // MARK: Mute by Process 45 | 46 | func esMuteProcess(_ auditToken: audit_token_t) -> es_return_t 47 | func esUnmuteProcess(_ auditToken: audit_token_t) -> es_return_t 48 | 49 | @available(macOS 12.0, *) 50 | func esMuteProcessEvents(_ auditToken: audit_token_t, _ events: [es_event_type_t]) -> es_return_t 51 | 52 | @available(macOS 12.0, *) 53 | func esUnmuteProcessEvents(_ auditToken: audit_token_t, _ events: [es_event_type_t]) -> es_return_t 54 | 55 | func esMutedProcesses() -> [audit_token_t: [es_event_type_t]] 56 | 57 | // MARK: Mute by Path 58 | 59 | func esMutePath(_ path: String, _ type: es_mute_path_type_t) -> es_return_t 60 | 61 | @available(macOS 12.0, *) 62 | func esUnmutePath(_ path: String, _ type: es_mute_path_type_t) -> es_return_t 63 | 64 | @available(macOS 12.0, *) 65 | func esMutePathEvents(_ path: String, _ type: es_mute_path_type_t, _ events: [es_event_type_t]) -> es_return_t 66 | 67 | @available(macOS 12.0, *) 68 | func esUnmutePathEvents(_ path: String, _ type: es_mute_path_type_t, _ events: [es_event_type_t]) -> es_return_t 69 | 70 | func esUnmuteAllPaths() -> es_return_t 71 | 72 | @available(macOS 13.0, *) 73 | func esUnmuteAllTargetPaths() -> es_return_t 74 | 75 | @available(macOS 12.0, *) 76 | func esMutedPaths() -> [(path: String, type: es_mute_path_type_t, events: [es_event_type_t])] 77 | } 78 | 79 | extension OpaquePointer: ESNativeClient { 80 | public var native: OpaquePointer { self } 81 | 82 | public func esRespond(_ message: UnsafePointer, flags: UInt32, cache: Bool) -> es_respond_result_t { 83 | switch message.pointee.event_type { 84 | // flags requests 85 | case ES_EVENT_TYPE_AUTH_OPEN: 86 | return es_respond_flags_result(self, message, flags, cache) 87 | 88 | // rest are auth requests 89 | default: 90 | return es_respond_auth_result(self, message, flags > 0 ? ES_AUTH_RESULT_ALLOW : ES_AUTH_RESULT_DENY, cache) 91 | } 92 | } 93 | 94 | public func esSubscribe(_ events: [es_event_type_t]) -> es_return_t { 95 | withValidRawEvents(events) { es_subscribe(self, $0, $1) } 96 | } 97 | 98 | public func esUnsubscribe(_ events: [es_event_type_t]) -> es_return_t { 99 | withValidRawEvents(events) { es_unsubscribe(self, $0, $1) } 100 | } 101 | 102 | public func esClearCache() -> es_clear_cache_result_t { 103 | es_clear_cache(self) 104 | } 105 | 106 | public func esDeleteClient() -> es_return_t { 107 | es_delete_client(self) 108 | } 109 | 110 | @available(macOS 13.0, *) 111 | public func esInvertMuting(_ muteType: es_mute_inversion_type_t) -> es_return_t { 112 | es_invert_muting(self, muteType) 113 | } 114 | 115 | @available(macOS 13.0, *) 116 | public func esMutingInverted(_ muteType: es_mute_inversion_type_t) -> es_mute_inverted_return_t { 117 | es_muting_inverted(self, muteType) 118 | } 119 | 120 | public func esUnsubscribeAll() -> es_return_t { 121 | es_unsubscribe_all(self) 122 | } 123 | 124 | public func esMuteProcess(_ auditToken: audit_token_t) -> es_return_t { 125 | withUnsafePointer(to: auditToken) { es_mute_process(self, $0) } 126 | } 127 | 128 | public func esUnmuteProcess(_ auditToken: audit_token_t) -> es_return_t { 129 | withUnsafePointer(to: auditToken) { es_unmute_process(self, $0) } 130 | } 131 | 132 | @available(macOS 12.0, *) 133 | public func esMuteProcessEvents(_ auditToken: audit_token_t, _ events: [es_event_type_t]) -> es_return_t { 134 | withValidRawEvents(events) { eventsPtr, eventsCount in 135 | withUnsafePointer(to: auditToken) { es_mute_process_events(self, $0, eventsPtr, eventsCount) } 136 | } 137 | } 138 | 139 | @available(macOS 12.0, *) 140 | public func esUnmuteProcessEvents(_ auditToken: audit_token_t, _ events: [es_event_type_t]) -> es_return_t { 141 | withValidRawEvents(events) { eventsPtr, eventsCount in 142 | withUnsafePointer(to: auditToken) { es_unmute_process_events(self, $0, eventsPtr, eventsCount) } 143 | } 144 | } 145 | 146 | public func esMutedProcesses() -> [audit_token_t: [es_event_type_t]] { 147 | if #available(macOS 12.0, *) { 148 | var processes: UnsafeMutablePointer! = .init(bitPattern: 0xdeadbeef)! 149 | guard es_muted_processes_events(self, &processes) == ES_RETURN_SUCCESS else { return [:] } 150 | defer { es_release_muted_processes(processes) } 151 | return Array(UnsafeBufferPointer(start: processes.pointee.processes, count: processes.pointee.count)) 152 | .reduce(into: [:]) { 153 | $0[$1.audit_token] = Array(UnsafeBufferPointer(start: $1.events, count: $1.event_count)) 154 | } 155 | } else { 156 | var count: Int = 0 157 | var tokens = UnsafeMutablePointer(bitPattern: 0xdeadbeef)! 158 | guard es_muted_processes(self, &count, &tokens) == ES_RETURN_SUCCESS else { return [:] } 159 | defer { tokens.deallocate() } 160 | return Array(UnsafeBufferPointer(start: tokens, count: count)) 161 | .reduce(into: [:]) { $0[$1] = Array(ESEventSet.all.events) } 162 | } 163 | } 164 | 165 | public func esMutePath(_ path: String, _ type: es_mute_path_type_t) -> es_return_t { 166 | guard #unavailable(macOS 12.0) else { 167 | return es_mute_path(self, path, type) 168 | } 169 | 170 | switch type { 171 | case ES_MUTE_PATH_TYPE_PREFIX: 172 | return es_mute_path_prefix(self, path) 173 | case ES_MUTE_PATH_TYPE_LITERAL: 174 | return es_mute_path_literal(self, path) 175 | default: 176 | return ES_RETURN_ERROR 177 | } 178 | } 179 | 180 | public func esUnmuteAllPaths() -> es_return_t { 181 | es_unmute_all_paths(self) 182 | } 183 | 184 | @available(macOS 12.0, *) 185 | public func esUnmutePath(_ path: String, _ type: es_mute_path_type_t) -> es_return_t { 186 | es_unmute_path(self, path, type) 187 | } 188 | 189 | @available(macOS 12.0, *) 190 | public func esMutePathEvents(_ path: String, _ type: es_mute_path_type_t, _ events: [es_event_type_t]) -> es_return_t { 191 | withValidRawEvents(events) { 192 | es_mute_path_events(self, path, type, $0, $1) 193 | } 194 | } 195 | 196 | @available(macOS 12.0, *) 197 | public func esUnmutePathEvents(_ path: String, _ type: es_mute_path_type_t, _ events: [es_event_type_t]) -> es_return_t { 198 | withValidRawEvents(events) { 199 | es_unmute_path_events(self, path, type, $0, $1) 200 | } 201 | } 202 | 203 | @available(macOS 13.0, *) 204 | public func esUnmuteAllTargetPaths() -> es_return_t { 205 | es_unmute_all_target_paths(self) 206 | } 207 | 208 | @available(macOS 12.0, *) 209 | public func esMutedPaths() -> [(path: String, type: es_mute_path_type_t, events: [es_event_type_t])] { 210 | var paths = UnsafeMutablePointer(bitPattern: 0xdeadbeef)! 211 | guard es_muted_paths_events(self, &paths) == ES_RETURN_SUCCESS else { return [] } 212 | defer { es_release_muted_paths(paths) } 213 | 214 | return Array(UnsafeBufferPointer(start: paths.pointee.paths, count: paths.pointee.count)) 215 | .map { 216 | let events = Array(UnsafeBufferPointer(start: $0.events, count: $0.event_count)) 217 | let path = $0.path.length > 0 ? String(cString: $0.path.data) : "" 218 | return (path, $0.type, events) 219 | } 220 | } 221 | 222 | private func withValidRawEvents( 223 | _ events: [es_event_type_t], 224 | body: (UnsafePointer, Count) -> es_return_t 225 | ) -> es_return_t { 226 | let validEvents = Array(validESEvents(self).intersection(events)) 227 | return validEvents.withUnsafeBufferPointer { buffer in 228 | if let ptr = buffer.baseAddress, !buffer.isEmpty { 229 | return body(ptr, Count(buffer.count)) 230 | } else { 231 | return ES_RETURN_SUCCESS 232 | } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESClient - Native/ESNativeTypeUtils.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | extension es_event_exec_t { 28 | public var args: [String] { 29 | parse(valueFn: es_exec_arg, countFn: es_exec_arg_count).map(Self.dummyConverter.esString) 30 | } 31 | 32 | public var env: [String] { 33 | parse(valueFn: es_exec_env, countFn: es_exec_env_count).map(Self.dummyConverter.esString) 34 | } 35 | 36 | private static let dummyConverter = ESConverter(version: 0) 37 | 38 | private func parse( 39 | valueFn: (UnsafePointer, UInt32) -> T, 40 | countFn: (UnsafePointer) -> UInt32 41 | ) -> [T] { 42 | withUnsafePointer(to: self) { 43 | var values: [T] = [] 44 | let count = countFn($0) 45 | for i in 0.. Set { 55 | guard #available(macOS 12.0, *) else { return fallbackESEvents } 56 | 57 | return validESEventsCacheLock.withLock { 58 | if let validESEventsCache { return validESEventsCache } 59 | 60 | let dummyPath = "/dummy_\(UUID())" 61 | guard client.esMutePath(dummyPath, ES_MUTE_PATH_TYPE_LITERAL) == ES_RETURN_SUCCESS else { 62 | return fallbackESEvents 63 | } 64 | defer { _ = client.esUnmutePath(dummyPath, ES_MUTE_PATH_TYPE_LITERAL) } 65 | 66 | guard let allMutes = client.esMutedPaths().first(where: { $0.path == dummyPath })?.events, 67 | !allMutes.isEmpty 68 | else { 69 | return fallbackESEvents 70 | } 71 | 72 | validESEventsCache = Set(allMutes) 73 | 74 | return validESEventsCache! 75 | } 76 | } 77 | 78 | private var validESEventsCache: Set? 79 | private var validESEventsCacheLock = UnfairLock() 80 | 81 | private let fallbackESEvents: Set = { 82 | let lastEvent: UInt32 83 | if #available(macOS 14.0, *) { 84 | lastEvent = ES_EVENT_TYPE_LAST.rawValue 85 | } else if #available(macOS 13.0, *) { 86 | lastEvent = ES_EVENT_TYPE_NOTIFY_BTM_LAUNCH_ITEM_REMOVE.rawValue 87 | } else if #available(macOS 12.0, *) { 88 | lastEvent = ES_EVENT_TYPE_NOTIFY_COPYFILE.rawValue 89 | } else if #available(macOS 11.3, *) { 90 | lastEvent = ES_EVENT_TYPE_NOTIFY_GET_TASK_INSPECT.rawValue + 1 91 | } else { 92 | lastEvent = ES_EVENT_TYPE_NOTIFY_REMOUNT.rawValue + 1 93 | } 94 | return Set((0..)?.result ?? ES_NEW_CLIENT_RESULT_ERR_INTERNAL 41 | return nil 42 | } 43 | } 44 | 45 | /// Initialise a new ESClient and connect to the ES subsystem 46 | /// - throws: ESClientCreateError in case of error 47 | public convenience init(_ name: String? = nil) throws { 48 | var client: OpaquePointer? 49 | weak var weakSelf: ESClient? 50 | let status = es_new_client(&client) { innerClient, rawMessage in 51 | if let self = weakSelf { 52 | let message = ESMessagePtr(message: rawMessage) 53 | self.handleMessage(message) 54 | } else { 55 | _ = innerClient.esRespond(rawMessage, flags: .max, cache: false) 56 | } 57 | } 58 | 59 | try self.init(name: name, client: client, status: status) 60 | weakSelf = self 61 | } 62 | 63 | private init(name: String?, client: ESNativeClient?, status: es_new_client_result_t) throws { 64 | let name = name ?? "ESClient" 65 | guard let client, status == ES_NEW_CLIENT_RESULT_SUCCESS else { 66 | throw ESError("es_new_client", result: status, client: name) 67 | } 68 | 69 | _ = validESEvents(client) 70 | 71 | self.name = name 72 | self.client = client 73 | self.pathMutes = ESMutePath(client: client) 74 | self.processMutes = ESMuteProcess(client: client) 75 | 76 | do { 77 | self.timebaseInfo = try mach_timebase_info.system() 78 | } catch { 79 | log.error("Failed to get timebase info: \(error)") 80 | self.timebaseInfo = nil 81 | } 82 | 83 | pathMutes.interestHandler = { [weak self] process in 84 | guard let self else { return .listen() } 85 | return self.queue.sync { self.pathInterestHandler?(process) ?? .listen() } 86 | } 87 | } 88 | 89 | deinit { 90 | if client.esUnsubscribeAll() != ES_RETURN_SUCCESS { 91 | log.warning("Failed to unsubscribeAll on ESClient.deinit") 92 | } 93 | if client.esDeleteClient() != ES_RETURN_SUCCESS { 94 | log.warning("Failed to deleteClient on ESClient.deinit") 95 | } 96 | } 97 | 98 | /// - Warning: The property MUST NOT be changed while the client is subscribed to any set of events. 99 | public var name: String 100 | 101 | /// - Warning: The property MUST NOT be changed while the client is subscribed to any set of events. 102 | public var config = Config() 103 | 104 | /// Reference to `es_client_t` used under the hood. 105 | /// DO NOT use it for modifyng any mutes/inversions/etc, the behaviour is undefined. 106 | /// You may want to use it for informational purposes (list of mutes, etc). 107 | public var unsafeNativeClient: OpaquePointer { client.native } 108 | 109 | // MARK: Messages 110 | 111 | /// Handler invoked each time AUTH message is coming from EndpointSecurity. 112 | /// The message SHOULD be responded using the second parameter - reply block. 113 | /// - Warning: The property MUST NOT be changed while the client is subscribed to any set of events. 114 | public var authMessageHandler: ((ESMessagePtr, @escaping (ESAuthResolution) -> Void) -> Void)? 115 | 116 | /// Handler invoked for each AUTH message after it has been responded. 117 | /// Userful for statistic and post-actions. 118 | /// - Warning: The property MUST NOT be changed while the client is subscribed to any set of events. 119 | public var postAuthMessageHandler: ((ESMessagePtr, ResponseInfo) -> Void)? 120 | 121 | /// Handler invoked each time NOTIFY message is coming from EndpointSecurity. 122 | /// - Warning: The property MUST NOT be changed while the client is subscribed to any set of events. 123 | public var notifyMessageHandler: ((ESMessagePtr) -> Void)? 124 | 125 | /// Queue where `pathInterestHandler`, `authMessageHandler`, `postAuthMessageHandler` 126 | /// and `notifyMessageHandler` handlers are called. 127 | /// Defaults to `nil` that means all handlers are called directly on native `es_client` queue. 128 | /// - Warning: The property MUST NOT be changed while the client is subscribed to any set of events. 129 | public var queue: DispatchQueue? 130 | 131 | /// Subscribe to some set of events 132 | /// - Parameters: 133 | /// - events: Array of es_event_type_t to subscribe to 134 | /// - returns: Boolean indicating success or error 135 | /// - Note: Subscribing to new event types does not remove previous subscriptions 136 | public func subscribe(_ events: [es_event_type_t]) throws { 137 | try tryAction("subscribe", success: ES_RETURN_SUCCESS) { 138 | client.esSubscribe(events) 139 | } 140 | } 141 | 142 | /// Unsubscribe from some set of events 143 | /// - Parameters: 144 | /// - events: Array of es_event_type_t to unsubscribe from 145 | /// - returns: Boolean indicating success or error 146 | /// - Note: Events not included in the given `events` array that were previously subscribed to 147 | /// will continue to be subscribed to 148 | public func unsubscribe(_ events: [es_event_type_t]) throws { 149 | try tryAction("unsubscribe", success: ES_RETURN_SUCCESS) { 150 | client.esUnsubscribe(events) 151 | } 152 | } 153 | 154 | /// Unsubscribe from all events 155 | /// - Parameters: 156 | /// - returns: Boolean indicating success or error 157 | public func unsubscribeAll() throws { 158 | try tryAction("unsubscribeAll", success: ES_RETURN_SUCCESS) { 159 | client.esUnsubscribeAll() 160 | } 161 | } 162 | 163 | /// Clear all cached results for all clients. 164 | /// - Parameters: 165 | /// - returns: es_clear_cache_result_t value indicating success or an error 166 | public func clearCache() throws { 167 | try tryAction("clearCache", success: ES_CLEAR_CACHE_RESULT_SUCCESS) { 168 | client.esClearCache() 169 | } 170 | } 171 | 172 | // MARK: Interest 173 | 174 | /// Perform process filtering, additionally to muting of path and processes. 175 | /// Filtering is based on `interest in process with particular executable path`. 176 | /// Designed to be used for granular process filtering by ignoring uninterest events. 177 | /// 178 | /// General idea is to mute or ignore processes we are not interested in using their binary paths. 179 | /// Usually the OS would not have more than ~1000 unique processes, so asking for interest in particular 180 | /// process path would occur very limited number of times. 181 | /// 182 | /// The process may be interested or ignored accoding to returned `ESInterest`. 183 | /// If the process is not interested, all related messages are skipped. 184 | /// More information on `ESInterest` see in related documentation. 185 | /// 186 | /// The final decision if the particular event is delivered or not relies on multiple sources. 187 | /// Sources considered: 188 | /// - `mute(path:)` rules 189 | /// - `mute(process:)` rules 190 | /// - `pathInterestHandler` resolution 191 | /// 192 | /// - Note: Interest does NOT depend on `inversion` of `ESClient`. 193 | /// - Note: Returned resolutions are cached to avoid often handler calls. 194 | /// To reset cache, call `clearPathInterestCache`. 195 | /// - Note: When the handler is not set, it defaults to returning `ESInterest.listen()`. 196 | /// 197 | /// - Warning: Perfonamce-sensitive handler, called **synchronously** once for each process path on `queue`. 198 | /// Do here as minimum work as possible. 199 | /// - Warning: The property MUST NOT be changed while the client is subscribed to any set of events. 200 | public var pathInterestHandler: ((ESProcess) -> ESInterest)? 201 | 202 | /// Clears the cache related to process interest by path. 203 | /// All processes will be re-evaluated against mute rules and `pathInterestHandler`. 204 | public func clearPathInterestCache() { 205 | pathMutes.clearIgnoreCache() 206 | } 207 | 208 | // MARK: Mute 209 | 210 | /// Suppress events from the process described by the given `mute` rule. 211 | /// - Parameters: 212 | /// - mute: process to mute. 213 | /// - events: set of events to mute. 214 | public func mute(process rule: ESMuteProcessRule, events: ESEventSet = .all) { 215 | guard let token = rule.token else { return } 216 | processMutes.mute(token, events: events.events) 217 | } 218 | 219 | /// Unmute events for the process described by the given `mute` rule. 220 | /// - Parameters: 221 | /// - mute: process to unmute. 222 | /// - events: set of events to mute. 223 | public func unmute(process rule: ESMuteProcessRule, events: ESEventSet = .all) { 224 | guard let token = rule.token else { return } 225 | processMutes.unmute(token, events: events.events) 226 | } 227 | 228 | /// Unmute all events for all processes. Clear the rules. 229 | public func unmuteAllProcesses() { 230 | processMutes.unmuteAll() 231 | } 232 | 233 | /// Suppress events for the the given at path and type. 234 | /// - Parameters: 235 | /// - mute: process path to mute. 236 | /// - type: path type. 237 | /// - events: set of events to mute. 238 | public func mute(path: String, type: es_mute_path_type_t, events: ESEventSet = .all) throws { 239 | switch type { 240 | case ES_MUTE_PATH_TYPE_PREFIX, ES_MUTE_PATH_TYPE_LITERAL: 241 | pathMutes.mute(path, type: type, events: events.events) 242 | default: 243 | if #available(macOS 12.0, *) { 244 | try tryAction("esMutePathEvents", success: ES_RETURN_SUCCESS) { 245 | client.esMutePathEvents(path, type, Array(events.events)) 246 | } 247 | } else { 248 | try tryAction("esMutePathEvents", success: ES_RETURN_SUCCESS) { ES_RETURN_ERROR } 249 | } 250 | } 251 | } 252 | 253 | /// Unmute events for the given at path and type. 254 | /// - Parameters: 255 | /// - mute: process path to unmute. 256 | /// - type: path type. 257 | /// - events: set of events to unmute. 258 | @available(macOS 12.0, *) 259 | public func unmute(path: String, type: es_mute_path_type_t, events: ESEventSet = .all) throws { 260 | switch type { 261 | case ES_MUTE_PATH_TYPE_PREFIX, ES_MUTE_PATH_TYPE_LITERAL: 262 | pathMutes.unmute(path, type: type, events: events.events) 263 | default: 264 | try tryAction("esUnmutePathEvents", success: ES_RETURN_SUCCESS) { 265 | client.esUnmutePathEvents(path, type, Array(events.events)) 266 | } 267 | } 268 | } 269 | 270 | /// Unmute all events for all process paths. 271 | public func unmuteAllPaths() throws { 272 | try tryAction("unmuteAllPaths", success: ES_RETURN_SUCCESS) { 273 | pathMutes.unmuteAll() ? ES_RETURN_SUCCESS : ES_RETURN_ERROR 274 | } 275 | } 276 | 277 | /// Unmute all target paths. Works only for macOS 13.0+. 278 | @available(macOS 13.0, *) 279 | public func unmuteAllTargetPaths() throws { 280 | try tryAction("esUnmuteAllTargetPaths", success: ES_RETURN_SUCCESS) { 281 | client.esUnmuteAllTargetPaths() 282 | } 283 | } 284 | 285 | /// Invert the mute state of a given mute dimension. 286 | @available(macOS 13.0, *) 287 | public func invertMuting(_ muteType: es_mute_inversion_type_t) throws { 288 | let result: Bool 289 | switch muteType { 290 | case ES_MUTE_INVERSION_TYPE_PROCESS: 291 | result = processMutes.invertMuting() 292 | case ES_MUTE_INVERSION_TYPE_PATH: 293 | result = pathMutes.invertMuting() 294 | default: 295 | result = client.esInvertMuting(muteType) == ES_RETURN_SUCCESS 296 | } 297 | try tryAction("invertMuting(\(muteType))", success: ES_RETURN_SUCCESS) { 298 | result ? ES_RETURN_SUCCESS : ES_RETURN_ERROR 299 | } 300 | } 301 | 302 | /// Mute state of a given mute dimension. 303 | @available(macOS 13.0, *) 304 | public func mutingInverted(_ muteType: es_mute_inversion_type_t) throws -> Bool { 305 | let status = client.esMutingInverted(muteType) 306 | switch status { 307 | case ES_MUTE_INVERTED: 308 | return true 309 | case ES_MUTE_NOT_INVERTED: 310 | return false 311 | default: 312 | throw ESError("mutingInverted(\(muteType))", result: status, client: name) 313 | } 314 | } 315 | 316 | // MARK: Private 317 | 318 | private var client: ESNativeClient 319 | private let pathMutes: ESMutePath 320 | private let processMutes: ESMuteProcess 321 | private let timebaseInfo: mach_timebase_info? 322 | 323 | @inline(__always) 324 | private func handleMessage(_ message: ESMessagePtr) { 325 | let isMuted = checkIgnored(message) 326 | switch message.action_type { 327 | case ES_ACTION_TYPE_AUTH: 328 | guard let authMessageHandler, !isMuted else { 329 | respond(message, resolution: .allowOnce, reason: .muted) 330 | return 331 | } 332 | 333 | var item: DispatchWorkItem? 334 | if let timebaseInfo, let messageTimeout = config.messageTimeout { 335 | item = scheduleCancel(for: message, timebaseInfo: timebaseInfo, timeout: messageTimeout) { 336 | self.respond(message, resolution: .allowOnce, reason: .timeout) 337 | } 338 | } 339 | 340 | queue.async { 341 | authMessageHandler(message) { 342 | self.respond(message, resolution: $0, reason: .normal, timeoutItem: item) 343 | } 344 | } 345 | case ES_ACTION_TYPE_NOTIFY: 346 | guard !isMuted else { return } 347 | queue.async { self.notifyMessageHandler?(message) } 348 | default: 349 | log.warning("Unknown es_action_type = \(message.action_type)") 350 | } 351 | } 352 | 353 | @inline(__always) 354 | private func checkIgnored(_ message: ESMessagePtr) -> Bool { 355 | let event = message.event_type 356 | let converter = ESConverter(version: message.version) 357 | 358 | let path = converter.esString(message.process.pointee.executable.pointee.path) 359 | let token = message.process.pointee.audit_token 360 | lazy var process = converter.esProcess(message.process.pointee) 361 | 362 | guard !pathMutes.checkIgnored(event, path: path, process: process) else { return true } 363 | guard !processMutes.checkMuted(event, process: token) else { return true } 364 | 365 | return false 366 | } 367 | 368 | @inline(__always) 369 | private func respond(_ message: ESMessagePtr, resolution: ESAuthResolution, reason: ResponseReason, timeoutItem: DispatchWorkItem? = nil) { 370 | guard timeoutItem?.isCancelled != true else { return } 371 | timeoutItem?.cancel() 372 | 373 | let status = client.esRespond(message.rawMessage, flags: resolution.result.rawValue, cache: resolution.cache) 374 | 375 | if let postAuthMessageHandler { 376 | let responseInfo = ResponseInfo(reason: reason, resolution: resolution, status: status) 377 | queue.async { postAuthMessageHandler(message, responseInfo) } 378 | } 379 | } 380 | 381 | private func scheduleCancel( 382 | for message: ESMessagePtr, 383 | timebaseInfo: mach_timebase_info, 384 | timeout: Config.MessageTimeout, 385 | cancellation: @escaping () -> Void 386 | ) -> DispatchWorkItem? { 387 | let machInterval = message.deadline - message.mach_time 388 | let fullInterval = TimeInterval(machTime: machInterval, timebase: timebaseInfo) 389 | 390 | let interval: TimeInterval 391 | switch timeout { 392 | case .seconds(let seconds): 393 | interval = min(seconds, fullInterval) 394 | case .ratio(let ratio): 395 | interval = fullInterval * ratio.clamped(to: 0.0...1.0) 396 | } 397 | 398 | let item = DispatchWorkItem(block: cancellation) 399 | DispatchQueue.global().asyncAfter(deadline: .now() + interval, execute: item) 400 | 401 | return item 402 | } 403 | } 404 | 405 | extension ESClient { 406 | public struct Config: Equatable, Codable { 407 | public var messageTimeout: MessageTimeout? 408 | 409 | public enum MessageTimeout: Equatable, Codable { 410 | case ratio(Double) // 0...1.0 411 | case seconds(TimeInterval) 412 | } 413 | 414 | public init() {} 415 | } 416 | 417 | public enum ResponseReason: Equatable, Codable { 418 | case muted 419 | case timeout 420 | case normal 421 | } 422 | 423 | public struct ResponseInfo: Equatable, Codable { 424 | public var reason: ResponseReason 425 | public var resolution: ESAuthResolution 426 | public var status: es_respond_result_t 427 | 428 | public init(reason: ESClient.ResponseReason, resolution: ESAuthResolution, status: es_respond_result_t) { 429 | self.reason = reason 430 | self.resolution = resolution 431 | self.status = status 432 | } 433 | } 434 | } 435 | 436 | extension ESClient { 437 | /// For testing purposes only. 438 | internal static func test(newClient: (inout ESNativeClient?, @escaping es_handler_block_t) -> es_new_client_result_t) throws -> ESClient { 439 | var native: ESNativeClient? 440 | weak var weakSelf: ESClient? 441 | let status = newClient(&native) { _, rawMessage in 442 | if let self = weakSelf { 443 | let message = ESMessagePtr(unowned: rawMessage) 444 | self.handleMessage(message) 445 | } else { 446 | fatalError("ESClient.test if nil") 447 | } 448 | } 449 | 450 | let client = try ESClient(name: nil, client: native, status: status) 451 | weakSelf = client 452 | 453 | return client 454 | } 455 | } 456 | 457 | extension ESMuteProcessRule { 458 | fileprivate var token: audit_token_t? { 459 | switch self { 460 | case .token(let token): 461 | return token 462 | case .pid(let pid): 463 | do { 464 | return try audit_token_t(pid: pid) 465 | } catch { 466 | log.warning("Failed to get auditToken for pid = \(pid)") 467 | return nil 468 | } 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESClient/ESClientProtocol.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import Combine 24 | import EndpointSecurity 25 | import Foundation 26 | 27 | public protocol ESClientProtocol: AnyObject { 28 | associatedtype Message 29 | 30 | var name: String { get set } 31 | var queue: DispatchQueue? { get set } 32 | 33 | var authMessageHandler: ((Message, @escaping (ESAuthResolution) -> Void) -> Void)? { get set } 34 | var notifyMessageHandler: ((Message) -> Void)? { get set } 35 | 36 | func subscribe(_ events: [es_event_type_t]) throws 37 | func unsubscribe(_ events: [es_event_type_t]) throws 38 | func unsubscribeAll() throws 39 | func clearCache() throws 40 | 41 | var pathInterestHandler: ((ESProcess) -> ESInterest)? { get set } 42 | func clearPathInterestCache() throws 43 | 44 | func mute(process rule: ESMuteProcessRule, events: ESEventSet) throws 45 | func unmute(process rule: ESMuteProcessRule, events: ESEventSet) throws 46 | func unmuteAllProcesses() throws 47 | func mute(path: String, type: es_mute_path_type_t, events: ESEventSet) throws 48 | @available(macOS 12.0, *) 49 | func unmute(path: String, type: es_mute_path_type_t, events: ESEventSet) throws 50 | func unmuteAllPaths() throws 51 | @available(macOS 13.0, *) 52 | func unmuteAllTargetPaths() throws 53 | 54 | @available(macOS 13.0, *) 55 | func invertMuting(_ muteType: es_mute_inversion_type_t) throws 56 | @available(macOS 13.0, *) 57 | func mutingInverted(_ muteType: es_mute_inversion_type_t) throws -> Bool 58 | } 59 | 60 | extension ESClientProtocol { 61 | internal func tryAction(_ action: String, success: T, body: () throws -> T) throws { 62 | let result = try body() 63 | if result != success { 64 | throw ESError(action, result: result, client: name) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESClient/ESClientTypes.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | public struct ESAuthResolution: Equatable, Codable { 28 | public var result: ESAuthResult 29 | public var cache: Bool 30 | 31 | public init(result: ESAuthResult, cache: Bool) { 32 | self.result = result 33 | self.cache = cache 34 | } 35 | } 36 | 37 | extension ESAuthResolution { 38 | public static let allow = ESAuthResolution(result: .auth(true), cache: true) 39 | public static let allowOnce = ESAuthResolution(result: .auth(true), cache: false) 40 | public static let deny = ESAuthResolution(result: .auth(false), cache: true) 41 | public static let denyOnce = ESAuthResolution(result: .auth(false), cache: false) 42 | } 43 | 44 | public struct ESError: Error, Codable where T: Codable { 45 | public var action: String 46 | public var result: T 47 | public var client: String 48 | 49 | public init(_ action: String, result: T, client: String) { 50 | self.action = action 51 | self.result = result 52 | self.client = client 53 | } 54 | } 55 | 56 | extension ESError: CustomStringConvertible { 57 | public var description: String { 58 | "Failed to \(action) with result \(result) by \(client)" 59 | } 60 | } 61 | 62 | extension ESAuthResolution { 63 | /// Restrictive combine of multiple `ESAuthResolution` values. 64 | /// 65 | /// Deny has precedence over allow. Non-cache has precedence over cache. 66 | public static func combine(_ resolutions: [ESAuthResolution]) -> ESAuthResolution { 67 | guard let first = resolutions.first else { return .allowOnce } 68 | guard resolutions.count > 1 else { return first } 69 | 70 | let flags = resolutions.map(\.result.rawValue).reduce(UInt32.max) { $0 & $1 } 71 | let cache = resolutions.map(\.cache).reduce(true) { $0 && $1 } 72 | 73 | return ESAuthResolution(result: .flags(flags), cache: cache) 74 | } 75 | } 76 | 77 | public struct ESEventSet: Equatable, Codable { 78 | public var events: Set 79 | } 80 | 81 | extension ESEventSet { 82 | public init(events: [es_event_type_t]) { 83 | self.init(events: Set(events)) 84 | } 85 | } 86 | 87 | extension ESEventSet: ExpressibleByArrayLiteral { 88 | public init(arrayLiteral elements: es_event_type_t...) { 89 | self.init(events: Set(elements)) 90 | } 91 | } 92 | 93 | extension ESEventSet { 94 | public static let empty = ESEventSet(events: []) 95 | public static let all = ESEventSet(events: (0.. ESEventSet { ESEventSet(events: ESEventSet.all.events.subtracting(events)) } 100 | } 101 | 102 | public struct ESInterest: Equatable, Codable { 103 | public var events: Set 104 | public internal(set) var suggestNativeMuting = false 105 | } 106 | 107 | extension ESInterest { 108 | public static func listen(_ events: ESEventSet = .all) -> ESInterest { 109 | ESInterest(events: events.events) 110 | } 111 | 112 | /// Ignore set of events. 113 | /// Additionally performs native muting of the path literal / process is suggested and possible. 114 | /// - Warning: muting natively too many paths or processes (200+) may cause performance degradation 115 | /// because of implementation specifics of `es_client` on some versions of macOS. 116 | /// - Note: suggestNativeMuting works only on macOS 12.0+. 117 | public static func ignore(_ events: ESEventSet = .all, suggestNativeMuting: Bool = false) -> ESInterest { 118 | ESInterest(events: events.inverted().events, suggestNativeMuting: suggestNativeMuting) 119 | } 120 | } 121 | 122 | extension ESInterest { 123 | public static func combine(_ type: CombineType, _ resolutions: [ESInterest]) -> ESInterest? { 124 | guard let first = resolutions.first else { return nil } 125 | guard resolutions.count > 1 else { return first } 126 | 127 | let events = resolutions.dropFirst().reduce(into: first.events) { 128 | switch type { 129 | case .restrictive: $0.formIntersection($1.events) 130 | case .permissive: $0.formUnion($1.events) 131 | } 132 | } 133 | let nativeMute = resolutions.map(\.suggestNativeMuting).reduce(true) { $0 && $1 } 134 | 135 | return ESInterest(events: events, suggestNativeMuting: nativeMute) 136 | } 137 | 138 | public enum CombineType: Equatable, Codable { 139 | /// Interest in intersection of events in resolutions. 140 | case restrictive 141 | 142 | /// Interest in union of events in resolutions. 143 | case permissive 144 | } 145 | } 146 | 147 | public enum ESMuteProcessRule: Hashable, Codable { 148 | case token(audit_token_t) 149 | case pid(pid_t) 150 | } 151 | 152 | public struct ESReturnError: Error { 153 | public var value: es_return_t 154 | public var action: String? 155 | 156 | public init(_ action: String? = nil, value: es_return_t = ES_RETURN_ERROR) { 157 | self.value = value 158 | self.action = action 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESClient/ESMessagePtr.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | @dynamicMemberLookup 28 | public final class ESMessagePtr { 29 | private enum Ownership { 30 | case retained 31 | case unowned 32 | } 33 | 34 | public let rawMessage: UnsafePointer 35 | private let shouldFree: Bool 36 | 37 | /// Initializes with message from `es_client` handler, retaining it and releasing when deallocated. 38 | public init(message: UnsafePointer) { 39 | es_retain_message(message) 40 | self.rawMessage = message 41 | self.shouldFree = true 42 | } 43 | 44 | /// Initializes with message from `es_client` handler without copying or retaining it. 45 | /// Use ONLY if you are sure that message outlives created instance. 46 | public init(unowned message: UnsafePointer) { 47 | self.rawMessage = message 48 | self.shouldFree = false 49 | } 50 | 51 | deinit { 52 | guard shouldFree else { return } 53 | es_release_message(rawMessage) 54 | } 55 | 56 | /// Converts raw message into ESMessage. 57 | public func converted(_ config: ESConverter.Config = .default) throws -> ESMessage { 58 | try ESConverter.esMessage(rawMessage.pointee, config: config) 59 | } 60 | } 61 | 62 | extension ESMessagePtr { 63 | public subscript(dynamicMember keyPath: KeyPath) -> Local { 64 | rawMessage.pointee[keyPath: keyPath] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESClient/ESMutePath.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | private let log = SpellbookLogger.internalLog(.client) 28 | 29 | internal final class ESMutePath { 30 | private let client: ESNativeClient 31 | private let useAPIv12: Bool 32 | 33 | private var cache: [String: CacheEntry] = [:] 34 | private var pathMutes: [MutePathKey: Set] = [:] 35 | private var pathMutesInverted = false 36 | private var lock = os_unfair_lock_s() 37 | 38 | init(client: ESNativeClient, useAPIv12: Bool = true) { 39 | self.client = client 40 | self.useAPIv12 = useAPIv12 41 | } 42 | 43 | // MARK: Ignore 44 | 45 | var interestHandler: (ESProcess) -> ESInterest = { _ in .listen() } 46 | 47 | func checkIgnored(_ event: es_event_type_t, path: String, process: @autoclosure () -> ESProcess) -> Bool { 48 | os_unfair_lock_lock(&lock) 49 | defer { os_unfair_lock_unlock(&lock) } 50 | 51 | let entry = findEntry(path: path, interest: nil) 52 | guard entry.muted.contains(event) == pathMutesInverted else { return true } 53 | 54 | if let ignored = entry.ignored { 55 | return ignored.contains(event) 56 | } 57 | 58 | os_unfair_lock_unlock(&lock) 59 | let interest = interestHandler(process()) 60 | os_unfair_lock_lock(&lock) 61 | 62 | return findEntry(path: path, interest: interest).ignored?.contains(event) == true 63 | } 64 | 65 | private func findEntry(path: String, interest: ESInterest?) -> CacheEntry { 66 | let entry: CacheEntry 67 | if let cached = cache[path] { 68 | entry = cached 69 | } else { 70 | entry = CacheEntry(muted: mutedEventsByRule(path: path)) 71 | cache[path] = entry 72 | } 73 | 74 | if let interest { 75 | entry.ignored = ESEventSet(events: interest.events).inverted().events 76 | entry.muteIgnoredNatively = interest.suggestNativeMuting 77 | updateMutedIgnores(entry, path: path, mute: true) 78 | } 79 | 80 | return entry 81 | } 82 | 83 | /// May be called from: 84 | /// - `findEntry` on event check 85 | /// - `clearIgnoreCache` 86 | /// - `invertMuting` 87 | private func updateMutedIgnores(_ entry: CacheEntry, path: String, mute: Bool) { 88 | guard entry.muteIgnoredNatively else { return } 89 | guard #available(macOS 12.0, *), useAPIv12 else { return } 90 | 91 | if !mute { 92 | let unmute = (entry.ignored ?? []).subtracting(entry.muted) 93 | nativeUnmute(path, type: ES_MUTE_PATH_TYPE_LITERAL, events: unmute) 94 | } else if !pathMutesInverted, let ignored = entry.ignored, !ignored.isEmpty { 95 | muteNative(path, type: ES_MUTE_PATH_TYPE_LITERAL, events: ignored) 96 | } 97 | } 98 | 99 | func clearIgnoreCache() { 100 | os_unfair_lock_lock(&lock) 101 | defer { os_unfair_lock_unlock(&lock) } 102 | 103 | for (path, entry) in cache { 104 | updateMutedIgnores(entry, path: path, mute: false) 105 | entry.ignored = nil 106 | } 107 | } 108 | 109 | // MARK: Mute 110 | 111 | func mute(_ path: String, type: es_mute_path_type_t, events: Set) { 112 | os_unfair_lock_lock(&lock) 113 | defer { os_unfair_lock_unlock(&lock) } 114 | 115 | let key = MutePathKey(pattern: path, type: type) 116 | pathMutes[key, default: []].formUnion(events) 117 | muteNative(path, type: type, events: events) 118 | 119 | for (entryPath, entry) in cache { 120 | guard key.match(path: entryPath) else { continue } 121 | entry.muted = mutedEventsByRule(path: entryPath) 122 | } 123 | } 124 | 125 | @available(macOS 12.0, *) 126 | func unmute(_ path: String, type: es_mute_path_type_t, events: Set) { 127 | os_unfair_lock_lock(&lock) 128 | defer { os_unfair_lock_unlock(&lock) } 129 | 130 | let key = MutePathKey(pattern: path, type: type) 131 | pathMutes[key]?.subtract(events) 132 | 133 | var events = events 134 | if type == ES_MUTE_PATH_TYPE_LITERAL, !pathMutesInverted, let entry = cache[path], entry.muteIgnoredNatively { 135 | events.subtract(entry.ignored ?? []) 136 | } 137 | nativeUnmute(path, type: type, events: events) 138 | 139 | for (entryPath, entry) in cache { 140 | guard key.match(path: entryPath) else { continue } 141 | entry.muted = mutedEventsByRule(path: entryPath) 142 | } 143 | } 144 | 145 | func unmuteAll() -> Bool { 146 | os_unfair_lock_lock(&lock) 147 | defer { os_unfair_lock_unlock(&lock) } 148 | 149 | guard client.esUnmuteAllPaths() == ES_RETURN_SUCCESS else { return false } 150 | cache.removeAll() 151 | pathMutes.removeAll() 152 | return true 153 | } 154 | 155 | private func mutedEventsByRule(path: String) -> Set { 156 | pathMutes 157 | .filter { $0.key.match(path: path) } 158 | .reduce(into: Set()) { $0.formUnion($1.value) } 159 | } 160 | 161 | // MARK: Mute - Native 162 | 163 | private func muteNative(_ path: String, type: es_mute_path_type_t, events: Set) { 164 | if useAPIv12, #available(macOS 12.0, *) { 165 | if client.esMutePathEvents(path, type, Array(events)) != ES_RETURN_SUCCESS { 166 | log.warning("Failed to mute path events: type = \(type), path = \(path)") 167 | } 168 | } else if events == ESEventSet.all.events { 169 | if client.esMutePath(path, type) != ES_RETURN_SUCCESS { 170 | log.warning("Failed to mute path: type = \(type), path = \(path)") 171 | } 172 | } 173 | } 174 | 175 | @available(macOS 12.0, *) 176 | private func nativeUnmute(_ path: String, type: es_mute_path_type_t, events: Set) { 177 | guard useAPIv12 else { return } 178 | 179 | if client.esUnmutePathEvents(path, type, Array(events)) != ES_RETURN_SUCCESS { 180 | log.warning("Failed to unmute path events: type = \(type), path = \(path)") 181 | } 182 | } 183 | 184 | // MARK: Other 185 | 186 | @available(macOS 13.0, *) 187 | func invertMuting() -> Bool { 188 | os_unfair_lock_lock(&lock) 189 | defer { os_unfair_lock_unlock(&lock) } 190 | 191 | guard client.esInvertMuting(ES_MUTE_INVERSION_TYPE_PATH) == ES_RETURN_SUCCESS else { return false } 192 | 193 | pathMutesInverted.toggle() 194 | cache.forEach { path, entry in 195 | guard entry.muteIgnoredNatively else { return } 196 | updateMutedIgnores(entry, path: path, mute: !pathMutesInverted) 197 | } 198 | 199 | return true 200 | } 201 | } 202 | 203 | extension ESMutePath { 204 | private class CacheEntry { 205 | var muted: Set 206 | var ignored: Set? 207 | var muteIgnoredNatively = false 208 | 209 | init(muted: Set) { 210 | self.muted = muted 211 | } 212 | } 213 | 214 | private struct MutePathKey: Hashable { 215 | var pattern: String 216 | var type: es_mute_path_type_t 217 | 218 | func match(path: String) -> Bool { 219 | switch type { 220 | case ES_MUTE_PATH_TYPE_LITERAL: return path == pattern 221 | case ES_MUTE_PATH_TYPE_PREFIX: return path.starts(with: pattern) 222 | default: return false 223 | } 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESClient/ESMuteProcess.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | private let log = SpellbookLogger.internalLog(.client) 28 | 29 | internal final class ESMuteProcess { 30 | private let client: ESNativeClient 31 | private let cleanupDelay: TimeInterval 32 | private let environment: Environment 33 | private var processMutes: [audit_token_t: Set] = [:] 34 | private var lock = os_unfair_lock_s() 35 | private var processMutesInverted = false 36 | 37 | init(client: ESNativeClient, cleanupDelay: TimeInterval = 60.0, environment: Environment = .init()) { 38 | self.client = client 39 | self.cleanupDelay = cleanupDelay 40 | self.environment = environment 41 | 42 | scheduleCleanupDiedProcesses() 43 | } 44 | 45 | private func scheduleCleanupDiedProcesses() { 46 | DispatchQueue.global().asyncAfter(deadline: .now() + cleanupDelay) { [weak self] in 47 | guard let self else { return } 48 | self.cleanupDiedProcesses() 49 | self.scheduleCleanupDiedProcesses() 50 | } 51 | } 52 | 53 | private func cleanupDiedProcesses() { 54 | os_unfair_lock_lock(&lock) 55 | let processMutesCopy = processMutes 56 | os_unfair_lock_unlock(&lock) 57 | 58 | let tokensToRemove = processMutesCopy.keys.filter { !environment.checkAlive($0) } 59 | os_unfair_lock_lock(&lock) 60 | tokensToRemove.forEach { processMutes.removeValue(forKey: $0) } 61 | os_unfair_lock_unlock(&lock) 62 | } 63 | 64 | // MARK: Mute check 65 | 66 | func checkMuted(_ event: es_event_type_t, process token: audit_token_t) -> Bool { 67 | os_unfair_lock_lock(&lock) 68 | defer { os_unfair_lock_unlock(&lock) } 69 | 70 | guard let processMuted = processMutes[token]?.contains(event) else { return false } 71 | return processMuted != processMutesInverted 72 | } 73 | 74 | // MARK: Mute management 75 | 76 | func mute(_ token: audit_token_t, events: Set) { 77 | os_unfair_lock_lock(&lock) 78 | defer { os_unfair_lock_unlock(&lock) } 79 | 80 | processMutes[token, default: []].formUnion(events) 81 | muteNative(token, events: events) 82 | } 83 | 84 | func unmute(_ token: audit_token_t, events: Set) { 85 | os_unfair_lock_lock(&lock) 86 | defer { os_unfair_lock_unlock(&lock) } 87 | 88 | if var cachedEvents = processMutes[token] { 89 | cachedEvents.subtract(events) 90 | if cachedEvents.isEmpty { 91 | processMutes.removeValue(forKey: token) 92 | } else { 93 | processMutes[token] = cachedEvents 94 | } 95 | } 96 | unmuteNative(token, events: events) 97 | } 98 | 99 | func unmuteAll() { 100 | os_unfair_lock_lock(&lock) 101 | defer { os_unfair_lock_unlock(&lock) } 102 | 103 | processMutes.keys.forEach { unmuteNative($0, events: ESEventSet.all.events) } 104 | processMutes.removeAll() 105 | } 106 | 107 | private func muteNative(_ token: audit_token_t, events: Set) { 108 | if environment.useAPIv12, #available(macOS 12.0, *) { 109 | if client.esMuteProcessEvents(token, Array(events)) != ES_RETURN_SUCCESS { 110 | log.warning("Failed to mute process events: pid = \(token.pid)") 111 | } 112 | } else if events == ESEventSet.all.events { 113 | if client.esMuteProcess(token) != ES_RETURN_SUCCESS { 114 | log.warning("Failed to mute process: pid = \(token.pid)") 115 | } 116 | } 117 | } 118 | 119 | private func unmuteNative(_ token: audit_token_t, events: Set) { 120 | if environment.useAPIv12, #available(macOS 12.0, *) { 121 | if client.esUnmuteProcessEvents(token, Array(events)) != ES_RETURN_SUCCESS { 122 | log.warning("Failed to unmute process events: pid = \(token.pid)") 123 | } 124 | } else { 125 | if client.esUnmuteProcess(token) != ES_RETURN_SUCCESS { 126 | log.warning("Failed to unmute process: pid = \(token.pid)") 127 | } 128 | } 129 | } 130 | 131 | // MARK: Other 132 | 133 | @available(macOS 13.0, *) 134 | func invertMuting() -> Bool { 135 | os_unfair_lock_lock(&lock) 136 | defer { os_unfair_lock_unlock(&lock) } 137 | 138 | guard client.esInvertMuting(ES_MUTE_INVERSION_TYPE_PROCESS) == ES_RETURN_SUCCESS else { return false } 139 | 140 | processMutesInverted.toggle() 141 | 142 | return true 143 | } 144 | } 145 | 146 | extension ESMuteProcess { 147 | internal struct Environment { 148 | var useAPIv12 = true 149 | var checkAlive: (audit_token_t) -> Bool = { $0.checkAlive() } 150 | } 151 | } 152 | 153 | private extension audit_token_t { 154 | func checkAlive() -> Bool { 155 | (try? audit_token_t(pid: pid) == self) == true 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESService/ESService.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | private let log = SpellbookLogger.internalLog(.service) 28 | 29 | /// ESService provides muliclient support for dealing with EndpointSecurity events 30 | /// without need of managing many native `es_client(s)` (at least there is OS limit for latter). 31 | /// 32 | /// ESService would be convenient when the product has multiple features that 33 | /// uses ES events internally. 34 | /// 35 | /// There is a strict order of deating with `ESService`: 36 | /// - create `ESService`, usually single instance for whole application. 37 | /// - `register` all components within it. 38 | /// - setup mutes, inversions, etc. 39 | /// - `activate` the service. 40 | /// 41 | /// There are some special cases in above scenario: 42 | /// 1. `activate` may fail because of underlying ES.framework creation error. 43 | /// That is normal scenario. Just fix error cause and `activate` again. 44 | /// All subscriptions, mutes, etc. are kept. 45 | /// 2. After `activate`, all subscriptions that are not suspended would receive ES events. 46 | /// Suspended subscriptions would NOT receive events while they remain suspended. 47 | public final class ESService: ESServiceRegistering { 48 | private typealias Client = any ESClientProtocol 49 | private let createES: (ESService) throws -> Client 50 | private let store = ESServiceSubscriptionStore() 51 | private var client: Client? 52 | private var isActivated = false 53 | private var activationLock = UnfairLock() 54 | 55 | /// Create `ESService` that internally creates and manages default-create `ESClient`. 56 | public convenience init() { 57 | self.init(createES: ESClient.init) 58 | } 59 | 60 | /// Create `ESService` with factory that creates `ESClient` instances. 61 | /// `ESService` will override any of created client handlers and rely on fact that 62 | /// client it totally owned and managed by the Service. 63 | public init(createES: @escaping (String) throws -> C) where C.Message == ESMessage { 64 | self.createES = { service in 65 | let client = try createES(service.clientName) 66 | service.setupClient(client) 67 | return client 68 | } 69 | } 70 | 71 | /// Create `ESService` with factory that creates `ESClient` instances. 72 | /// `ESService` will override any of created client handlers and rely on fact that 73 | /// client it totally owned and managed by the Service. 74 | public init(createES: @escaping (String) throws -> C) where C.Message == ESMessagePtr { 75 | self.createES = { service in 76 | let client = try createES(service.clientName) 77 | service.setupClient(client) 78 | return client 79 | } 80 | } 81 | 82 | /// Create `ESService` with externally created `ESClient`. 83 | /// `ESService` will override any of passed client handlers and rely on fact that 84 | /// client it totally owned and managed by the Service. 85 | public init(_ client: C) where C.Message == ESMessage { 86 | self.createES = { _ in client } 87 | setupClient(client) 88 | } 89 | 90 | /// Create `ESService` with externally created `ESClient`. 91 | /// `ESService` will override any of passed client handlers and rely on fact that 92 | /// client it totally owned and managed by the Service. 93 | public init(_ client: C) where C.Message == ESMessagePtr { 94 | self.createES = { _ in client } 95 | setupClient(client) 96 | } 97 | 98 | private var clientName: String { "ESService_\(ObjectIdentifier(self))" } 99 | 100 | private func setupClient(_ client: C) where C.Message == ESMessage { 101 | client.authMessageHandler = store.handleAuthMessage 102 | client.notifyMessageHandler = store.handleNotifyMessage 103 | return setupAnyClient(client) 104 | } 105 | 106 | private func setupClient(_ client: C) where C.Message == ESMessagePtr { 107 | client.authMessageHandler = store.handleAuthMessage 108 | client.notifyMessageHandler = store.handleNotifyMessage 109 | return setupAnyClient(client) 110 | } 111 | 112 | private func setupAnyClient(_ client: some ESClientProtocol) { 113 | client.pathInterestHandler = store.pathInterest 114 | client.queue = nil 115 | } 116 | 117 | /// Perform service-level process filtering, additionally to muting of path and processes for all clients. 118 | /// Filtering is based on `interest in particular process executable path`. 119 | /// Designed to be used for granular process filtering by ignoring uninterest events. 120 | /// 121 | /// General idea is to mute or ignore processes we are not interested in using their binary paths. 122 | /// Usually the OS would not have more than ~1000 unique processes, so asking for interest in particular 123 | /// process path would occur very limited number of times. 124 | /// 125 | /// The process may be interested or ignored accoding to returned `ESInterest`. 126 | /// If the process is not interested, all related messages are skipped. 127 | /// More information on `ESInterest` see in related documentation. 128 | /// 129 | /// The final decision if the particular event is delivered or not relies on multiple sources. 130 | /// Sources considered: 131 | /// - `mute(path:)` rules 132 | /// - `mute(process:)` rules 133 | /// - `pathInterestHandler` resolution 134 | /// - subscriptions `pathInterestHandler` resolutions 135 | /// 136 | /// - Note: Interest does NOT depend on `inversion` of underlying `ESClient`. 137 | /// - Note: Returned resolutions are cached to avoid often handler calls. 138 | /// To reset cache, call `clearCaches`. 139 | /// - Note: When the handler is not set, it defaults to returning `ESInterest.listen()`. 140 | /// 141 | /// - Warning: Perfonamce-sensitive handler, called **synchronously** once for each process path. 142 | /// Do here as minimum work as possible. 143 | /// - Warning: The property MUST NOT be changed while the service is activated. 144 | public var pathInterestHandler: (ESProcess) -> ESInterest { 145 | get { store.pathInterestHandler } 146 | set { store.pathInterestHandler = newValue } 147 | } 148 | 149 | /// Registers the subscription. MUST be called before `activate`. 150 | /// At the moment registration is one-way operation. 151 | /// 152 | /// The caller must retain returned `ESSubscriptionControl` to keep events coming. 153 | public func register(_ subscription: ESSubscription) -> ESSubscriptionControl { 154 | let token = ESSubscriptionControl() 155 | guard !subscription.events.isEmpty else { 156 | assertionFailure("Registering subscription with no events is prohibited") 157 | return token 158 | } 159 | 160 | token._subscribe = { [weak self, events = subscription.events] in 161 | guard let self else { return } 162 | try self.activationLock.withLock { 163 | guard self.isActivated else { return } 164 | try self.client?.subscribe(events) 165 | } 166 | } 167 | token._unsubscribe = { [weak self, events = subscription.events, id = subscription.id] in 168 | guard let self else { return } 169 | 170 | try self.activationLock.withLock { 171 | guard self.isActivated else { return } 172 | 173 | let uniqueEvents = self.store.subscriptions 174 | .filter { $0.state.isSubscribed && $0.subscription.id != id } 175 | .reduce(into: Set(events)) { $0.subtract($1.subscription.events) } 176 | guard !uniqueEvents.isEmpty else { return } 177 | 178 | try self.client?.unsubscribe(Array(uniqueEvents)) 179 | } 180 | } 181 | 182 | activationLock.withLock { 183 | store.addSubscription(subscription, state: token.sharedState) 184 | } 185 | 186 | if let client { 187 | try? client.clearCache() 188 | try? client.clearPathInterestCache() 189 | } 190 | 191 | return token 192 | } 193 | 194 | /// The handler is called in activation process: after ESClient is created but before any subscription is made. 195 | /// Good point to add service-wide mutes. 196 | /// - Warning: Do NOT call `activate` or `invalidate` routines from the handler. 197 | /// - Warning: The property MUST NOT be changed while the service is activated. 198 | public var preSubscriptionHandler: (() throws -> Void)? 199 | 200 | /// Activates the service. On success all subscriptions would start receiving ES events if subscibed. 201 | public func activate() throws { 202 | guard !activationLock.withLock({ isActivated }) else { return } 203 | 204 | let client = try createES(self) 205 | self.client = client 206 | 207 | do { 208 | try preSubscriptionHandler?() 209 | 210 | try activationLock.withLock { 211 | let events = store.subscriptions 212 | .filter { $0.state.isSubscribed } 213 | .reduce(into: Set()) { $0.formUnion($1.subscription.events) } 214 | if !events.isEmpty { 215 | try client.subscribe(Array(events)) 216 | } 217 | 218 | isActivated = true 219 | } 220 | } catch { 221 | self.client = nil 222 | throw error 223 | } 224 | } 225 | 226 | /// Invalidates the service. Discards underlying `es_client`, clears all mutes. 227 | /// Registrations are kept. 228 | public func invalidate() { 229 | activationLock.withLock { 230 | try? client?.unsubscribeAll() 231 | client = nil 232 | isActivated = false 233 | } 234 | } 235 | 236 | /// Config used to convert native `es_message_t` into `ESMessage`. 237 | /// - Warning: The property MUST NOT be changed while the service is activated. 238 | public var converterConfig: ESConverter.Config { 239 | get { store.converterConfig } 240 | set { store.converterConfig = newValue } 241 | } 242 | 243 | /// Reference to `ESClient` used under the hood. 244 | /// DO NOT use it for modifyng any mutes/inversions/etc, the behaviour is undefined. 245 | /// You may want to use it for informational purposes (list of mutes, etc). 246 | public var unsafeClient: (any ESClientProtocol)? { client } 247 | 248 | /// Clear all cached results for all clients. Clears both `interest` and `auth` caches. 249 | public func clearCaches() throws { 250 | guard let client else { return } 251 | store.resetInterestCache() 252 | try client.clearPathInterestCache() 253 | try client.clearCache() 254 | } 255 | 256 | // MARK: Mute 257 | 258 | /// Suppress events from the process described by the given `mute` rule. 259 | /// - Parameters: 260 | /// - mute: process to mute. 261 | /// - events: set of events to mute. 262 | public func mute(process rule: ESMuteProcessRule, events: ESEventSet = .all) throws { 263 | try withActiveClient { try $0.mute(process: rule, events: events) } 264 | } 265 | 266 | /// Unmute events for the process described by the given `mute` rule. 267 | /// - Parameters: 268 | /// - mute: process to unmute. 269 | /// - events: set of events to mute. 270 | public func unmute(process rule: ESMuteProcessRule, events: ESEventSet = .all) throws { 271 | try withActiveClient { try $0.unmute(process: rule, events: events) } 272 | } 273 | 274 | /// Unmute all events for all processes. Clear the rules. 275 | public func unmuteAllProcesses() throws { 276 | try withActiveClient { try $0.unmuteAllProcesses() } 277 | } 278 | 279 | /// Suppress events for the the given at path and type. 280 | /// - Parameters: 281 | /// - mute: process path to mute. 282 | /// - type: path type. 283 | /// - events: set of events to mute. 284 | public func mute(path: String, type: es_mute_path_type_t, events: ESEventSet = .all) throws { 285 | try withActiveClient { try $0.mute(path: path, type: type, events: events) } 286 | } 287 | 288 | /// Unmute events for the given at path and type. 289 | /// - Parameters: 290 | /// - mute: process path to unmute. 291 | /// - type: path type. 292 | /// - events: set of events to unmute. 293 | @available(macOS 12.0, *) 294 | public func unmute(path: String, type: es_mute_path_type_t, events: ESEventSet = .all) throws { 295 | try withActiveClient { try $0.unmute(path: path, type: type, events: events) } 296 | } 297 | 298 | /// Unmute all events for all process paths. 299 | public func unmuteAllPaths() throws { 300 | try withActiveClient { try $0.unmuteAllPaths() } 301 | } 302 | 303 | /// Unmute all target paths. Works only for macOS 13.0+. 304 | @available(macOS 13.0, *) 305 | public func unmuteAllTargetPaths() throws { 306 | try withActiveClient { try $0.unmuteAllTargetPaths() } 307 | } 308 | 309 | /// Invert the mute state of a given mute dimension. 310 | @available(macOS 13.0, *) 311 | public func invertMuting(_ muteType: es_mute_inversion_type_t) throws { 312 | try withActiveClient { try $0.invertMuting(muteType) } 313 | } 314 | 315 | private func withActiveClient(_ function: String = #function, body: @escaping (Client) throws -> Void) throws { 316 | if let client { 317 | try body(client) 318 | } else { 319 | throw CommonError.unexpected("Trying to call \(function) on non-activated ESService") 320 | } 321 | } 322 | } 323 | 324 | public protocol ESServiceRegistering { 325 | func register(_ subscription: ESSubscription) -> ESSubscriptionControl 326 | } 327 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESService/ESServiceSubscriptionStore.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | private let log = SpellbookLogger.internalLog(.service) 28 | 29 | internal final class ESServiceSubscriptionStore { 30 | final class Entry { 31 | var subscription: ESSubscription 32 | var state: SubscriptionState 33 | 34 | init(subscription: ESSubscription, state: SubscriptionState) { 35 | self.subscription = subscription 36 | self.state = state 37 | } 38 | } 39 | 40 | private var pathInterests: [String: [ObjectIdentifier: Set]] = [:] 41 | private var pathInterestsActual = atomic_flag() 42 | internal private(set) var subscriptions: [Entry] = [] 43 | private var subscriptionEvents: [es_event_type_t: [Entry]] = [:] 44 | 45 | var pathInterestHandler: (ESProcess) -> ESInterest = { _ in .listen() } 46 | var converterConfig: ESConverter.Config = .default 47 | 48 | // MARK: Managing subscriptions 49 | 50 | func addSubscription(_ subscription: ESSubscription, state: SubscriptionState) { 51 | let entry = Entry(subscription: subscription, state: state) 52 | subscriptions.append(entry) 53 | 54 | subscription.events.forEach { 55 | subscriptionEvents[$0, default: []].append(entry) 56 | } 57 | } 58 | 59 | func resetInterestCache() { 60 | atomic_flag_clear(&pathInterestsActual) 61 | } 62 | 63 | // MARK: Handling ES events 64 | 65 | func pathInterest(in process: ESProcess) -> ESInterest { 66 | if !atomic_flag_test_and_set(&pathInterestsActual) { 67 | pathInterests.removeAll(keepingCapacity: true) 68 | } 69 | 70 | var resolutions: [ESInterest] = [] 71 | for entry in subscriptions { 72 | guard entry.state.isAlive else { continue } 73 | 74 | let interest = entry.subscription.queue.sync { entry.subscription.pathInterestHandler(process) } 75 | resolutions.append(interest) 76 | 77 | let identifier = ObjectIdentifier(entry) 78 | pathInterests[process.executable.path, default: [:]][identifier] = interest.events 79 | } 80 | 81 | let subscriptionsInterest = ESInterest.combine(.permissive, resolutions) ?? .listen() 82 | 83 | let totalInterest = ESInterest.combine( 84 | .restrictive, 85 | [subscriptionsInterest, pathInterestHandler(process)] 86 | ) ?? .listen() 87 | return totalInterest 88 | } 89 | 90 | func handleAuthMessage(_ rawMessage: ESMessagePtr, reply: @escaping (ESAuthResolution) -> Void) { 91 | handleAuthMessage( 92 | event: rawMessage.event_type, 93 | path: rawMessage.executablePath, 94 | message: { rawMessage.convertedWithLog(converterConfig) }, 95 | reply: reply 96 | ) 97 | } 98 | 99 | func handleAuthMessage(_ message: ESMessage, reply: @escaping (ESAuthResolution) -> Void) { 100 | handleAuthMessage( 101 | event: message.eventType, 102 | path: message.process.executable.path, 103 | message: { message }, 104 | reply: reply 105 | ) 106 | } 107 | 108 | @inline(__always) 109 | private func handleAuthMessage( 110 | event: es_event_type_t, 111 | path: @autoclosure () -> String, 112 | message: () -> ESMessage?, 113 | reply: @escaping (ESAuthResolution) -> Void 114 | ) { 115 | let subscribers = subscriptions(for: event, path: path()) 116 | guard !subscribers.isEmpty else { 117 | reply(.allowOnce) 118 | return 119 | } 120 | guard let message = message() else { 121 | reply(.allowOnce) 122 | return 123 | } 124 | 125 | let group = ESMultipleResolution(count: subscribers.count, reply: reply) 126 | subscribers.forEach { entry in 127 | entry.subscription.queue.async { 128 | entry.subscription.authMessageHandler(message, group.resolve) 129 | } 130 | } 131 | } 132 | 133 | func handleNotifyMessage(_ rawMessage: ESMessagePtr) { 134 | handleNotifyMessage(event: rawMessage.event_type, path: rawMessage.executablePath) { 135 | rawMessage.convertedWithLog(converterConfig) 136 | } 137 | } 138 | 139 | func handleNotifyMessage(_ message: ESMessage) { 140 | handleNotifyMessage(event: message.eventType, path: message.process.executable.path) { message } 141 | } 142 | 143 | @inline(__always) 144 | private func handleNotifyMessage( 145 | event: es_event_type_t, 146 | path: @autoclosure () -> String, 147 | message: () -> ESMessage? 148 | ) { 149 | let subscribers = subscriptions(for: event, path: path()) 150 | guard !subscribers.isEmpty else { return } 151 | guard let message = message() else { return } 152 | 153 | subscribers.forEach { entry in 154 | entry.subscription.queue.async { 155 | entry.subscription.notifyMessageHandler(message) 156 | } 157 | } 158 | } 159 | 160 | @inline(__always) 161 | private func subscriptions(for event: es_event_type_t, path: @autoclosure () -> String) -> [Entry] { 162 | guard let eventSubscriptions = subscriptionEvents[event] else { return [] } 163 | let activeSubscriptions = eventSubscriptions.filter { $0.state.isSubscribed } 164 | guard !activeSubscriptions.isEmpty else { return [] } 165 | 166 | let path = path() 167 | return activeSubscriptions 168 | .filter { pathInterests[path]?[ObjectIdentifier($0)]?.contains(event) == true } 169 | } 170 | } 171 | 172 | internal final class ESMultipleResolution { 173 | private var lock = UnfairLock() 174 | private var fulfilled = 0 175 | private var resolutions: [ESAuthResolution] 176 | private let reply: (ESAuthResolution) -> Void 177 | 178 | init(count: Int, reply: @escaping (ESAuthResolution) -> Void) { 179 | self.resolutions = .init(repeating: .allow, count: count) 180 | self.reply = reply 181 | } 182 | 183 | func resolve(_ resolution: ESAuthResolution) { 184 | lock.withLock { 185 | resolutions[fulfilled] = resolution 186 | fulfilled += 1 187 | 188 | if fulfilled == resolutions.count { 189 | let combined = ESAuthResolution.combine(resolutions) 190 | reply(combined) 191 | } 192 | } 193 | } 194 | } 195 | 196 | extension ESMessagePtr { 197 | @inline(__always) 198 | fileprivate func convertedWithLog(_ config: ESConverter.Config) -> ESMessage? { 199 | do { 200 | return try converted(config) 201 | } catch { 202 | log.error("Failed to decode message \(rawMessage.pointee.event_type). Error: \(error)") 203 | return nil 204 | } 205 | } 206 | 207 | @inline(__always) 208 | fileprivate var executablePath: String { 209 | ESConverter(version: rawMessage.pointee.version) 210 | .esString(rawMessage.pointee.process.pointee.executable.pointee.path) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/ESService/ESSubscription.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import EndpointSecurity 24 | import Foundation 25 | import SpellbookFoundation 26 | 27 | public struct ESSubscription { 28 | internal let id = UUID() 29 | 30 | public init() {} 31 | 32 | /// Set of events to subscribe on. 33 | public var events: [es_event_type_t] = [] 34 | 35 | /// Queue where `pathInterestHandler`, `authMessageHandler` 36 | /// and `notifyMessageHandler` handlers are called. 37 | /// Defaults to `nil` that means all handlers are called directly on underlying queue. 38 | public var queue: DispatchQueue? 39 | 40 | /// Perform process filtering, additionally to muting of path and processes. 41 | /// Filtering is based on `interest in process with particular executable path`. 42 | /// Designed to be used for granular process filtering by ignoring uninterest events. 43 | /// 44 | /// General idea is to mute or ignore processes we are not interested in using their binary paths. 45 | /// Usually the OS would not have more than ~1000 unique processes, so asking for interest in particular 46 | /// process path would occur very limited number of times. 47 | /// 48 | /// The process may be interested or ignored accoding to returned `ESInterest`. 49 | /// If the process is not interested, all related messages are skipped. 50 | /// More information on `ESInterest` see in related documentation. 51 | /// 52 | /// The final decision if the particular event is delivered or not relies on multiple sources. 53 | /// Sources considered: 54 | /// - `mutePath` rules 55 | /// - `muteProcess` rules 56 | /// - `pathInterestHandler` resolution 57 | /// - `pathInterestRules` rules 58 | /// 59 | /// - Note: Interest does NOT depend on `inversion` of `ESClient`. 60 | /// - Note: Returned resolutions are cached to avoid often handler calls. 61 | /// To reset cache, call `clearPathInterestCache`. 62 | /// - Note: When the handler is not set, it defaults to returning `ESInterest.listen()`. 63 | /// 64 | /// - Warning: Perfonamce-sensitive handler, called **synchronously** once for each process path on `queue`. 65 | /// Do here as minimum work as possible. 66 | public var pathInterestHandler: (ESProcess) -> ESInterest = { _ in .listen() } 67 | 68 | /// Handler invoked each time AUTH message is coming from EndpointSecurity. 69 | /// The message SHOULD be responded using the second parameter - reply block. 70 | public var authMessageHandler: (ESMessage, @escaping (ESAuthResolution) -> Void) -> Void = { $1(.allow) } 71 | 72 | /// Handler invoked each time NOTIFY message is coming from EndpointSecurity. 73 | public var notifyMessageHandler: (ESMessage) -> Void = { _ in } 74 | } 75 | 76 | public final class ESSubscriptionControl { 77 | internal let sharedState = SubscriptionState() 78 | 79 | internal init() { 80 | sharedState.control = self 81 | } 82 | 83 | deinit { 84 | try? unsubscribe() 85 | } 86 | 87 | internal var _subscribe: () throws -> Void = {} 88 | internal var _unsubscribe: () throws -> Void = {} 89 | 90 | public var isSubscribed: Bool { sharedState.isSubscribed } 91 | 92 | /// Resume receiving ES events into `authMessageHandler` and `notifyMessageHandler`. 93 | public func subscribe() throws { 94 | try sharedState.lock.withLock { 95 | guard !sharedState.isSubscribed else { return } 96 | try _subscribe() 97 | OSAtomicCompareAndSwap64(0, 1, sharedState.subscribed) 98 | } 99 | } 100 | 101 | /// Suspend receiving ES events into `authMessageHandler` and `notifyMessageHandler`. 102 | /// `pathInterestHandler` will be still called when needed. 103 | public func unsubscribe() throws { 104 | try sharedState.lock.withLock { 105 | guard sharedState.isSubscribed else { return } 106 | try _unsubscribe() 107 | OSAtomicCompareAndSwap64(1, 0, sharedState.subscribed) 108 | } 109 | } 110 | } 111 | 112 | internal final class SubscriptionState { 113 | @Resource fileprivate var subscribed: UnsafeMutablePointer 114 | fileprivate weak var control: ESSubscriptionControl? 115 | fileprivate var lock = UnfairLock() 116 | 117 | init() { 118 | _subscribed = .pointer(value: 0) 119 | } 120 | 121 | var isSubscribed: Bool { OSAtomicAdd64(0, subscribed) == 1 } 122 | var isAlive: Bool { control != nil } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/Log.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import Foundation 24 | import SpellbookFoundation 25 | 26 | public enum sEndpointSecurityLog { 27 | public static let subsystem = "sEndpointSecurity" 28 | 29 | public enum Category: String { 30 | case client = "ESClient" 31 | case xpc = "ESXPC" 32 | case service = "ESService" 33 | } 34 | } 35 | 36 | extension SpellbookLog { 37 | package static func internalLog(_ category: sEndpointSecurityLog.Category) -> SpellbookLog { 38 | SpellbookLogger.default.with(subsystem: sEndpointSecurityLog.subsystem, category: category.rawValue) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurity/Utils.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import Foundation 24 | 25 | extension Optional where Wrapped == DispatchQueue { 26 | @inline(__always) 27 | package func async(flags: DispatchWorkItemFlags = [], execute work: @escaping () -> Void) { 28 | if let self { 29 | self.async(flags: flags, execute: work) 30 | } else { 31 | work() 32 | } 33 | } 34 | 35 | @inline(__always) 36 | package func sync(execute work: () -> R) -> R { 37 | if let self { 38 | return self.sync(execute: work) 39 | } else { 40 | return work() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityTests/ESClientTests.swift: -------------------------------------------------------------------------------- 1 | @testable import sEndpointSecurity 2 | 3 | import EndpointSecurity 4 | import Foundation 5 | import SpellbookFoundation 6 | import SpellbookTestUtils 7 | import XCTest 8 | 9 | class ESClientTests: XCTestCase { 10 | static let emitQueue = DispatchQueue(label: "ESClientTest.es_native_queue") 11 | var native: MockNativeClient! 12 | var client: ESClient! 13 | var handler: es_handler_block_t! 14 | 15 | override func setUpWithError() throws { 16 | native = MockNativeClient() 17 | client = try ESClient.test { 18 | $0 = native 19 | handler = $1 20 | return ES_NEW_CLIENT_RESULT_SUCCESS 21 | } 22 | } 23 | 24 | func test_usualFlow() throws { 25 | let queue = DispatchQueue.main 26 | client.queue = queue 27 | 28 | let processMuteHandlerExp = expectation(description: "Process mute handler called once per process") 29 | client.pathInterestHandler = { 30 | dispatchPrecondition(condition: .onQueue(queue)) 31 | 32 | XCTAssertEqual($0.name, "test") 33 | processMuteHandlerExp.fulfill() 34 | return .listen() 35 | } 36 | 37 | let authMessageHandlerExp = expectation(description: "Auth handler is called") 38 | client.authMessageHandler = { 39 | dispatchPrecondition(condition: .onQueue(queue)) 40 | 41 | XCTAssertEqual($0.event_type, ES_EVENT_TYPE_AUTH_SETTIME) 42 | $1(.allow) 43 | authMessageHandlerExp.fulfill() 44 | } 45 | 46 | let postAuthMessageHandlerExp = expectation(description: "Post-auth handler is called") 47 | client.postAuthMessageHandler = { [weak self] in 48 | dispatchPrecondition(condition: .onQueue(queue)) 49 | 50 | XCTAssertEqual($0.event_type, ES_EVENT_TYPE_AUTH_SETTIME) 51 | XCTAssertEqual($1, ESClient.ResponseInfo(reason: .normal, resolution: .allow, status: ES_RESPOND_RESULT_SUCCESS)) 52 | postAuthMessageHandlerExp.fulfill() 53 | 54 | XCTAssertEqual(self?.native.responses[$0.global_seq_num], .allow) 55 | } 56 | 57 | let notifyMessageHandlerExp = expectation(description: "Notify handler is called") 58 | client.notifyMessageHandler = { 59 | dispatchPrecondition(condition: .onQueue(queue)) 60 | 61 | XCTAssertEqual($0.event_type, ES_EVENT_TYPE_NOTIFY_SETTIME) 62 | notifyMessageHandlerExp.fulfill() 63 | } 64 | 65 | XCTAssertNoThrow(try client.subscribe([ES_EVENT_TYPE_AUTH_SETTIME, ES_EVENT_TYPE_NOTIFY_SETTIME])) 66 | XCTAssertEqual(native.subscriptions, [ES_EVENT_TYPE_AUTH_SETTIME, ES_EVENT_TYPE_NOTIFY_SETTIME]) 67 | 68 | emitMessage(path: "/path/to/test", signingID: "s1", teamID: "t1", event: ES_EVENT_TYPE_AUTH_SETTIME, isAuth: true) 69 | emitMessage(path: "/path/to/test", signingID: "s1", teamID: "t1", event: ES_EVENT_TYPE_NOTIFY_SETTIME, isAuth: false) 70 | 71 | waitForExpectations(timeout: 0.1) 72 | } 73 | 74 | func test_mutes_ignores() { 75 | // Case 1. 76 | XCTAssertNoThrow(try client.mute(path: "test1", type: ES_MUTE_PATH_TYPE_LITERAL)) 77 | 78 | let expCase1Test1NotCalled = expectation(description: "case 1: test1 process should be muted") 79 | expCase1Test1NotCalled.isInverted = true 80 | let expCase1Other = expectation(description: "case 1: other processes not muted") 81 | expCase1Other.expectedFulfillmentCount = 2 82 | client.notifyMessageHandler = { 83 | let name = ESConverter(version: $0.version).esProcess($0.process.pointee).name 84 | if name == "test1" { 85 | expCase1Test1NotCalled.fulfill() 86 | } else { 87 | XCTAssertTrue(name.contains("other")) 88 | expCase1Other.fulfill() 89 | } 90 | } 91 | emitMessage(path: "other1", signingID: "", teamID: "", event: ES_EVENT_TYPE_NOTIFY_OPEN, isAuth: false) 92 | emitMessage(path: "test1", signingID: "", teamID: "", event: ES_EVENT_TYPE_NOTIFY_OPEN, isAuth: false) 93 | emitMessage(path: "other2", signingID: "", teamID: "", event: ES_EVENT_TYPE_NOTIFY_CLOSE, isAuth: false) 94 | 95 | waitForExpectations() 96 | 97 | // Case 2. 98 | XCTAssertNoThrow(try client.mute(path: "test2", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN])) 99 | 100 | let expCase2OpenNotCalled = expectation(description: "case 2: OPEN event is mutes") 101 | expCase2OpenNotCalled.isInverted = true 102 | let expCase2OtherCalled = expectation(description: "case 2: other events not muted") 103 | expCase2OtherCalled.expectedFulfillmentCount = 2 104 | client.notifyMessageHandler = { 105 | if $0.event_type == ES_EVENT_TYPE_NOTIFY_OPEN { 106 | expCase2OpenNotCalled.fulfill() 107 | } else { 108 | expCase2OtherCalled.fulfill() 109 | } 110 | } 111 | emitMessage(path: "test2", signingID: "", teamID: "", event: ES_EVENT_TYPE_NOTIFY_EXEC, isAuth: false) 112 | emitMessage(path: "test2", signingID: "", teamID: "", event: ES_EVENT_TYPE_NOTIFY_OPEN, isAuth: false) 113 | emitMessage(path: "test2", signingID: "", teamID: "", event: ES_EVENT_TYPE_NOTIFY_OPEN, isAuth: false) 114 | emitMessage(path: "test2", signingID: "", teamID: "", event: ES_EVENT_TYPE_NOTIFY_CLOSE, isAuth: false) 115 | 116 | waitForExpectations() 117 | 118 | // Case 3. 119 | client.clearPathInterestCache() 120 | let expCase3Test3Handler = expectation(description: "case 3: test3 muteHandler should be called once") 121 | let expCase3Test4Handler = expectation(description: "case 3: test4 muteHandler should be called once") 122 | let expCase3Test5Handler = expectation(description: "case 3: test5 muteHandler should be called once") 123 | let expCase3OtherHandler = expectation(description: "case 3: others muteHandler should be called once per path") 124 | expCase3OtherHandler.expectedFulfillmentCount = 2 // `other1` and `other2`. 125 | client.pathInterestHandler = { 126 | if $0.name == "test3" { 127 | expCase3Test3Handler.fulfill() 128 | return .ignore() 129 | } 130 | if $0.name == "test4" { 131 | expCase3Test4Handler.fulfill() 132 | return .ignore() 133 | } 134 | if $0.name == "test5" { 135 | expCase3Test5Handler.fulfill() 136 | return .listen() 137 | } 138 | expCase3OtherHandler.fulfill() 139 | return .listen() 140 | } 141 | 142 | let expCase3Test3NotCalled = expectation(description: "case 3: test3 is muted for all events") 143 | expCase3Test3NotCalled.isInverted = true 144 | let expCase3Test4NotCalled = expectation(description: "case 3: test4 is muted for all events") 145 | expCase3Test4NotCalled.isInverted = true 146 | let expCase3Test5Called = expectation(description: "case 3: test5 is not muted") 147 | expCase3Test5Called.expectedFulfillmentCount = 2 148 | let expCase3OtherCalled = expectation(description: "case 3: others are not muted") 149 | expCase3OtherCalled.expectedFulfillmentCount = 4 // 2 by `other1` and 2 by `other2`. 150 | client.notifyMessageHandler = { 151 | let name = ESConverter(version: $0.version).esProcess($0.process.pointee).name 152 | switch name { 153 | case "test3": expCase3Test3NotCalled.fulfill() 154 | case "test4": expCase3Test4NotCalled.fulfill() 155 | case "test5": expCase3Test5Called.fulfill() 156 | default: expCase3OtherCalled.fulfill() 157 | } 158 | } 159 | for event in [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE] { 160 | for path in ["other1", "test3", "test4", "test5", "other2"] { 161 | emitMessage(path: path, signingID: "", teamID: "", event: event, isAuth: false) 162 | } 163 | } 164 | 165 | waitForExpectations() 166 | } 167 | 168 | @available(macOS 13.0, *) 169 | func test_inverted() { 170 | XCTAssertNoThrow(try client.invertMuting(ES_MUTE_INVERSION_TYPE_PATH)) 171 | 172 | /// Only events from `test...` shoud come. 173 | XCTAssertNoThrow(try client.mute(path: "test", type: ES_MUTE_PATH_TYPE_PREFIX)) 174 | 175 | let processMuteHandlerExp = expectation(description: "Process mute handler called once per process") 176 | processMuteHandlerExp.expectedFulfillmentCount = 2 177 | client.pathInterestHandler = { 178 | XCTAssertEqual($0.name.starts(with: "test"), true) 179 | processMuteHandlerExp.fulfill() 180 | var ignores = [ES_EVENT_TYPE_AUTH_KEXTLOAD] 181 | if $0.name == "test2" { 182 | ignores.append(ES_EVENT_TYPE_NOTIFY_EXIT) 183 | } 184 | return .ignore(ESEventSet(events: ignores), suggestNativeMuting: true) 185 | } 186 | 187 | let authMessageHandlerExp = expectation(description: "Auth handler is called") 188 | authMessageHandlerExp.expectedFulfillmentCount = 2 189 | client.authMessageHandler = { 190 | XCTAssertEqual($0.event_type, ES_EVENT_TYPE_AUTH_SETTIME) 191 | $1(.allow) 192 | authMessageHandlerExp.fulfill() 193 | } 194 | 195 | let postAuthMessageHandlerExp = expectation(description: "Post-auth handler is called") 196 | postAuthMessageHandlerExp.expectedFulfillmentCount = 8 197 | client.postAuthMessageHandler = { _, _ in 198 | postAuthMessageHandlerExp.fulfill() 199 | } 200 | 201 | let notifyMessageHandlerExp = expectation(description: "Notify handler is called") 202 | notifyMessageHandlerExp.expectedFulfillmentCount = 3 // 2 for test1 + 1 for test2 203 | client.notifyMessageHandler = { _ in 204 | notifyMessageHandlerExp.fulfill() 205 | } 206 | 207 | for event in [ES_EVENT_TYPE_AUTH_SETTIME, ES_EVENT_TYPE_AUTH_KEXTLOAD] { 208 | for path in ["other1", "test1", "test2", "other2"] { 209 | emitMessage(path: path, signingID: "", teamID: "", event: event, isAuth: true) 210 | } 211 | } 212 | for event in [ES_EVENT_TYPE_NOTIFY_SETTIME, ES_EVENT_TYPE_NOTIFY_EXIT] { 213 | for path in ["other1", "test1", "test2", "other2"] { 214 | emitMessage(path: path, signingID: "", teamID: "", event: event, isAuth: false) 215 | } 216 | } 217 | 218 | waitForExpectations(timeout: 0.1) 219 | } 220 | 221 | private func emitMessage(path: String, signingID: String, teamID: String, event: es_event_type_t, isAuth: Bool) { 222 | let message = createMessage(path: path, signingID: signingID, teamID: teamID, event: event, isAuth: isAuth) 223 | Self.emitQueue.async { [self] in 224 | handler(OpaquePointer(Unmanaged.passUnretained(native).toOpaque()), message.wrappedValue) 225 | Self.emitQueue.asyncAfter(deadline: .now() + 1) { message.reset() } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityTests/ESClientTypesTests.swift: -------------------------------------------------------------------------------- 1 | @testable import sEndpointSecurity 2 | 3 | import EndpointSecurity 4 | import Foundation 5 | import SpellbookFoundation 6 | import XCTest 7 | 8 | class ESClientTypesTests: XCTestCase { 9 | func test_ESAuthResult_flags() { 10 | XCTAssertEqual(ESAuthResult.auth(true), .flags(.max)) 11 | XCTAssertEqual(ESAuthResult.auth(false), .flags(0)) 12 | } 13 | 14 | func test_ESAuthResult_equal() { 15 | XCTAssertEqual(ESAuthResult.flags(0), .auth(false)) 16 | XCTAssertEqual(ESAuthResult.flags(.max), .auth(true)) 17 | } 18 | 19 | func test_ESAuthResolution_combine() { 20 | XCTAssertEqual( 21 | ESAuthResolution.combine([]), 22 | ESAuthResolution(result: .auth(true), cache: false) 23 | ) 24 | XCTAssertEqual( 25 | ESAuthResolution.combine([ 26 | ESAuthResolution(result: .flags(123), cache: true), 27 | ]), 28 | ESAuthResolution(result: .flags(123), cache: true) 29 | ) 30 | XCTAssertEqual( 31 | ESAuthResolution.combine([ 32 | ESAuthResolution(result: .auth(true), cache: false), 33 | ESAuthResolution(result: .flags(123), cache: true), 34 | ]), 35 | ESAuthResolution(result: .flags(123), cache: false) 36 | ) 37 | XCTAssertEqual( 38 | ESAuthResolution.combine([ 39 | ESAuthResolution(result: .auth(false), cache: false), 40 | ESAuthResolution(result: .flags(123), cache: true), 41 | ]), 42 | ESAuthResolution(result: .auth(false), cache: false) 43 | ) 44 | XCTAssertEqual( 45 | ESAuthResolution.combine([ 46 | ESAuthResolution(result: .auth(true), cache: false), 47 | ESAuthResolution(result: .flags(0), cache: true), 48 | ]), 49 | ESAuthResolution(result: .auth(false), cache: false) 50 | ) 51 | } 52 | 53 | func test_ESInterest() { 54 | XCTAssertEqual(ESInterest.listen(), ESInterest(events: ESEventSet.all.events)) 55 | XCTAssertEqual(ESInterest.listen([ES_EVENT_TYPE_NOTIFY_OPEN]), ESInterest(events: [ES_EVENT_TYPE_NOTIFY_OPEN])) 56 | 57 | XCTAssertEqual(ESInterest.ignore(), ESInterest(events: [])) 58 | XCTAssertEqual( 59 | ESInterest.ignore([ES_EVENT_TYPE_NOTIFY_OPEN]), 60 | ESInterest(events: ESEventSet(events: [ES_EVENT_TYPE_NOTIFY_OPEN]).inverted().events) 61 | ) 62 | } 63 | 64 | func test_ESInterest_combine() { 65 | XCTAssertEqual(ESInterest.combine(.permissive, []), nil) 66 | XCTAssertEqual(ESInterest.combine(.restrictive, []), nil) 67 | 68 | XCTAssertEqual(ESInterest.combine(.permissive, [.listen()]), ESInterest(events: ESEventSet.all.events)) 69 | XCTAssertEqual(ESInterest.combine(.restrictive, [.listen()]), ESInterest(events: ESEventSet.all.events)) 70 | 71 | XCTAssertEqual(ESInterest.combine(.permissive, [.ignore()]), ESInterest(events: [])) 72 | XCTAssertEqual(ESInterest.combine(.restrictive, [.ignore()]), ESInterest(events: [])) 73 | 74 | XCTAssertEqual( 75 | ESInterest.combine(.permissive, [ 76 | .listen([ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]), 77 | .listen([ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_EXEC]), 78 | ]), 79 | ESInterest(events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXEC]) 80 | ) 81 | XCTAssertEqual( 82 | ESInterest.combine(.restrictive, [ 83 | .listen([ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]), 84 | .listen([ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_EXEC]), 85 | ]), 86 | ESInterest(events: [ES_EVENT_TYPE_NOTIFY_OPEN]) 87 | ) 88 | } 89 | 90 | func test_ESMultipleResolution() { 91 | let count = 3 92 | let exp = expectation(description: "") 93 | let group = ESMultipleResolution(count: count) { 94 | XCTAssertEqual($0, .allowOnce) 95 | exp.fulfill() 96 | } 97 | (0.. Void) -> Void)? 13 | var postAuthMessageHandler: ((ESMessagePtr, ESClient.ResponseInfo) -> Void)? 14 | var notifyMessageHandler: ((ESMessagePtr) -> Void)? 15 | 16 | var subscriptions: Set = [] 17 | 18 | func subscribe(_ events: [es_event_type_t]) { 19 | subscriptions.formUnion(events) 20 | } 21 | 22 | func unsubscribe(_ events: [es_event_type_t]) { 23 | subscriptions.subtract(events) 24 | } 25 | 26 | func unsubscribeAll() { 27 | subscriptions.removeAll() 28 | } 29 | 30 | func clearCache() {} 31 | 32 | var pathInterestHandler: ((ESProcess) -> ESInterest)? 33 | 34 | func clearPathInterestCache() {} 35 | 36 | func mute(process rule: ESMuteProcessRule, events: ESEventSet) {} 37 | 38 | func unmute(process rule: ESMuteProcessRule, events: ESEventSet) {} 39 | 40 | func unmuteAllProcesses() {} 41 | 42 | func mute(path: String, type: es_mute_path_type_t, events: ESEventSet) {} 43 | 44 | func unmute(path: String, type: es_mute_path_type_t, events: ESEventSet) {} 45 | 46 | func unmuteAllPaths() {} 47 | 48 | func unmuteAllTargetPaths() {} 49 | 50 | func invertMuting(_ muteType: es_mute_inversion_type_t) {} 51 | 52 | func mutingInverted(_ muteType: es_mute_inversion_type_t) -> Bool { 53 | false 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityTests/ESMutePathTests.swift: -------------------------------------------------------------------------------- 1 | @testable import sEndpointSecurity 2 | 3 | import EndpointSecurity 4 | import Foundation 5 | import SpellbookFoundation 6 | import XCTest 7 | 8 | @available(macOS 13.0, *) 9 | class ESMutePathTests: XCTestCase { 10 | private let client = MockNativeClient() 11 | 12 | override func setUp() { 13 | client.pathMutes.removeAll() 14 | } 15 | 16 | func test_checkIgnored_mute() { 17 | func test(useAPIv12: Bool) { 18 | let mutes = ESMutePath(client: client, useAPIv12: useAPIv12) 19 | 20 | mutes.interestHandler = { 21 | switch $0.executable.path { 22 | case "path3": return .ignore([ES_EVENT_TYPE_NOTIFY_OPEN]) 23 | case "path4": return .ignore([ES_EVENT_TYPE_NOTIFY_OPEN]) 24 | default: return .listen() 25 | } 26 | } 27 | 28 | mutes.mute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: ESEventSet.all.events) 29 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_ACCESS, path: "path1", process: .test("path1")), true) 30 | 31 | mutes.mute("path2", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN]) 32 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path1", process: .test("path1")), true) 33 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path2", process: .test("path2")), true) 34 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_EXEC, path: "path2", process: .test("path2")), false) 35 | 36 | mutes.mute("path3", type: ES_MUTE_PATH_TYPE_LITERAL, events: []) 37 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path3", process: .test("path3")), true) 38 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_EXEC, path: "path3", process: .test("path3")), false) 39 | 40 | mutes.mute("path4", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_CLOSE]) 41 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path4", process: .test("path4")), true) 42 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path4", process: .test("path4")), true) 43 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_EXEC, path: "path4", process: .test("path4")), false) 44 | } 45 | test(useAPIv12: true) 46 | test(useAPIv12: false) 47 | } 48 | 49 | func test_checkIgnored_unmute() { 50 | func test(useAPIv12: Bool) { 51 | let mutes = ESMutePath(client: client, useAPIv12: useAPIv12) 52 | 53 | mutes.interestHandler = { 54 | switch $0.executable.path { 55 | case "path1": return .ignore([ES_EVENT_TYPE_NOTIFY_RENAME]) 56 | default: return .listen() 57 | } 58 | } 59 | 60 | mutes.mute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]) 61 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path1", process: .test("path1")), true) 62 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path1", process: .test("path1")), true) 63 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_RENAME, path: "path1", process: .test("path1")), true) 64 | 65 | /// The check always return `nil` if `Ignores` not set. 66 | mutes.clearIgnoreCache() 67 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_ACCESS, path: "path1", process: .test("path1")), false) 68 | 69 | /// Unmute in opposite way keeps check verdicts after unmute. 70 | mutes.unmute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN]) 71 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path1", process: .test("path1")), false) 72 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path1", process: .test("path1")), true) 73 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_RENAME, path: "path1", process: .test("path1")), true) 74 | 75 | mutes.unmute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: ESEventSet.all.events) 76 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path1", process: .test("path1")), false) 77 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path1", process: .test("path1")), false) 78 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_RENAME, path: "path1", process: .test("path1")), true) 79 | } 80 | 81 | test(useAPIv12: true) 82 | test(useAPIv12: false) 83 | } 84 | 85 | func test_checkIgnored_mute_inverted() { 86 | let mutes = ESMutePath(client: client) 87 | 88 | mutes.interestHandler = { _ in 89 | .ignore([ES_EVENT_TYPE_NOTIFY_EXEC]) 90 | } 91 | 92 | mutes.mute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: ESEventSet.all.events) 93 | mutes.mute("path2", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_CLOSE]) 94 | 95 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path1", process: .test("path1")), true) 96 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path1", process: .test("path1")), true) 97 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_EXEC, path: "path1", process: .test("path1")), true) 98 | 99 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path2", process: .test("path2")), false) 100 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path2", process: .test("path2")), true) 101 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_EXEC, path: "path2", process: .test("path2")), true) 102 | 103 | XCTAssertTrue(mutes.invertMuting()) 104 | 105 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path1", process: .test("path1")), false) 106 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path1", process: .test("path1")), false) 107 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_EXEC, path: "path1", process: .test("path1")), true) 108 | 109 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_OPEN, path: "path2", process: .test("path2")), true) 110 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_CLOSE, path: "path2", process: .test("path2")), false) 111 | XCTAssertEqual(mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_EXEC, path: "path2", process: .test("path2")), true) 112 | } 113 | 114 | func test_esmutes_v12() { 115 | let mutes = ESMutePath(client: client, useAPIv12: true) 116 | 117 | mutes.interestHandler = { 118 | switch $0.executable.path { 119 | case "path1": 120 | return .ignore([ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXEC], suggestNativeMuting: true) 121 | case "path2": 122 | return .ignore([ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXEC], suggestNativeMuting: false) 123 | default: 124 | return .listen() 125 | } 126 | } 127 | mutes.mute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXIT]) 128 | mutes.mute("path2", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXIT]) 129 | 130 | XCTAssertEqual( 131 | client.pathMutes["path1"], 132 | [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXIT] 133 | ) 134 | XCTAssertEqual( 135 | client.pathMutes["path2"], 136 | [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXIT] 137 | ) 138 | 139 | _ = mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_ACCESS, path: "path1", process: .test("path1")) 140 | _ = mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_ACCESS, path: "path2", process: .test("path2")) 141 | XCTAssertEqual( 142 | client.pathMutes["path1"], 143 | [ 144 | ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, 145 | ES_EVENT_TYPE_NOTIFY_EXIT, 146 | ES_EVENT_TYPE_NOTIFY_EXEC, 147 | ] 148 | ) 149 | XCTAssertEqual( 150 | client.pathMutes["path2"], 151 | [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXIT] 152 | ) 153 | 154 | mutes.unmute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_EXIT]) 155 | mutes.unmute("path2", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_EXIT]) 156 | XCTAssertEqual( 157 | client.pathMutes["path1"], 158 | [ 159 | ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, 160 | ES_EVENT_TYPE_NOTIFY_EXEC, 161 | ] 162 | ) 163 | XCTAssertEqual( 164 | client.pathMutes["path2"], 165 | [ES_EVENT_TYPE_NOTIFY_CLOSE] 166 | ) 167 | 168 | mutes.clearIgnoreCache() 169 | XCTAssertEqual( 170 | client.pathMutes["path1"], 171 | [ES_EVENT_TYPE_NOTIFY_CLOSE] 172 | ) 173 | XCTAssertEqual( 174 | client.pathMutes["path2"], 175 | [ES_EVENT_TYPE_NOTIFY_CLOSE] 176 | ) 177 | 178 | _ = mutes.unmuteAll() 179 | XCTAssertEqual(client.pathMutes["path1"] ?? [], []) 180 | XCTAssertEqual(client.pathMutes["path2"] ?? [], []) 181 | } 182 | 183 | func test_esmutes_v12_unmutePartial() { 184 | let mutes = ESMutePath(client: client, useAPIv12: true) 185 | 186 | mutes.mute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]) 187 | XCTAssertEqual(client.pathMutes["path1"], [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]) 188 | mutes.unmute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN]) 189 | XCTAssertEqual(client.pathMutes["path1"], [ES_EVENT_TYPE_NOTIFY_CLOSE]) 190 | 191 | mutes.mute("path2", type: ES_MUTE_PATH_TYPE_LITERAL, events: ESEventSet.all.events) 192 | XCTAssertEqual(client.pathMutes["path2"], ESEventSet.all.events) 193 | mutes.unmute("path2", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]) 194 | XCTAssertEqual( 195 | client.pathMutes["path2"], 196 | ESEventSet(events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]).inverted().events 197 | ) 198 | } 199 | 200 | func test_esmutes_legacy() { 201 | let mutes = ESMutePath(client: client, useAPIv12: false) 202 | 203 | mutes.interestHandler = { 204 | switch $0.executable.path { 205 | case "path1": 206 | return .ignore(.all, suggestNativeMuting: true) 207 | case "path2": 208 | return .ignore(.all, suggestNativeMuting: true) 209 | default: 210 | return .listen() 211 | } 212 | } 213 | mutes.mute("path1", type: ES_MUTE_PATH_TYPE_LITERAL, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXIT]) 214 | mutes.mute("path2", type: ES_MUTE_PATH_TYPE_LITERAL, events: ESEventSet.all.events) 215 | 216 | XCTAssertEqual(client.pathMutes["path1"] ?? [], []) 217 | XCTAssertEqual(client.pathMutes["path2"], ESEventSet.all.events) 218 | 219 | _ = mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_ACCESS, path: "path1", process: .test("path1")) 220 | _ = mutes.checkIgnored(ES_EVENT_TYPE_NOTIFY_ACCESS, path: "path2", process: .test("path2")) 221 | XCTAssertEqual(client.pathMutes["path1"] ?? [], []) 222 | XCTAssertEqual(client.pathMutes["path2"], ESEventSet.all.events) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityTests/ESMuteProcessTests.swift: -------------------------------------------------------------------------------- 1 | @testable import sEndpointSecurity 2 | 3 | import EndpointSecurity 4 | import Foundation 5 | import SpellbookFoundation 6 | import XCTest 7 | 8 | class ESMuteProcessTests: XCTestCase { 9 | private let client = MockNativeClient() 10 | private let token1 = audit_token_t.random() 11 | private let token2 = audit_token_t.random() 12 | private let token3 = audit_token_t.random() 13 | private let token4 = audit_token_t.random() 14 | private let token5 = audit_token_t.random() 15 | 16 | override func setUp() { 17 | client.processMutes.removeAll() 18 | } 19 | 20 | func test() { 21 | func test(useAPIv12: Bool) { 22 | let mutes = ESMuteProcess(client: client, environment: .init(useAPIv12: useAPIv12, checkAlive: { _ in true })) 23 | 24 | mutes.mute(token1, events: ESEventSet.all.events) 25 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_ACCESS, process: token1), true) 26 | 27 | mutes.mute(token2, events: [ES_EVENT_TYPE_NOTIFY_OPEN]) 28 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_OPEN, process: token2), true) 29 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_EXEC, process: token2), false) 30 | 31 | mutes.mute(token3, events: []) 32 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_OPEN, process: token3), false) 33 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_EXEC, process: token3), false) 34 | } 35 | 36 | test(useAPIv12: true) 37 | test(useAPIv12: false) 38 | } 39 | 40 | func test_checkMuted_unmute() { 41 | func test(useAPIv12: Bool) { 42 | let mutes = ESMuteProcess(client: client, environment: .init(useAPIv12: useAPIv12, checkAlive: { _ in true })) 43 | 44 | mutes.mute(token1, events: [ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_CLOSE]) 45 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_ACCESS, process: token1), false) 46 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_OPEN, process: token1), true) 47 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_CLOSE, process: token1), true) 48 | 49 | mutes.unmute(token1, events: [ES_EVENT_TYPE_NOTIFY_OPEN]) 50 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_RENAME, process: token1), false) 51 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_OPEN, process: token1), false) 52 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_CLOSE, process: token1), true) 53 | 54 | mutes.unmute(token1, events: ESEventSet.all.events) 55 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_RENAME, process: token1), false) 56 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_OPEN, process: token1), false) 57 | XCTAssertEqual(mutes.checkMuted(ES_EVENT_TYPE_NOTIFY_CLOSE, process: token1), false) 58 | } 59 | 60 | test(useAPIv12: true) 61 | test(useAPIv12: false) 62 | } 63 | 64 | func test_esmutes_v12() { 65 | let mutes = ESMuteProcess(client: client, environment: .init(useAPIv12: true, checkAlive: { _ in true })) 66 | 67 | mutes.mute(token1, events: [ES_EVENT_TYPE_NOTIFY_EXEC]) 68 | XCTAssertEqual( 69 | client.processMutes[token1], 70 | [ES_EVENT_TYPE_NOTIFY_EXEC] 71 | ) 72 | 73 | mutes.mute(token1, events: [ES_EVENT_TYPE_NOTIFY_EXIT]) 74 | XCTAssertEqual( 75 | client.processMutes[token1], 76 | [ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_EXIT] 77 | ) 78 | 79 | mutes.unmute(token1, events: [ES_EVENT_TYPE_NOTIFY_EXEC]) 80 | XCTAssertEqual( 81 | client.processMutes[token1], 82 | [ES_EVENT_TYPE_NOTIFY_EXIT] 83 | ) 84 | 85 | mutes.mute(token1, events: ESEventSet.all.events) 86 | XCTAssertEqual( 87 | client.processMutes[token1], 88 | ESEventSet.all.events 89 | ) 90 | 91 | mutes.unmute(token1, events: ESEventSet.all.events) 92 | XCTAssertEqual(client.processMutes[token1], []) 93 | } 94 | 95 | func test_esmutes_legacy() { 96 | let mutes = ESMuteProcess(client: client, environment: .init(useAPIv12: false, checkAlive: { _ in true })) 97 | 98 | mutes.mute(token1, events: [ES_EVENT_TYPE_NOTIFY_EXEC]) 99 | XCTAssertEqual(client.processMutes[token1] ?? [], []) 100 | 101 | mutes.mute(token1, events: [ES_EVENT_TYPE_NOTIFY_EXIT]) 102 | XCTAssertEqual(client.processMutes[token1] ?? [], []) 103 | 104 | mutes.mute(token1, events: ESEventSet.all.events) 105 | XCTAssertEqual( 106 | client.processMutes[token1], 107 | ESEventSet.all.events 108 | ) 109 | 110 | mutes.unmute(token1, events: [ES_EVENT_TYPE_NOTIFY_EXEC]) 111 | XCTAssertEqual(client.processMutes[token1] ?? [], []) 112 | 113 | mutes.unmute(token1, events: ESEventSet.all.events) 114 | XCTAssertEqual(client.processMutes[token1] ?? [], []) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityTests/MockNativeClient.swift: -------------------------------------------------------------------------------- 1 | import EndpointSecurity 2 | import Foundation 3 | import sEndpointSecurity 4 | 5 | class MockNativeClient: ESNativeClient { 6 | struct MutePathKey: Hashable { 7 | var path: String 8 | var type: es_mute_path_type_t 9 | } 10 | 11 | var subscriptions: Set = [] 12 | var invertMuting: [es_mute_inversion_type_t: Bool] = [:] 13 | var pathMutes: [String: Set] = [:] 14 | var prefixMutes: [String: Set] = [:] 15 | var processMutes: [audit_token_t: Set] = [:] 16 | var responses: [UInt64: ESAuthResolution] = [:] 17 | 18 | var native: OpaquePointer { OpaquePointer(bitPattern: 0xdeadbeef)! } 19 | 20 | func esRespond(_ message: UnsafePointer, flags: UInt32, cache: Bool) -> es_respond_result_t { 21 | responses[message.pointee.global_seq_num] = ESAuthResolution(result: .flags(flags), cache: cache) 22 | return ES_RESPOND_RESULT_SUCCESS 23 | } 24 | 25 | func esSubscribe(_ events: [es_event_type_t]) -> es_return_t { 26 | subscriptions.formUnion(events) 27 | return ES_RETURN_SUCCESS 28 | } 29 | 30 | func esUnsubscribe(_ events: [es_event_type_t]) -> es_return_t { 31 | subscriptions.subtract(events) 32 | return ES_RETURN_SUCCESS 33 | } 34 | 35 | func esUnsubscribeAll() -> es_return_t { 36 | subscriptions.removeAll() 37 | return ES_RETURN_SUCCESS 38 | } 39 | 40 | func esClearCache() -> es_clear_cache_result_t { 41 | return ES_CLEAR_CACHE_RESULT_SUCCESS 42 | } 43 | 44 | func esInvertMuting(_ muteType: es_mute_inversion_type_t) -> es_return_t { 45 | invertMuting[muteType, default: false].toggle() 46 | return ES_RETURN_SUCCESS 47 | } 48 | 49 | func esMutingInverted(_ muteType: es_mute_inversion_type_t) -> es_mute_inverted_return_t { 50 | return invertMuting[muteType, default: false] ? ES_MUTE_INVERTED : ES_MUTE_NOT_INVERTED 51 | } 52 | 53 | func esDeleteClient() -> es_return_t { 54 | return ES_RETURN_SUCCESS 55 | } 56 | 57 | func esMutePath(_ path: String, _ type: es_mute_path_type_t) -> es_return_t { 58 | if type == ES_MUTE_PATH_TYPE_LITERAL { 59 | pathMutes[path, default: []] = ESEventSet.all.events 60 | } else { 61 | prefixMutes[path, default: []] = ESEventSet.all.events 62 | } 63 | return ES_RETURN_SUCCESS 64 | } 65 | 66 | func esUnmutePath(_ path: String, _ type: es_mute_path_type_t) -> es_return_t { 67 | if type == ES_MUTE_PATH_TYPE_LITERAL { 68 | pathMutes.removeValue(forKey: path) 69 | } else { 70 | prefixMutes.removeValue(forKey: path) 71 | } 72 | return ES_RETURN_SUCCESS 73 | } 74 | 75 | func esMutePathEvents(_ path: String, _ type: es_mute_path_type_t, _ events: [es_event_type_t]) -> es_return_t { 76 | if type == ES_MUTE_PATH_TYPE_LITERAL { 77 | pathMutes[path, default: []].formUnion(events) 78 | } else { 79 | prefixMutes[path, default: []].formUnion(events) 80 | } 81 | return ES_RETURN_SUCCESS 82 | } 83 | 84 | func esUnmutePathEvents(_ path: String, _ type: es_mute_path_type_t, _ events: [es_event_type_t]) -> es_return_t { 85 | if type == ES_MUTE_PATH_TYPE_LITERAL { 86 | pathMutes[path, default: []].subtract(events) 87 | } else { 88 | prefixMutes[path, default: []].subtract(events) 89 | } 90 | return ES_RETURN_SUCCESS 91 | } 92 | 93 | func esUnmuteAllPaths() -> es_return_t { 94 | pathMutes.removeAll() 95 | return ES_RETURN_SUCCESS 96 | } 97 | 98 | func esMutedPaths() -> [(path: String, type: es_mute_path_type_t, events: [es_event_type_t])] { 99 | pathMutes.map { ($0, ES_MUTE_PATH_TYPE_LITERAL, Array($1)) } + prefixMutes.map { ($0, ES_MUTE_PATH_TYPE_PREFIX, Array($1)) } 100 | } 101 | 102 | func esUnmuteAllTargetPaths() -> es_return_t { 103 | return ES_RETURN_SUCCESS 104 | } 105 | 106 | func esMuteProcess(_ auditToken: audit_token_t) -> es_return_t { 107 | processMutes[auditToken] = ESEventSet.all.events 108 | return ES_RETURN_SUCCESS 109 | } 110 | 111 | func esUnmuteProcess(_ auditToken: audit_token_t) -> es_return_t { 112 | processMutes.removeValue(forKey: auditToken) 113 | return ES_RETURN_SUCCESS 114 | } 115 | 116 | func esMutedProcesses() -> [audit_token_t]? { 117 | Array(processMutes.filter { $0.value == ESEventSet.all.events }.keys) 118 | } 119 | 120 | func esMuteProcessEvents(_ auditToken: audit_token_t, _ events: [es_event_type_t]) -> es_return_t { 121 | processMutes[auditToken, default: []].formUnion(events) 122 | return ES_RETURN_SUCCESS 123 | } 124 | 125 | func esUnmuteProcessEvents(_ auditToken: audit_token_t, _ events: [es_event_type_t]) -> es_return_t { 126 | processMutes[auditToken]?.subtract(events) 127 | return ES_RETURN_SUCCESS 128 | } 129 | 130 | func esMutedProcesses() -> [audit_token_t: [es_event_type_t]] { 131 | processMutes.mapValues { Array($0) } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityTests/TestUtils.swift: -------------------------------------------------------------------------------- 1 | import sEndpointSecurity 2 | 3 | import EndpointSecurity 4 | import Foundation 5 | import SpellbookFoundation 6 | 7 | extension audit_token_t { 8 | static func random() -> audit_token_t { 9 | var token = audit_token_t() 10 | withUnsafeMutablePointer(to: &token) { 11 | _ = SecRandomCopyBytes(kSecRandomDefault, MemoryLayout.size, $0) 12 | } 13 | return token 14 | } 15 | } 16 | 17 | extension ESProcess { 18 | static func test(_ path: String) -> ESProcess { 19 | test(path: path, token: nil) 20 | } 21 | 22 | static func test(_ token: audit_token_t) -> ESProcess { 23 | test(path: nil, token: token) 24 | } 25 | 26 | static func test(path: String? = nil, token: audit_token_t? = nil, teamID: String? = nil) -> ESProcess { 27 | ESProcess( 28 | auditToken: token ?? .random(), 29 | ppid: 10, 30 | originalPpid: 20, 31 | groupID: 30, 32 | sessionID: 40, 33 | codesigningFlags: 50, 34 | isPlatformBinary: true, 35 | isESClient: true, 36 | cdHash: Data([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]), 37 | signingID: "signing_id", 38 | teamID: teamID ?? "team_id", 39 | executable: ESFile( 40 | path: path ?? "/root/path/to/executable/test_process", 41 | truncated: false, 42 | stat: .init() 43 | ), 44 | tty: nil, 45 | startTime: nil, 46 | responsibleAuditToken: nil, 47 | parentAuditToken: nil 48 | ) 49 | } 50 | } 51 | 52 | private var nextMessageID: UInt64 = 1 53 | 54 | func createMessage(path: String, signingID: String, teamID: String, event: es_event_type_t, isAuth: Bool) -> Resource> { 55 | let message = UnsafeMutablePointer.allocate(capacity: 1) 56 | message.pointee.version = 4 57 | message.pointee.global_seq_num = nextMessageID 58 | nextMessageID += 1 59 | 60 | message.pointee.process = .allocate(capacity: 1) 61 | message.pointee.process.pointee = .init( 62 | audit_token: .random(), ppid: 10, original_ppid: 10, group_id: 20, session_id: 500, 63 | codesigning_flags: 0x800, is_platform_binary: false, is_es_client: false, 64 | cdhash: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 65 | signing_id: .init(string: signingID), 66 | team_id: .init(string: teamID), 67 | executable: .allocate(capacity: 1), 68 | tty: nil, 69 | start_time: .init(tv_sec: 100, tv_usec: 500), 70 | responsible_audit_token: .random(), 71 | parent_audit_token: .random() 72 | ) 73 | message.pointee.process.pointee.executable.pointee.path = .init(string: path) 74 | 75 | message.pointee.action_type = isAuth ? ES_ACTION_TYPE_AUTH : ES_ACTION_TYPE_NOTIFY 76 | message.pointee.event_type = event 77 | 78 | return .raii(message) { _ in 79 | message.pointee.process.pointee.team_id.data?.deallocate() 80 | message.pointee.process.pointee.signing_id.data?.deallocate() 81 | message.pointee.process.pointee.executable.pointee.path.data?.deallocate() 82 | message.pointee.process.pointee.executable.deallocate() 83 | message.pointee.process.deallocate() 84 | message.deallocate() 85 | } 86 | } 87 | 88 | private extension es_string_token_t { 89 | init(string: String) { 90 | let ptr = strdup(string) 91 | self.init(length: UnsafePointer(ptr).flatMap(strlen) ?? 0, data: ptr) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityXPC/ESXPCClient.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import sEndpointSecurity 24 | 25 | import EndpointSecurity 26 | import Foundation 27 | import SpellbookFoundation 28 | 29 | private let log = SpellbookLogger.internalLog(.xpc) 30 | 31 | public final class ESXPCClient: ESClientProtocol { 32 | private let connection: ESXPCConnection 33 | private let delegate: ESClientXPCDelegate 34 | private let syncExecutor: SynchronousExecutor 35 | private let connectionLock = NSRecursiveLock() 36 | 37 | // MARK: - Initialization & Activation 38 | 39 | public init(name: String = "ESXPCClient", timeout: TimeInterval? = nil, _ createConnection: @escaping @autoclosure () -> NSXPCConnection) { 40 | let delegate = ESClientXPCDelegate() 41 | self.connection = ESXPCConnection(delegate: delegate, createConnection: createConnection) 42 | self.delegate = delegate 43 | self.name = name 44 | self.syncExecutor = SynchronousExecutor(name, timeout: timeout) 45 | } 46 | 47 | deinit { 48 | invalidate() 49 | } 50 | 51 | public var name: String 52 | public var connectionStateHandler: ((Result) -> Void)? 53 | public var converterConfig: ESConverter.Config = .default 54 | public var reconnectDelay: TimeInterval { 55 | get { connection.reconnectDelay } 56 | set { connection.reconnectDelay = newValue } 57 | } 58 | 59 | public func tryActivate(completion: @escaping (Result) -> Void) { 60 | activate(async: true, completion: completion) 61 | } 62 | 63 | public func tryActivate() throws -> es_new_client_result_t { 64 | var result: Result! 65 | activate(async: false) { result = $0 } 66 | return try result.get() 67 | } 68 | 69 | public func activate() { 70 | activate(async: true, completion: nil) 71 | } 72 | 73 | public func invalidate() { 74 | connection.invalidate() 75 | } 76 | 77 | private func activate(async: Bool, completion: ((Result) -> Void)?) { 78 | delegate.queue = queue 79 | delegate.pathInterestHandler = pathInterestHandler 80 | delegate.authMessageHandler = authMessageHandler 81 | delegate.notifyMessageHandler = notifyMessageHandler 82 | delegate.receiveCustomMessageHandler = receiveCustomMessageHandler 83 | 84 | connection.connectionStateHandler = { [weak self] in self?.handleConnectionStateChanged($0) } 85 | connection.converterConfig = converterConfig 86 | 87 | // Mandatory because behaviour depends on if `completion` is nil or not. 88 | if let completion { 89 | connection.connect(async: async) { [queue] result in queue.async { completion(result) } } 90 | } else { 91 | connection.connect(async: async, notify: nil) 92 | } 93 | } 94 | 95 | private func handleConnectionStateChanged(_ result: Result) { 96 | queue.async(flags: .barrier) { 97 | self.connectionStateHandler?(result) 98 | } 99 | } 100 | 101 | // MARK: - ES Client 102 | 103 | // MARK: Messages 104 | 105 | /// Handler invoked each time AUTH message is coming from EndpointSecurity. 106 | /// The message SHOULD be responded using the second parameter - reply block. 107 | public var authMessageHandler: ((ESMessage, @escaping (ESAuthResolution) -> Void) -> Void)? 108 | 109 | /// Handler invoked each time NOTIFY message is coming from EndpointSecurity. 110 | public var notifyMessageHandler: ((ESMessage) -> Void)? 111 | 112 | /// Queue where `pathInterestHandler`, `authMessageHandler`, `postAuthMessageHandler` 113 | /// and `notifyMessageHandler` handlers are called. 114 | public var queue: DispatchQueue? 115 | 116 | /// Subscribe to some set of events 117 | /// - Parameters: 118 | /// - events: Array of es_event_type_t to subscribe to 119 | /// - returns: Boolean indicating success or error 120 | /// - Note: Subscribing to new event types does not remove previous subscriptions 121 | public func subscribe(_ events: [es_event_type_t]) throws { 122 | try withRemoteClient { client, reply in 123 | client.subscribe(events.map { NSNumber(value: $0.rawValue) }, reply: reply) 124 | } 125 | } 126 | 127 | /// Unsubscribe from some set of events 128 | /// - Parameters: 129 | /// - events: Array of es_event_type_t to unsubscribe from 130 | /// - returns: Boolean indicating success or error 131 | /// - Note: Events not included in the given `events` array that were previously subscribed to 132 | /// will continue to be subscribed to 133 | public func unsubscribe(_ events: [es_event_type_t]) throws { 134 | try withRemoteClient { client, reply in 135 | client.unsubscribe(events.map { NSNumber(value: $0.rawValue) }, reply: reply) 136 | } 137 | } 138 | 139 | /// Unsubscribe from all events 140 | /// - Parameters: 141 | /// - returns: Boolean indicating success or error 142 | public func unsubscribeAll() throws { 143 | try withRemoteClient { client, reply in 144 | client.unsubscribeAll(reply: reply) 145 | } 146 | } 147 | 148 | /// Clear all cached results for all clients. 149 | /// - Parameters: 150 | /// - returns: es_clear_cache_result_t value indicating success or an error 151 | public func clearCache() throws { 152 | try withRemoteClient { client, reply in 153 | client.clearCache(reply: reply) 154 | } 155 | } 156 | 157 | // MARK: Interest 158 | 159 | /// Perform process filtering, additionally to muting of path and processes. 160 | /// Filtering is based on `interest in particular process executable path`. 161 | /// Designed to be used for granular process filtering by ignoring uninterest events. 162 | /// 163 | /// General idea is to mute or ignore processes we are not interested in using their binary paths. 164 | /// Usually the OS would not have more than ~1000 unique processes, so asking for interest in particular 165 | /// process path would occur very limited number of times. 166 | /// 167 | /// The process may be interested or ignored accoding to returned `ESInterest`. 168 | /// If the process is not interested, all related messages are skipped. 169 | /// More information on `ESInterest` see in related documentation. 170 | /// 171 | /// The final decision if the particular event is delivered or not relies on multiple sources. 172 | /// Sources considered: 173 | /// - `mute(path:)` rules 174 | /// - `mute(process:)` rules 175 | /// - `pathInterestHandler` resolution 176 | /// 177 | /// - Note: Interest does NOT depend on `inversion` of `ESClient`. 178 | /// - Note: Returned resolutions are cached to avoid often handler calls. 179 | /// To reset cache, call `clearPathInterestCache`. 180 | /// - Note: When the handler is not set, it defaults to returning `ESInterest.listen()`. 181 | /// 182 | /// - Warning: Perfonamce-sensitive handler, called **synchronously** once for each process path on `queue`. 183 | /// Do here as minimum work as possible. 184 | public var pathInterestHandler: ((ESProcess) -> ESInterest)? 185 | 186 | /// Clears the cache related to process interest by path. 187 | /// All processes will be re-evaluated against mute rules and `pathInterestHandler`. 188 | public func clearPathInterestCache() throws { 189 | try withRemoteClient { client, reply in 190 | client.clearPathInterestCache(reply: reply) 191 | } 192 | } 193 | 194 | // MARK: Mute 195 | 196 | /// Suppress events from the process described by the given `mute` rule. 197 | /// - Parameters: 198 | /// - mute: process to mute. 199 | /// - events: set of events to mute. 200 | public func mute(process rule: ESMuteProcessRule, events: ESEventSet = .all) throws { 201 | let encoded = try xpcEncoder.encode(rule) 202 | try withRemoteClient { client, reply in 203 | client.mute(process: encoded, events: events.asNumbers, reply: reply) 204 | } 205 | } 206 | 207 | /// Unmute events for the process described by the given `mute` rule. 208 | /// - Parameters: 209 | /// - mute: process to unmute. 210 | /// - events: set of events to mute. 211 | public func unmute(process rule: ESMuteProcessRule, events: ESEventSet = .all) throws { 212 | let encoded = try xpcEncoder.encode(rule) 213 | try withRemoteClient { client, reply in 214 | client.unmute(process: encoded, events: events.asNumbers, reply: reply) 215 | } 216 | } 217 | 218 | /// Unmute all events for all processes. Clear the rules. 219 | public func unmuteAllProcesses() throws { 220 | try withRemoteClient { client, reply in 221 | client.unmuteAllProcesses(reply: reply) 222 | } 223 | } 224 | 225 | /// Suppress events for the the given at path and type. 226 | /// - Parameters: 227 | /// - mute: process path to mute. 228 | /// - type: path type. 229 | /// - events: set of events to mute. 230 | public func mute(path: String, type: es_mute_path_type_t, events: ESEventSet = .all) throws { 231 | try withRemoteClient { client, reply in 232 | client.mute(path: path, type: type, events: events.asNumbers, reply: reply) 233 | } 234 | } 235 | 236 | /// Unmute events for the given at path and type. 237 | /// - Parameters: 238 | /// - mute: process path to unmute. 239 | /// - type: path type. 240 | /// - events: set of events to unmute. 241 | @available(macOS 12.0, *) 242 | public func unmute(path: String, type: es_mute_path_type_t, events: ESEventSet = .all) throws { 243 | try withRemoteClient { client, reply in 244 | client.unmute(path: path, type: type, events: events.asNumbers, reply: reply) 245 | } 246 | } 247 | 248 | /// Unmute all events for all process paths. 249 | public func unmuteAllPaths() throws { 250 | try withRemoteClient { client, reply in 251 | client.unmuteAllPaths(reply: reply) 252 | } 253 | } 254 | 255 | /// Unmute all target paths. Works only for macOS 13.0+. 256 | @available(macOS 13.0, *) 257 | public func unmuteAllTargetPaths() throws { 258 | try withRemoteClient { client, reply in 259 | client.unmuteAllTargetPaths(reply: reply) 260 | } 261 | } 262 | 263 | /// Invert the mute state of a given mute dimension. 264 | @available(macOS 13.0, *) 265 | public func invertMuting(_ muteType: es_mute_inversion_type_t) throws { 266 | try withRemoteClient { client, reply in 267 | client.invertMuting(muteType, reply: reply) 268 | } 269 | } 270 | 271 | /// Mute state of a given mute dimension. 272 | @available(macOS 13.0, *) 273 | public func mutingInverted(_ muteType: es_mute_inversion_type_t) throws -> Bool { 274 | try withRemoteClient { client, reply in 275 | client.mutingInverted(muteType) { 276 | reply(Result(success: $0, failure: $1)) 277 | } 278 | } 279 | } 280 | 281 | // MARK: - Custom Messages 282 | 283 | public var receiveCustomMessageHandler: ((Data, @escaping (Result) -> Void) -> Void)? 284 | 285 | public func sendCustomMessage(_ data: Data, completion: @escaping (Result) -> Void) { 286 | if let proxy = connection.remoteObjectProxy({ completion(.failure($0)) }) { 287 | proxy.sendCustomMessage(data) { completion(Result(success: $0, failure: $1)) } 288 | } else { 289 | completion(.failure(CommonError.unexpected("ESXPCConnection not established"))) 290 | } 291 | } 292 | 293 | // MARK: Utils 294 | 295 | private func withRemoteClient( 296 | _ function: String = #function, 297 | body: @escaping (ESClientXPCProtocol, @escaping (Error?) -> Void) throws -> Void 298 | ) throws { 299 | _ = try withRemoteClient(function) { client, reply in 300 | try body(client) { reply($0.flatMap(Result.failure) ?? .success(())) } 301 | } 302 | 303 | try connectionLock.withLock { 304 | try syncExecutor { callback in 305 | let proxy = try connection.remoteObjectProxy { callback($0) } 306 | .get(name: "ESXPCConnection", description: "ES XPC client is not connected") 307 | try body(proxy, callback) 308 | } 309 | } 310 | } 311 | 312 | private func withRemoteClient( 313 | _ function: String = #function, 314 | body: @escaping (ESClientXPCProtocol, @escaping (Result) -> Void) throws -> Void 315 | ) throws -> T { 316 | try connectionLock.withLock { 317 | try syncExecutor { (callback: @escaping (Result) -> Void) in 318 | let proxy = try connection.remoteObjectProxy { callback(.failure($0)) } 319 | .get(name: "ESXPCConnection", description: "ES XPC client is not connected") 320 | try body(proxy, callback) 321 | } 322 | } 323 | } 324 | } 325 | 326 | private final class ESClientXPCDelegate: NSObject, ESClientXPCDelegateProtocol { 327 | var queue: DispatchQueue? 328 | var pathInterestHandler: ((ESProcess) -> ESInterest)? 329 | var authMessageHandler: ((ESMessage, @escaping (ESAuthResolution) -> Void) -> Void)? 330 | var notifyMessageHandler: ((ESMessage) -> Void)? 331 | var receiveCustomMessageHandler: ((Data, @escaping (Result) -> Void) -> Void)? 332 | 333 | private static let fallback = ESAuthResolution.allowOnce 334 | 335 | func handlePathInterest(_ data: Data, reply: @escaping (Data?) -> Void) { 336 | guard let pathInterestHandler else { 337 | reply(nil); return 338 | } 339 | guard let process = decode(ESProcess.self, from: data, actionName: "handlePathInterest") else { 340 | reply(nil); return 341 | } 342 | 343 | queue.async { 344 | let interest = pathInterestHandler(process) 345 | let encoded = interest.encode(with: .json(encoder: xpcEncoder), log: log) 346 | reply(encoded) 347 | } 348 | } 349 | 350 | func handleAuth(_ message: Data, reply: @escaping (UInt32, Bool) -> Void) { 351 | guard let authMessageHandler else { 352 | reply(Self.fallback.result.rawValue, Self.fallback.cache) 353 | log.warning("Auth message came but no authMessageHandler installed") 354 | return 355 | } 356 | guard let decoded = decode(ESMessage.self, from: message, actionName: "handleAuth") else { 357 | return 358 | } 359 | queue.async { authMessageHandler(decoded) { reply($0.result.rawValue, $0.cache) } } 360 | } 361 | 362 | func handleNotify(_ message: Data) { 363 | guard let notifyMessageHandler = notifyMessageHandler else { 364 | log.warning("Notify message came but no notifyMessageHandler installed") 365 | return 366 | } 367 | guard let decoded = decode(ESMessage.self, from: message, actionName: "handleNotify") else { 368 | return 369 | } 370 | queue.async { notifyMessageHandler(decoded) } 371 | } 372 | 373 | func receiveCustomMessage(_ data: Data, completion: @escaping (Data?, Error?) -> Void) { 374 | if let receiveCustomMessageHandler { 375 | queue.async { receiveCustomMessageHandler(data) { completion($0.success, $0.failure) } } 376 | } else { 377 | completion(nil, CommonError.unexpected("receiveCustomMessageHandler not set")) 378 | } 379 | } 380 | 381 | private func decode(_ type: T.Type, from data: Data, actionName: String) -> T? { 382 | do { 383 | return try xpcDecoder.decode(T.self, from: data) 384 | } catch { 385 | log.error("Failed to decode \(type) for \(actionName). Error: \(error)") 386 | return nil 387 | } 388 | } 389 | } 390 | 391 | extension ESEventSet { 392 | fileprivate var asNumbers: [NSNumber] { 393 | events.map { NSNumber(value: $0.rawValue) } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityXPC/ESXPCConnection.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import sEndpointSecurity 24 | 25 | import EndpointSecurity 26 | import Foundation 27 | import SpellbookFoundation 28 | 29 | private let log = SpellbookLogger.internalLog(.xpc) 30 | 31 | internal final class ESXPCConnection { 32 | typealias ConnectResult = Result 33 | var connectionStateHandler: ((ConnectResult) -> Void)? 34 | var reconnectDelay: TimeInterval = 3.0 35 | var converterConfig: ESConverter.Config = .default 36 | 37 | init(delegate: ESClientXPCDelegateProtocol, createConnection: @escaping () -> NSXPCConnection) { 38 | self.delegate = delegate 39 | 40 | let prepareConnection = { () -> NSXPCConnection in 41 | let connection = createConnection() 42 | connection.remoteObjectInterface = .esClient 43 | connection.exportedInterface = .esClientDelegate 44 | return connection 45 | } 46 | self.createConnection = prepareConnection 47 | 48 | let dummyConnection = prepareConnection() 49 | dummyConnection.resume() 50 | dummyConnection.invalidate() 51 | self.xpcConnection = dummyConnection 52 | } 53 | 54 | func connect(async: Bool, notify: ((ConnectResult) -> Void)?) { 55 | let encodedConfig: Data 56 | do { 57 | encodedConfig = try xpcEncoder.encode(converterConfig) 58 | } catch { 59 | handleConnect(.failure(error), notify: notify) 60 | return 61 | } 62 | 63 | let connection = createConnection() 64 | connection.exportedObject = delegate 65 | connection.resume() 66 | 67 | let remoteObject = (async ? connection.remoteObjectProxyWithErrorHandler : connection.synchronousRemoteObjectProxyWithErrorHandler) { [weak self] in 68 | self?.handleConnect(.failure($0), notify: notify) 69 | } 70 | guard let proxy = remoteObject as? ESClientXPCProtocol else { 71 | let error = CommonError.cast(remoteObject, to: ESClientXPCProtocol.self) 72 | handleConnect(.failure(error), notify: notify) 73 | return 74 | } 75 | 76 | proxy.create(converterConfig: encodedConfig) { [weak self] in 77 | self?.handleConnect(.success(($0, connection)), notify: notify) 78 | } 79 | } 80 | 81 | func remoteObjectProxy(_ errorHandler: @escaping (Error) -> Void) -> ESClientXPCProtocol? { 82 | let remoteObject = xpcConnection.remoteObjectProxyWithErrorHandler { 83 | errorHandler($0) 84 | } 85 | guard let proxy = remoteObject as? ESClientXPCProtocol else { 86 | let error = CommonError.cast(remoteObject, to: ESClientXPCProtocol.self) 87 | errorHandler(error) 88 | return nil 89 | } 90 | return proxy 91 | } 92 | 93 | func invalidate() { 94 | reconnectOnFailure = false 95 | xpcConnection.invalidate() 96 | } 97 | 98 | // MARK: Private 99 | 100 | private let delegate: ESClientXPCDelegateProtocol 101 | private let createConnection: () -> NSXPCConnection 102 | @Atomic private var xpcConnection: NSXPCConnection 103 | @Atomic private var reconnectOnFailure = true 104 | private let queue = DispatchQueue(label: "ESXPCConnection.connectionQueue") 105 | 106 | private func reconnect() { 107 | guard reconnectOnFailure else { return } 108 | connect(async: true, notify: nil) 109 | } 110 | 111 | private func handleConnect( 112 | _ result: Result<(result: es_new_client_result_t, connection: NSXPCConnection), Error>, 113 | notify: ((ConnectResult) -> Void)? 114 | ) { 115 | defer { 116 | let notifyResult = result.map(\.result) 117 | notify?(notifyResult) 118 | queue.async { self.connectionStateHandler?(notifyResult) } 119 | } 120 | 121 | guard let value = result.success, value.result == ES_NEW_CLIENT_RESULT_SUCCESS else { 122 | log.error("Connect failed with result = \(result)") 123 | result.success?.connection.invalidate() 124 | if notify == nil { 125 | scheduleReconnect() 126 | } 127 | return 128 | } 129 | 130 | value.connection.invalidationHandler = { [weak self, weak connection = value.connection] in 131 | log.warning("ESXPC connection invalidated") 132 | 133 | connection?.invalidationHandler = nil 134 | 135 | guard let self else { return } 136 | self.queue.async { self.connectionStateHandler?(.failure(CocoaError(.xpcConnectionInterrupted))) } 137 | self.scheduleReconnect() 138 | } 139 | value.connection.interruptionHandler = { [weak connection = value.connection] in 140 | log.warning("ESXPC connection interrupted. Invalidating...") 141 | 142 | connection?.interruptionHandler = nil 143 | connection?.invalidate() 144 | } 145 | 146 | xpcConnection = value.connection 147 | } 148 | 149 | private func scheduleReconnect() { 150 | DispatchQueue.global().asyncAfter(deadline: .now() + reconnectDelay, execute: reconnect) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityXPC/ESXPCInternals.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import sEndpointSecurity 24 | 25 | import EndpointSecurity 26 | import Foundation 27 | 28 | @objc(ESClientXPCProtocol) 29 | internal protocol ESClientXPCProtocol { 30 | func create(converterConfig: Data, completion: @escaping (es_new_client_result_t) -> Void) 31 | 32 | func clearPathInterestCache(reply: @escaping (Error?) -> Void) 33 | 34 | func mute(process mute: Data, events: [NSNumber], reply: @escaping (Error?) -> Void) 35 | func unmute(process mute: Data, events: [NSNumber], reply: @escaping (Error?) -> Void) 36 | func unmuteAllProcesses(reply: @escaping (Error?) -> Void) 37 | func mute(path: String, type: es_mute_path_type_t, events: [NSNumber], reply: @escaping (Error?) -> Void) 38 | func unmute(path: String, type: es_mute_path_type_t, events: [NSNumber], reply: @escaping (Error?) -> Void) 39 | func unmuteAllPaths(reply: @escaping (Error?) -> Void) 40 | func unmuteAllTargetPaths(reply: @escaping (Error?) -> Void) 41 | 42 | func subscribe(_ events: [NSNumber], reply: @escaping (Error?) -> Void) 43 | func unsubscribe(_ events: [NSNumber], reply: @escaping (Error?) -> Void) 44 | func unsubscribeAll(reply: @escaping (Error?) -> Void) 45 | func clearCache(reply: @escaping (Error?) -> Void) 46 | 47 | func invertMuting(_ muteType: es_mute_inversion_type_t, reply: @escaping (Error?) -> Void) 48 | func mutingInverted(_ muteType: es_mute_inversion_type_t, reply: @escaping (Bool, Error?) -> Void) 49 | 50 | func sendCustomMessage(_ data: Data, reply: @escaping (Data?, Error?) -> Void) 51 | } 52 | 53 | @objc(ESClientXPCDelegateProtocol) 54 | internal protocol ESClientXPCDelegateProtocol { 55 | func handlePathInterest(_ process: Data, reply: @escaping (Data?) -> Void) 56 | func handleAuth(_ message: Data, reply: @escaping (UInt32, Bool) -> Void) 57 | func handleNotify(_ message: Data) 58 | 59 | func receiveCustomMessage(_ data: Data, completion: @escaping (Data?, Error?) -> Void) 60 | } 61 | 62 | extension NSXPCInterface { 63 | internal static var esClient: NSXPCInterface { 64 | let interface = NSXPCInterface(with: ESClientXPCProtocol.self) 65 | return interface 66 | } 67 | 68 | internal static var esClientDelegate: NSXPCInterface { 69 | let interface = NSXPCInterface(with: ESClientXPCDelegateProtocol.self) 70 | return interface 71 | } 72 | } 73 | 74 | internal let xpcEncoder = JSONEncoder() 75 | internal let xpcDecoder = JSONDecoder() 76 | -------------------------------------------------------------------------------- /Sources/sEndpointSecurityXPC/ESXPCListener.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2022 Alkenso (Vladimir Vashurkin) 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. 22 | 23 | import sEndpointSecurity 24 | 25 | import Combine 26 | import EndpointSecurity 27 | import Foundation 28 | import SpellbookFoundation 29 | 30 | private let log = SpellbookLogger.internalLog(.xpc) 31 | 32 | public final class ESXPCListener: NSObject { 33 | private let createClient: () throws -> ESClient 34 | private let listener: NSXPCListener 35 | private let sendCustomMessage = EventNotify<(data: Data, peer: UUID, reply: (Result) -> Void)>() 36 | 37 | /// When receiving incoming conneciton, ESXPCListener creates one ESClient for each connection. 38 | /// `pathInterestHandler`, `authMessageHandler`, `notifyMessageHandler` are overriden by XPC engine. 39 | /// Rest handles can be setup prior to returning new client from `createClient`. 40 | public init(listener: NSXPCListener, createClient: @escaping () throws -> ESClient) { 41 | self.listener = listener 42 | self.createClient = createClient 43 | 44 | super.init() 45 | 46 | listener.delegate = self 47 | } 48 | 49 | public var verifyConnectionHandler: ((audit_token_t) -> Bool)? 50 | public var receiveCustomMessageHandler: ((Data, UUID, @escaping (Result) -> Void) -> Void)? 51 | 52 | public func activate() { 53 | listener.resume() 54 | } 55 | 56 | public func sendCustomMessage(_ data: Data, to peer: UUID, reply: @escaping (Result) -> Void) { 57 | sendCustomMessage.notify((data, peer, reply)) 58 | } 59 | 60 | // MARK: Private 61 | } 62 | 63 | extension ESXPCListener: NSXPCListenerDelegate { 64 | public func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { 65 | guard verifyConnectionHandler?(newConnection.auditToken) ?? true else { return false } 66 | 67 | newConnection.exportedInterface = .esClient 68 | newConnection.remoteObjectInterface = .esClientDelegate 69 | guard let delegate = newConnection.remoteObjectProxy as? ESClientXPCDelegateProtocol else { 70 | let error = CommonError.cast(newConnection.remoteObjectProxy, to: ESClientXPCDelegateProtocol.self) 71 | log.fatal("Failed to accept new connection. Error: \(error)") 72 | return false 73 | } 74 | 75 | let client = createExportedObject(delegate) 76 | newConnection.exportedObject = client 77 | newConnection.invalidationHandler = { [weak client, weak newConnection] in 78 | client?.unsubscribeAll(reply: { _ in }) 79 | newConnection?.invalidationHandler = nil 80 | } 81 | newConnection.interruptionHandler = { [weak newConnection] in 82 | newConnection?.interruptionHandler = nil 83 | newConnection?.invalidate() 84 | } 85 | newConnection.resume() 86 | 87 | return true 88 | } 89 | 90 | private func createExportedObject(_ delegate: ESClientXPCDelegateProtocol) -> ESXPCExportedObject { 91 | let exportedClient = ESXPCExportedObject(delegate: delegate, createClient: createClient) 92 | 93 | exportedClient.receiveCustomMessageHandler = { [weak self, clientID = exportedClient.id] in 94 | self?.receiveCustomMessageHandler?($0, clientID, $1) 95 | } 96 | 97 | exportedClient.parentSubscription = sendCustomMessage.subscribe { [weak exportedClient] in 98 | guard let exportedClient, exportedClient.id == $0.peer else { return } 99 | exportedClient.receiveCustomMessage($0.data, reply: $0.reply) 100 | } 101 | 102 | return exportedClient 103 | } 104 | } 105 | 106 | private final class ESXPCExportedObject: NSObject, ESClientXPCProtocol { 107 | private let actionQueue: DispatchQueue 108 | let id: UUID 109 | 110 | init(delegate: ESClientXPCDelegateProtocol, createClient: @escaping () throws -> ESClient) { 111 | let id = UUID() 112 | self.id = id 113 | self.actionQueue = DispatchQueue(label: "ESXPCExportedObject.\(id).actions.queue") 114 | self.delegate = delegate 115 | self.createClient = createClient 116 | } 117 | 118 | var receiveCustomMessageHandler: ((Data, @escaping (Result) -> Void) -> Void)? 119 | var parentSubscription: Any? 120 | 121 | func receiveCustomMessage(_ data: Data, reply: @escaping (Result) -> Void) { 122 | delegate.receiveCustomMessage(data) { reply(Result(success: $0, failure: $1)) } 123 | } 124 | 125 | func create(converterConfig: Data, completion: @escaping (es_new_client_result_t) -> Void) { 126 | do { 127 | self.converterConfig = try xpcDecoder.decode(ESConverter.Config.self, from: converterConfig) 128 | 129 | let client = try createClient() 130 | client.pathInterestHandler = { [weak self] in self?.handlePathInterest($0) ?? .listen() } 131 | client.authMessageHandler = { [weak self] in self?.handleAuthMessage($0, completion: $1) } 132 | client.notifyMessageHandler = { [weak self] in self?.handleNotifyMessage($0) } 133 | 134 | self.client = client 135 | 136 | completion(ES_NEW_CLIENT_RESULT_SUCCESS) 137 | } catch { 138 | if let error = error as? ESError { 139 | completion(error.result) 140 | } else { 141 | completion(ES_NEW_CLIENT_RESULT_ERR_INTERNAL) 142 | } 143 | } 144 | } 145 | 146 | func subscribe(_ events: [NSNumber], reply: @escaping (Error?) -> Void) { 147 | let converted = events.map(\.uint32Value).map(es_event_type_t.init(rawValue:)) 148 | withClientOnActionQueue(reply: reply) { try $0.subscribe(converted) } 149 | } 150 | 151 | func unsubscribe(_ events: [NSNumber], reply: @escaping (Error?) -> Void) { 152 | let converted = events.map(\.uint32Value).map(es_event_type_t.init(rawValue:)) 153 | withClientOnActionQueue(reply: reply) { try $0.unsubscribe(converted) } 154 | } 155 | 156 | func unsubscribeAll(reply: @escaping (Error?) -> Void) { 157 | withClientOnActionQueue(reply: reply) { try $0.unsubscribeAll() } 158 | } 159 | 160 | func clearCache(reply: @escaping (Error?) -> Void) { 161 | withClientOnActionQueue(reply: reply) { try $0.clearCache() } 162 | } 163 | 164 | func clearPathInterestCache(reply: @escaping (Error?) -> Void) { 165 | withClientOnActionQueue(reply: reply) { $0.clearPathInterestCache() } 166 | } 167 | 168 | func mute(process mute: Data, events: [NSNumber], reply: @escaping (Error?) -> Void) { 169 | withClientOnActionQueue(reply: reply) { 170 | let decoded = try xpcDecoder.decode(ESMuteProcessRule.self, from: mute) 171 | $0.mute(process: decoded, events: .fromNumbers(events)) 172 | } 173 | } 174 | 175 | func unmute(process mute: Data, events: [NSNumber], reply: @escaping (Error?) -> Void) { 176 | withClientOnActionQueue(reply: reply) { 177 | let decoded = try xpcDecoder.decode(ESMuteProcessRule.self, from: mute) 178 | $0.unmute(process: decoded, events: .fromNumbers(events)) 179 | } 180 | } 181 | 182 | func unmuteAllProcesses(reply: @escaping (Error?) -> Void) { 183 | withClientOnActionQueue(reply: reply) { $0.unmuteAllProcesses() } 184 | } 185 | 186 | func mute(path: String, type: es_mute_path_type_t, events: [NSNumber], reply: @escaping (Error?) -> Void) { 187 | withClientOnActionQueue(reply: reply) { try $0.mute(path: path, type: type, events: .fromNumbers(events)) } 188 | } 189 | 190 | func unmute(path: String, type: es_mute_path_type_t, events: [NSNumber], reply: @escaping (Error?) -> Void) { 191 | if #available(macOS 12.0, *) { 192 | withClientOnActionQueue(reply: reply) { try $0.unmute(path: path, type: type, events: .fromNumbers(events)) } 193 | } else { 194 | reply(CommonError.unexpected("unmute(path:) not available")) 195 | } 196 | } 197 | 198 | func unmuteAllPaths(reply: @escaping (Error?) -> Void) { 199 | withClientOnActionQueue(reply: reply) { try $0.unmuteAllPaths() } 200 | } 201 | 202 | func unmuteAllTargetPaths(reply: @escaping (Error?) -> Void) { 203 | if #available(macOS 13.0, *) { 204 | withClientOnActionQueue(reply: reply) { try $0.unmuteAllTargetPaths() } 205 | } else { 206 | reply(CommonError.unexpected("unmuteAllTargetPaths not available")) 207 | } 208 | } 209 | 210 | func invertMuting(_ muteType: es_mute_inversion_type_t, reply: @escaping (Error?) -> Void) { 211 | if #available(macOS 13.0, *) { 212 | withClientOnActionQueue(reply: reply) { try $0.invertMuting(muteType) } 213 | } else { 214 | reply(CommonError.unexpected("invertMuting not available")) 215 | } 216 | } 217 | 218 | func mutingInverted(_ muteType: es_mute_inversion_type_t, reply: @escaping (Bool, Error?) -> Void) { 219 | actionQueue.async { [self] in 220 | do { 221 | if #available(macOS 13.0, *) { 222 | let client = try client.get(name: "ESClient") 223 | let result = try client.mutingInverted(muteType) 224 | reply(result, nil) 225 | } else { 226 | throw CommonError.unexpected("mutingInverted not available") 227 | } 228 | } catch { 229 | reply(false, error.xpcCompatible()) 230 | } 231 | } 232 | } 233 | 234 | func sendCustomMessage(_ data: Data, reply: @escaping (Data?, Error?) -> Void) { 235 | if let receiveCustomMessageHandler { 236 | receiveCustomMessageHandler(data) { reply($0.success, $0.failure) } 237 | } else { 238 | reply(nil, CommonError.unexpected("receiveCustomMessageHandler not set")) 239 | } 240 | } 241 | 242 | // MARK: Private 243 | 244 | private let createClient: () throws -> ESClient 245 | private let delegate: ESClientXPCDelegateProtocol 246 | private var client: ESClient? 247 | private var converterConfig: ESConverter.Config = .default 248 | 249 | private func handlePathInterest(_ process: ESProcess) -> ESInterest { 250 | do { 251 | let encoded = try xpcEncoder.encode(process) 252 | let executor = SynchronousExecutor("HandlePathInterest", timeout: 5.0) 253 | guard let interest = try executor({ reply in 254 | DispatchQueue.global().async { self.delegate.handlePathInterest(encoded, reply: reply) } 255 | }) else { 256 | return .listen() 257 | } 258 | let decoded = try xpcDecoder.decode(ESInterest.self, from: interest) 259 | return decoded 260 | } catch { 261 | log.error("handlePathInterest failed. Error: \(error)") 262 | return .listen() 263 | } 264 | } 265 | 266 | private func handleAuthMessage(_ message: ESMessagePtr, completion: @escaping (ESAuthResolution) -> Void) { 267 | processMessage( 268 | message, 269 | errorHandler: { 270 | log.error("handleAuthMessage failed. Error: \($0)") 271 | completion(.allowOnce) 272 | }, 273 | actionHandler: { 274 | $0.handleAuth($1) { 275 | completion(ESAuthResolution(result: .flags($0), cache: $1)) 276 | } 277 | } 278 | ) 279 | } 280 | 281 | private func handleNotifyMessage(_ message: ESMessagePtr) { 282 | processMessage( 283 | message, 284 | errorHandler: { log.error("handleNotifyMessage failed. Error: \($0)") }, 285 | actionHandler: { $0.handleNotify($1) } 286 | ) 287 | } 288 | 289 | private func processMessage( 290 | _ message: ESMessagePtr, 291 | errorHandler: @escaping (Error) -> Void, 292 | actionHandler: @escaping (ESClientXPCDelegateProtocol, Data) -> Void 293 | ) { 294 | guard let remoteObject = delegate as? NSXPCProxyCreating else { 295 | let error = CommonError.cast(delegate, to: NSXPCProxyCreating.self) 296 | errorHandler(error) 297 | return 298 | } 299 | 300 | let proxy = remoteObject.remoteObjectProxyWithErrorHandler(errorHandler) 301 | 302 | guard let delegateProxy = proxy as? ESClientXPCDelegateProtocol else { 303 | let error = CommonError.cast(proxy, to: ESClientXPCDelegateProtocol.self) 304 | errorHandler(error) 305 | return 306 | } 307 | 308 | do { 309 | let converted = try message.converted(converterConfig) 310 | let encoded = try xpcEncoder.encode(converted) 311 | actionHandler(delegateProxy, encoded) 312 | } catch { 313 | errorHandler(error) 314 | } 315 | } 316 | 317 | private func withClientOnActionQueue(reply: @escaping (Error?) -> Void, body: @escaping (ESClient) throws -> Void) { 318 | actionQueue.async { [self] in 319 | do { 320 | let client = try client.get(name: "ESClient") 321 | try body(client) 322 | reply(nil) 323 | } catch { 324 | reply(error.xpcCompatible()) 325 | } 326 | } 327 | } 328 | } 329 | 330 | extension ESEventSet { 331 | fileprivate static func fromNumbers(_ numbers: [NSNumber]) -> ESEventSet { 332 | ESEventSet(events: numbers.map { es_event_type_t($0.uint32Value) }) 333 | } 334 | } 335 | --------------------------------------------------------------------------------