├── .gitignore ├── LICENSE ├── README.md ├── jamfStatus.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── lesliehelou.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ ├── leslie.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ └── lesliehelou.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── jamfStatus.xcscheme │ └── xcschememanagement.plist └── jamfStatus ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ ├── Contents.json │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ └── icon_512x512@2x.png ├── Contents.json ├── cloudStatus-green.imageset │ ├── Contents.json │ ├── cloudStatus-green.png │ └── cloudStatus-green@2x.png ├── cloudStatus-red.imageset │ ├── Contents.json │ ├── cloudStatus-red.png │ └── cloudStatus-red@2x.png ├── cloudStatus-red1.imageset │ ├── Contents.json │ ├── cloudStatus-red1.png │ └── cloudStatus-red2.png ├── cloudStatus-yellow.imageset │ ├── Contents.json │ ├── cloudStatus-yellow.png │ └── cloudStatus-yellow@2x.png ├── cloudStatus-yellow1.imageset │ ├── Contents.json │ ├── cloudStatus-yellow1.png │ └── cloudStatus-yellow2.png ├── green-dot.imageset │ ├── Contents.json │ └── green-dot.png └── red-dot.imageset │ ├── Contents.json │ └── red-dot.png ├── Base.lproj └── MainMenu.xib ├── Credentials.swift ├── Globals.swift ├── Images.xcassets ├── Contents.json ├── greenCloud.imageset │ ├── Contents.json │ └── greenCloud.png ├── minimizedIcon.imageset │ ├── Contents.json │ ├── minimizedIcon-1.png │ └── minimizedIcon-2.png ├── redCloud.imageset │ ├── Contents.json │ └── redCloud.png └── yellowCloud.imageset │ ├── Contents.json │ └── yellowCloud.png ├── Info.plist ├── NotificationAlert.swift ├── StatusMenuController.swift ├── TokenDelegate.swift ├── UapiCall.swift ├── VersionCheck.swift ├── WriteToLog.swift ├── com.jamf.cloudmonitor.plist ├── images ├── alert.png ├── major.png ├── major1.png ├── major2.png ├── menubar.png ├── minor1.png ├── minor2.png ├── notifications.png └── prefs.png ├── index.html ├── jamfStatus.entitlements └── prefs.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/Notes/ 3 | **/.DS_Store 4 | localOnly/ 5 | *.xcuserstate 6 | *.xcbkptlist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jamf Professional Services 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jamfStatus 2 | Download: [jamfStatus](https://github.com/jamf/jamfStatus/releases/latest/download/jamfStatus.zip) 3 | 4 | Keep an eye on the status of Jamf Cloud with jamfStatus. The app will place an icon in the menu bar to reflect the current cloud status. 5 | 6 | menu bar 7 |

8 | An alert window will be displayed as the cloud status changes. You can configure how the alert window display refreshes, either at every status check or only when the status changes. 9 | 10 | For minor Jamf Cloud issues something similar to the following be displayed. 11 | 12 | alert 13 | 14 | For major Jamf Cloud issues something similar to the following be displayed. 15 | 16 | alert 17 | 18 | Access Preferences from the menu bar icon. Here you'll be able to set the following:
19 | - Polling interval.
20 | - Whether the alert window is displayed at every polling interval or only when the status changes.
21 | - How the menubar icon is displayed. Minimizing will place a thin transparent icon in the menubar.
22 | - Use of a LaunchAgent, to automatically start the app when logging in.
23 | - Information for your specific Jamf Cloud instance. Use either a local user account or API client. 24 | - Most notification can be viewed using an account with no permissions set in Jamf Pro. Using an account with ready-only on all objects ensure you'll see all notifications. If your cloud server does not utilize the standard HTTPS port (443) be sure to include the port you use in the URL. 25 | 26 | notifications
27 | 28 | There are two different menu bar icon styles to choose from. One uses colors to indicate the status and the other uses slashes.

29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 |
Statusminormajorminormajor
Icon 41 | 42 | 43 | 44 |

46 | 47 | Notifications, if any, will appear after the next polling cycle once the information has been entered. 48 | 49 | Preferences 50 | 51 | Status changes are logged to ~/Library/Logs/jamfStatus/jamfStatus.log. Once the log exceeds 5MB it will be zipped and a new log will be created. A maximum of 10 zipped log files are retained. Sample log data: 52 | 53 | ``` 54 | Thu Sep 17 20:24:30 Jamf Cloud Critical Issue Alert 55 | Thu Sep 17 20:24:30 Please be aware there is a major issue that may affect your Jamf Cloud instance. 56 | Thu Sep 17 20:24:30 eu-central-1: JCDS: Major Outage 57 | Thu Sep 17 20:24:30 Jamf Cloud Distribution Service (JCDS): Major Outage 58 | 59 | Thu Sep 17 20:25:30 Jamf Cloud Minor Issue Alert 60 | Thu Sep 17 20:25:30 Please be aware there is a minor issue that may affect your Jamf Cloud instance. 61 | Thu Sep 17 20:25:30 Compute Services - US: Degraded Performance 62 | Thu Sep 17 20:25:30 Database Services - US: Degraded Performance 63 | 64 | Thu Sep 17 20:27:30 Notice 65 | Thu Sep 17 20:27:30 Jamf Cloud: All systems go. 66 | ``` 67 | 68 | ## Notifications for the following will be displayed: 69 | 70 | * <certType> Certificate Expired 71 | * Cloud Identity Provider Certificate Expired 72 | * <certType> Certificate Expiring in <days> days 73 | * Cloud Identity Provider Certificate Expiring in <validDays> Days 74 | * Scripts contain invalid references to /usr/sbin/jamf 75 | * Extension attributes contain invalid references to /usr/sbin/jamf 76 | * Policies contain invalid references to /usr/sbin/jamf 77 | * Multiple policies have a management account password configuration that is not recommended 78 | * A policy has a management account password configuration that is not recommended 79 | * A configured management account feature is not recommended 80 | * Volume Purchasing Location <name> Expiring In <days> days 81 | * Volume Purchasing Location <name> Expired 82 | * Volume Purchasing Server Token Revoked for the location <name> 83 | * Automated Device Enrollment Instance <name> Expiring In <days> days 84 | * Automated Device Enrollment Instance <name> Expired 85 | * PreStage imaging and Autorun imaging requires a Jamf Pro user account with the Use PreStage Imaging and Autorun Imaging privilege. 86 | * <name> updates inventory on all computers at recurring check-in. This may cause stability issues. 87 | * <softwareTitleName> v<latestVersion> is available 88 | * <softwareTitleName> has an extension attribute requiring attention 89 | * Device Enrollment instance out of date with Apple’s Terms and Conditions. 90 | * Sync failed. The associated Automated Device Enrollment instance is out of date with Apple’s Terms and Conditions. The updated agreement must be accepted to sync information. See your Apple School Manager instance to accept the updated agreement. 91 | * <appName> is no longer available for device-assigned managed distribution and any device assignments have been disabled for this app. 92 | * There was an error configuring <hclName> Healthcare Listener on <jsamName> 93 | * Port number of <hclName> Healthcare Listener is invalid on <jsamName> 94 | * Verification of SSL certificates is disabled 95 | * <jsamName> Infrastructure Manager instance has not checked in with Jamf Pro. 96 | * Device Count Exceeded 97 | * Unable to send inventory information to Microsoft Intune 98 | * Unable to connect to Microsoft Intune 99 | * Integration disabled 100 | * Third-Party Signing Certificate Expired 101 | * Third-Party Signing Certificate Expiring in <days> Days 102 | * Third-Party Signing Certificate Expiring Today 103 | * LDAP Server Configuration Error 104 | * Verification status for the <serverName> LDAP Proxy Server Connection 105 | * Verification Status for the <serverName> LDAP Proxy Server Connection 106 | * <userName>'s Managed Apple ID does not match the Managed Apple ID reported in Apple School Manager. 107 | * The <maid> Managed Apple ID is used by multiple users. 108 | * The Jamf Pro JSS Built-in Certificate Authority is set to expire soon. 109 | * The Jamf Pro JSS Built-in Certificate Authority is expired. 110 | * The Jamf Pro JSS Built-in Certificate Authority has been successfully renewed. 111 | * The Jamf Pro JSS Built-in Certificate Authority renewal process failed. 112 | * Unable to connect to APNs because the push certificate was revoked. Navigate to Global Management > Push Certificates and renew the certificate or generate a new one. 113 | * Connection to the APN Service Failed. Could not connect to the APNs server. The server is down or network is unreachable. 114 | * Jamf Protect <latestVersion> Now Available 115 | * Jamf Connect <latestVersion> Now Available 116 | * Major Update for Jamf Connect Now Available (Jamf Connect <latestVersion>) 117 | * Device Compliance Connection Interrupted 118 | * Conditional Access Connection Interrupted 119 | 120 | 121 | ## Change log 122 | 123 | 2023-11-11: v2.4.0 - Fix issue with notifications not being displayed. Add ability to use API client. 124 | 125 | 2023-04-07: v2.3.6 - Update logging to prevent potential looping. 126 | 127 | 2022-10-02: v2.3.2 - Rework authentication/token refresh. 128 | 129 | 2022-06-12: v2.3.1 - Clean up notificatations not displaying properly. 130 | 131 | 2021-10-15: v2.3.0 - Updated notifications display. 132 | -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B5013BC71E95817C0099300A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5013BC61E95817C0099300A /* AppDelegate.swift */; }; 11 | B5013BC91E95817C0099300A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5013BC81E95817C0099300A /* Assets.xcassets */; }; 12 | B5013BCC1E95817C0099300A /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5013BCA1E95817C0099300A /* MainMenu.xib */; }; 13 | B515969F1E95BB2200ED3CA3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B515969E1E95BB2200ED3CA3 /* Images.xcassets */; }; 14 | B51596A11E963D2D00ED3CA3 /* StatusMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51596A01E963D2D00ED3CA3 /* StatusMenuController.swift */; }; 15 | B542A1F124BA902C00EE33C8 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = B542A1F024BA902C00EE33C8 /* Globals.swift */; }; 16 | B542A1F324BA90AA00EE33C8 /* WriteToLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B542A1F224BA90A900EE33C8 /* WriteToLog.swift */; }; 17 | B5A470B02271CE7200C2A88D /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A470AF2271CE7200C2A88D /* WebKit.framework */; }; 18 | B5AC20321F36479A00C16EAA /* com.jamf.cloudmonitor.plist in Resources */ = {isa = PBXBuildFile; fileRef = B5AC20311F36479A00C16EAA /* com.jamf.cloudmonitor.plist */; }; 19 | B5AC20351F37858800C16EAA /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = B5AC20341F37858800C16EAA /* index.html */; }; 20 | B5AC20371F37859F00C16EAA /* images in Resources */ = {isa = PBXBuildFile; fileRef = B5AC20361F37859F00C16EAA /* images */; }; 21 | B5BD6AD723297762001D244A /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD6AD623297762001D244A /* Credentials.swift */; }; 22 | B5BD6AD9232F108A001D244A /* VersionCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BD6AD8232F108A001D244A /* VersionCheck.swift */; }; 23 | B5BE0F9E2AFEF434000CBEBE /* TokenDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BE0F9D2AFEF434000CBEBE /* TokenDelegate.swift */; }; 24 | B5E62F32231CAB640012FF5A /* UapiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E62F31231CAB640012FF5A /* UapiCall.swift */; }; 25 | B5E62F34231DA09F0012FF5A /* NotificationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E62F33231DA09F0012FF5A /* NotificationAlert.swift */; }; 26 | B5FD9DAC2742AAC60044C321 /* Misc in Resources */ = {isa = PBXBuildFile; fileRef = B5FD9DAB2742AAC60044C321 /* Misc */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | B5013BC31E95817C0099300A /* jamfStatus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = jamfStatus.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | B5013BC61E95817C0099300A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | B5013BC81E95817C0099300A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | B5013BCB1E95817C0099300A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 34 | B5013BCD1E95817C0099300A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | B515969E1E95BB2200ED3CA3 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 36 | B51596A01E963D2D00ED3CA3 /* StatusMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusMenuController.swift; sourceTree = ""; }; 37 | B542A1F024BA902C00EE33C8 /* Globals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; 38 | B542A1F224BA90A900EE33C8 /* WriteToLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WriteToLog.swift; sourceTree = ""; }; 39 | B5A46CCE226FB1E300C2A88D /* jamfStatus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = jamfStatus.entitlements; sourceTree = ""; }; 40 | B5A470AF2271CE7200C2A88D /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 41 | B5AC20311F36479A00C16EAA /* com.jamf.cloudmonitor.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.jamf.cloudmonitor.plist; sourceTree = ""; }; 42 | B5AC20341F37858800C16EAA /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = index.html; sourceTree = ""; }; 43 | B5AC20361F37859F00C16EAA /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = ""; }; 44 | B5BD6AD623297762001D244A /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; 45 | B5BD6AD8232F108A001D244A /* VersionCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionCheck.swift; sourceTree = ""; }; 46 | B5BE0F9D2AFEF434000CBEBE /* TokenDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenDelegate.swift; sourceTree = ""; }; 47 | B5E62F31231CAB640012FF5A /* UapiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UapiCall.swift; sourceTree = ""; }; 48 | B5E62F33231DA09F0012FF5A /* NotificationAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAlert.swift; sourceTree = ""; }; 49 | B5FD9DAB2742AAC60044C321 /* Misc */ = {isa = PBXFileReference; lastKnownFileType = text; path = Misc; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | B5013BC01E95817C0099300A /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | B5A470B02271CE7200C2A88D /* WebKit.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | B5013BBA1E95817C0099300A = { 65 | isa = PBXGroup; 66 | children = ( 67 | B56CA92428EA7C3200236A8A /* Notes */, 68 | B5013BC51E95817C0099300A /* jamfStatus */, 69 | B5013BC41E95817C0099300A /* Products */, 70 | B5A46D07227145EA00C2A88D /* Frameworks */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | B5013BC41E95817C0099300A /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | B5013BC31E95817C0099300A /* jamfStatus.app */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | B5013BC51E95817C0099300A /* jamfStatus */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | B5A46CCE226FB1E300C2A88D /* jamfStatus.entitlements */, 86 | B5AC20331F37855800C16EAA /* about */, 87 | B5013BC61E95817C0099300A /* AppDelegate.swift */, 88 | B5BD6AD623297762001D244A /* Credentials.swift */, 89 | B542A1F024BA902C00EE33C8 /* Globals.swift */, 90 | B5E62F33231DA09F0012FF5A /* NotificationAlert.swift */, 91 | B51596A01E963D2D00ED3CA3 /* StatusMenuController.swift */, 92 | B5BE0F9D2AFEF434000CBEBE /* TokenDelegate.swift */, 93 | B5E62F31231CAB640012FF5A /* UapiCall.swift */, 94 | B5BD6AD8232F108A001D244A /* VersionCheck.swift */, 95 | B542A1F224BA90A900EE33C8 /* WriteToLog.swift */, 96 | B5013BC81E95817C0099300A /* Assets.xcassets */, 97 | B515969E1E95BB2200ED3CA3 /* Images.xcassets */, 98 | B5013BCA1E95817C0099300A /* MainMenu.xib */, 99 | B5013BCD1E95817C0099300A /* Info.plist */, 100 | B5AC20311F36479A00C16EAA /* com.jamf.cloudmonitor.plist */, 101 | ); 102 | path = jamfStatus; 103 | sourceTree = ""; 104 | }; 105 | B56CA92428EA7C3200236A8A /* Notes */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | B5FD9DAB2742AAC60044C321 /* Misc */, 109 | ); 110 | name = Notes; 111 | path = jamfStatus/Notes; 112 | sourceTree = ""; 113 | }; 114 | B5A46D07227145EA00C2A88D /* Frameworks */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | B5A470AF2271CE7200C2A88D /* WebKit.framework */, 118 | ); 119 | name = Frameworks; 120 | sourceTree = ""; 121 | }; 122 | B5AC20331F37855800C16EAA /* about */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | B5AC20341F37858800C16EAA /* index.html */, 126 | B5AC20361F37859F00C16EAA /* images */, 127 | ); 128 | name = about; 129 | sourceTree = ""; 130 | }; 131 | /* End PBXGroup section */ 132 | 133 | /* Begin PBXNativeTarget section */ 134 | B5013BC21E95817C0099300A /* jamfStatus */ = { 135 | isa = PBXNativeTarget; 136 | buildConfigurationList = B5013BD01E95817C0099300A /* Build configuration list for PBXNativeTarget "jamfStatus" */; 137 | buildPhases = ( 138 | B5013BBF1E95817C0099300A /* Sources */, 139 | B5013BC01E95817C0099300A /* Frameworks */, 140 | B5013BC11E95817C0099300A /* Resources */, 141 | ); 142 | buildRules = ( 143 | ); 144 | dependencies = ( 145 | ); 146 | name = jamfStatus; 147 | productName = jamfStatus; 148 | productReference = B5013BC31E95817C0099300A /* jamfStatus.app */; 149 | productType = "com.apple.product-type.application"; 150 | }; 151 | /* End PBXNativeTarget section */ 152 | 153 | /* Begin PBXProject section */ 154 | B5013BBB1E95817C0099300A /* Project object */ = { 155 | isa = PBXProject; 156 | attributes = { 157 | LastSwiftUpdateCheck = 0820; 158 | LastUpgradeCheck = 1410; 159 | ORGANIZATIONNAME = "Leslie Helou"; 160 | TargetAttributes = { 161 | B5013BC21E95817C0099300A = { 162 | CreatedOnToolsVersion = 8.2.1; 163 | DevelopmentTeam = T82LNZG37Z; 164 | LastSwiftMigration = 1020; 165 | ProvisioningStyle = Automatic; 166 | SystemCapabilities = { 167 | com.apple.HardenedRuntime = { 168 | enabled = 1; 169 | }; 170 | com.apple.Sandbox = { 171 | enabled = 1; 172 | }; 173 | }; 174 | }; 175 | }; 176 | }; 177 | buildConfigurationList = B5013BBE1E95817C0099300A /* Build configuration list for PBXProject "jamfStatus" */; 178 | compatibilityVersion = "Xcode 3.2"; 179 | developmentRegion = en; 180 | hasScannedForEncodings = 0; 181 | knownRegions = ( 182 | en, 183 | Base, 184 | ); 185 | mainGroup = B5013BBA1E95817C0099300A; 186 | productRefGroup = B5013BC41E95817C0099300A /* Products */; 187 | projectDirPath = ""; 188 | projectRoot = ""; 189 | targets = ( 190 | B5013BC21E95817C0099300A /* jamfStatus */, 191 | ); 192 | }; 193 | /* End PBXProject section */ 194 | 195 | /* Begin PBXResourcesBuildPhase section */ 196 | B5013BC11E95817C0099300A /* Resources */ = { 197 | isa = PBXResourcesBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | B515969F1E95BB2200ED3CA3 /* Images.xcassets in Resources */, 201 | B5013BC91E95817C0099300A /* Assets.xcassets in Resources */, 202 | B5AC20351F37858800C16EAA /* index.html in Resources */, 203 | B5AC20321F36479A00C16EAA /* com.jamf.cloudmonitor.plist in Resources */, 204 | B5AC20371F37859F00C16EAA /* images in Resources */, 205 | B5FD9DAC2742AAC60044C321 /* Misc in Resources */, 206 | B5013BCC1E95817C0099300A /* MainMenu.xib in Resources */, 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXResourcesBuildPhase section */ 211 | 212 | /* Begin PBXSourcesBuildPhase section */ 213 | B5013BBF1E95817C0099300A /* Sources */ = { 214 | isa = PBXSourcesBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | B5E62F34231DA09F0012FF5A /* NotificationAlert.swift in Sources */, 218 | B542A1F324BA90AA00EE33C8 /* WriteToLog.swift in Sources */, 219 | B5E62F32231CAB640012FF5A /* UapiCall.swift in Sources */, 220 | B51596A11E963D2D00ED3CA3 /* StatusMenuController.swift in Sources */, 221 | B5BD6AD723297762001D244A /* Credentials.swift in Sources */, 222 | B542A1F124BA902C00EE33C8 /* Globals.swift in Sources */, 223 | B5BE0F9E2AFEF434000CBEBE /* TokenDelegate.swift in Sources */, 224 | B5013BC71E95817C0099300A /* AppDelegate.swift in Sources */, 225 | B5BD6AD9232F108A001D244A /* VersionCheck.swift in Sources */, 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXSourcesBuildPhase section */ 230 | 231 | /* Begin PBXVariantGroup section */ 232 | B5013BCA1E95817C0099300A /* MainMenu.xib */ = { 233 | isa = PBXVariantGroup; 234 | children = ( 235 | B5013BCB1E95817C0099300A /* Base */, 236 | ); 237 | name = MainMenu.xib; 238 | sourceTree = ""; 239 | }; 240 | /* End PBXVariantGroup section */ 241 | 242 | /* Begin XCBuildConfiguration section */ 243 | B5013BCE1E95817C0099300A /* Debug */ = { 244 | isa = XCBuildConfiguration; 245 | buildSettings = { 246 | ALWAYS_SEARCH_USER_PATHS = NO; 247 | CLANG_ANALYZER_NONNULL = YES; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 253 | CLANG_WARN_BOOL_CONVERSION = YES; 254 | CLANG_WARN_COMMA = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 259 | CLANG_WARN_EMPTY_BODY = YES; 260 | CLANG_WARN_ENUM_CONVERSION = YES; 261 | CLANG_WARN_INFINITE_RECURSION = YES; 262 | CLANG_WARN_INT_CONVERSION = YES; 263 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 265 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 267 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 268 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 269 | CLANG_WARN_STRICT_PROTOTYPES = YES; 270 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 271 | CLANG_WARN_UNREACHABLE_CODE = YES; 272 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 273 | CODE_SIGN_IDENTITY = "-"; 274 | COPY_PHASE_STRIP = NO; 275 | DEAD_CODE_STRIPPING = YES; 276 | DEBUG_INFORMATION_FORMAT = dwarf; 277 | ENABLE_STRICT_OBJC_MSGSEND = YES; 278 | ENABLE_TESTABILITY = YES; 279 | GCC_C_LANGUAGE_STANDARD = gnu99; 280 | GCC_DYNAMIC_NO_PIC = NO; 281 | GCC_NO_COMMON_BLOCKS = YES; 282 | GCC_OPTIMIZATION_LEVEL = 0; 283 | GCC_PREPROCESSOR_DEFINITIONS = ( 284 | "DEBUG=1", 285 | "$(inherited)", 286 | ); 287 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 288 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 289 | GCC_WARN_UNDECLARED_SELECTOR = YES; 290 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 291 | GCC_WARN_UNUSED_FUNCTION = YES; 292 | GCC_WARN_UNUSED_VARIABLE = YES; 293 | MACOSX_DEPLOYMENT_TARGET = 11.0; 294 | MTL_ENABLE_DEBUG_INFO = YES; 295 | ONLY_ACTIVE_ARCH = YES; 296 | SDKROOT = macosx; 297 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 298 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 299 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 300 | }; 301 | name = Debug; 302 | }; 303 | B5013BCF1E95817C0099300A /* Release */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | ALWAYS_SEARCH_USER_PATHS = NO; 307 | CLANG_ANALYZER_NONNULL = YES; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 309 | CLANG_CXX_LIBRARY = "libc++"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 313 | CLANG_WARN_BOOL_CONVERSION = YES; 314 | CLANG_WARN_COMMA = YES; 315 | CLANG_WARN_CONSTANT_CONVERSION = YES; 316 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 317 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 318 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 319 | CLANG_WARN_EMPTY_BODY = YES; 320 | CLANG_WARN_ENUM_CONVERSION = YES; 321 | CLANG_WARN_INFINITE_RECURSION = YES; 322 | CLANG_WARN_INT_CONVERSION = YES; 323 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 325 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 326 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 327 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 329 | CLANG_WARN_STRICT_PROTOTYPES = YES; 330 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | CODE_SIGN_IDENTITY = "-"; 334 | COPY_PHASE_STRIP = NO; 335 | DEAD_CODE_STRIPPING = YES; 336 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 337 | ENABLE_NS_ASSERTIONS = NO; 338 | ENABLE_STRICT_OBJC_MSGSEND = YES; 339 | GCC_C_LANGUAGE_STANDARD = gnu99; 340 | GCC_NO_COMMON_BLOCKS = YES; 341 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 342 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 343 | GCC_WARN_UNDECLARED_SELECTOR = YES; 344 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 345 | GCC_WARN_UNUSED_FUNCTION = YES; 346 | GCC_WARN_UNUSED_VARIABLE = YES; 347 | MACOSX_DEPLOYMENT_TARGET = 11.0; 348 | MTL_ENABLE_DEBUG_INFO = NO; 349 | SDKROOT = macosx; 350 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 351 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 352 | }; 353 | name = Release; 354 | }; 355 | B5013BD11E95817C0099300A /* Debug */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 359 | CODE_SIGN_ENTITLEMENTS = jamfStatus/jamfStatus.entitlements; 360 | CODE_SIGN_IDENTITY = "Mac Developer"; 361 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 362 | CODE_SIGN_STYLE = Automatic; 363 | COMBINE_HIDPI_IMAGES = YES; 364 | CURRENT_PROJECT_VERSION = 1; 365 | DEAD_CODE_STRIPPING = YES; 366 | DEVELOPMENT_TEAM = T82LNZG37Z; 367 | ENABLE_HARDENED_RUNTIME = YES; 368 | EXCLUDED_SOURCE_FILE_NAMES = Notes; 369 | INFOPLIST_FILE = jamfStatus/Info.plist; 370 | INFOPLIST_KEY_CFBundleDisplayName = JamfStatus; 371 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 372 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 373 | LIBRARY_SEARCH_PATHS = ( 374 | "$(inherited)", 375 | "$(SDKROOT)/usr/lib/system", 376 | "$(SDKROOT)/usr/lib/log", 377 | "$(SDKROOT)/usr/lib/system/introspection", 378 | ); 379 | MACOSX_DEPLOYMENT_TARGET = 11.0; 380 | MARKETING_VERSION = 2.4.0; 381 | PRODUCT_BUNDLE_IDENTIFIER = com.jamf.pse.jamfStatus; 382 | PRODUCT_NAME = "$(TARGET_NAME)"; 383 | PROVISIONING_PROFILE_SPECIFIER = ""; 384 | SWIFT_VERSION = 5.0; 385 | }; 386 | name = Debug; 387 | }; 388 | B5013BD21E95817C0099300A /* Release */ = { 389 | isa = XCBuildConfiguration; 390 | buildSettings = { 391 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 392 | CODE_SIGN_ENTITLEMENTS = jamfStatus/jamfStatus.entitlements; 393 | CODE_SIGN_IDENTITY = "Mac Developer"; 394 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 395 | CODE_SIGN_STYLE = Automatic; 396 | COMBINE_HIDPI_IMAGES = YES; 397 | CURRENT_PROJECT_VERSION = 1; 398 | DEAD_CODE_STRIPPING = YES; 399 | DEVELOPMENT_TEAM = T82LNZG37Z; 400 | ENABLE_HARDENED_RUNTIME = YES; 401 | EXCLUDED_SOURCE_FILE_NAMES = Notes; 402 | INFOPLIST_FILE = jamfStatus/Info.plist; 403 | INFOPLIST_KEY_CFBundleDisplayName = JamfStatus; 404 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 405 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 406 | LIBRARY_SEARCH_PATHS = ( 407 | "$(inherited)", 408 | "$(SDKROOT)/usr/lib/system", 409 | "$(SDKROOT)/usr/lib/log", 410 | "$(SDKROOT)/usr/lib/system/introspection", 411 | ); 412 | MACOSX_DEPLOYMENT_TARGET = 11.0; 413 | MARKETING_VERSION = 2.4.0; 414 | PRODUCT_BUNDLE_IDENTIFIER = com.jamf.pse.jamfStatus; 415 | PRODUCT_NAME = "$(TARGET_NAME)"; 416 | PROVISIONING_PROFILE_SPECIFIER = ""; 417 | SWIFT_VERSION = 5.0; 418 | }; 419 | name = Release; 420 | }; 421 | /* End XCBuildConfiguration section */ 422 | 423 | /* Begin XCConfigurationList section */ 424 | B5013BBE1E95817C0099300A /* Build configuration list for PBXProject "jamfStatus" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | B5013BCE1E95817C0099300A /* Debug */, 428 | B5013BCF1E95817C0099300A /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | B5013BD01E95817C0099300A /* Build configuration list for PBXNativeTarget "jamfStatus" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | B5013BD11E95817C0099300A /* Debug */, 437 | B5013BD21E95817C0099300A /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | defaultConfigurationName = Release; 441 | }; 442 | /* End XCConfigurationList section */ 443 | }; 444 | rootObject = B5013BBB1E95817C0099300A /* Project object */; 445 | } 446 | -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/project.xcworkspace/xcuserdata/lesliehelou.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus.xcodeproj/project.xcworkspace/xcuserdata/lesliehelou.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/xcuserdata/leslie.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | jamfStatus.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/jamfStatus.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 89 | 92 | 93 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /jamfStatus.xcodeproj/xcuserdata/lesliehelou.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | jamfStatus.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | B5013BC21E95817C0099300A 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /jamfStatus/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | //AppDelegate.swift 2 | //Author: Leslie Helou 3 | //Copyright 2017 Jamf Professional Services 4 | // 5 | 6 | import Cocoa 7 | import WebKit 8 | 9 | @NSApplicationMain 10 | class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate { 11 | 12 | @IBOutlet weak var cloudStatus_Toolbar: NSToolbar! 13 | @IBOutlet weak var cloudStatusWindow: NSWindow! 14 | 15 | @IBOutlet var page_WebView: WKWebView! 16 | @IBOutlet weak var prefs_Window: NSWindow! 17 | @IBOutlet weak var aboutVersion_TextField: NSTextField! 18 | @IBOutlet weak var aboutHomeUrl_Button: NSButton! 19 | 20 | 21 | @IBOutlet weak var pollingInterval_TextField: NSTextField! 22 | @IBOutlet weak var launchAgent_Button: NSButton! 23 | @IBOutlet weak var iconStyle_Button: NSPopUpButton! 24 | 25 | 26 | // site specific settings 27 | @IBOutlet weak var jamfServerUrl_TextField: NSTextField! 28 | @IBOutlet weak var username_Label: NSTextField! 29 | @IBOutlet weak var password_Label: NSTextField! 30 | @IBOutlet weak var username_TextField: NSTextField! 31 | @IBOutlet weak var password_TextField: NSSecureTextField! 32 | @IBOutlet weak var useApiClient_button: NSButton! 33 | @IBAction func useApiClient_action(_ sender: NSButton) { 34 | setLabels() 35 | useApiClient = useApiClient_button.state.rawValue 36 | defaults.set(useApiClient_button.state.rawValue, forKey: "useApiClient") 37 | fetchPassword() 38 | } 39 | 40 | @IBOutlet weak var siteConnectionStatus_ImageView: NSImageView! 41 | let statusImage:[NSImage] = [NSImage(named: "red-dot")!, 42 | NSImage(named: "green-dot")!] 43 | 44 | // @IBOutlet weak var monitorUrl_TextField: NSTextField! 45 | 46 | @IBOutlet weak var about_NSWindow: NSWindow! 47 | @IBOutlet weak var about_WebView: WKWebView! 48 | 49 | let prefs = Preferences.self 50 | let defaults = UserDefaults() 51 | 52 | let fm = FileManager() 53 | var pollingInterval: Int = 300 54 | var hideIcon: Bool = false 55 | let launchAgentPath = NSHomeDirectory()+"/Library/LaunchAgents/com.jamf.cloudmonitor.plist" 56 | 57 | @objc func notificationsAction(_ sender: NSMenuItem) { 58 | // print("\(sender.identifier!.rawValue)") 59 | // WriteToLog().message(stringOfText: ["\(sender.identifier!.rawValue)"]) 60 | } 61 | 62 | 63 | @IBAction func iconStyle_Action(_ sender: Any) { 64 | if iconStyle_Button.indexOfSelectedItem == 0 { 65 | prefs.menuIconStyle = "color" 66 | } else { 67 | prefs.menuIconStyle = "slash" 68 | } 69 | defaults.set(prefs.menuIconStyle, forKey: "menuIconStyle") 70 | } 71 | 72 | 73 | @IBAction func showAbout_MenuItem(_ sender: NSMenuItem) { 74 | 75 | let appInfo = Bundle.main.infoDictionary! 76 | let version = appInfo["CFBundleShortVersionString"] as! String 77 | 78 | let filePath = Bundle.main.path(forResource: "index", ofType: "html") 79 | let folderPath = Bundle.main.resourcePath 80 | // 81 | let fileUrl = NSURL(fileURLWithPath: filePath!) 82 | let baseUrl = NSURL(fileURLWithPath: folderPath!, isDirectory: true) 83 | 84 | about_WebView.loadFileURL(fileUrl as URL, allowingReadAccessTo: baseUrl as URL) 85 | 86 | aboutVersion_TextField.stringValue = "Version: \(version)" 87 | 88 | let stringAttributes: [NSAttributedString.Key: Any] = [.underlineStyle: NSUnderlineStyle.single.rawValue, .foregroundColor: NSColor.systemBlue] 89 | let attributedTitle = NSAttributedString(string: "Home", attributes: stringAttributes) 90 | aboutHomeUrl_Button.attributedTitle = attributedTitle 91 | aboutHomeUrl_Button.toolTip = "https://github.com/jamf/jamfStatus" 92 | 93 | showOnActiveScreen(windowName: about_NSWindow) 94 | 95 | } 96 | @IBAction func aboutHomeUrl_Button(_ sender: NSButton) { 97 | NSWorkspace.shared.open(URL(string: "https://github.com/jamf/jamfStatus")!) 98 | } 99 | 100 | @IBAction func checkForUpdates(_ sender: AnyObject) { 101 | 102 | let appInfo = Bundle.main.infoDictionary! 103 | let version = appInfo["CFBundleShortVersionString"] as! String 104 | 105 | VersionCheck().versionCheck() { 106 | (result: Bool) in 107 | if result { 108 | self.alert_dialog(header: "Running jamfStatus: \(version)", message: "A new versions is available.", updateAvail: result) 109 | } else { 110 | self.alert_dialog(header: "Running jamfStatus: \(version)", message: "No updates are currently available.", updateAvail: result) 111 | } 112 | } 113 | } 114 | 115 | @IBAction func viewStatus(_ sender: Any) { 116 | 117 | DispatchQueue.main.async { 118 | if let url = URL(string: "https://status.jamf.com") { 119 | let request = URLRequest(url: url) 120 | 121 | self.page_WebView?.load(request) 122 | 123 | } 124 | self.cloudStatusWindow.titleVisibility = .hidden 125 | 126 | self.showOnActiveScreen(windowName: self.cloudStatusWindow) 127 | } 128 | } 129 | 130 | @IBOutlet weak var prefWindowAlerts_Button: NSButton! 131 | @IBOutlet weak var prefWindowIcon_Button: NSButton! 132 | 133 | @IBAction func prefs_MenuItem(_ sender: NSMenuItem) { 134 | showPrefsWindow() 135 | } 136 | 137 | // actions for preferences window - start 138 | @IBAction func pollInterval_Action(_ sender: NSTextField) { 139 | prefs.pollingInterval = Int(pollingInterval_TextField.stringValue) ?? 0 140 | if prefs.pollingInterval! < 60 { 141 | prefs.pollingInterval = 300 142 | pollingInterval_TextField.stringValue = "300" 143 | } 144 | defaults.set(prefs.pollingInterval, forKey: "pollingInterval") 145 | // defaults.synchronize() 146 | prefs.pollingInterval = defaults.object(forKey: "pollingInterval") as? Int 147 | } 148 | @IBAction func prefWindowAlerts_Action(_ sender: NSButton) { 149 | // print("sender: \(String(describing: sender.identifier?.rawValue))") 150 | // if ("\(String(describing: sender.identifier?.rawValue))" == "_NS:18") { 151 | // 152 | // } 153 | prefs.hideUntilStatusChange = (prefWindowAlerts_Button.state.rawValue == 0 ? false:true) 154 | defaults.set(prefs.hideUntilStatusChange, forKey: "hideUntilStatusChange") 155 | // defaults.synchronize() 156 | } 157 | @IBAction func hideMenubarIcon_Action(_ sender: NSButton) { 158 | prefs.hideMenubarIcon = (prefWindowIcon_Button.state.rawValue == 0 ? false:true) 159 | defaults.set(prefs.hideMenubarIcon, forKey: "hideMenubarIcon") 160 | // defaults.synchronize() 161 | } 162 | @IBAction func launchAgent_Action(_ sender: NSButton) { 163 | var isDir: ObjCBool = true 164 | prefs.launchAgent = (launchAgent_Button.state.rawValue == 0 ? false:true) 165 | defaults.set(prefs.launchAgent, forKey: "launchAgent") 166 | // defaults.synchronize() 167 | if launchAgent_Button.state.rawValue == 0 { 168 | if fm.fileExists(atPath: launchAgentPath) { 169 | do { 170 | try fm.removeItem(atPath: launchAgentPath) 171 | } catch { 172 | print("failed to remove LaunchAgent") 173 | } 174 | } 175 | } else { 176 | if !fm.fileExists(atPath: launchAgentPath) { 177 | do { 178 | if !(fm.fileExists(atPath: NSHomeDirectory() + "/Library/LaunchAgents", isDirectory: &isDir)) { 179 | do { 180 | try fm.createDirectory(atPath: NSHomeDirectory() + "/Library/LaunchAgents", withIntermediateDirectories: true, attributes: nil) 181 | } catch { 182 | NSLog("Problem creating '/Library/LaunchAgents' folder: \(error)") 183 | } 184 | } 185 | try fm.copyItem(atPath: Bundle.main.bundlePath+"/Contents/Resources/com.jamf.cloudmonitor.plist", toPath: launchAgentPath) 186 | } catch { 187 | print("failed to write LaunchAgent") 188 | } 189 | } 190 | } 191 | } 192 | 193 | @IBAction func credentials_Action(_ sender: Any) { 194 | JamfProServer.url = jamfServerUrl_TextField.stringValue 195 | 196 | let urlRegex = try! NSRegularExpression(pattern: "/?failover(.*?)", options:.caseInsensitive) 197 | JamfProServer.url = urlRegex.stringByReplacingMatches(in: JamfProServer.url, options: [], range: NSRange(0.. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 171 | 172 | 173 | 174 | 175 | 176 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 241 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 370 | 380 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | NSAllRomanInputSourcesLocaleIdentifier 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | -------------------------------------------------------------------------------- /jamfStatus/Credentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Credentials.swift 3 | // jamfStatus 4 | // 5 | // Created by Leslie Helou on 9/11/19. 6 | // Copyright © 2019 Leslie Helou. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Security 11 | 12 | let kSecAttrAccountString = NSString(format: kSecAttrAccount) 13 | let kSecValueDataString = NSString(format: kSecValueData) 14 | let kSecClassGenericPasswordString = NSString(format: kSecClassGenericPassword) 15 | let keychainQ = DispatchQueue(label: "com.jamfie.jamfstatus", qos: DispatchQoS.background) 16 | 17 | class Credentials { 18 | 19 | func save(service: String, account: String, data: String) { 20 | 21 | let theService = (useApiClient == 0) ? "jamfStatus: \(service)":"jamfStatus-apiClient: \(service)" 22 | 23 | keychainQ.async { [self] in 24 | if let password = data.data(using: String.Encoding.utf8) { 25 | var keychainQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword, 26 | kSecAttrService as String: theService, 27 | kSecAttrAccount as String: account, 28 | kSecValueData as String: password] 29 | 30 | // see if credentials already exist for server 31 | let accountCheck = itemLookup(service: service) 32 | if accountCheck.count == 0 { 33 | // try to add new credentials, if account exists we'll try updating it 34 | let addStatus = SecItemAdd(keychainQuery as CFDictionary, nil) 35 | if (addStatus != errSecSuccess) { 36 | if let addErr = SecCopyErrorMessageString(addStatus, nil) { 37 | print("[addStatus] Write failed for new credentials: \(addErr)") 38 | let deleteStatus = SecItemDelete(keychainQuery as CFDictionary) 39 | print("[Credentials.save] the deleteStatus: \(deleteStatus)") 40 | sleep(1) 41 | let addStatus = SecItemAdd(keychainQuery as CFDictionary, nil) 42 | if (addStatus != errSecSuccess) { 43 | if let addErr = SecCopyErrorMessageString(addStatus, nil) { 44 | print("[addStatus] Write failed for new credentials after deleting: \(addErr)") 45 | } 46 | } 47 | } 48 | } 49 | } else { 50 | // credentials already exist, try to update 51 | keychainQuery = [kSecClass as String: kSecClassGenericPasswordString, 52 | kSecAttrService as String: theService, 53 | kSecMatchLimit as String: kSecMatchLimitOne, 54 | kSecReturnAttributes as String: true] 55 | let updateStatus = SecItemUpdate(keychainQuery as CFDictionary, [kSecAttrAccountString:account,kSecValueDataString:password] as CFDictionary) 56 | if (updateStatus != errSecSuccess) { 57 | if let updateErr = SecCopyErrorMessageString(updateStatus, nil) { 58 | print("[updateStatus] Update failed for existing credentials: \(updateErr)") 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } // func save - end 65 | 66 | func itemLookup(service: String) -> [String] { 67 | var existingCredientials = retrieve(service: service) 68 | if existingCredientials.count == 0 { 69 | existingCredientials = retrieve(service: "\(service)/") 70 | if existingCredientials.count == 0 { 71 | existingCredientials = retrieve(service: "\(service):8443") 72 | if existingCredientials.count == 0 { 73 | existingCredientials = retrieve(service: "\(service):8443/") 74 | } 75 | } 76 | } 77 | return existingCredientials 78 | } 79 | 80 | func retrieve(service: String) -> [String] { 81 | 82 | var storedCreds = [String]() 83 | 84 | let theService = (useApiClient == 0) ? "jamfStatus: \(service)":"jamfStatus-apiClient: \(service)" 85 | 86 | let keychainQuery: [String: Any] = [kSecClass as String: kSecClassGenericPasswordString, 87 | kSecAttrService as String: theService, 88 | kSecMatchLimit as String: kSecMatchLimitOne, 89 | kSecReturnAttributes as String: true, 90 | kSecReturnData as String: true] 91 | var item: CFTypeRef? 92 | let status = SecItemCopyMatching(keychainQuery as CFDictionary, &item) 93 | guard status != errSecItemNotFound else { return [] } 94 | guard status == errSecSuccess else { return [] } 95 | 96 | guard let existingItem = item as? [String : Any], 97 | let passwordData = existingItem[kSecValueData as String] as? Data, 98 | let account = existingItem[kSecAttrAccount as String] as? String, 99 | let password = String(data: passwordData, encoding: String.Encoding.utf8) 100 | else { 101 | return [] 102 | } 103 | 104 | storedCreds.append(account) 105 | storedCreds.append(password) 106 | return storedCreds 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /jamfStatus/Globals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Globals.swift 3 | // jamfStatus 4 | // 5 | // Created by Leslie Helou on 7/11/20. 6 | // Copyright © 2020 Leslie Helou. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let httpSuccess = 200...299 12 | let refreshInterval: UInt32 = 25*60 // 25 minutes 13 | var useApiClient = 0 14 | 15 | struct AppInfo { 16 | static let dict = Bundle.main.infoDictionary! 17 | static let version = dict["CFBundleShortVersionString"] as! String 18 | static let build = dict["CFBundleVersion"] as! String 19 | static let name = dict["CFBundleExecutable"] as! String 20 | 21 | static let userAgentHeader = "\(String(describing: name.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!))/\(AppInfo.version)" 22 | 23 | static var bundlePath = Bundle.main.bundleURL 24 | static var iconFile = bundlePath.appendingPathComponent("/Resources/AppIcon.icns") 25 | } 26 | 27 | struct JamfNotification { 28 | static let key = ["TOMCAT_SSL_CERT_EXPIRED":"CERT_EXPIRED", 29 | "TOMCAT_SSL_CERT_WILL_EXPIRE":"CERT_WILL_EXPIRE", 30 | "SSO_CERT_EXPIRED":"CERT_EXPIRED", 31 | "SSO_CERT_WILL_EXPIRE":"CERT_WILL_EXPIRE", 32 | "GSX_CERT_EXPIRED":"CERT_EXPIRED", 33 | "GSX_CERT_WILL_EXPIRE":"CERT_WILL_EXPIRE", 34 | "INVALID_REFERENCES_SCRIPTS":"INVALID_REFERENCES_SCRIPTS", 35 | "INVALID_REFERENCES_EXT_ATTR":"INVALID_REFERENCES_EXT_ATTR", 36 | "INVALID_REFERENCES_POLICIES":"INVALID_REFERENCES_POLICIES", 37 | "POLICY_MANAGEMENT_ACCOUNT_PAYLOAD_SECURITY_MULTIPLE":"POLICY_MANAGEMENT_ACCOUNT_PAYLOAD_SECURITY_MULTIPLE", 38 | "POLICY_MANAGEMENT_ACCOUNT_PAYLOAD_SECURITY_SINGLE":"POLICY_MANAGEMENT_ACCOUNT_PAYLOAD_SECURITY_SINGLE", 39 | "USER_INITIATED_ENROLLMENT_MANAGEMENT_ACCOUNT_SECURITY_ISSUE":"USER_INITIATED_ENROLLMENT_MANAGEMENT_ACCOUNT_SECURITY_ISSUE", 40 | "VPP_ACCOUNT_WILL_EXPIRE":"VPP_ACCOUNT_WILL_EXPIRE", 41 | "VPP_ACCOUNT_EXPIRED":"VPP_ACCOUNT_EXPIRED", 42 | "VPP_TOKEN_REVOKED":"VPP_TOKEN_REVOKED", 43 | "DEP_INSTANCE_WILL_EXPIRE":"DEP_INSTANCE_WILL_EXPIRE", 44 | "DEP_INSTANCE_EXPIRED":"DEP_INSTANCE_EXPIRED", 45 | "PRESTAGE_IMAGING_SECURITY":"PRESTAGE_IMAGING_SECURITY", 46 | "PUSH_PROXY_CERT_EXPIRED":"CERT_EXPIRED", 47 | "PUSH_CERT_WILL_EXPIRE":"CERT_WILL_EXPIRE", 48 | "PUSH_CERT_EXPIRED":"CERT_EXPIRED", 49 | "FREQUENT_INVENTORY_COLLECTION_POLICY":"FREQUENT_INVENTORY_COLLECTION_POLICY", 50 | "PATCH_UPDATE":"PATCH_MANAGEMENT_UPDATE_AVAILABLE_DESCRIPTION", 51 | "PATCH_EXTENTION_ATTRIBUTE":"PATCH_EXTENSION_ATTRIBUTE_REQUIRES_ATTENTION_DESCRIPTION", 52 | "DEVICE_ENROLLMENT_PROGRAM_T_C_NOT_SIGNED":"DEVICE_ENROLLMENT_PROGRAM_GLOBAL_NOTIFICATION_T_C_NOT_SIGNED_DESCRIPTION", 53 | "APPLE_SCHOOL_MANAGER_T_C_NOT_SIGNED":"APPLE_SCHOOL_MANAGER_GLOBAL_NOTIFICATION_T_C_NOT_SIGNED_DESCRIPTION", 54 | "NO_LONGER_DEVICE_ASSIGNABLE":"NO_LONGER_DEVICE_ASSIGNABLE_DESCRIPTION", 55 | "HCL_ERROR":"HCL_ERROR_DESCRIPTION", 56 | "HCL_BIND_ERROR":"HCL_BIND_ERROR_DESCRIPTION", 57 | "COMPUTER_SECURITY_SSL_DISABLED":"COMPUTER_SECURITY_IS_CERTIFICATE_VALID_DESCRIPTION", 58 | "JIM_ERROR":"JIM_ERROR_DESCRIPTION", 59 | "EXCEEDED_LICENSE_COUNT":"EXCEEDED_LICENSE_COUNT_DESCRIPTION", 60 | "MII_INVENTORY_UPLOAD_FAILED_NOTIFICATION":"MII_INVENTORY_FAILED_PROBLEM_DESCRIPTION", 61 | "MII_HEARTBEAT_FAILED_NOTIFICATION":"MII_HEARTBEAT_FAILURE_PROBLEM_DESCRIPTION", 62 | "MII_UNATHORIZED_RESPONSE_NOTIFICATION":"MII_UNAUTHORIZED_PROBLEM_DESCRIPTION", 63 | "MDM_EXTERNAL_SIGNING_CERTIFICATE_EXPIRED":"MDM_EXTERNAL_SIGNING_CERTIFICATE_EXPIRED_DESCRIPTION", 64 | "MDM_EXTERNAL_SIGNING_CERTIFICATE_EXPIRING":"MDM_EXTERNAL_SIGNING_CERTIFICATE_GOING_TO_EXPIRE_DESCRIPTION", 65 | "MDM_EXTERNAL_SIGNING_CERTIFICATE_EXPIRING_TODAY":"MDM_EXTERNAL_SIGNING_CERTIFICATE_GOING_TO_EXPIRE_TODAY_DESCRIPTION", 66 | "INSECURE_LDAP":"INSECURE_LDAP_DESCRIPTION", 67 | "LDAP_CONNECTION_CHECK_THROUGH_JIM_SUCCESSFUL":"LDAP_CONNECTION_CHECK_THROUGH_JIM_SUCCESSFUL_DESCRIPTION", 68 | "LDAP_CONNECTION_CHECK_THROUGH_JIM_FAILED":"LDAP_CONNECTION_CHECK_THROUGH_JIM_FAILED_DESCRIPTION", 69 | "CLOUD_LDAP_CERT_EXPIRED":"CLOUD_LDAP_CERT_EXPIRED_DESCRIPTION", 70 | "CLOUD_LDAP_CERT_WILL_EXPIRE":"CLOUD_LDAP_CERT_WILL_EXPIRE_DESCRIPTION", 71 | "USER_MAID_MISMATCH_ERROR":"USER_MAID_MISMATCH_ERROR_DESCRIPTION", 72 | "USER_MAID_DUPLICATE_ERROR":"USER_MAID_DUPLICATE_ERROR_DESCRIPTION", 73 | "BUILT_IN_CA_EXPIRING":"BUILT_IN_CA_EXPIRING_DESCRIPTION", 74 | "BUILT_IN_CA_EXPIRED":"BUILT_IN_CA_EXPIRED_DESCRIPTION", 75 | "BUILT_IN_CA_RENEWAL_SUCCESS":"BUILT_IN_CA_RENEWAL_SUCCESS", 76 | "BUILT_IN_CA_RENEWAL_FAILED":"BUILT_IN_CA_RENEWAL_FAILED", 77 | "APNS_CERT_REVOKED":"APNS_CERT_REVOKED", 78 | "APNS_CONNECTION_FAILURE":"APNS_CONNECTION_FAILURE", 79 | "JAMF_PROTECT_UPDATE":"JAMF_PROTECT_UPDATE_AVAILABLE", 80 | "JAMF_CONNECT_UPDATE":"JAMF_CONNECT_UPDATE_AVAILABLE", 81 | "JAMF_CONNECT_MAJOR_UPDATE":"JAMF_CONNECT_MAJOR_UPDATE_AVAILABLE", 82 | "DEVICE_COMPLIANCE_CONNECTION_ERROR":"DEVICE_COMPLIANCE_CONNECTION_ERROR_DESCRIPTION", 83 | "CONDITIONAL_ACCESS_CONNECTION_ERROR":"CONDITIONAL_ACCESS_CONNECTION_ERROR_DESCRIPTION"] 84 | 85 | static let displayTitle = ["CERT_EXPIRED":"{{certType}} Certificate Expired", 86 | "CLOUD_LDAP_CERT_EXPIRED_DESCRIPTION":"Cloud Identity Provider Certificate Expired", 87 | "CERT_WILL_EXPIRE":"{{certType}} Certificate Expiring in {{days}} days", 88 | "CLOUD_LDAP_CERT_WILL_EXPIRE_DESCRIPTION":"Cloud Identity Provider Certificate Expiring in {{validDays}} Days", 89 | "INVALID_REFERENCES_SCRIPTS":"Scripts contain invalid references to /usr/sbin/jamf", 90 | "INVALID_REFERENCES_EXT_ATTR":"Extension attributes contain invalid references to /usr/sbin/jamf", 91 | "INVALID_REFERENCES_POLICIES":"Policies contain invalid references to /usr/sbin/jamf", 92 | "POLICY_MANAGEMENT_ACCOUNT_PAYLOAD_SECURITY_MULTIPLE":"Multiple policies have a management account password configuration that is not recommended", 93 | "POLICY_MANAGEMENT_ACCOUNT_PAYLOAD_SECURITY_SINGLE":"A policy has a management account password configuration that is not recommended", 94 | "USER_INITIATED_ENROLLMENT_MANAGEMENT_ACCOUNT_SECURITY_ISSUE":"A configured management account feature is not recommended", 95 | "VPP_ACCOUNT_WILL_EXPIRE":"Volume Purchasing Location {{name}} Expiring In {{days}} days", 96 | "VPP_ACCOUNT_EXPIRED":"Volume Purchasing Location {{name}} Expired", 97 | "VPP_TOKEN_REVOKED":"Volume Purchasing Server Token Revoked for the location {{name}}", 98 | "DEP_INSTANCE_WILL_EXPIRE":"Automated Device Enrollment Instance {{name}} Expiring In {{days}} days", 99 | "DEP_INSTANCE_EXPIRED":"Automated Device Enrollment Instance {{name}} Expired", 100 | "PRESTAGE_IMAGING_SECURITY":"PreStage imaging and Autorun imaging requires a Jamf Pro user account with the \"Use PreStage Imaging and Autorun Imaging\" privilege.", 101 | "FREQUENT_INVENTORY_COLLECTION_POLICY":"{{name}} updates inventory on all computers at recurring check-in. This may cause stability issues.", 102 | "PATCH_MANAGEMENT_UPDATE_AVAILABLE_DESCRIPTION":"{{softwareTitleName}} v{{latestVersion}} is available", 103 | "PATCH_EXTENSION_ATTRIBUTE_REQUIRES_ATTENTION_DESCRIPTION":"{{softwareTitleName}} has an extension attribute requiring attention", 104 | "DEVICE_ENROLLMENT_PROGRAM_GLOBAL_NOTIFICATION_T_C_NOT_SIGNED_DESCRIPTION":"Device Enrollment instance out of date with Apple’s Terms and Conditions.", 105 | "APPLE_SCHOOL_MANAGER_GLOBAL_NOTIFICATION_T_C_NOT_SIGNED_DESCRIPTION":"Sync failed. The associated Automated Device Enrollment instance is out of date with Apple’s Terms and Conditions. The updated agreement must be accepted to sync information. See your Apple School Manager instance to accept the updated agreement.", 106 | "NO_LONGER_DEVICE_ASSIGNABLE_DESCRIPTION":"{{appName}} is no longer available for device-assigned managed distribution and any device assignments have been disabled for this app.", 107 | "HCL_ERROR_DESCRIPTION":"There was an error configuring {{hclName}} Healthcare Listener on {{jsamName}}", 108 | "HCL_BIND_ERROR_DESCRIPTION":"Port number of {{hclName}} Healthcare Listener is invalid on {{jsamName}}", 109 | "COMPUTER_SECURITY_IS_CERTIFICATE_VALID_DESCRIPTION":"Verification of SSL certificates is disabled", 110 | "JIM_ERROR_DESCRIPTION":"{{jsamName}} Infrastructure Manager instance has not checked in with Jamf Pro.", 111 | "EXCEEDED_LICENSE_COUNT_DESCRIPTION":"Device Count Exceeded", 112 | "MII_INVENTORY_FAILED_PROBLEM_DESCRIPTION":"Unable to send inventory information to Microsoft Intune", 113 | "MII_HEARTBEAT_FAILURE_PROBLEM_DESCRIPTION":"Unable to connect to Microsoft Intune", 114 | "MII_UNAUTHORIZED_PROBLEM_DESCRIPTION":"Integration disabled: \"Not Authorized\" response from Microsoft Intune", 115 | "MDM_EXTERNAL_SIGNING_CERTIFICATE_EXPIRED_DESCRIPTION":"Third-Party Signing Certificate Expired", 116 | "MDM_EXTERNAL_SIGNING_CERTIFICATE_GOING_TO_EXPIRE_DESCRIPTION":"Third-Party Signing Certificate Expiring in {{days}} Days", 117 | "MDM_EXTERNAL_SIGNING_CERTIFICATE_GOING_TO_EXPIRE_TODAY_DESCRIPTION":"Third-Party Signing Certificate Expiring Today", 118 | "INSECURE_LDAP_DESCRIPTION":"LDAP Server Configuration Error", 119 | "LDAP_CONNECTION_CHECK_THROUGH_JIM_SUCCESSFUL_DESCRIPTION":"Verification status for the {{serverName}} LDAP Proxy Server Connection: Success", 120 | "LDAP_CONNECTION_CHECK_THROUGH_JIM_FAILED_DESCRIPTION":"Verification Status for the {{serverName}} LDAP Proxy Server Connection: Failed", 121 | "USER_MAID_MISMATCH_ERROR_DESCRIPTION":"{{userName}}'s Managed Apple ID does not match the Managed Apple ID reported in Apple School Manager.", 122 | "USER_MAID_DUPLICATE_ERROR_DESCRIPTION":"The {{maid}} Managed Apple ID is used by multiple users.", 123 | "BUILT_IN_CA_EXPIRING_DESCRIPTION":"The Jamf Pro JSS Built-in Certificate Authority is set to expire soon.", 124 | "BUILT_IN_CA_EXPIRED_DESCRIPTION":"The Jamf Pro JSS Built-in Certificate Authority is expired.", 125 | "BUILT_IN_CA_RENEWAL_SUCCESS":"The Jamf Pro JSS Built-in Certificate Authority has been successfully renewed.", 126 | "BUILT_IN_CA_RENEWAL_FAILED":"The Jamf Pro JSS Built-in Certificate Authority renewal process failed.", 127 | "APNS_CERT_REVOKED":"Unable to connect to APNs because the push certificate was revoked. Navigate to Global Management > Push Certificates and renew the certificate or generate a new one.", 128 | "APNS_CONNECTION_FAILURE":"Connection to the APN Service Failed. Could not connect to the APNs server. The server is down or network is unreachable.", 129 | "JAMF_PROTECT_UPDATE_AVAILABLE":"Jamf Protect {{latestVersion}} Now Available", 130 | "JAMF_CONNECT_UPDATE_AVAILABLE":"Jamf Connect {{latestVersion}} Now Available", 131 | "JAMF_CONNECT_MAJOR_UPDATE_AVAILABLE":"Major Update for Jamf Connect Now Available (Jamf Connect {{latestVersion}})", 132 | "DEVICE_COMPLIANCE_CONNECTION_ERROR_DESCRIPTION":"Device Compliance Connection Interrupted", 133 | "CONDITIONAL_ACCESS_CONNECTION_ERROR_DESCRIPTION":"Conditional Access Connection Interrupted"] 134 | 135 | static let humanReadable = ["TOMCAT_SSL_CERT_EXPIRED":"Tomcat SSL", 136 | "TOMCAT_SSL_CERT_WILL_EXPIRE":"Tomcat SSL", 137 | "PUSH_PROXY_CERT_EXPIRED":"Push Proxy", 138 | "PUSH_CERT_WILL_EXPIRE":"Push Notification", 139 | "PUSH_CERT_EXPIRED":"Push Notification"] 140 | } 141 | 142 | struct JamfProServer { 143 | static var accessToken = "" 144 | static var authCreds = "" 145 | static var authExpires = 30.0 146 | static var authType = "Basic" 147 | static var base64Creds = "" 148 | static var build = "" 149 | static var currentCred = "" 150 | static let maxThreads = 2 151 | static var majorVersion = 0 152 | static var minorVersion = 0 153 | static var password = "" 154 | static var patchVersion = 0 155 | static var tokenCreated = Date() 156 | static var url = "" 157 | static var username = "" 158 | static var validToken = false 159 | static var version = "" 160 | } 161 | 162 | struct Log { 163 | static var path: String? = (NSHomeDirectory() + "/Library/Logs/jamfStatus/") 164 | static var file = "jamfStatus.log" 165 | static var maxFiles = 10 166 | static var maxSize = 500000 // 5MB 167 | } 168 | 169 | struct Preferences { 170 | static var hideMenubarIcon: Bool? = false 171 | static var hideUntilStatusChange: Bool? = true 172 | static var launchAgent: Bool? = false 173 | static var pollingInterval: Int? = 300 174 | static var baseUrl: String? = "https://status.jamf.com" 175 | static var jamfServerUrl = "" 176 | static var username = "" 177 | static var password = "" 178 | static var menuIconStyle = "color" 179 | } 180 | 181 | 182 | struct token { 183 | static var refreshInterval:UInt32 = 10*60 // 10 minutes 184 | static var sourceServer = "" 185 | static var sourceExpires = "" 186 | static var startTime = Date() 187 | static var isValid = false 188 | } 189 | 190 | public func timeDiff(startTime: Date) -> (Int, Int, Int, Double) { 191 | let endTime = Date() 192 | // let components = Calendar.current.dateComponents([.second, .nanosecond], from: startTime, to: endTime) 193 | // let timeDifference = Double(components.second!) + Double(components.nanosecond!)/1000000000 194 | // WriteToLog().message(stringOfText: "[ViewController.download] time difference: \(timeDifference) seconds") 195 | let components = Calendar.current.dateComponents([ 196 | .hour, .minute, .second, .nanosecond], from: startTime, to: endTime) 197 | var diffInSeconds = Double(components.hour!)*3600 + Double(components.minute!)*60 + Double(components.second!) + Double(components.nanosecond!)/1000000000 198 | diffInSeconds = Double(round(diffInSeconds * 1000) / 1000) 199 | // let timeDifference = Int(components.second!) //+ Double(components.nanosecond!)/1000000000 200 | // let (h,r) = timeDifference.quotientAndRemainder(dividingBy: 3600) 201 | // let (m,s) = r.quotientAndRemainder(dividingBy: 60) 202 | // WriteToLog().message(stringOfText: "[ViewController.download] download time: \(h):\(m):\(s) (h:m:s)") 203 | return (Int(components.hour!), Int(components.minute!), Int(components.second!), diffInSeconds) 204 | // return (h, m, s) 205 | } 206 | 207 | extension String { 208 | var fqdnFromUrl: String { 209 | get { 210 | var fqdn = "" 211 | let nameArray = self.components(separatedBy: "/") 212 | if nameArray.count > 2 { 213 | fqdn = nameArray[2] 214 | } else { 215 | fqdn = self 216 | } 217 | if fqdn.contains(":") { 218 | let fqdnArray = fqdn.components(separatedBy: ":") 219 | fqdn = fqdnArray[0] 220 | } 221 | let urlRegex = try! NSRegularExpression(pattern: "/?failover(.*?)", options:.caseInsensitive) 222 | fqdn = urlRegex.stringByReplacingMatches(in: fqdn, options: [], range: NSRange(0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | Copyright © 2017 Leslie Helou. All rights reserved. 36 | NSMainNibFile 37 | MainMenu 38 | NSPrincipalClass 39 | NSApplication 40 | 41 | 42 | -------------------------------------------------------------------------------- /jamfStatus/NotificationAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationAlerts.swift 3 | // jamfStatus 4 | // 5 | // Created by Leslie Helou on 9/2/19. 6 | // Copyright © 2019 Leslie Helou. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class NotificationAlert { 12 | var title: String 13 | var params: Dictionary 14 | 15 | init(title: String) { 16 | self.title = title 17 | self.params = [:] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /jamfStatus/StatusMenuController.swift: -------------------------------------------------------------------------------- 1 | //StatusMenuController.swift 2 | //Author: Leslie Helou 3 | //Copyright 2017 Jamf Professional Services 4 | 5 | 6 | import AppKit 7 | import Cocoa 8 | import Foundation 9 | 10 | class StatusMenuController: NSObject, URLSessionDelegate, URLSessionTaskDelegate { 11 | 12 | let defaults = UserDefaults.standard 13 | let prefs = Preferences.self 14 | 15 | @IBOutlet weak var alert_window: NSPanel! 16 | @IBOutlet weak var cloudStatusMenu: NSMenu! 17 | @IBOutlet weak var notifications_MenuItem: NSMenuItem! 18 | @IBOutlet weak var status_Toolbar: NSToolbar! 19 | 20 | @IBOutlet weak var alert_TextView: NSTextField! 21 | @IBOutlet weak var alert_TextFieldCell: NSTextFieldCell! 22 | 23 | @IBOutlet weak var alert_ImageCell: NSImageCell! 24 | 25 | let fileManager = FileManager.default 26 | let cloudStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 27 | // let statusURL = "https://status.jamf.com" 28 | var statusPageString = "" 29 | var dataString = "" 30 | var theResult = "" 31 | var displayedStatus = "" 32 | var iconName = "" 33 | var icon = NSImage(named: "cloudStatus-red") 34 | 35 | @IBOutlet weak var alertWindowPref_Button: NSButton! 36 | var alert_header = "" 37 | var alert_message = "" 38 | var serviceCount = 0 39 | 40 | var alert_image_green = NSImage(named: "greenCloud") 41 | var alert_image_yellow = NSImage(named: "yellowCloud") 42 | var alert_image_red = NSImage(named: "redCloud") 43 | var current_alert_pref = "green" 44 | var prevState = "cloudStatus-green" 45 | 46 | // Settings vars 47 | let myBundlePath = Bundle.main.bundlePath 48 | let SettingsPlistPath = NSHomeDirectory()+"/Library/Preferences/com.jamf.jamfstatus.plist" 49 | var format = PropertyListSerialization.PropertyListFormat.xml //format of the property list file 50 | 51 | var settingsPlistData:[String:Any] = [:] 52 | 53 | var affectedServices = "" 54 | 55 | @IBOutlet weak var alert_ImageView: NSImageView! 56 | 57 | @IBAction func quitCloudStatus(_ sender: NSMenuItem) { 58 | NSApplication.shared.terminate(self) 59 | } 60 | 61 | override func awakeFromNib() { 62 | 63 | useApiClient = defaults.integer(forKey: "useApiClient") 64 | 65 | if (defaults.object(forKey:"pollingInterval") as? Int == nil) { 66 | defaults.set(300, forKey: "pollingInterval") 67 | prefs.pollingInterval = 300 68 | } else { 69 | prefs.pollingInterval = defaults.object(forKey:"pollingInterval") as? Int 70 | } 71 | 72 | if (defaults.object(forKey:"hideUntilStatusChange") as? Bool == nil){ 73 | defaults.set(true, forKey: "hideUntilStatusChange") 74 | prefs.hideUntilStatusChange = true 75 | } else { 76 | prefs.hideUntilStatusChange = defaults.bool(forKey:"hideUntilStatusChange") 77 | } 78 | 79 | if (defaults.object(forKey:"hideMenubarIcon") as? Bool == nil) { 80 | defaults.set(false, forKey: "hideMenubarIcon") 81 | prefs.hideMenubarIcon = false 82 | } else { 83 | prefs.hideMenubarIcon = defaults.bool(forKey: "hideMenubarIcon") 84 | } 85 | 86 | if (defaults.object(forKey:"launchAgent") as? Bool == nil){ 87 | defaults.set(false, forKey: "launchAgent") 88 | prefs.launchAgent = false 89 | } else { 90 | prefs.launchAgent = defaults.bool(forKey:"launchAgent") 91 | } 92 | 93 | if (defaults.object(forKey:"baseUrl") as? String == nil) { 94 | defaults.set("https://status.jamf.com", forKey: "baseUrl") 95 | prefs.baseUrl = "https://status.jamf.com" 96 | } else { 97 | prefs.baseUrl = defaults.string(forKey:"baseUrl") 98 | } 99 | 100 | // set menu icon style 101 | prefs.menuIconStyle = defaults.string(forKey: "menuIconStyle") ?? prefs.menuIconStyle 102 | 103 | if (defaults.object(forKey:"jamfServerUrl") as? String == nil) { 104 | defaults.set("", forKey: "jamfServerUrl") 105 | JamfProServer.url = "" 106 | } else { 107 | JamfProServer.url = defaults.string(forKey:"jamfServerUrl")! 108 | let credentialsArray = Credentials().itemLookup(service: JamfProServer.url.fqdnFromUrl) 109 | if credentialsArray.count == 2 { 110 | JamfProServer.username = credentialsArray[0] 111 | JamfProServer.password = credentialsArray[1] 112 | } else { 113 | JamfProServer.password = "" 114 | } 115 | } 116 | // 117 | // defaults.synchronize() 118 | // 119 | // defaults.synchronize() 120 | 121 | icon = NSImage(named: iconName) 122 | // icon?.isTemplate = true // best for dark mode? 123 | // cloudStatusItem.image = icon 124 | cloudStatusItem.button?.image = icon 125 | cloudStatusItem.menu = cloudStatusMenu 126 | 127 | JamfProServer.base64Creds = ("\(JamfProServer.username):\(JamfProServer.password)".data(using: .utf8)?.base64EncodedString())! 128 | // JamfPro().getVersion(jpURL: Preferences.jamfServerUrl, base64Creds: JamfProServer.base64Creds) { [self] 129 | // (result: String) in 130 | // move UapiCall fn to JamfPro 131 | // don't check notifications if creds/server are not valid 132 | monitor() 133 | // } 134 | 135 | } 136 | 137 | func monitor() { 138 | DispatchQueue.global(qos: .background).async { [self] in 139 | while true { 140 | 141 | // check site server - start 142 | WriteToLog().message(stringOfText: ["checking server: \(Preferences.jamfServerUrl)"]) 143 | UapiCall().get(endpoint: "v1/notifications") { [self] 144 | (notificationAlerts: [[String: Any]]) in 145 | 146 | if notificationAlerts.count == 0 { 147 | notifications_MenuItem.isHidden = true 148 | } else { 149 | notifications_MenuItem.title = "Notifications (\(notificationAlerts.count))" 150 | notifications_MenuItem.isHidden = false 151 | let subMenu = NSMenu() 152 | var displayTitleKey = "" 153 | var displayTitle = "" 154 | cloudStatusMenu.setSubmenu(subMenu, for: notifications_MenuItem) 155 | for alert in notificationAlerts { 156 | // print("notification alert: \(alert)") 157 | let alertTitle = alert["type"]! as! String 158 | displayTitleKey = JamfNotification.key[alertTitle] ?? "Unknown" 159 | displayTitle = JamfNotification.displayTitle[displayTitleKey] ?? "Unknown" 160 | switch displayTitleKey { 161 | case "CERT_WILL_EXPIRE", "CERT_EXPIRED": 162 | displayTitle = displayTitle.replacingOccurrences(of: "{{certType}}", with: "\(String(describing: JamfNotification.humanReadable[alertTitle]!))") 163 | default: 164 | break 165 | } 166 | let paramDict = alert["params"] as! [String: Any] 167 | for (key,value) in paramDict { 168 | // print("key: \(key) value: \(value)") 169 | displayTitle = displayTitle.replacingOccurrences(of: "{{\(key)}}", with: "\(value)") 170 | } 171 | subMenu.addItem(NSMenuItem(title: "\(displayTitle)", action: #selector(AppDelegate.notificationsAction(_:)), keyEquivalent: "")) 172 | subMenu.item(withTitle: "\(displayTitle)")?.identifier = NSUserInterfaceItemIdentifier.init(rawValue: displayTitleKey) 173 | } 174 | } 175 | 176 | } 177 | // check site server - end 178 | 179 | // print("checking status") 180 | prefs.pollingInterval = defaults.integer(forKey: "pollingInterval") 181 | prefs.hideMenubarIcon = defaults.bool(forKey: "hideMenubarIcon") 182 | getStatus2() { 183 | (result: String) in 184 | 185 | DispatchQueue.main.async { [self] in 186 | iconName = result 187 | // AppDlg.hideIcon ? (icon = NSImage.init(named: NSImage.Name(rawValue: "minimizedIcon"))):(icon = NSImage.init(named: NSImage.Name(rawValue: iconName))) 188 | // print("iconName: \(result)") 189 | // print("hidemenubar is \(prefs.hideMenubarIcon!)") 190 | prefs.hideMenubarIcon! ? (icon = NSImage.init(named: "minimizedIcon")):(icon = NSImage.init(named: iconName)) 191 | 192 | // cloudStatusItem.image = icon 193 | cloudStatusItem.button?.image = icon 194 | } 195 | } 196 | sleep(UInt32(Int(prefs.pollingInterval!))) 197 | } 198 | } 199 | } 200 | 201 | @IBAction func alertWindowPref_Action(_ sender: NSButton) { 202 | 203 | if alertWindowPref_Button.state.rawValue == 0 { 204 | defaults.set(false, forKey: "hideUntilStatusChange") 205 | } else { 206 | defaults.set(false, forKey: "hideUntilStatusChange") 207 | } 208 | } 209 | 210 | func displayAlert(currentState: String) { 211 | var alertHeight = 0 212 | DispatchQueue.main.async { 213 | // adjust font size so that alert message fits in text box. 214 | alertHeight = 99 215 | // print("count: \(alert_message.count)") 216 | let alertLines = self.alert_message.split(separator: "\n") 217 | // print("alerts: \(alertLines)") 218 | for i in 1.. 2 ? (alertHeight = 99 + 18*(self.serviceCount-2)):(alertHeight = 99) 229 | self.alert_window.setContentSize(NSSize(width: 398, height:alertHeight)) 230 | if self.alert_message.count > 55 { 231 | self.alert_TextView.font = NSFont(name: "Arial", size: 12.0) 232 | } else { 233 | self.alert_TextView.font = NSFont(name: "Arial", size: 18.0) 234 | } 235 | if (self.defaults.bool(forKey:"hideUntilStatusChange")) { 236 | self.alertWindowPref_Button.state = NSControl.StateValue.on 237 | } else { 238 | self.alertWindowPref_Button.state = NSControl.StateValue.off 239 | } 240 | if self.prevState != currentState { 241 | DispatchQueue.main.async { 242 | self.refreshAlert() 243 | } 244 | } else { 245 | if !(self.defaults.bool(forKey:"hideUntilStatusChange")) && self.prevState != "cloudStatus-green" { 246 | DispatchQueue.main.async { 247 | self.refreshAlert() 248 | } 249 | } 250 | } 251 | self.prevState = currentState 252 | } // DispatchQueue.main.async - end 253 | } 254 | 255 | @IBAction func showLogs_Action(_ sender: Any) { 256 | if fileManager.fileExists(atPath: Log.path! + Log.file) { 257 | // NSWorkspace.shared.openFile(Log.path! + Log.file) 258 | NSWorkspace.shared.open(URL(fileURLWithPath: "\(Log.path!)\(Log.file)")) 259 | } 260 | } 261 | 262 | 263 | func refreshAlert() { 264 | self.alert_window.title = "\(alert_header)" 265 | self.alert_TextFieldCell.stringValue = self.alert_message 266 | WriteToLog().message(stringOfText: [alert_header, self.alert_message]) 267 | if self.alert_window.isVisible { 268 | self.alert_window.setIsVisible(false) 269 | sleep(1) 270 | } 271 | self.alert_window.setIsVisible(true) 272 | } 273 | 274 | func getStatus2(completion: @escaping (_ result: String) -> Void) { 275 | var localResult = "" 276 | 277 | var operationalArray = [String]() 278 | var warningArray = [String]() 279 | var criticalArray = [String]() 280 | 281 | // clear current arrays 282 | operationalArray.removeAll() 283 | warningArray.removeAll() 284 | criticalArray.removeAll() 285 | affectedServices = "" 286 | URLCache.shared.removeAllCachedResponses() 287 | 288 | WriteToLog().message(stringOfText: ["checking Jamf Cloud"]) 289 | // JSON parsing - start 290 | let apiStatusUrl = "\(String(describing: prefs.baseUrl!))/api/v2/components.json" 291 | // url to test app - need to set up your own 292 | // need to create the folder /jamfStatus and populate the page: components.json 293 | // let apiStatusUrl = "http://your.jamfpro.server/jamfStatus/components.json" 294 | 295 | URLCache.shared.removeAllCachedResponses() 296 | let encodedURL = NSURL(string: apiStatusUrl) 297 | let request = NSMutableURLRequest(url: encodedURL! as URL) 298 | request.httpMethod = "GET" 299 | let configuration = URLSessionConfiguration.default 300 | 301 | configuration.httpAdditionalHeaders = ["Accept" : "application/json"] 302 | let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) 303 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 304 | (data, response, error) -> Void in 305 | if (response as? HTTPURLResponse) != nil { 306 | do { 307 | if let data = data, 308 | let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any], 309 | let cloudServices = json["components"] as? [[String: Any]] { 310 | // print("cloudServices: \(cloudServices)") 311 | for cloudService in cloudServices { 312 | if let name = cloudService["name"] as? String, 313 | let status = cloudService["status"] as? String { 314 | switch status { 315 | case "degraded_performance", "partial_outage": 316 | if status == "partial_outage" { 317 | self.displayedStatus = "Partial Outage" 318 | } else { 319 | self.displayedStatus = "Degraded Performance" 320 | } 321 | warningArray.append(name + ": " + self.displayedStatus) 322 | case "major_outage": 323 | self.displayedStatus = "Major Outage" 324 | criticalArray.append(name + ": " + self.displayedStatus) 325 | default: 326 | self.displayedStatus = "Operational" 327 | operationalArray.append(name + ": " + self.displayedStatus) 328 | } 329 | } 330 | } 331 | } 332 | } catch { 333 | print("Error deserializing JSON: \(error)") 334 | } // do - end 335 | } // if let httpResponse - end 336 | if criticalArray.count > 0 { 337 | self.alert_header = "Jamf Cloud Critical Issue Alert" 338 | localResult = "cloudStatus-red" 339 | for service in criticalArray { 340 | self.affectedServices.append(" \(service)\n") 341 | } 342 | self.alert_ImageView.image = self.alert_image_red 343 | self.alert_message = "Please be aware there is a major issue that may affect your Jamf Cloud instance.\n\(self.affectedServices)" 344 | self.serviceCount = criticalArray.count 345 | self.displayAlert(currentState: localResult) 346 | } else if warningArray.count > 0 { 347 | self.alert_header = "Jamf Cloud Minor Issue Alert" 348 | localResult = "cloudStatus-yellow" 349 | for service in warningArray { 350 | self.affectedServices.append(" \(service)\n") 351 | } 352 | self.alert_ImageView.image = self.alert_image_yellow 353 | self.alert_message = "Please be aware there is a minor issue that may affect your Jamf Cloud instance.\n\(self.affectedServices)" 354 | self.serviceCount = warningArray.count 355 | self.displayAlert(currentState: localResult) 356 | } else if operationalArray.count > 0 { 357 | self.alert_header = "Notice" 358 | localResult = "cloudStatus-green" 359 | // localResult = "cloudMajor-18" 360 | self.affectedServices = "" 361 | self.alert_ImageView.image = self.alert_image_green 362 | self.alert_message = "\nJamf Cloud: All systems go." 363 | self.serviceCount = 0 364 | self.displayAlert(currentState: localResult) 365 | } 366 | 367 | // print("operationalArray: \(operationalArray)\n") 368 | // print("warningArray: \(warningArray)\n") 369 | // print("criticalArray: \(criticalArray)\n") 370 | 371 | if (localResult != "cloudStatus-green") && (localResult != "cloudStatus-yellow") && (localResult != "cloudStatus-red") { 372 | self.iconName = "minimizedIcon" 373 | } else { 374 | if (self.prefs.menuIconStyle == "color") || (localResult == "cloudStatus-green") { 375 | // display icon with color 376 | self.iconName = localResult 377 | } else { 378 | // display icon with slash 379 | self.iconName = (localResult == "cloudStatus-yellow") ? "cloudStatus-yellow1":"cloudStatus-red1" 380 | localResult = self.iconName 381 | } 382 | } 383 | 384 | completion(localResult) 385 | }) // let task - end 386 | task.resume() 387 | // print("") 388 | // JSON parsing - end 389 | } 390 | 391 | func readSettings() -> NSMutableDictionary? { 392 | if fileManager.fileExists(atPath: SettingsPlistPath) { 393 | guard let dict = NSMutableDictionary(contentsOfFile: SettingsPlistPath) else { return .none } 394 | return dict 395 | } else { 396 | return .none 397 | } 398 | } 399 | 400 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping( URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 401 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 402 | } 403 | 404 | } 405 | -------------------------------------------------------------------------------- /jamfStatus/TokenDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenDelegate.swift 3 | // jamfStatus 4 | // 5 | 6 | import Cocoa 7 | 8 | class TokenDelegate: NSObject, URLSessionDelegate { 9 | 10 | let userDefaults = UserDefaults.standard 11 | var components = DateComponents() 12 | 13 | func getToken(serverUrl: String, base64creds: String, completion: @escaping (_ authResult: (Int,String)) -> Void) { 14 | 15 | URLCache.shared.removeAllCachedResponses() 16 | 17 | var tokenUrlString = "\(serverUrl)/api/v1/auth/token" 18 | 19 | var apiClient = false 20 | if useApiClient == 1 { 21 | tokenUrlString = "\(serverUrl)/api/oauth/token" 22 | apiClient = true 23 | } 24 | 25 | tokenUrlString = tokenUrlString.replacingOccurrences(of: "//api", with: "/api") 26 | // print("[getToken] tokenUrlString: \(tokenUrlString)") 27 | 28 | let tokenUrl = URL(string: "\(tokenUrlString)") 29 | guard let _ = URL(string: "\(tokenUrlString)") else { 30 | print("problem constructing the URL from \(tokenUrlString)") 31 | writeToLog.message(stringOfText: ["[getToken] problem constructing the URL from \(tokenUrlString)"]) 32 | completion((500, "failed")) 33 | return 34 | } 35 | // print("[getToken] tokenUrl: \(tokenUrl!)") 36 | let configuration = URLSessionConfiguration.ephemeral 37 | var request = URLRequest(url: tokenUrl!) 38 | request.httpMethod = "POST" 39 | 40 | let (_, _, _, tokenAgeInSeconds) = timeDiff(startTime: JamfProServer.tokenCreated) 41 | 42 | if !( JamfProServer.validToken && tokenAgeInSeconds < (JamfProServer.authExpires)*60 ) || (JamfProServer.currentCred != base64creds) { 43 | writeToLog.message(stringOfText: ["[getToken] tokenAgeInSeconds: \(tokenAgeInSeconds)"]) 44 | writeToLog.message(stringOfText: ["[getToken] Attempting to retrieve token from \(String(describing: tokenUrl))"]) 45 | 46 | if apiClient { 47 | let clientId = JamfProServer.username 48 | let secret = JamfProServer.password 49 | let clientString = "grant_type=client_credentials&client_id=\(String(describing: clientId))&client_secret=\(String(describing: secret))" 50 | // print("[getToken] \(whichServer) clientString: \(clientString)") 51 | 52 | let requestData = clientString.data(using: .utf8) 53 | request.httpBody = requestData 54 | configuration.httpAdditionalHeaders = ["Content-Type" : "application/x-www-form-urlencoded", "Accept" : "application/json", "User-Agent" : AppInfo.userAgentHeader] 55 | JamfProServer.currentCred = clientString 56 | } else { 57 | configuration.httpAdditionalHeaders = ["Authorization" : "Basic \(base64creds)", "Content-Type" : "application/json", "Accept" : "application/json", "User-Agent" : AppInfo.userAgentHeader] 58 | JamfProServer.currentCred = base64creds 59 | } 60 | 61 | let session = Foundation.URLSession(configuration: configuration, delegate: self as URLSessionDelegate, delegateQueue: OperationQueue.main) 62 | let task = session.dataTask(with: request as URLRequest, completionHandler: { [self] 63 | (data, response, error) -> Void in 64 | session.finishTasksAndInvalidate() 65 | if let httpResponse = response as? HTTPURLResponse { 66 | if httpSuccess.contains(httpResponse.statusCode) { 67 | if let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) { 68 | if let endpointJSON = json as? [String: Any] { 69 | JamfProServer.accessToken = apiClient ? (endpointJSON["access_token"] as? String ?? "")!:(endpointJSON["token"] as? String ?? "")! 70 | 71 | JamfProServer.base64Creds = base64creds 72 | if apiClient { 73 | JamfProServer.authExpires = 30 //(endpointJSON["expires_in"] as? String ?? "")! 74 | } else { 75 | JamfProServer.authExpires = (endpointJSON["expires"] as? Double ?? 30)! 76 | } 77 | JamfProServer.tokenCreated = Date() 78 | JamfProServer.validToken = true 79 | JamfProServer.authType = "Bearer" 80 | 81 | // print("[JamfPro] result of token request: \(endpointJSON)") 82 | writeToLog.message(stringOfText: ["[getToken] new token created for \(serverUrl)"]) 83 | 84 | if JamfProServer.version == "" { 85 | // get Jamf Pro version - start 86 | getVersion(serverUrl: serverUrl, endpoint: "jamf-pro-version", apiData: [:], id: "", token: JamfProServer.accessToken, method: "GET") { 87 | (result: [String:Any]) in 88 | let versionString = result["version"] as! String 89 | 90 | if versionString != "" { 91 | writeToLog.message(stringOfText: ["[JamfPro.getVersion] Jamf Pro Version: \(versionString)"]) 92 | JamfProServer.version = versionString 93 | let tmpArray = versionString.components(separatedBy: ".") 94 | if tmpArray.count > 2 { 95 | for i in 0...2 { 96 | switch i { 97 | case 0: 98 | JamfProServer.majorVersion = Int(tmpArray[i]) ?? 0 99 | case 1: 100 | JamfProServer.minorVersion = Int(tmpArray[i]) ?? 0 101 | case 2: 102 | let tmp = tmpArray[i].components(separatedBy: "-") 103 | JamfProServer.patchVersion = Int(tmp[0]) ?? 0 104 | if tmp.count > 1 { 105 | JamfProServer.build = tmp[1] 106 | } 107 | default: 108 | break 109 | } 110 | } 111 | if ( JamfProServer.majorVersion > 10 || (JamfProServer.majorVersion > 9 && JamfProServer.minorVersion > 34) ) { 112 | JamfProServer.authType = "Bearer" 113 | writeToLog.message(stringOfText: ["[JamfPro.getVersion] \(serverUrl) set to use OAuth"]) 114 | 115 | } else { 116 | JamfProServer.authType = "Basic" 117 | JamfProServer.accessToken = base64creds 118 | writeToLog.message(stringOfText: ["[JamfPro.getVersion] \(serverUrl) set to use Basic"]) 119 | } 120 | completion((200, "success")) 121 | return 122 | } 123 | } 124 | } 125 | // get Jamf Pro version - end 126 | } else { 127 | completion((200, "success")) 128 | return 129 | } 130 | } else { // if let endpointJSON error 131 | writeToLog.message(stringOfText: ["[getToken] JSON error.\n\(String(describing: json))"]) 132 | JamfProServer.validToken = false 133 | completion((httpResponse.statusCode, "failed")) 134 | return 135 | } 136 | } else { 137 | // server down? 138 | writeToLog.message(stringOfText: ["[TokenDelegate.getToken] Failed to get an expected response from \(String(describing: serverUrl)). Status Code: \(httpResponse.statusCode)"]) 139 | JamfProServer.validToken = false 140 | completion((httpResponse.statusCode, "failed")) 141 | return 142 | } 143 | } else { // if httpResponse.statusCode <200 or >299 144 | writeToLog.message(stringOfText: ["[getToken] Failed to authenticate to \(serverUrl). Response error: \(httpResponse.statusCode)"]) 145 | JamfProServer.validToken = false 146 | completion((httpResponse.statusCode, "failed")) 147 | return 148 | } 149 | } else { 150 | writeToLog.message(stringOfText: ["[getToken] token response error from \(serverUrl). Verify url and port"]) 151 | JamfProServer.validToken = false 152 | completion((0, "failed")) 153 | return 154 | } 155 | }) 156 | task.resume() 157 | } else { 158 | // writeToLog.message(stringOfText: "[getToken] Use existing token from \(String(describing: tokenUrl))") 159 | completion((200, "success")) 160 | return 161 | } 162 | } 163 | 164 | func getVersion(serverUrl: String, endpoint: String, apiData: [String:Any], id: String, token: String, method: String, completion: @escaping (_ returnedJSON: [String: Any]) -> Void) { 165 | 166 | if method.lowercased() == "skip" { 167 | // if LogLevel.debug { writeToLog.message(stringOfText: "[Jpapi.action] skipping \(endpoint) endpoint with id \(id).") } 168 | let JPAPI_result = (endpoint == "auth/invalidate-token") ? "no valid token":"failed" 169 | completion(["JPAPI_result":JPAPI_result, "JPAPI_response":000]) 170 | return 171 | } 172 | 173 | URLCache.shared.removeAllCachedResponses() 174 | var path = "" 175 | 176 | switch endpoint { 177 | case "buildings", "csa/token", "icon", "jamf-pro-version", "auth/invalidate-token": 178 | path = "v1/\(endpoint)" 179 | default: 180 | path = "v2/\(endpoint)" 181 | } 182 | 183 | var urlString = "\(serverUrl)/api/\(path)" 184 | urlString = urlString.replacingOccurrences(of: "//api", with: "/api") 185 | if id != "" && id != "0" { 186 | urlString = urlString + "/\(id)" 187 | } 188 | // print("[Jpapi] urlString: \(urlString)") 189 | 190 | let url = URL(string: "\(urlString)") 191 | let configuration = URLSessionConfiguration.default 192 | var request = URLRequest(url: url!) 193 | switch method.lowercased() { 194 | case "get": 195 | request.httpMethod = "GET" 196 | case "create", "post": 197 | request.httpMethod = "POST" 198 | default: 199 | request.httpMethod = "PUT" 200 | } 201 | 202 | if apiData.count > 0 { 203 | do { 204 | request.httpBody = try JSONSerialization.data(withJSONObject: apiData, options: .prettyPrinted) 205 | } catch let error { 206 | print(error.localizedDescription) 207 | } 208 | } 209 | 210 | writeToLog.message(stringOfText: ["[TokenDelegate.getVersion] Attempting \(method) on \(urlString)."]) 211 | // print("[Jpapi.action] Attempting \(method) on \(urlString).") 212 | 213 | configuration.httpAdditionalHeaders = ["Authorization" : "Bearer \(token)", "Content-Type" : "application/json", "Accept" : "application/json", "User-Agent" : AppInfo.userAgentHeader] 214 | 215 | let session = Foundation.URLSession(configuration: configuration, delegate: self as URLSessionDelegate, delegateQueue: OperationQueue.main) 216 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 217 | (data, response, error) -> Void in 218 | session.finishTasksAndInvalidate() 219 | if let httpResponse = response as? HTTPURLResponse { 220 | if httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299 { 221 | 222 | let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) 223 | if let endpointJSON = json as? [String:Any] { 224 | completion(endpointJSON) 225 | return 226 | } else { // if let endpointJSON error 227 | if httpResponse.statusCode == 204 && endpoint == "auth/invalidate-token" { 228 | completion(["JPAPI_result":"token terminated", "JPAPI_response":httpResponse.statusCode]) 229 | } else { 230 | completion(["JPAPI_result":"failed", "JPAPI_response":httpResponse.statusCode]) 231 | } 232 | return 233 | } 234 | } else { // if httpResponse.statusCode <200 or >299 235 | writeToLog.message(stringOfText: ["[TokenDelegate.getVersion] Response error: \(httpResponse.statusCode)."]) 236 | completion(["JPAPI_result":"failed", "JPAPI_method":request.httpMethod ?? method, "JPAPI_response":httpResponse.statusCode, "JPAPI_server":urlString, "JPAPI_token":token]) 237 | return 238 | } 239 | } else { 240 | writeToLog.message(stringOfText: ["[TokenDelegate.getVersion] GET response error. Verify url and port."]) 241 | completion([:]) 242 | return 243 | } 244 | }) 245 | task.resume() 246 | 247 | } // func getVersion - end 248 | } 249 | -------------------------------------------------------------------------------- /jamfStatus/UapiCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UapiCall.swift 3 | // jamfStatus 4 | // 5 | // Created by Leslie Helou on 9/1/19. 6 | // Copyright © 2019 Leslie Helou. All rights reserved. 7 | // 8 | 9 | // get notifications from https://jamf.pro.server/uapi/notifications/alerts - old 10 | // get notifications from https://jamf.pro.server/api/v1/notifications 11 | 12 | 13 | import Foundation 14 | 15 | class UapiCall: NSObject, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate { 16 | 17 | let defaults = UserDefaults.standard 18 | var theUapiQ = OperationQueue() // create operation queue for API calls 19 | 20 | func get(endpoint: String, completion: @escaping (_ notificationAlerts: [Dictionary]) -> Void) { 21 | 22 | let jps = defaults.string(forKey:"jamfServerUrl") ?? "" 23 | 24 | TokenDelegate().getToken(serverUrl: jps, base64creds: JamfProServer.base64Creds) { 25 | (authResult: (Int,String)) in 26 | 27 | if authResult.1 == "success" { 28 | 29 | URLCache.shared.removeAllCachedResponses() 30 | 31 | var workingUrlString = "\(jps)/api/\(endpoint)" 32 | workingUrlString = workingUrlString.replacingOccurrences(of: "//api", with: "/api") 33 | 34 | self.theUapiQ.maxConcurrentOperationCount = 1 35 | 36 | self.theUapiQ.addOperation { 37 | URLCache.shared.removeAllCachedResponses() 38 | 39 | let encodedURL = NSURL(string: workingUrlString) 40 | let request = NSMutableURLRequest(url: encodedURL! as URL) 41 | 42 | let configuration = URLSessionConfiguration.default 43 | request.httpMethod = "GET" 44 | 45 | configuration.httpAdditionalHeaders = ["Authorization" : "\(JamfProServer.authType) \(JamfProServer.accessToken)", "Content-Type" : "application/json", "Accept" : "application/json", "User-Agent" : AppInfo.userAgentHeader] 46 | 47 | let session = Foundation.URLSession(configuration: configuration, delegate: self as URLSessionDelegate, delegateQueue: OperationQueue.main) 48 | 49 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 50 | (data, response, error) -> Void in 51 | if let httpResponse = response as? HTTPURLResponse { 52 | if httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299 { 53 | let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) 54 | if let notificationsDictArray = json! as? [[String: Any]] { 55 | completion(notificationsDictArray) 56 | return 57 | } else { // if let endpointJSON error 58 | print("[UapiCall] get JSON error") 59 | completion([]) 60 | return 61 | } 62 | } else { // if httpResponse.statusCode <200 or >299 63 | print("[UapiCall] \(endpoint) - get response error: \(httpResponse.statusCode)") 64 | completion([]) 65 | return 66 | } 67 | } else { 68 | print("\n HTTP error \n") 69 | } 70 | }) 71 | task.resume() 72 | } // theUapiQ.addOperation - end 73 | } 74 | } 75 | } // func get - end 76 | 77 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping( URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 78 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /jamfStatus/VersionCheck.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionCheck.swift 3 | // jamfStatus 4 | // 5 | // Created by Leslie Helou on 9/15/19. 6 | // Copyright © 2019 Leslie Helou. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class VersionCheck: NSObject, URLSessionDelegate { 12 | 13 | func versionCheck(completion: @escaping (_ result: Bool) -> Void) { 14 | 15 | URLCache.shared.removeAllCachedResponses() 16 | 17 | let appInfo = Bundle.main.infoDictionary! 18 | let version = appInfo["CFBundleShortVersionString"] as! String 19 | 20 | let (currMajor, currMinor, currPatch, runningBeta, currBeta) = versionDetails(theVersion: version) 21 | 22 | var updateAvailable = false 23 | 24 | let versionUrl = URL(string: "https://api.github.com/repos/jamf/jamfStatus/releases") 25 | let configuration = URLSessionConfiguration.default 26 | var request = URLRequest(url: versionUrl!) 27 | request.httpMethod = "GET" 28 | 29 | configuration.httpAdditionalHeaders = ["Accept" : "application/vnd.github.jean-grey-preview+json"] 30 | let session = Foundation.URLSession(configuration: configuration, delegate: self as URLSessionDelegate, delegateQueue: OperationQueue.main) 31 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 32 | (data, response, error) -> Void in 33 | if let httpResponse = response as? HTTPURLResponse { 34 | if httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299 { 35 | let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) 36 | if let endpointJSON = json as? [Dictionary] { 37 | 38 | for release in endpointJSON { 39 | let statusInfo = release as Dictionary 40 | let releaseInfo = statusInfo as Dictionary 41 | let tmpArray = "\(releaseInfo["name"]!)".components(separatedBy: " ") 42 | let fullVersion = (tmpArray[1] as String).replacingOccurrences(of: "v", with: "") 43 | 44 | let versionTest = self.compareVersions(currMajor: currMajor, 45 | currMinor: currMinor, 46 | currPatch: currPatch, 47 | runningBeta: runningBeta, 48 | currBeta: currBeta, 49 | available: fullVersion) 50 | if !versionTest { 51 | updateAvailable = true 52 | } 53 | } 54 | completion(updateAvailable) 55 | return 56 | } else { // if let endpointJSON error 57 | completion(false) 58 | return 59 | } 60 | } else { // if httpResponse.statusCode <200 or >299 61 | print("response error: \(httpResponse.statusCode)") 62 | completion(false) 63 | return 64 | } 65 | 66 | } 67 | }) 68 | task.resume() 69 | } 70 | 71 | func compareVersions(currMajor: Int, currMinor: Int, currPatch: Int, runningBeta: Bool, currBeta: Int, available: String) -> Bool { 72 | var runningCurrent = true 73 | var betaVer = "" 74 | if runningBeta { 75 | betaVer = "b\(currBeta)" 76 | } 77 | if available != "\(currMajor).\(currMinor).\(currPatch)\(betaVer)" { 78 | let (availMajor, availMinor, availPatch, availBeta, availBetaVer) = versionDetails(theVersion: available) 79 | if availMajor > currMajor { 80 | runningCurrent = false 81 | } else if availMajor == currMajor { 82 | if availMinor > currMinor { 83 | runningCurrent = false 84 | } else if availMinor == currMinor { 85 | if availPatch > currPatch { 86 | runningCurrent = false 87 | } else if availPatch == currPatch && ((runningBeta && availBeta) || (runningBeta && !availBeta)) { 88 | if availBetaVer > currBeta { 89 | runningCurrent = false 90 | } 91 | } 92 | } 93 | } 94 | } 95 | return runningCurrent 96 | } 97 | 98 | func versionDetails(theVersion: String) -> (Int, Int, Int, Bool, Int) { 99 | var major = 0 100 | var minor = 0 101 | var patch = 0 102 | var betaVer = 0 103 | var isBeta = false 104 | 105 | let versionArray = theVersion.split(separator: ".") 106 | major = Int(versionArray[0])! 107 | minor = Int(versionArray[1])! 108 | let patchArray = versionArray[2].lowercased().split(separator: "b") 109 | patch = Int(patchArray[0])! 110 | if patchArray.count > 1 { 111 | isBeta = true 112 | betaVer = Int(patchArray[1])! 113 | } 114 | return (major, minor, patch, isBeta, betaVer) 115 | } 116 | 117 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping( URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 118 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /jamfStatus/WriteToLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteToLog.swift 3 | // jamfStatus 4 | // 5 | // Created by Leslie Helou on 7/11/20. 6 | // Copyright © 2019 jamf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import os.log 11 | 12 | let writeToLog = WriteToLog() 13 | 14 | class WriteToLog { 15 | 16 | let logFileW = FileHandle(forUpdatingAtPath: Log.path! + Log.file) 17 | let fm = FileManager() 18 | 19 | func createLogFolder(completionHandler: @escaping (_ result: String) -> Void) { 20 | DispatchQueue.main.async { [self] in 21 | var attributes = [FileAttributeKey: Any]() 22 | attributes[.posixPermissions] = 0o700 23 | do { 24 | try fm.createDirectory(atPath: Log.path!, withIntermediateDirectories: true, attributes: attributes) 25 | completionHandler("log folder created") 26 | } catch { 27 | completionHandler("log folder not created") 28 | } 29 | } 30 | } 31 | 32 | func createLogFile(completionHandler: @escaping (_ result: String) -> Void) { 33 | if !fm.fileExists(atPath: Log.path!) { 34 | createLogFolder() { [self] 35 | (result: String) in 36 | print(result) 37 | 38 | fm.createFile(atPath: Log.path! + Log.file, contents: nil, attributes: nil) 39 | } 40 | 41 | } else if !fm.fileExists(atPath: Log.path! + Log.file) { 42 | fm.createFile(atPath: Log.path! + Log.file, contents: nil, attributes: nil) 43 | } 44 | completionHandler("created") 45 | } 46 | 47 | // func logCleanup - start 48 | func logCleanup(completionHandler: @escaping (_ result: String) -> Void) { 49 | 50 | // check if log is over 5MB 51 | do { 52 | let fileAttributes = try fm.attributesOfItem(atPath: Log.path! + Log.file) 53 | let logSize = fileAttributes[.size] as? Int 54 | if Int("\(logSize ?? 0)") ?? 0 < Log.maxSize { 55 | completionHandler("") 56 | return 57 | } 58 | } catch { 59 | print("no history") 60 | completionHandler("") 61 | return 62 | } 63 | 64 | var logArray: [String] = [] 65 | var logCount: Int = 0 66 | do { 67 | let logFiles = try fm.contentsOfDirectory(atPath: Log.path!) 68 | 69 | for logFile in logFiles { 70 | if logFile.contains(".zip") { 71 | let filePath: String = Log.path! + logFile 72 | logArray.append(filePath) 73 | } 74 | } 75 | logArray.sort() 76 | logCount = logArray.count 77 | 78 | // remove old log files 79 | if logCount > Log.maxFiles { 80 | for i in (0.. String { 140 | let current = Date() 141 | let localCalendar = Calendar.current 142 | let dateObjects: Set = [.year, .month, .day, .hour, .minute, .second] 143 | let dateTime = localCalendar.dateComponents(dateObjects, from: current) 144 | let currentMonth = leadingZero(value: dateTime.month!) 145 | let currentDay = leadingZero(value: dateTime.day!) 146 | let currentHour = leadingZero(value: dateTime.hour!) 147 | let currentMinute = leadingZero(value: dateTime.minute!) 148 | let currentSecond = leadingZero(value: dateTime.second!) 149 | let stringDate = "\(dateTime.year!)\(currentMonth)\(currentDay)_\(currentHour)\(currentMinute)\(currentSecond)" 150 | return stringDate 151 | } 152 | 153 | func logDate() -> String { 154 | let logDate = DateFormatter() 155 | logDate.dateFormat = "E MMM dd HH:mm:ss" 156 | return("\(logDate.string(from: Date()))") 157 | } 158 | 159 | // add leading zero to single digit integers 160 | func leadingZero(value: Int) -> String { 161 | var formattedValue = "" 162 | if value < 10 { 163 | formattedValue = "0\(value)" 164 | } else { 165 | formattedValue = "\(value)" 166 | } 167 | return formattedValue 168 | } 169 | 170 | func zipIt(args: String..., completion: @escaping (_ result: String) -> Void) { 171 | 172 | var cmdArgs = ["-c"] 173 | for theArg in args { 174 | cmdArgs.append(theArg) 175 | } 176 | var status = "" 177 | var statusArray = [String]() 178 | let pipe = Pipe() 179 | let task = Process() 180 | 181 | task.launchPath = "/bin/sh" 182 | task.arguments = cmdArgs 183 | task.standardOutput = pipe 184 | 185 | task.launch() 186 | 187 | let outdata = pipe.fileHandleForReading.readDataToEndOfFile() 188 | if var string = String(data: outdata, encoding: .utf8) { 189 | string = string.trimmingCharacters(in: .newlines) 190 | statusArray = string.components(separatedBy: "\n") 191 | status = statusArray[0] 192 | } 193 | 194 | task.waitUntilExit() 195 | completion(status) 196 | } 197 | 198 | } 199 | 200 | extension Logger { 201 | private static var subsystem = Bundle.main.bundleIdentifier! 202 | 203 | //Categories 204 | static let jamfstatus = Logger(subsystem: subsystem, category: "jamfstatus") 205 | } 206 | 207 | -------------------------------------------------------------------------------- /jamfStatus/com.jamf.cloudmonitor.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EnvironmentVariables 6 | 7 | PATH 8 | /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/sbin 9 | 10 | Label 11 | com.jamf.cloudmonitor 12 | ProgramArguments 13 | 14 | /Applications/jamfStatus.app/Contents/MacOS/jamfStatus 15 | 16 | RunAtLoad 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /jamfStatus/images/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/alert.png -------------------------------------------------------------------------------- /jamfStatus/images/major.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/major.png -------------------------------------------------------------------------------- /jamfStatus/images/major1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/major1.png -------------------------------------------------------------------------------- /jamfStatus/images/major2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/major2.png -------------------------------------------------------------------------------- /jamfStatus/images/menubar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/menubar.png -------------------------------------------------------------------------------- /jamfStatus/images/minor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/minor1.png -------------------------------------------------------------------------------- /jamfStatus/images/minor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/minor2.png -------------------------------------------------------------------------------- /jamfStatus/images/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/notifications.png -------------------------------------------------------------------------------- /jamfStatus/images/prefs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamfStatus/1a79bd1187d707d6107ccc9d0608f3aa1ce1e193/jamfStatus/images/prefs.png -------------------------------------------------------------------------------- /jamfStatus/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About 6 | 167 | 168 | 169 |
170 |

jamfStatus

171 |
172 |
173 |
174 |
Keep an eye on the status of the Jamf Cloud environment with jamfStatus.app.  The application will place an icon in your menu bar to reflect the current cloud status.
175 |
176 | 177 |
178 |
179 |
An alert window will be displayed as the cloud status changes.  You can configure how the alert window display refreshes, either at every status check or only when the status changes.
180 |
For minor Jamf Cloud issues something similar to the following be displayed. 181 | 182 | 183 |
184 |
For major Jamf Cloud issues something similar to the following be displayed. 185 | 186 |
187 |
188 |
Access Preferences from the menu bar icon.  Here you'll be able to set the following:
189 |
190 |
- Polling interval.
191 |
- Whether the alert window is displayed at every polling interval or only when the status changes.
192 |
- How the menubar icon is displayed.  Minimizing will place a thin transparent icon in the menubar.
193 |
- Use of a LaunchAgent, to automatically start the app when logging in.*
194 |
- Information for your specific Jamf Cloud instance. If your cloud server does not utilize the HTTPS port 443 be sure to include the port you use in the URL.
195 |
196 |
197 | 198 | 199 |
200 | There are two different menu bar icon styles to choose from. One uses colors to indicate the status and the other uses slashes.

201 |
202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 217 |
Statusminormajorminormajor
Icon 213 | 214 | 215 | 216 |

218 |
By entering the information for your Jamf Cloud instance you'll be able to see the alerts present on the server, if any exist.
219 | 220 |
221 | 222 |
223 | 224 | * Status changes are written to ~/Library/Logs/jamfStatus/jamfStatus.log 225 |
226 |
227 |
228 |
229 |
Author: Leslie N. Helou
230 |

231 |
License: Copyright 2017 Jamf Professional Services
232 |

233 |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
234 |

235 |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
236 |

237 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
238 |

239 |
240 |
241 |
242 |
243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /jamfStatus/jamfStatus.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /jamfStatus/prefs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // prefs.swift 3 | // jamfStatus 4 | // 5 | // Created by Leslie Helou on 4/20/17. 6 | // Copyright © 2017 Leslie Helou. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Prefs: NSObject { 12 | let myBundlePath = Bundle.main.bundlePath 13 | let SettingsPlistPath = Bundle.main.bundlePath+"/settings.plist" 14 | var format = PropertyListSerialization.PropertyListFormat.xml //format of the property list file 15 | 16 | var settingsPlistData:[String:AnyObject] = [:] 17 | 18 | func readSettings(object: String) -> AnyObject { 19 | let plistXML = FileManager.default.contents(atPath: SettingsPlistPath)! 20 | do{ 21 | settingsPlistData = try PropertyListSerialization.propertyList(from: plistXML, 22 | options: .mutableContainersAndLeaves, 23 | format: &format) 24 | as! [String:AnyObject] 25 | } catch { 26 | //writeToLog(theMessage: "Error reading plist: \(error), format: \(format)") 27 | } 28 | let theSetting = settingsPlistData[object] 29 | return theSetting as AnyObject 30 | } 31 | } 32 | --------------------------------------------------------------------------------