├── .gitignore ├── LICENSE ├── MUT.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── MUT.xcscheme └── xcuserdata │ ├── andrew.pirkl.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ └── mlev.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MUT.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── README.md ├── The MUT.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── ben.whitis.xcuserdatad │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist ├── The MUT ├── APIAccess │ ├── APIAccess.swift │ ├── APIDelegate.swift │ ├── APIFunctions2.swift │ ├── HTTPMethod.swift │ ├── HTTPStatusCode.swift │ ├── SessionHandler.swift │ └── StringExtension.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256-1.png │ │ ├── 256.png │ │ ├── 32-1.png │ │ ├── 32.png │ │ ├── 512-1.png │ │ ├── 512.png │ │ ├── 64.png │ │ └── Contents.json │ ├── Contents.json │ ├── IconLocked.imageset │ │ ├── Contents.json │ │ └── IconLocked.png │ ├── IconSafari.imageset │ │ ├── Contents.json │ │ └── IconSafari.png │ ├── IconUnlocked.imageset │ │ ├── Contents.json │ │ └── IconUnlocked.png │ ├── IconUser.imageset │ │ ├── Contents.json │ │ └── IconUser.png │ ├── V6.imageset │ │ ├── Contents.json │ │ └── V6.png │ └── V6Glow.imageset │ │ ├── Contents.json │ │ └── V6Glow.png ├── Base.lproj │ └── Main.storyboard ├── CSVFunctions.swift ├── Info.plist ├── KeychainHelper.swift ├── LogManager.swift ├── MUT Templates.zip ├── MobileDeviceXMLParser.swift ├── Models │ ├── JamfProVersionV1.swift │ └── MobileDeviceV2.swift ├── Settings Menu │ ├── InsecureSSLPopover.swift │ ├── KeychainDefaultsPopover.swift │ └── MenuController.swift ├── The MUT.entitlements ├── The_MUT.xcdatamodeld │ ├── .xccurrentversion │ └── The_MUT.xcdatamodel │ │ └── contents ├── ViewController.swift ├── dataPreparation.swift ├── jsonBuilder.swift ├── loginWindow.swift ├── popPrompt.swift ├── tokenManagement.swift └── xmlBuilder.swift └── catalog-info.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | .DS_Store 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mike Levenick 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 | -------------------------------------------------------------------------------- /MUT.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2D30EF2E2BDAD05B00BB7E1E /* MUT Templates.zip in Resources */ = {isa = PBXBuildFile; fileRef = 2D30EF2D2BDAD05B00BB7E1E /* MUT Templates.zip */; }; 11 | 50D075C226DED17900252AC0 /* MobileDeviceXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D075C126DED17900252AC0 /* MobileDeviceXMLParser.swift */; }; 12 | 50D075C526DED19900252AC0 /* MobileDeviceV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D075C426DED19900252AC0 /* MobileDeviceV2.swift */; }; 13 | 50D075C726DFEAF200252AC0 /* JamfProVersionV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D075C626DFEAF200252AC0 /* JamfProVersionV1.swift */; }; 14 | 7166CB71238CD161002B221A /* APIFunctions2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7166CB70238CD161002B221A /* APIFunctions2.swift */; }; 15 | 71FEC542238CD07B00A7AAB5 /* APIAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEC53B238CD07B00A7AAB5 /* APIAccess.swift */; }; 16 | 71FEC543238CD07B00A7AAB5 /* APIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEC53C238CD07B00A7AAB5 /* APIDelegate.swift */; }; 17 | 71FEC544238CD07B00A7AAB5 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEC53D238CD07B00A7AAB5 /* HTTPMethod.swift */; }; 18 | 71FEC545238CD07B00A7AAB5 /* HTTPStatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEC53E238CD07B00A7AAB5 /* HTTPStatusCode.swift */; }; 19 | 71FEC546238CD07B00A7AAB5 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEC53F238CD07B00A7AAB5 /* StringExtension.swift */; }; 20 | 71FEC547238CD07B00A7AAB5 /* SessionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEC540238CD07B00A7AAB5 /* SessionHandler.swift */; }; 21 | 85C4D46922AAEF6A00512000 /* CSVFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C4D46822AAEF6A00512000 /* CSVFunctions.swift */; }; 22 | F71E4D0E2B965BCB0097BE57 /* CSV in Frameworks */ = {isa = PBXBuildFile; productRef = F71E4D0D2B965BCB0097BE57 /* CSV */; }; 23 | F71E4D112B965BEA0097BE57 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = F71E4D102B965BEA0097BE57 /* SwiftyJSON */; }; 24 | F721E4EF2980444A003284A0 /* KeychainDefaultsPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = F721E4EE2980444A003284A0 /* KeychainDefaultsPopover.swift */; }; 25 | F7333BD11DB55EDF00F89C00 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7333BD01DB55EDF00F89C00 /* AppDelegate.swift */; }; 26 | F7333BD31DB55EDF00F89C00 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7333BD21DB55EDF00F89C00 /* ViewController.swift */; }; 27 | F7333BD61DB55EDF00F89C00 /* The_MUT.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F7333BD41DB55EDF00F89C00 /* The_MUT.xcdatamodeld */; }; 28 | F7333BD81DB55EDF00F89C00 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7333BD71DB55EDF00F89C00 /* Assets.xcassets */; }; 29 | F7333BDB1DB55EDF00F89C00 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7333BD91DB55EDF00F89C00 /* Main.storyboard */; }; 30 | F743057F22D515CC00FF229E /* jsonBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F743057E22D515CC00FF229E /* jsonBuilder.swift */; }; 31 | F762D7FC22D98BBC009702D4 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F762D7FB22D98BBC009702D4 /* LogManager.swift */; }; 32 | F76AE9A529366FCF00A6F443 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76AE9A429366FCF00A6F443 /* KeychainHelper.swift */; }; 33 | F7A4EE48293D9ACF001263CA /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A4EE47293D9ACF001263CA /* MenuController.swift */; }; 34 | F7C5F4B4294459D400BE7694 /* InsecureSSLPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C5F4B3294459D400BE7694 /* InsecureSSLPopover.swift */; }; 35 | F7CEE52A229E3C420027CAE8 /* loginWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CEE529229E3C420027CAE8 /* loginWindow.swift */; }; 36 | F7CF61EA2298671E003A0CBB /* tokenManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CF61E92298671E003A0CBB /* tokenManagement.swift */; }; 37 | F7CF61EC22986736003A0CBB /* dataPreparation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CF61EB22986736003A0CBB /* dataPreparation.swift */; }; 38 | F7D670BE1EA54E51006010AA /* popPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D670BD1EA54E51006010AA /* popPrompt.swift */; }; 39 | F7FCA4E52A6F553800950820 /* catalog-info.yaml in Resources */ = {isa = PBXBuildFile; fileRef = F7FCA4E42A6F553800950820 /* catalog-info.yaml */; }; 40 | F7FF428B1EA645680044F126 /* xmlBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FF428A1EA645680044F126 /* xmlBuilder.swift */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 2D30EF2D2BDAD05B00BB7E1E /* MUT Templates.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = "MUT Templates.zip"; sourceTree = ""; }; 45 | 50D075C126DED17900252AC0 /* MobileDeviceXMLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileDeviceXMLParser.swift; sourceTree = ""; }; 46 | 50D075C426DED19900252AC0 /* MobileDeviceV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileDeviceV2.swift; sourceTree = ""; }; 47 | 50D075C626DFEAF200252AC0 /* JamfProVersionV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamfProVersionV1.swift; sourceTree = ""; }; 48 | 7166CB70238CD161002B221A /* APIFunctions2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIFunctions2.swift; sourceTree = ""; }; 49 | 71FEC53B238CD07B00A7AAB5 /* APIAccess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIAccess.swift; sourceTree = ""; }; 50 | 71FEC53C238CD07B00A7AAB5 /* APIDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIDelegate.swift; sourceTree = ""; }; 51 | 71FEC53D238CD07B00A7AAB5 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 52 | 71FEC53E238CD07B00A7AAB5 /* HTTPStatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPStatusCode.swift; sourceTree = ""; }; 53 | 71FEC53F238CD07B00A7AAB5 /* StringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 54 | 71FEC540238CD07B00A7AAB5 /* SessionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionHandler.swift; sourceTree = ""; }; 55 | 85C4D46822AAEF6A00512000 /* CSVFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVFunctions.swift; sourceTree = ""; }; 56 | F721E4EE2980444A003284A0 /* KeychainDefaultsPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainDefaultsPopover.swift; sourceTree = ""; }; 57 | F7333BCD1DB55EDF00F89C00 /* MUT.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MUT.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | F7333BD01DB55EDF00F89C00 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | F7333BD21DB55EDF00F89C00 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 60 | F7333BD51DB55EDF00F89C00 /* The_MUT.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = The_MUT.xcdatamodel; sourceTree = ""; }; 61 | F7333BD71DB55EDF00F89C00 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | F7333BDA1DB55EDF00F89C00 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 63 | F7333BDC1DB55EDF00F89C00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | F743057E22D515CC00FF229E /* jsonBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = jsonBuilder.swift; sourceTree = ""; }; 65 | F762D7FB22D98BBC009702D4 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; 66 | F76AE9A429366FCF00A6F443 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; 67 | F7A4EE47293D9ACF001263CA /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = ""; }; 68 | F7C5F4B3294459D400BE7694 /* InsecureSSLPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsecureSSLPopover.swift; sourceTree = ""; }; 69 | F7CEE529229E3C420027CAE8 /* loginWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = loginWindow.swift; sourceTree = ""; }; 70 | F7CF61E92298671E003A0CBB /* tokenManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tokenManagement.swift; sourceTree = ""; }; 71 | F7CF61EB22986736003A0CBB /* dataPreparation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dataPreparation.swift; sourceTree = ""; }; 72 | F7D623911DE2C745004040F1 /* The MUT.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "The MUT.entitlements"; sourceTree = ""; }; 73 | F7D670BD1EA54E51006010AA /* popPrompt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = popPrompt.swift; sourceTree = ""; }; 74 | F7FCA4E42A6F553800950820 /* catalog-info.yaml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = "catalog-info.yaml"; sourceTree = ""; }; 75 | F7FF428A1EA645680044F126 /* xmlBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = xmlBuilder.swift; sourceTree = ""; }; 76 | /* End PBXFileReference section */ 77 | 78 | /* Begin PBXFrameworksBuildPhase section */ 79 | F7333BCA1DB55EDF00F89C00 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | F71E4D112B965BEA0097BE57 /* SwiftyJSON in Frameworks */, 84 | F71E4D0E2B965BCB0097BE57 /* CSV in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | 50D075C326DED18600252AC0 /* Models */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 50D075C426DED19900252AC0 /* MobileDeviceV2.swift */, 95 | 50D075C626DFEAF200252AC0 /* JamfProVersionV1.swift */, 96 | ); 97 | path = Models; 98 | sourceTree = ""; 99 | }; 100 | 71FEC549238CD07E00A7AAB5 /* APIAccess */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 71FEC53B238CD07B00A7AAB5 /* APIAccess.swift */, 104 | 71FEC53C238CD07B00A7AAB5 /* APIDelegate.swift */, 105 | 71FEC53D238CD07B00A7AAB5 /* HTTPMethod.swift */, 106 | 71FEC53E238CD07B00A7AAB5 /* HTTPStatusCode.swift */, 107 | 71FEC540238CD07B00A7AAB5 /* SessionHandler.swift */, 108 | 71FEC53F238CD07B00A7AAB5 /* StringExtension.swift */, 109 | 7166CB70238CD161002B221A /* APIFunctions2.swift */, 110 | ); 111 | path = APIAccess; 112 | sourceTree = ""; 113 | }; 114 | F7333BC41DB55EDF00F89C00 = { 115 | isa = PBXGroup; 116 | children = ( 117 | F7FCA4E42A6F553800950820 /* catalog-info.yaml */, 118 | F7333BCF1DB55EDF00F89C00 /* The MUT */, 119 | F7333BCE1DB55EDF00F89C00 /* Products */, 120 | ); 121 | sourceTree = ""; 122 | }; 123 | F7333BCE1DB55EDF00F89C00 /* Products */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | F7333BCD1DB55EDF00F89C00 /* MUT.app */, 127 | ); 128 | name = Products; 129 | sourceTree = ""; 130 | }; 131 | F7333BCF1DB55EDF00F89C00 /* The MUT */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | F7C5F4B629445D2E00BE7694 /* Settings Menu */, 135 | 50D075C326DED18600252AC0 /* Models */, 136 | 71FEC549238CD07E00A7AAB5 /* APIAccess */, 137 | F7D623911DE2C745004040F1 /* The MUT.entitlements */, 138 | F7333BD01DB55EDF00F89C00 /* AppDelegate.swift */, 139 | F7333BD21DB55EDF00F89C00 /* ViewController.swift */, 140 | F762D7FB22D98BBC009702D4 /* LogManager.swift */, 141 | 2D30EF2D2BDAD05B00BB7E1E /* MUT Templates.zip */, 142 | 85C4D46822AAEF6A00512000 /* CSVFunctions.swift */, 143 | F7CF61E92298671E003A0CBB /* tokenManagement.swift */, 144 | F7CF61EB22986736003A0CBB /* dataPreparation.swift */, 145 | F743057E22D515CC00FF229E /* jsonBuilder.swift */, 146 | F7333BD71DB55EDF00F89C00 /* Assets.xcassets */, 147 | F7333BD91DB55EDF00F89C00 /* Main.storyboard */, 148 | F7CEE529229E3C420027CAE8 /* loginWindow.swift */, 149 | F7FF428A1EA645680044F126 /* xmlBuilder.swift */, 150 | F7D670BD1EA54E51006010AA /* popPrompt.swift */, 151 | F7333BDC1DB55EDF00F89C00 /* Info.plist */, 152 | F7333BD41DB55EDF00F89C00 /* The_MUT.xcdatamodeld */, 153 | 50D075C126DED17900252AC0 /* MobileDeviceXMLParser.swift */, 154 | F76AE9A429366FCF00A6F443 /* KeychainHelper.swift */, 155 | ); 156 | path = "The MUT"; 157 | sourceTree = ""; 158 | }; 159 | F7C5F4B629445D2E00BE7694 /* Settings Menu */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | F7A4EE47293D9ACF001263CA /* MenuController.swift */, 163 | F7C5F4B3294459D400BE7694 /* InsecureSSLPopover.swift */, 164 | F721E4EE2980444A003284A0 /* KeychainDefaultsPopover.swift */, 165 | ); 166 | path = "Settings Menu"; 167 | sourceTree = ""; 168 | }; 169 | /* End PBXGroup section */ 170 | 171 | /* Begin PBXNativeTarget section */ 172 | F7333BCC1DB55EDF00F89C00 /* MUT */ = { 173 | isa = PBXNativeTarget; 174 | buildConfigurationList = F7333BF51DB55EDF00F89C00 /* Build configuration list for PBXNativeTarget "MUT" */; 175 | buildPhases = ( 176 | F7333BC91DB55EDF00F89C00 /* Sources */, 177 | F7333BCA1DB55EDF00F89C00 /* Frameworks */, 178 | F7333BCB1DB55EDF00F89C00 /* Resources */, 179 | ); 180 | buildRules = ( 181 | ); 182 | dependencies = ( 183 | ); 184 | name = MUT; 185 | packageProductDependencies = ( 186 | F71E4D0D2B965BCB0097BE57 /* CSV */, 187 | F71E4D102B965BEA0097BE57 /* SwiftyJSON */, 188 | ); 189 | productName = "The MUT"; 190 | productReference = F7333BCD1DB55EDF00F89C00 /* MUT.app */; 191 | productType = "com.apple.product-type.application"; 192 | }; 193 | /* End PBXNativeTarget section */ 194 | 195 | /* Begin PBXProject section */ 196 | F7333BC51DB55EDF00F89C00 /* Project object */ = { 197 | isa = PBXProject; 198 | attributes = { 199 | LastSwiftUpdateCheck = 1020; 200 | LastUpgradeCheck = 1020; 201 | ORGANIZATIONNAME = "Levenick Enterprises LLC"; 202 | TargetAttributes = { 203 | F7333BCC1DB55EDF00F89C00 = { 204 | CreatedOnToolsVersion = 8.0; 205 | LastSwiftMigration = 1010; 206 | ProvisioningStyle = Automatic; 207 | SystemCapabilities = { 208 | com.apple.HardenedRuntime = { 209 | enabled = 1; 210 | }; 211 | com.apple.Sandbox = { 212 | enabled = 1; 213 | }; 214 | }; 215 | }; 216 | }; 217 | }; 218 | buildConfigurationList = F7333BC81DB55EDF00F89C00 /* Build configuration list for PBXProject "MUT" */; 219 | compatibilityVersion = "Xcode 10.0"; 220 | developmentRegion = en; 221 | hasScannedForEncodings = 0; 222 | knownRegions = ( 223 | en, 224 | Base, 225 | ); 226 | mainGroup = F7333BC41DB55EDF00F89C00; 227 | packageReferences = ( 228 | F71E4D0C2B965BCB0097BE57 /* XCRemoteSwiftPackageReference "CSV" */, 229 | F71E4D0F2B965BEA0097BE57 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 230 | ); 231 | productRefGroup = F7333BCE1DB55EDF00F89C00 /* Products */; 232 | projectDirPath = ""; 233 | projectRoot = ""; 234 | targets = ( 235 | F7333BCC1DB55EDF00F89C00 /* MUT */, 236 | ); 237 | }; 238 | /* End PBXProject section */ 239 | 240 | /* Begin PBXResourcesBuildPhase section */ 241 | F7333BCB1DB55EDF00F89C00 /* Resources */ = { 242 | isa = PBXResourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | F7333BD81DB55EDF00F89C00 /* Assets.xcassets in Resources */, 246 | F7FCA4E52A6F553800950820 /* catalog-info.yaml in Resources */, 247 | F7333BDB1DB55EDF00F89C00 /* Main.storyboard in Resources */, 248 | 2D30EF2E2BDAD05B00BB7E1E /* MUT Templates.zip in Resources */, 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXResourcesBuildPhase section */ 253 | 254 | /* Begin PBXSourcesBuildPhase section */ 255 | F7333BC91DB55EDF00F89C00 /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | F762D7FC22D98BBC009702D4 /* LogManager.swift in Sources */, 260 | 71FEC547238CD07B00A7AAB5 /* SessionHandler.swift in Sources */, 261 | F7333BD31DB55EDF00F89C00 /* ViewController.swift in Sources */, 262 | F7333BD61DB55EDF00F89C00 /* The_MUT.xcdatamodeld in Sources */, 263 | F7FF428B1EA645680044F126 /* xmlBuilder.swift in Sources */, 264 | F76AE9A529366FCF00A6F443 /* KeychainHelper.swift in Sources */, 265 | F7CF61EC22986736003A0CBB /* dataPreparation.swift in Sources */, 266 | 71FEC542238CD07B00A7AAB5 /* APIAccess.swift in Sources */, 267 | 71FEC543238CD07B00A7AAB5 /* APIDelegate.swift in Sources */, 268 | 71FEC546238CD07B00A7AAB5 /* StringExtension.swift in Sources */, 269 | 50D075C726DFEAF200252AC0 /* JamfProVersionV1.swift in Sources */, 270 | 71FEC544238CD07B00A7AAB5 /* HTTPMethod.swift in Sources */, 271 | 50D075C226DED17900252AC0 /* MobileDeviceXMLParser.swift in Sources */, 272 | F721E4EF2980444A003284A0 /* KeychainDefaultsPopover.swift in Sources */, 273 | F7D670BE1EA54E51006010AA /* popPrompt.swift in Sources */, 274 | F7CEE52A229E3C420027CAE8 /* loginWindow.swift in Sources */, 275 | 50D075C526DED19900252AC0 /* MobileDeviceV2.swift in Sources */, 276 | F7CF61EA2298671E003A0CBB /* tokenManagement.swift in Sources */, 277 | F7A4EE48293D9ACF001263CA /* MenuController.swift in Sources */, 278 | F7333BD11DB55EDF00F89C00 /* AppDelegate.swift in Sources */, 279 | 7166CB71238CD161002B221A /* APIFunctions2.swift in Sources */, 280 | F7C5F4B4294459D400BE7694 /* InsecureSSLPopover.swift in Sources */, 281 | 85C4D46922AAEF6A00512000 /* CSVFunctions.swift in Sources */, 282 | 71FEC545238CD07B00A7AAB5 /* HTTPStatusCode.swift in Sources */, 283 | F743057F22D515CC00FF229E /* jsonBuilder.swift in Sources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | /* End PBXSourcesBuildPhase section */ 288 | 289 | /* Begin PBXVariantGroup section */ 290 | F7333BD91DB55EDF00F89C00 /* Main.storyboard */ = { 291 | isa = PBXVariantGroup; 292 | children = ( 293 | F7333BDA1DB55EDF00F89C00 /* Base */, 294 | ); 295 | name = Main.storyboard; 296 | sourceTree = ""; 297 | }; 298 | /* End PBXVariantGroup section */ 299 | 300 | /* Begin XCBuildConfiguration section */ 301 | F7333BF31DB55EDF00F89C00 /* Debug */ = { 302 | isa = XCBuildConfiguration; 303 | buildSettings = { 304 | ALWAYS_SEARCH_USER_PATHS = NO; 305 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 318 | CLANG_WARN_EMPTY_BODY = YES; 319 | CLANG_WARN_ENUM_CONVERSION = YES; 320 | CLANG_WARN_INFINITE_RECURSION = YES; 321 | CLANG_WARN_INT_CONVERSION = YES; 322 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 324 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 327 | CLANG_WARN_STRICT_PROTOTYPES = YES; 328 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 329 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | CODE_SIGN_IDENTITY = "-"; 333 | COPY_PHASE_STRIP = NO; 334 | DEBUG_INFORMATION_FORMAT = dwarf; 335 | ENABLE_STRICT_OBJC_MSGSEND = YES; 336 | ENABLE_TESTABILITY = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu99; 338 | GCC_DYNAMIC_NO_PIC = NO; 339 | GCC_NO_COMMON_BLOCKS = YES; 340 | GCC_OPTIMIZATION_LEVEL = 0; 341 | GCC_PREPROCESSOR_DEFINITIONS = ( 342 | "DEBUG=1", 343 | "$(inherited)", 344 | ); 345 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 346 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 347 | GCC_WARN_UNDECLARED_SELECTOR = YES; 348 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 349 | GCC_WARN_UNUSED_FUNCTION = YES; 350 | GCC_WARN_UNUSED_VARIABLE = YES; 351 | MACOSX_DEPLOYMENT_TARGET = 10.14; 352 | MTL_ENABLE_DEBUG_INFO = YES; 353 | ONLY_ACTIVE_ARCH = YES; 354 | SDKROOT = macosx; 355 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 356 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 357 | }; 358 | name = Debug; 359 | }; 360 | F7333BF41DB55EDF00F89C00 /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ALWAYS_SEARCH_USER_PATHS = NO; 364 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 365 | CLANG_ANALYZER_NONNULL = YES; 366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 367 | CLANG_CXX_LIBRARY = "libc++"; 368 | CLANG_ENABLE_MODULES = YES; 369 | CLANG_ENABLE_OBJC_ARC = YES; 370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 371 | CLANG_WARN_BOOL_CONVERSION = YES; 372 | CLANG_WARN_COMMA = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 377 | CLANG_WARN_EMPTY_BODY = YES; 378 | CLANG_WARN_ENUM_CONVERSION = YES; 379 | CLANG_WARN_INFINITE_RECURSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 385 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 386 | CLANG_WARN_STRICT_PROTOTYPES = YES; 387 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 389 | CLANG_WARN_UNREACHABLE_CODE = YES; 390 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 391 | CODE_SIGN_IDENTITY = "-"; 392 | COPY_PHASE_STRIP = NO; 393 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 394 | ENABLE_NS_ASSERTIONS = NO; 395 | ENABLE_STRICT_OBJC_MSGSEND = YES; 396 | GCC_C_LANGUAGE_STANDARD = gnu99; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 400 | GCC_WARN_UNDECLARED_SELECTOR = YES; 401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 402 | GCC_WARN_UNUSED_FUNCTION = YES; 403 | GCC_WARN_UNUSED_VARIABLE = YES; 404 | MACOSX_DEPLOYMENT_TARGET = 10.14; 405 | MTL_ENABLE_DEBUG_INFO = NO; 406 | SDKROOT = macosx; 407 | SWIFT_COMPILATION_MODE = wholemodule; 408 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 409 | }; 410 | name = Release; 411 | }; 412 | F7333BF61DB55EDF00F89C00 /* Debug */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 416 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 417 | CODE_SIGN_ENTITLEMENTS = "The MUT/The MUT.entitlements"; 418 | CODE_SIGN_IDENTITY = "Apple Development"; 419 | COMBINE_HIDPI_IMAGES = YES; 420 | CURRENT_PROJECT_VERSION = 6.3.0; 421 | DEVELOPMENT_TEAM = 483DWKW443; 422 | ENABLE_HARDENED_RUNTIME = YES; 423 | INFOPLIST_FILE = "The MUT/Info.plist"; 424 | LD_RUNPATH_SEARCH_PATHS = ( 425 | "$(inherited)", 426 | "@executable_path/../Frameworks", 427 | ); 428 | MACOSX_DEPLOYMENT_TARGET = 10.14; 429 | MARKETING_VERSION = 6.3.0; 430 | PRODUCT_BUNDLE_IDENTIFIER = com.jssmut.jssmut; 431 | PRODUCT_NAME = "$(TARGET_NAME)"; 432 | PROVISIONING_PROFILE = ""; 433 | PROVISIONING_PROFILE_SPECIFIER = ""; 434 | SWIFT_VERSION = 5.0; 435 | }; 436 | name = Debug; 437 | }; 438 | F7333BF71DB55EDF00F89C00 /* Release */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | CODE_SIGN_ENTITLEMENTS = "The MUT/The MUT.entitlements"; 444 | CODE_SIGN_IDENTITY = "Apple Development"; 445 | COMBINE_HIDPI_IMAGES = YES; 446 | CURRENT_PROJECT_VERSION = 6.3.0; 447 | DEVELOPMENT_TEAM = 483DWKW443; 448 | ENABLE_HARDENED_RUNTIME = YES; 449 | INFOPLIST_FILE = "The MUT/Info.plist"; 450 | LD_RUNPATH_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "@executable_path/../Frameworks", 453 | ); 454 | MACOSX_DEPLOYMENT_TARGET = 10.14; 455 | MARKETING_VERSION = 6.3.0; 456 | PRODUCT_BUNDLE_IDENTIFIER = com.jssmut.jssmut; 457 | PRODUCT_NAME = "$(TARGET_NAME)"; 458 | PROVISIONING_PROFILE = ""; 459 | PROVISIONING_PROFILE_SPECIFIER = ""; 460 | SWIFT_VERSION = 5.0; 461 | }; 462 | name = Release; 463 | }; 464 | /* End XCBuildConfiguration section */ 465 | 466 | /* Begin XCConfigurationList section */ 467 | F7333BC81DB55EDF00F89C00 /* Build configuration list for PBXProject "MUT" */ = { 468 | isa = XCConfigurationList; 469 | buildConfigurations = ( 470 | F7333BF31DB55EDF00F89C00 /* Debug */, 471 | F7333BF41DB55EDF00F89C00 /* Release */, 472 | ); 473 | defaultConfigurationIsVisible = 0; 474 | defaultConfigurationName = Release; 475 | }; 476 | F7333BF51DB55EDF00F89C00 /* Build configuration list for PBXNativeTarget "MUT" */ = { 477 | isa = XCConfigurationList; 478 | buildConfigurations = ( 479 | F7333BF61DB55EDF00F89C00 /* Debug */, 480 | F7333BF71DB55EDF00F89C00 /* Release */, 481 | ); 482 | defaultConfigurationIsVisible = 0; 483 | defaultConfigurationName = Release; 484 | }; 485 | /* End XCConfigurationList section */ 486 | 487 | /* Begin XCRemoteSwiftPackageReference section */ 488 | F71E4D0C2B965BCB0097BE57 /* XCRemoteSwiftPackageReference "CSV" */ = { 489 | isa = XCRemoteSwiftPackageReference; 490 | repositoryURL = "https://github.com/yaslab/CSV.swift"; 491 | requirement = { 492 | kind = upToNextMajorVersion; 493 | minimumVersion = 2.4.3; 494 | }; 495 | }; 496 | F71E4D0F2B965BEA0097BE57 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { 497 | isa = XCRemoteSwiftPackageReference; 498 | repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git"; 499 | requirement = { 500 | kind = upToNextMajorVersion; 501 | minimumVersion = 5.0.1; 502 | }; 503 | }; 504 | /* End XCRemoteSwiftPackageReference section */ 505 | 506 | /* Begin XCSwiftPackageProductDependency section */ 507 | F71E4D0D2B965BCB0097BE57 /* CSV */ = { 508 | isa = XCSwiftPackageProductDependency; 509 | package = F71E4D0C2B965BCB0097BE57 /* XCRemoteSwiftPackageReference "CSV" */; 510 | productName = CSV; 511 | }; 512 | F71E4D102B965BEA0097BE57 /* SwiftyJSON */ = { 513 | isa = XCSwiftPackageProductDependency; 514 | package = F71E4D0F2B965BEA0097BE57 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; 515 | productName = SwiftyJSON; 516 | }; 517 | /* End XCSwiftPackageProductDependency section */ 518 | 519 | /* Begin XCVersionGroup section */ 520 | F7333BD41DB55EDF00F89C00 /* The_MUT.xcdatamodeld */ = { 521 | isa = XCVersionGroup; 522 | children = ( 523 | F7333BD51DB55EDF00F89C00 /* The_MUT.xcdatamodel */, 524 | ); 525 | currentVersion = F7333BD51DB55EDF00F89C00 /* The_MUT.xcdatamodel */; 526 | path = The_MUT.xcdatamodeld; 527 | sourceTree = ""; 528 | versionGroupType = wrapper.xcdatamodel; 529 | }; 530 | /* End XCVersionGroup section */ 531 | }; 532 | rootObject = F7333BC51DB55EDF00F89C00 /* Project object */; 533 | } 534 | -------------------------------------------------------------------------------- /MUT.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MUT.xcodeproj/xcshareddata/xcschemes/MUT.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /MUT.xcodeproj/xcuserdata/andrew.pirkl.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MUT.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MUT.xcodeproj/xcuserdata/mlev.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MUT.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 4 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | F70A48E622B7106A003145E0 16 | 17 | primary 18 | 19 | 20 | F7333BCC1DB55EDF00F89C00 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MUT.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MUT.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MUT.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "5c90e43ced418c97649ce419de2c1007492787de981c13f7324849584830eeb4", 3 | "pins" : [ 4 | { 5 | "identity" : "csv.swift", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/yaslab/CSV.swift", 8 | "state" : { 9 | "revision" : "81d2874c51db364d7e1d71b0d99018a294c87ac1", 10 | "version" : "2.4.3" 11 | } 12 | }, 13 | { 14 | "identity" : "swiftyjson", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git", 17 | "state" : { 18 | "revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", 19 | "version" : "5.0.1" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The MUT ![MUT Logo](https://imgur.com/hGAg9Ry.png "The MUT Logo") 2 | 3 | _The **unofficial**, all-in-one mass update tool designed to be the perfect companion to Jamf Admins_ 4 | 5 | - [A note on v6:](#a-note-on-mut-and-v6) 6 | - [Introduction:](#introduction) 7 | - [Steps for use:](#steps-for-use) 8 | - [Log in and verify credentials:](#log-in-and-verify-credentials) 9 | - [User Privileges](#user-privileges) 10 | - [Download templates](#download-templates) 11 | - [Formatting your CSV](#formatting-your-csv) 12 | - [Object Updates](#object-updates) 13 | - [Single Attribute Updates](#single-attribute-updates) 14 | - [Multiple Attribute Updates](#multiple-attribute-updates) 15 | - [Enforcing Mobile Device Names](#enforcing-mobile-device-names) 16 | - [Updating Extension Attributes](#updating-extension-attributes) 17 | - [Clearing Existing Attribute Values](#clearing-existing-attribute-values) 18 | - [Static Group Updates](#static-group-updates) 19 | - [Prestage Scope Updates](#prestage-scope-updates) 20 | - [Classic Mode Group/Prestage Updates](#classic-mode-groupprestage-updates) 21 | - [Preflight and Preview](#preflight-and-preview) 22 | - [Send your updates](#send-your-updates) 23 | - [Top Tips](#top-tips) 24 | 25 | ## [A note on MUT and v6:](#a-note-on-mut-and-v6) 26 | 27 | Welcome to MUT v6. If you're familiar with MUT v5, and MUT Classic, MUT v6 will probably feel very familiar to you. If this is your first time here, I recommend you read the ReadMe in its entirety. 28 | 29 | If you'd like a quick video tour of the new features of v6, check our the intro video here: https://www.youtube.com/watch?v=G1CWoWbr_TI 30 | 31 | **MUT is an incredibly powerful tool, and with great power comes great ability-to-break-things. Always, ALWAYS run a small test update on just a couple devices to make sure your updates are working as intended, and your scoping does not break due to the updates.** 32 | 33 | ## [Introduction:](#introduction) 34 | 35 | The MUT is a native macOS application written in Swift, which allows Jamf admins to make mass updates to attributes (such as username, asset tag, or extension attribute) of their devices and users in Jamf. 36 | 37 | Admins can also make mass changes to static groups, and the scope of prestage enrollments via MUT. 38 | 39 | ![The MUT Main Screen](https://imgur.com/vY00wLc.png "The MUT Main Screen") 40 | 41 | ## [Steps for use:](#steps-for-use) 42 | 43 | ### [Log in and verify credentials:](#log-in) 44 | 45 | MUT will perform checks on your credentials automatically when you log in. If it senses a problem with the credentials you provide, it will let you know what those problems are. 46 | 47 | MUT performs these checks by generating a token for the new JPAPI. Any user is able to generate a token for the JPAPI, so there is no longer a need for the "bypass authentication" checkbox to exist. This checkbox has been changed to an "allow insecure SSL" checkbox. You can use this checkbox if you'd like to allow insecure SSL, but MUT will perform standard SSL checks per ATS by default. 48 | 49 | #### [User Privileges](#user-privileges) 50 | 51 | All Privileges will be found in Jamf Pro Server Actions and only need to have Update checked — unless noted otherwise. 52 | 53 | **Computer Template** - Computers, Users 54 | 55 | **Groups and PreStage Templates** (recommended to have separate users for least privilege access): 56 | 57 | *Computer Groups / PreStage* - Static Computer Groups, Computer Prestage Enrollments 58 | 59 | *Device Groups / PreStage* - Static Mobile Device Groups, Mobile Device PreStage Enrollments 60 | 61 | *User Groups* - Static User Groups 62 | 63 | **Mobile Device template** - Mobile Devices, Users 64 | 65 | **User Template** - Users (Update, Create) 66 | 67 | ### [Download templates](#download-templates) 68 | 69 | When you first authenticate, you will be presented with a relatively simplistic screen, which will have a large button to download the CSV templates needed to use MUT. Note that these templates tend to change with MUT upgrades, in order to allow new features, so it is recommended that you re-download these templates after updates. 70 | 71 | Upon pressing the Download CSV Templates button, MUT will ask you where you'd like to save the MUT Templates.zip. The MUT.log is no longer located in the Templates directory, and can now be found under the Settings menu at the top of the page. 72 | 73 | ![The MUT CSV Download Prompt](https://imgur.com/hOfgE3O.png "The MUT CSV Download Prompt") 74 | 75 | ### [Formatting your CSV](#format-csv) 76 | ##### [Object Updates](#objects) 77 | In order to update information for an object (such as a computer or mobile device) in Jamf Pro, you will need to use the associated CSV template that MUT saved where you specified. For example, to update Computer objects, you will need to use the "ComputerTemplate.csv". 78 | 79 | MUT performs verification checks against the header row of this CSV file, and it is very important that you do not modify the header row (such as deleting columns, or rearranging the columns) prior to uploading your CSV file. If you do, MUT will reject the file. 80 | 81 | ###### [Single Attribute Updates](#single-attribute) 82 | One common use for MUT is to update single attributes, such as updating the username assigned to a set of devices, or populating the Asset Tag or Barcode for a device. 83 | 84 | The most important thing to remember is that any cell left completely blank in your CSV will be ignored. Please note that a space is not the same as completely blank. There is a big difference between "" and " ". 85 | 86 | If a field is going to be ignored in MUT, your preflight check will show the phrase "(unchanged)" in blue for that field. 87 | 88 | If you wanted to update the Username on a set of devices, the CSV file would look like this (with more columns after the ellipsis.): 89 | 90 | | Computer Serial | Display Name | Asset Tag | Barcode 1 | Barcode 2 | Username | Real Name | ... | 91 | | --------------- | ------------ | --------- | --------- | --------- | ------------- | --------- | --- | 92 | | C13371337 | | | 1337 | | | | | 93 | 94 | And MUT will display a screen such as the following when you run your pre-flight check: 95 | 96 | ![Single Attribute Updates](https://imgur.com/Qw4cHH4.png "Single Attribute Updates") 97 | 98 | 99 | ###### [Multiple Attribute Updates](#multiple-attributes) 100 | Perhaps the MOST requested feature for MUT has been the ability to update multiple attributes at once. This feature is now available in MUT. 101 | 102 | To update multiple attributes for an object at once, simply populate all of those fields in the CSV file. When you run your pre-flight check, you will be presented with all of the information that will be updating (and any blank fields will still display as "(unchanged)"). 103 | 104 | If you wanted to update the Asset Tag, Barcodes, Username, as well as Real Name on a set of devices, the CSV file would look like this (with more columns after the ellipsis.): 105 | 106 | | Computer Serial | Display Name | Asset Tag | Barcode 1 | Barcode 2 | Username | Real Name | ... | 107 | | --------------- | ------------ | --------- | ---------- | ---------- | ------------- | ------------- | --- | 108 | | C1111111 | | MUT-111 | 0123456789 | 0123456789 | mike.levenick | Mike Levenick | | 109 | | C2222222 | | MUT-222 | 1234567890 | 1234567890 | ben.whitis | Ben Whitis | | 110 | 111 | And MUT will display a screen such as the following when you run your pre-flight check: 112 | 113 | ![Multiple Attribute Updates](https://imgur.com/EWdFvjp.png "Multiple Attribute Updates") 114 | 115 | ###### [Enforcing Mobile Device Names](#enforcing-mobile-device-names) 116 | As of Jamf Pro 10.33, there is an endpoint which allows for the Enforce Name checkbox to be checked or unchecked via the Jamf Pro API. 117 | 118 | MUT v6 can leverage this endpoint, and can allow you to either enforce or unenforce the name of your Mobile Device. There is a new "Enforce Name" field in the Mobile Devices template, and this field accepts a boolean value of TRUE or FALSE. 119 | 120 | These updates can be done on their own, or in combination with any other updates. To set a mobile device name and enforce that name, as well as update the Asset Tag, Barcode, and Username, your CSV would look something like this: 121 | 122 | | Computer Serial | Display Name | Enforce Name | Asset Tag | Barcode 1 | Barcode 2 | Username | ... | 123 | | --------------- | ------------ | --------- | ---------- | ---------- | ------------- | ------------- | --- | 124 | | C1111111 | Mikes iPhone | TRUE | MUT-111 | 0123456789 | | mike.levenick | | 125 | | C2222222 |Mikes iPad | TRUE | MUT-222 | 1234567890 | | ben.whitis | | 126 | 127 | ###### [Updating Extension Attributes](#extension-attributes) 128 | MUT is also able to update Extension Attributes for a device or a user. In order to do this, you must first identify the Extension Attribute ID number. You can find this number in the URL while you are viewing an extension attribute in Jamf Pro's GUI under Settings (gear icon) > Computer Management > Extension Attributes > Click on the EA you want to update to bring it up. 129 | 130 | For example, the EA ID of the displayed Extension Attribute here is "2". 131 | 132 | ![Extension Attribute ID 2](https://i.imgur.com/iO0Pyjs.png) 133 | 134 | To update an Extension Attribute, simply add your own header for a new column **AFTER** all of the existing columns of your template, and put the string "EA_#" in the header, where # is the ID of the EA you would like to update. 135 | 136 | For example, to update an Extension Attribute with the ID: "2", we would add a new column with header "EA_2", and then place the values for that EA in the column. 137 | 138 | Your CSV would look something like this (Some columns have been removed simply to make it fit. Please DO NOT remove columns from your CSV): 139 | 140 | | Computer Serial | Display Name | Asset Tag | Barcode 1 | ... | ... | Site (ID or Name) | EA_2 | 141 | | --------------- | ------------ | --------- | ---------- | --- | --- | ----------------- | --------- | 142 | | C1111111 | | MUT-111 | 0123456789 | | | | New Value | 143 | | C2222222 | | MUT-222 | 1234567890 | | | | New Value | 144 | 145 | And MUT will display a screen such as the following when you run your pre-flight check. Note the new field added at the bottom with EA_2. Also note that you will need to scroll down in the right hand window in order to see all of the fields that MUT can update now. There are quite a few!: 146 | 147 | ![Extension Attribute Updates](https://imgur.com/GhB7y0G.png "Extension Attribute Updates") 148 | 149 | ###### [Clearing Existing Attribute Values](#clearing-attributes) 150 | Another common workflow with MUT is to clear out existing attributes. This happens for example in situations where a group of devices are being redistributed to new users, or retired, and need the username and related information cleared off of them. 151 | 152 | Because MUT ignores blank fields in your CSV now, a specific string must be used to tell MUT to clear values. This string is currently "CLEAR!" (with exclamation point, without quotes.) In the Preflight GUI, MUT will display the string "WILL BE CLEARED" in all red, to let you know that the field is being cleared. 153 | 154 | If you wanted to clear user information from a device, your CSV would look something like this (with more columns after the ellipsis.): 155 | 156 | | Computer Serial | Display Name | Asset Tag | Barcode 1 | Barcode 2 | Username | Real Name | ... | 157 | | --------------- | ------------ | --------- | ---------- | ---------- | -------- | --------- | --- | 158 | | C1111111 | | MUT-111 | 0123456789 | 0123456789 | CLEAR! | CLEAR! | | 159 | | C2222222 | | MUT-222 | 1234567890 | 1234567890 | CLEAR! | CLEAR! | | 160 | 161 | And MUT will display a screen such as the following when you run your pre-flight check (I went a little bit overboard with clearing values for this screenshot...): 162 | 163 | ![Clear Attribute Values](https://imgur.com/lrCSwp6.png "Clear Attribute Values") 164 | 165 | ##### [Static Group Updates](#groups) 166 | MUT v6 is able to update the contents of a Static Group (computers, mobile devices, or users). It is able to either add objects to a group, remove objects from a group, or replace the entire current contents of that group. 167 | 168 | In order to do this, your CSV file should contain nothing but a single column of identifiers for the objects to be added, removed, or replaced in the scope of that group. This identifier can be either Serial Number or ID for computers and mobile devices, or Username or ID for users. 169 | 170 | Your CSV file should look like this: 171 | 172 | | Serial Numbers or Usernames | 173 | | --- | 174 | | C1111111 | 175 | | C2222222 | 176 | | C3333333 | 177 | | C4444444 | 178 | | C5555555 | 179 | 180 | When you upload this CSV to MUT, you will be taken to a slightly different screen which contains dropdowns. These dropdowns are how you will select what action to take place. It also contains a box, where you must put the ID of the static group to be modified. This ID can be found in the URL while viewing the group to be modified. 181 | 182 | For example, the Group ID for the following group is "3". 183 | 184 | ![Group ID](https://i.imgur.com/5iAawXe.png) 185 | 186 | But let's pretend our group number was 1337; to add the devices in question to Computer Static Group 1337, your MUT GUI would look like this: 187 | 188 | ![Static Group Update](https://imgur.com/BsDX0IH.png "Static Group Update") 189 | 190 | ##### [Prestage Scope Updates](#prestages) 191 | One of the new features of MUT v6 is the ability to modify the scope of prestages. This feature REQUIRES Jamf Pro v10.24+ in order to function properly. 192 | 193 | In order to do this, your CSV file should contain nothing but a single column of identifiers for the objects to be added, removed, or replaced in the scope of that prestage. This identifier can be either Serial Number or ID for computers and mobile devices. 194 | 195 | Your CSV file should look like this: 196 | 197 | | Serial Numbers or Usernames | 198 | | --- | 199 | | C1111111 | 200 | | C2222222 | 201 | | C3333333 | 202 | | C4444444 | 203 | | C5555555 | 204 | 205 | When you upload this CSV to MUT, you will be taken to a slightly different screen which contains dropdowns. These dropdowns are how you will select what action to take place. It also contains a box, where you must put the ID of the prestage to be modified. This ID can be found in the URL while viewing the prestage to be modified. 206 | 207 | For example, the Prestage ID for the following group is "1". 208 | 209 | ![Prestage ID](https://i.imgur.com/B87eWPT.png) 210 | 211 | To add the devices in question to Prestage 1, your MUT GUI would look like this: 212 | 213 | ![Prestage 1 Update](https://imgur.com/6Q5RP1d.png "Prestage 1 Update") 214 | 215 | ##### [Classic Mode Group/Prestage Updates](#classic-mode-groupprestage-updates) 216 | The MUT v5 used a new method to update groups and prestages. This new method was far more efficient, but required the CSV to be perfect. Any lines with devices that were already in scope, or no longer in the environment would cause the entire update run to fail. Because of this, MUT Classic was made available, which updated group or prestage line-by-line, just as MUT v4 did. 217 | 218 | These line-by-line submissions are far less efficient, and take significantly longer, but if there is a bad line in the CSV, MUT will simply skip over it and move on. 219 | 220 | Now, in MUT v6, you get the best of both worlds. MUT v6 will initially attempt the new, more efficient update method, but on the off chance that it fails, you will be presented with the option to attempt a "Classic Mode" update. 221 | 222 | ![Classic mode prompt](https://imgur.com/7YF7Mtr.png "Classic mode prompt") 223 | 224 | It is important to note that incorrect lines will still fail with this Classic Mode, but those lines will be reported in the MUT.log for later review, and any other lines will still go through successfully. 225 | 226 | It is important to note that Classic Mode is not compatible with "Replace" update attempts via MUT, as the entire Group or Prestage would simply be replaced with the last working line of the CSV. 227 | 228 | ![Classic mode cannot do replace](https://imgur.com/3TbREuN.png "Classic mode cannot do replace") 229 | 230 | ### [Preflight and Preview](#preflight) 231 | Veterans of MUT are likely used to needing to run a PreFlight Check prior to every update, and then reviewing the information prior to submitting. 232 | 233 | PreFlight Checks in v6 for Object Attribute updates now happen as soon as you upload your CSV. If there is an issue with your CSV file, you will be alerted as soon as you attempt to upload it. MUT should also not let you run any updates if your CSV contains errors. 234 | 235 | Preflight Checks in v6 for group and prestage scope updates will happen partly when you upload the CSV, but you must run a separate PreFlight Check once you have populated the dropdowns and identifier boxes. The Submit Updates button will not appear until you have populated those fields, and then run the PreFlight Check. 236 | 237 | ### [Send your updates](#send-your-updates) 238 | It is STRONGLY recommended that you do a small, test update of just a couple devices before making mass updates with MUT--especially if you are new to the tool. 239 | 240 | Once you are confident in the updates to be submitted to your Jamf Pro server, you can hit the "Submit Updates" button. 241 | 242 | Very little status/result information is displayed in the main GUI of MUT. You will now find a MUT.log by heading to the Settings menu at the top of your screen. This new log file contains much more verbose information about the status of your updates, and should help with troubleshooting significantly. 243 | 244 | The log file looks a bit like this: 245 | 246 | ![MUT.log](https://i.imgur.com/nJruxUe.png) 247 | 248 | ## [Top Tips](#top-tips) 249 | 250 | * There is an option in the top Menu Bar under "Settings" to change the character which separates items on your CSV file to either a comma (,) or a semicolon (;). This is especially useful for international folks who delimit their CSV files by semicolon by default, or for folks who wish to include commas in their attribute values. 251 | * There is an option in the top Menu Bar under "Settings" to clear any stored values that you may have by default, including Delimiter, Username, and your server URL. 252 | * MUT attempts to determine if you are using Usernames or User IDs for user updates, by checking if Column A contains Integers--but on the chance that your usernames are integers (which happens most often if an Employee ID or Student ID is also the Username) MUT can get confused. If this is the case in your environment, select "My Usernames are Ints" from the settings menu 253 | -------------------------------------------------------------------------------- /The MUT.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /The MUT.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /The MUT.xcworkspace/xcuserdata/ben.whitis.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /The MUT/APIAccess/APIAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIAccess.swift 3 | // testProj 4 | // 5 | // Created by Andrew Pirkl on 11/24/19. 6 | // Copyright © 2019 PIrklator. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | /** 11 | Run operations against an API 12 | */ 13 | public class APIAccess { 14 | 15 | /** 16 | Add and run a datatask by a URLRequest with the SessionHandler singleton's URLSession 17 | */ 18 | func runCall(mySession: URLSession, myRequest : URLRequest,completion: @escaping (Data?,URLResponse?,Error?) -> Void) 19 | { 20 | mySession.dataTask(with: myRequest) 21 | { (data: Data?, response: URLResponse?, error: Error?) in 22 | return completion(data,response,error) 23 | }.resume() 24 | } 25 | 26 | /** 27 | Handle result of a dataTask 28 | */ 29 | public func parseCall(data: Data?, response: URLResponse?, error: Error?) -> Void 30 | { 31 | if let error = error { 32 | //print("got an error") 33 | //print(error) 34 | return 35 | } 36 | guard let response = response else { 37 | //print("empty response") 38 | return 39 | } 40 | guard let data = data else { 41 | //print("empty data") 42 | return 43 | } 44 | // check for response errors, and handle data 45 | //print("We got some data up in here") 46 | let responseData = String(data: data, encoding: String.Encoding.utf8) 47 | //print((String(describing: responseData))) 48 | } 49 | 50 | 51 | public func testCall() 52 | { 53 | let creds = String("user:pass").toBase64() 54 | var request = URLRequest(url: URL(string: "https://tryitout.jamfcloud.com/JSSResource/computers")!) 55 | request.httpMethod = HTTPMethod.get.rawValue 56 | request.addValue("Basic \(creds)", forHTTPHeaderField: "Authorization") 57 | runCall(mySession: SessionHandler.SharedSessionHandler.mySession,myRequest: request) 58 | {data,response,error in self.parseCall(data: data,response: response,error: error)} 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /The MUT/APIAccess/APIDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIDelegate.swift 3 | // testProj 4 | // 5 | // Created by Andrew Pirkl on 11/24/19. 6 | // Copyright © 2019 PIrklator. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | /** 11 | URLSessionDelegate to ignore or force authentication to the server. 12 | */ 13 | public class APIDelegate: NSObject, URLSessionDelegate 14 | { 15 | private var allowUntrusted: Bool = false 16 | 17 | let logMan = logManager() 18 | 19 | func setTrust(_allowUntrusted: Bool) 20 | { 21 | self.allowUntrusted = _allowUntrusted 22 | } 23 | 24 | public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping( URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 25 | if allowUntrusted { 26 | logMan.writeLog(level: .warn, logString: "The user has selected to allow untrusted SSL. MUT will not be performing SSL verification. This is potentially unsafe.") 27 | completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 28 | } else { 29 | completionHandler(.performDefaultHandling, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /The MUT/APIAccess/APIFunctions2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // The MUT 4 | // 5 | // Created by Andrew Pirkl on 11/25/19 6 | // Abstracted from APIFunctions by Benjamin Whitis on 6/14/19. 7 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | import Cocoa 12 | 13 | /** 14 | APIFunctions using SessionHandler for a global URLSession 15 | */ 16 | public class APIFunctions { 17 | 18 | let dataPrep = dataPreparation() 19 | let logMan = logManager() 20 | let tokenMan = tokenManagement() 21 | // set sessionHandler to SessionHandler singleton for easy access 22 | let sessionHandler = SessionHandler.SharedSessionHandler 23 | 24 | public func putData(endpoint: String, identifierType: String, identifier: String, allowUntrusted: Bool, xmlToPut: Data) -> (code: Int, body: Data?) { 25 | tokenMan.tokenRefresher() 26 | 27 | var responseCode = 400 28 | 29 | var responseBody: Data? 30 | 31 | let baseURL = dataPrep.generateURL(endpoint: endpoint, identifierType: identifierType, identifier: identifier, jpapi: false, jpapiVersion: "") 32 | 33 | let encodedURL = NSURL(string: "\(baseURL)")! as URL 34 | logMan.writeLog(level: .info, logString: "Submitting a PUT to \(encodedURL.absoluteString)") 35 | // Changed to use SessionHandler to configure trust 36 | sessionHandler.setAllowUntrusted(allowUntrusted: allowUntrusted) 37 | // The semaphore is what allows us to force the code to wait for this request to complete 38 | // Without the semaphore, MUT will queue up a request for every single line of the CSV simultaneously 39 | let semaphore = DispatchSemaphore(value: 0) 40 | let request = NSMutableURLRequest(url: encodedURL) 41 | 42 | // Determine the request type. If we pass this in with a variable, we could use this function for PUT as well. 43 | request.httpMethod = "PUT" 44 | request.httpBody = xmlToPut 45 | // add headers to request for content-type and authorization since not using URLSession headers 46 | request.addValue("Bearer \(Token.value!)", forHTTPHeaderField: "Authorization" ) 47 | request.addValue("text/xml", forHTTPHeaderField: "Content-Type") 48 | // set session to use 49 | let session = sessionHandler.mySession 50 | 51 | // Completion handler. This is what ensures that the response is good/bad 52 | // and also what handles the semaphore 53 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 54 | (data, response, error) -> Void in 55 | if let httpResponse = response as? HTTPURLResponse { 56 | responseCode = httpResponse.statusCode 57 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 58 | // Good response from API 59 | self.logMan.writeLog(level: .info, logString: "Successful PUT completed. \(httpResponse.statusCode).") 60 | self.logMan.writeLog(level: .info, logString: String(decoding: data!, as: UTF8.self)) 61 | } else { 62 | // Bad Response from API 63 | self.logMan.writeLog(level: .error, logString: "Failed PUT. \(httpResponse.statusCode).") 64 | self.logMan.writeLog(level: .error, logString: String(decoding: data!, as: UTF8.self)) // ADVANCED DEBUGGING 65 | } 66 | responseBody = data 67 | semaphore.signal() // Signal completion to the semaphore 68 | } 69 | 70 | if error != nil { 71 | self.logMan.writeLog(level: .fatal, logString: error!.localizedDescription) 72 | semaphore.signal() // Signal completion to the semaphore 73 | 74 | } 75 | }) 76 | task.resume() // Kick off the actual GET here 77 | semaphore.wait() // Wait for the semaphore before moving on to the return value 78 | return (responseCode, responseBody) 79 | } 80 | 81 | public func patchData(passedUrl: String, endpoint: String, endpointVersion: String, identifier: String, allowUntrusted: Bool, jsonData: Data) -> Int { 82 | tokenMan.tokenRefresher() 83 | 84 | var responseCode = 400 85 | let encodedURL = dataPrep.generateJpapiURL(endpoint: endpoint, endpointVersion: endpointVersion, identifier: identifier) 86 | 87 | //logMan.writeLog(level: .info, logString: "Submitting a PATCH to \(encodedURL.absoluteString)") // Re-enable these in debug mode when available. 88 | //logMan.writeLog(level: .info, logString: String(decoding: jsonData, as: UTF8.self)) // Re-enable these in debug mode when available. 89 | // Changed to use SessionHandler to configure trust 90 | sessionHandler.setAllowUntrusted(allowUntrusted: allowUntrusted) 91 | // The semaphore is what allows us to force the code to wait for this request to complete 92 | // Without the semaphore, MUT will queue up a request for every single line of the CSV simultaneously 93 | let semaphore = DispatchSemaphore(value: 0) 94 | let request = NSMutableURLRequest(url: encodedURL) 95 | 96 | request.httpMethod = "PATCH" 97 | request.httpBody = jsonData 98 | // add headers to request for content-type and authorization since not using URLSession headers 99 | request.addValue("Bearer \(Token.value!)", forHTTPHeaderField: "Authorization" ) 100 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 101 | // set session to use 102 | let session = sessionHandler.mySession 103 | // Completion handler. This is what ensures that the response is good/bad 104 | // and also what handles the semaphore 105 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 106 | (data, response, error) -> Void in 107 | if let httpResponse = response as? HTTPURLResponse { 108 | responseCode = httpResponse.statusCode 109 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 110 | // Good response from API 111 | self.logMan.writeLog(level: .info, logString: "Successful name enforcement request. \(httpResponse.statusCode).") 112 | // DEBUGGING 113 | //self.logMan.writeLog(level: .info, logString: String(decoding: data!, as: UTF8.self)) 114 | } else { 115 | // Bad Response from API 116 | self.logMan.writeLog(level: .error, logString: "Failed name enforcement request. \(httpResponse.statusCode).") 117 | self.logMan.writeLog(level: .error, logString: String(decoding: data!, as: UTF8.self)) 118 | } 119 | semaphore.signal() // Signal completion to the semaphore 120 | } 121 | 122 | if error != nil { 123 | self.logMan.writeLog(level: .fatal, logString: error!.localizedDescription) 124 | semaphore.signal() // Signal completion to the semaphore 125 | 126 | } 127 | }) 128 | task.resume() // Kick off the actual PATCH here 129 | semaphore.wait() // Wait for the semaphore before moving on to the return value 130 | return responseCode 131 | } 132 | 133 | public func getData(passedUrl: String, endpoint: String, endpointVersion: String, identifier: String, allowUntrusted: Bool) -> (code: Int, body: Data?) { 134 | tokenMan.tokenRefresher() 135 | 136 | var responseCode = 400 137 | var responseBody: Data? 138 | 139 | let encodedURL = dataPrep.generateJpapiURL(endpoint: endpoint, endpointVersion: endpointVersion, identifier: identifier) 140 | 141 | //logMan.writeLog(level: .info, logString: "Submitting a GET to \(encodedURL.absoluteString)") 142 | // Changed to use SessionHandler to configure trust 143 | sessionHandler.setAllowUntrusted(allowUntrusted: allowUntrusted) 144 | // The semaphore is what allows us to force the code to wait for this request to complete 145 | // Without the semaphore, MUT will queue up a request for every single line of the CSV simultaneously 146 | let semaphore = DispatchSemaphore(value: 0) 147 | let request = NSMutableURLRequest(url: encodedURL) 148 | request.httpMethod = "GET" 149 | // add headers to request for content-type and authorization since not using URLSession headers 150 | request.addValue("Bearer \(Token.value!)", forHTTPHeaderField: "Authorization" ) 151 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 152 | // set session to use 153 | let session = sessionHandler.mySession 154 | // Completion handler. This is what ensures that the response is good/bad 155 | // and also what handles the semaphore 156 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 157 | (data, response, error) -> Void in 158 | if let httpResponse = response as? HTTPURLResponse { 159 | responseCode = httpResponse.statusCode 160 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 161 | // Good response from API 162 | self.logMan.writeLog(level: .info, logString: "Successful GET completed. \(httpResponse.statusCode).") 163 | // DEBUGGING 164 | //self.logMan.writeLog(level: .info, logString: String(decoding: data!, as: UTF8.self)) 165 | } else { 166 | // Bad Response from API 167 | self.logMan.writeLog(level: .error, logString: "Failed PATCH. \(httpResponse.statusCode).") 168 | self.logMan.writeLog(level: .error, logString: String(decoding: data!, as: UTF8.self)) 169 | } 170 | responseBody = data 171 | semaphore.signal() // Signal completion to the semaphore 172 | } 173 | 174 | if error != nil { 175 | self.logMan.writeLog(level: .fatal, logString: error!.localizedDescription) 176 | semaphore.signal() // Signal completion to the semaphore 177 | 178 | } 179 | }) 180 | task.resume() // Kick off the actual PATCH here 181 | semaphore.wait() // Wait for the semaphore before moving on to the return value 182 | return (responseCode, responseBody) 183 | } 184 | 185 | public func getPrestageScope(passedUrl: URL, token: String, endpoint: String, allowUntrusted: Bool) -> Data { 186 | tokenMan.tokenRefresher() 187 | logMan.writeLog(level: .info, logString: "Getting current prestage scope from \(passedUrl.absoluteString)") 188 | // Changed to use SessionHandler to configure trust 189 | sessionHandler.setAllowUntrusted(allowUntrusted: allowUntrusted) 190 | var globalResponse = "nil".data(using: String.Encoding.utf8, allowLossyConversion: false)! // The semaphore is what allows us to force the code to wait for this request to complete 191 | // Without the semaphore, MUT will queue up a request for every single line of the CSV simultaneously 192 | let semaphore = DispatchSemaphore(value: 0) 193 | let request = NSMutableURLRequest(url: passedUrl) 194 | 195 | // Determine the request type. If we pass this in with a variable, we could use this function for PUT as well. 196 | request.httpMethod = "GET" 197 | // add headers to request for content-type and authorization since not using URLSession headers 198 | request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization" ) 199 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 200 | // set session to use 201 | let session = sessionHandler.mySession 202 | 203 | // Completion handler. This is what ensures that the response is good/bad 204 | // and also what handles the semaphore 205 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 206 | (data, response, error) -> Void in 207 | if let httpResponse = response as? HTTPURLResponse { 208 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 209 | // Good response from API 210 | globalResponse = data! 211 | self.logMan.writeLog(level: .info, logString: "Successful GET completed. \(httpResponse.statusCode).") 212 | //self.logMan.writeLog(level: .info, logString: String(decoding: data!, as: UTF8.self)) 213 | } else { 214 | // Bad Response from API 215 | globalResponse = data! 216 | self.logMan.writeLog(level: .error, logString: "Failed GET. \(httpResponse.statusCode).") 217 | self.logMan.writeLog(level: .error, logString: String(decoding: data!, as: UTF8.self)) // ADVANCED DEBUGGING 218 | } 219 | semaphore.signal() // Signal completion to the semaphore 220 | } 221 | 222 | if error != nil { 223 | globalResponse = data! 224 | self.logMan.writeLog(level: .fatal, logString: error!.localizedDescription) 225 | semaphore.signal() // Signal completion to the semaphore 226 | 227 | } 228 | }) 229 | task.resume() // Kick off the actual GET here 230 | semaphore.wait() // Wait for the semaphore before moving on to the return value 231 | return globalResponse 232 | } 233 | 234 | // HTTP method DELETE no longer supported for prestage updates. must use /scope/delete-multiple URL 235 | public func updatePrestage(endpoint: String, prestageID: String, jpapiVersion: String, token: String, jsonToSubmit: Data, httpMethod: String, allowUntrusted: Bool) -> Int { 236 | tokenMan.tokenRefresher() 237 | var returnCode = 400 238 | 239 | let baseURL = dataPrep.generatePrestageURL(endpoint: endpoint, prestageID: prestageID, jpapiVersion: jpapiVersion, httpMethod: httpMethod) 240 | 241 | let encodedURL = NSURL(string: "\(baseURL)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://null")! as URL 242 | logMan.writeLog(level: .info, logString: "Updating the current prestage scope at \(encodedURL.absoluteString)") 243 | // Changed to use SessionHandler to configure trust 244 | sessionHandler.setAllowUntrusted(allowUntrusted: allowUntrusted) 245 | var globalResponse = "nil".data(using: String.Encoding.utf8, allowLossyConversion: false)! // The semaphore is what allows us to force the code to wait for this request to complete 246 | // Without the semaphore, MUT will queue up a request for every single line of the CSV simultaneously 247 | let semaphore = DispatchSemaphore(value: 0) 248 | let request = NSMutableURLRequest(url: encodedURL) 249 | 250 | // Determine the request type. If we pass this in with a variable, we could use this function for PUT as well. 251 | request.httpMethod = httpMethod 252 | 253 | // DELETE is no longer supported for prestages 254 | if (endpoint == "computer-prestages" || endpoint == "mobile-device-prestages" ) && httpMethod == "DELETE" { 255 | request.httpMethod = "POST" 256 | } 257 | 258 | request.httpBody = jsonToSubmit 259 | // add headers to request for content-type and authorization since not using URLSession headers 260 | request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization" ) 261 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 262 | // set session to use 263 | let session = sessionHandler.mySession 264 | 265 | // Completion handler. This is what ensures that the response is good/bad 266 | // and also what handles the semaphore 267 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 268 | (data, response, error) -> Void in 269 | if let httpResponse = response as? HTTPURLResponse { 270 | returnCode = httpResponse.statusCode 271 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 272 | // Good response from API 273 | globalResponse = data! 274 | self.logMan.writeLog(level: .info, logString: "Successful scope update completed. \(httpResponse.statusCode).") 275 | //self.logMan.writeLog(level: .info, logString: String(decoding: data!, as: UTF8.self)) 276 | } else { 277 | // Bad Response from API 278 | globalResponse = data! 279 | self.logMan.writeLog(level: .error, logString: "Failed scope update. \(httpResponse.statusCode).") 280 | self.logMan.writeLog(level: .error, logString: String(decoding: data!, as: UTF8.self)) // ADVANCED DEBUGGING 281 | } 282 | semaphore.signal() // Signal completion to the semaphore 283 | } 284 | 285 | if error != nil { 286 | globalResponse = data! 287 | self.logMan.writeLog(level: .fatal, logString: error!.localizedDescription) 288 | semaphore.signal() // Signal completion to the semaphore 289 | 290 | } 291 | }) 292 | task.resume() // Kick off the actual GET here 293 | semaphore.wait() // Wait for the semaphore before moving on to the return value 294 | return returnCode 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /The MUT/APIAccess/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethods.swift 3 | // testProj 4 | // 5 | // Created by Andrew Pirkl on 11/24/19. 6 | // Copyright © 2019 PIrklator. All rights reserved. 7 | // 8 | 9 | enum HTTPMethod: String { 10 | case get, put, post, delete 11 | } 12 | -------------------------------------------------------------------------------- /The MUT/APIAccess/HTTPStatusCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPStatusCode.swift 3 | // testProj 4 | // 5 | // Created by Andrew Pirkl on 11/24/19. 6 | // Copyright © 2019 PIrklator. All rights reserved. 7 | // 8 | 9 | enum HTTPStatusCode: Int { 10 | case Continue = 100 11 | case Success = 200 12 | case Unauthorized = 401 13 | case NotFound = 404 14 | } 15 | -------------------------------------------------------------------------------- /The MUT/APIAccess/SessionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionHandler.swift 3 | // testProj 4 | // 5 | // Created by Andrew Pirkl on 11/24/19. 6 | // Copyright © 2019 PIrklator. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | /** 11 | Global URLSession Handler 12 | */ 13 | public class SessionHandler 14 | { 15 | /** 16 | Access singleton session handler 17 | */ 18 | public static let SharedSessionHandler = SessionHandler() 19 | /** 20 | Access singleton URLSession 21 | */ 22 | public let mySession: URLSession 23 | 24 | private let myDelQueue = OperationQueue() 25 | private let myDelegate = APIDelegate() 26 | private init() 27 | { 28 | //print("initializing session") 29 | let config = URLSessionConfiguration.default 30 | config.httpMaximumConnectionsPerHost = 1 31 | self.mySession = URLSession(configuration: config, delegate: myDelegate, delegateQueue: myDelQueue) 32 | } 33 | /* 34 | Set trust for singleton. This will switch trust for queued tasks. 35 | */ 36 | public func setAllowUntrusted(allowUntrusted : Bool){ 37 | myDelegate.setTrust(_allowUntrusted: allowUntrusted) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /The MUT/APIAccess/StringExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtension.swift 3 | // testProj 4 | // 5 | // Created by Andrew Pirkl on 11/24/19. 6 | // Copyright © 2019 PIrklator. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | /** 11 | Add conversion to and from base64 to String 12 | */ 13 | extension String { 14 | func fromBase64() -> String? { 15 | guard let data = Data(base64Encoded: self) else { 16 | return nil 17 | } 18 | return String(data: data, encoding: .utf8) 19 | } 20 | func toBase64() -> String { 21 | return Data(self.utf8).base64EncodedString() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /The MUT/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // The MUT v5 4 | // 5 | // Created by Michael Levenick on 5/24/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | let delegateDefaults = UserDefaults.standard 15 | let popUp = popPrompt() 16 | let logMan = logManager() 17 | 18 | func applicationDidFinishLaunching(_ aNotification: Notification) { 19 | // Insert code here to initialize your application 20 | } 21 | 22 | func applicationWillTerminate(_ aNotification: Notification) { 23 | // Insert code here to tear down your application 24 | } 25 | 26 | // MARK: - Core Data stack 27 | 28 | lazy var applicationDocumentsDirectory: Foundation.URL = { 29 | // The directory the application uses to store the Core Data store file. This code uses a directory named "com.apple.toolsQA.CocoaApp_CD" in the user's Application Support directory. 30 | let urls = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) 31 | let appSupportURL = urls[urls.count - 1] 32 | return appSupportURL.appendingPathComponent("com.apple.toolsQA.CocoaApp_CD") 33 | }() 34 | 35 | lazy var managedObjectModel: NSManagedObjectModel = { 36 | // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model. 37 | let modelURL = Bundle.main.url(forResource: "The_MUT", withExtension: "momd")! 38 | return NSManagedObjectModel(contentsOf: modelURL)! 39 | }() 40 | 41 | lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { 42 | // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) This property is optional since there are legitimate error conditions that could cause the creation of the store to fail. 43 | let fileManager = FileManager.default 44 | var failError: NSError? = nil 45 | var shouldFail = false 46 | var failureReason = "There was an error creating or loading the application's saved data." 47 | 48 | // Make sure the application files directory is there 49 | do { 50 | let properties = try self.applicationDocumentsDirectory.resourceValues(forKeys: [URLResourceKey.isDirectoryKey]) 51 | if !properties.isDirectory! { 52 | failureReason = "Expected a folder to store application data, found a file \(self.applicationDocumentsDirectory.path)." 53 | shouldFail = true 54 | } 55 | } catch { 56 | let nserror = error as NSError 57 | if nserror.code == NSFileReadNoSuchFileError { 58 | do { 59 | try fileManager.createDirectory(atPath: self.applicationDocumentsDirectory.path, withIntermediateDirectories: true, attributes: nil) 60 | } catch { 61 | failError = nserror 62 | } 63 | } else { 64 | failError = nserror 65 | } 66 | } 67 | 68 | // Create the coordinator and store 69 | var coordinator: NSPersistentStoreCoordinator? = nil 70 | if failError == nil { 71 | coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) 72 | let url = self.applicationDocumentsDirectory.appendingPathComponent("The_MUT.storedata") 73 | do { 74 | try coordinator!.addPersistentStore(ofType: NSXMLStoreType, configurationName: nil, at: url, options: nil) 75 | } catch { 76 | // Replace this implementation with code to handle the error appropriately. 77 | 78 | /* 79 | Typical reasons for an error here include: 80 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 81 | * The device is out of space. 82 | * The store could not be migrated to the current model version. 83 | Check the error message to determine what the actual problem was. 84 | */ 85 | failError = error as NSError 86 | } 87 | } 88 | 89 | if shouldFail || (failError != nil) { 90 | // Report any error we got. 91 | if let error = failError { 92 | NSApplication.shared.presentError(error) 93 | fatalError("Unresolved error: \(error), \(error.userInfo)") 94 | } 95 | fatalError("Unsresolved error: \(failureReason)") 96 | } else { 97 | return coordinator! 98 | } 99 | }() 100 | 101 | lazy var managedObjectContext: NSManagedObjectContext = { 102 | // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail. 103 | let coordinator = self.persistentStoreCoordinator 104 | var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 105 | managedObjectContext.persistentStoreCoordinator = coordinator 106 | return managedObjectContext 107 | }() 108 | 109 | // MARK: - Core Data Saving and Undo support 110 | 111 | @IBAction func saveAction(_ sender: AnyObject?) { 112 | // Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user. 113 | if !managedObjectContext.commitEditing() { 114 | NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing before saving") 115 | } 116 | if managedObjectContext.hasChanges { 117 | do { 118 | try managedObjectContext.save() 119 | } catch { 120 | let nserror = error as NSError 121 | NSApplication.shared.presentError(nserror) 122 | } 123 | } 124 | } 125 | 126 | func windowWillReturnUndoManager(window: NSWindow) -> UndoManager? { 127 | // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application. 128 | return managedObjectContext.undoManager 129 | } 130 | 131 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 132 | return true 133 | } 134 | 135 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { 136 | // Save changes in the application's managed object context before the application terminates. 137 | 138 | if !managedObjectContext.commitEditing() { 139 | NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing to terminate") 140 | return .terminateCancel 141 | } 142 | 143 | if !managedObjectContext.hasChanges { 144 | return .terminateNow 145 | } 146 | 147 | do { 148 | try managedObjectContext.save() 149 | } catch { 150 | let nserror = error as NSError 151 | // Customize this code block to include application-specific recovery steps. 152 | let result = sender.presentError(nserror) 153 | if (result) { 154 | return .terminateCancel 155 | } 156 | 157 | let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message") 158 | let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info"); 159 | let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title") 160 | let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title") 161 | let alert = NSAlert() 162 | alert.messageText = question 163 | alert.informativeText = info 164 | alert.addButton(withTitle: quitButton) 165 | alert.addButton(withTitle: cancelButton) 166 | 167 | let answer = alert.runModal() 168 | if answer == NSApplication.ModalResponse.alertSecondButtonReturn { 169 | return .terminateCancel 170 | } 171 | } 172 | // If we got here, it is time to quit. 173 | return .terminateNow 174 | } 175 | 176 | @IBAction func btnOpenReadMe(_ sender: NSMenuItem) { 177 | 178 | if let url = URL(string: "https://github.com/mike-levenick/mut/blob/master/README.md") { 179 | if NSWorkspace.shared.open(url) { 180 | logMan.writeLog(level: .info, logString: "Opening ReadMe.") 181 | } 182 | } 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/256-1.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/32-1.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/512-1.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32-1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256-1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512-1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconLocked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconLocked.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconLocked.imageset/IconLocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/IconLocked.imageset/IconLocked.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconSafari.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconSafari.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconSafari.imageset/IconSafari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/IconSafari.imageset/IconSafari.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconUnlocked.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconUnlocked.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconUnlocked.imageset/IconUnlocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/IconUnlocked.imageset/IconUnlocked.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconUser.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IconUser.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/IconUser.imageset/IconUser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/IconUser.imageset/IconUser.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/V6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "V6.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/V6.imageset/V6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/V6.imageset/V6.png -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/V6Glow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "V6Glow.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /The MUT/Assets.xcassets/V6Glow.imageset/V6Glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/Assets.xcassets/V6Glow.imageset/V6Glow.png -------------------------------------------------------------------------------- /The MUT/CSVFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSVFunctions.swift 3 | // The MUT 4 | // 5 | // Created by Benjamin Whitis on 6/7/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | import CSV 12 | 13 | public class CSVManipulation { 14 | let popMan = popPrompt() 15 | let logMan = logManager() 16 | 17 | func copyZip() { 18 | let savePanel = NSSavePanel() 19 | savePanel.canCreateDirectories = true 20 | savePanel.title = "Location to save MUT Templates.zip" 21 | savePanel.prompt = "Save" 22 | savePanel.showsTagField = false 23 | savePanel.nameFieldStringValue = "MUT Templates.zip" 24 | savePanel.allowedFileTypes = ["zip"] 25 | savePanel.begin { [self] (result) in 26 | if result.rawValue == NSApplication.ModalResponse.OK.rawValue { 27 | guard let saveURL = savePanel.url else {return} 28 | guard let sourceURL = Bundle.main.url(forResource: "MUT Templates", withExtension: "zip") 29 | else { 30 | self.logMan.writeLog(level: .error, logString: "Error with getting the URL of the README file.") 31 | return 32 | } 33 | let fileManager = FileManager.default 34 | do { 35 | try fileManager.copyItem(at: sourceURL, to: saveURL) 36 | logMan.writeLog(level: .info, logString: "Saving template zip file to \(savePanel.url!.absoluteString.description.replacingOccurrences(of: "file://", with: ""))") 37 | } catch { 38 | logMan.writeLog(level: .error, logString: "Error copying the readme file to the templates directory.") 39 | } 40 | } 41 | } 42 | } 43 | 44 | func readCSV(pathToCSV: String, delimiter: UnicodeScalar) -> [[String]]{ 45 | let stream = InputStream(fileAtPath: pathToCSV)! 46 | 47 | // Initialize the array 48 | var csvArray = [[String]]() 49 | let csv = try! CSVReader(stream: stream, delimiter: delimiter) 50 | 51 | // For each row in the CSV, append it to the end of the array 52 | while let row = csv.next() { 53 | csvArray = (csvArray + [row]) 54 | } 55 | return csvArray 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /The MUT/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.2.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | $(MARKETING_VERSION) 25 | LSApplicationCategoryType 26 | public.app-category.developer-tools 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | Copyright © 2020 Levenick Enterprises LLC. All rights reserved. 36 | NSMainStoryboardFile 37 | Main 38 | NSPrincipalClass 39 | NSApplication 40 | 41 | 42 | -------------------------------------------------------------------------------- /The MUT/KeychainHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainHelper.swift 3 | // MUT 4 | // 5 | // Created by Michael Levenick on 11/29/22. 6 | // Copyright © 2022 Levenick Enterprises LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class KeyChainHelper { 12 | 13 | class func save(username: String, password: String, server: String) throws { 14 | let passData = password.data(using: String.Encoding.utf8)! 15 | 16 | // When deleting old credentials, only care if it's another MUT password. 17 | let deleteQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword, 18 | kSecAttrLabel as String: KeyVars.label, 19 | kSecAttrApplicationTag as String: KeyVars.tag] 20 | 21 | // When saving, care about everything. 22 | let saveQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword, 23 | kSecAttrAccount as String: username, 24 | kSecAttrServer as String: server, 25 | kSecAttrComment as String: "Server: \(server)", 26 | kSecAttrLabel as String: KeyVars.label, 27 | kSecAttrApplicationTag as String: KeyVars.tag, 28 | kSecValueData as String: passData] 29 | 30 | // Delete old credentials before re-saving new, valid credentials. 31 | SecItemDelete(deleteQuery as CFDictionary) 32 | 33 | // Save the new credentials that are confirmed-good. 34 | let status = SecItemAdd(saveQuery as CFDictionary, nil) 35 | guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status)} 36 | } 37 | 38 | class func load() throws { 39 | // Build the query for what to find 40 | let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, 41 | kSecAttrLabel as String: KeyVars.label, 42 | kSecAttrApplicationTag as String: KeyVars.tag, 43 | kSecMatchLimit as String: kSecMatchLimitOne, // Limiting to one result 44 | kSecReturnAttributes as String: true, 45 | kSecReturnData as String: true] 46 | 47 | var item: CFTypeRef? 48 | let status = SecItemCopyMatching(query as CFDictionary, &item) 49 | guard status != errSecItemNotFound else { throw KeychainError.noPassword } 50 | guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } 51 | 52 | guard let existingItem = item as? [String : Any], 53 | let passwordData = existingItem[kSecValueData as String] as? Data, 54 | let password = String(data: passwordData, encoding: String.Encoding.utf8), 55 | let username = existingItem[kSecAttrAccount as String] as? String, 56 | let server = existingItem[kSecAttrServer as String] as? String 57 | else { 58 | throw KeychainError.unexpectedPasswordData 59 | } 60 | Credentials.server = server 61 | Credentials.password = password 62 | Credentials.username = username 63 | } 64 | 65 | class func delete() throws { 66 | 67 | let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, 68 | kSecAttrLabel as String: KeyVars.label, 69 | kSecAttrApplicationTag as String: KeyVars.tag, 70 | kSecMatchLimit as String: kSecMatchLimitOne, // Limiting to one result 71 | ] 72 | let status = SecItemDelete(query as CFDictionary) 73 | guard status != errSecItemNotFound else { throw KeychainError.noPassword } 74 | guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } 75 | } 76 | 77 | class func createUniqueID() -> String { 78 | let uuid: CFUUID = CFUUIDCreate(nil) 79 | let cfStr: CFString = CFUUIDCreateString(nil, uuid) 80 | 81 | let swiftString: String = cfStr as String 82 | return swiftString 83 | } 84 | } 85 | 86 | public struct KeyVars { 87 | static let label = "com.jamf.mut.credentials" 88 | static let tag = label.data(using: .utf8)! 89 | } 90 | 91 | public struct Credentials { 92 | static var username: String? 93 | static var password: String? 94 | static var server: String? 95 | static var base64Encoded: String? 96 | } 97 | 98 | public struct Token { 99 | static var value: String? 100 | static var expiration: Int? 101 | static var data: Data? 102 | } 103 | 104 | enum KeychainError: Error { 105 | case noPassword 106 | case unexpectedPasswordData 107 | case unhandledError(status: OSStatus) 108 | } 109 | -------------------------------------------------------------------------------- /The MUT/LogManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogManager.swift 3 | // MUT 4 | // 5 | // Created by Michael Levenick on 7/12/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | // LOG LEVEL INFO 13 | // Level 0 - Error 14 | // Level 1 - Warning 15 | // Level 2 - Info 16 | // Level 3 - Verbose (Default) 17 | 18 | public enum logLevel: String { 19 | case debug 20 | case info 21 | case warn 22 | case error 23 | case fatal 24 | 25 | public var severity: Int { 26 | switch self { 27 | case .debug: return 3 28 | case .info: return 2 29 | case .warn: return 1 30 | case .error: return 0 31 | case .fatal: return -1 32 | } 33 | } 34 | } 35 | 36 | public class logManager { 37 | 38 | let logDefaults = UserDefaults.standard 39 | let fileManager = FileManager.default 40 | let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first 41 | let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("MUT") 42 | 43 | func openLog(){ 44 | let pathToOpen = libraryDirectory!.resolvingSymlinksInPath().standardizedFileURL.absoluteString.replacingOccurrences(of: "file://", with: "") + "MUT/MUT.log" 45 | NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: pathToOpen) 46 | //print(pathToOpen) 47 | } 48 | 49 | func createDirectory(){ 50 | if fileManager.fileExists(atPath: libraryURL!.path) { 51 | //NSLog("[INFO ]: Template Directory already exists. Skipping creation.") 52 | } else { 53 | //NSLog("[INFO ]: Template Directory does not exist. Creating.") 54 | do { 55 | try FileManager.default.createDirectory(at: libraryURL!, withIntermediateDirectories: true, attributes: nil) 56 | } catch { 57 | //NSLog("[ERROR ]: An error occured while creating the Template Directory. \(error).") 58 | } 59 | } 60 | } 61 | 62 | func generateCurrentTimeStamp () -> String { 63 | let formatter = DateFormatter() 64 | formatter.dateFormat = "yyyy-MM-dd hh:mm:ss" 65 | return (formatter.string(from: Date()) as NSString) as String 66 | } 67 | 68 | func writeLog(level: logLevel, logString: String) { 69 | 70 | let logLabel = "[\(level.rawValue.uppercased().padding(toLength: 6, withPad: " ", startingAt: 0))]:" 71 | 72 | let defaultLogLevelInt: Int 73 | if let defaultLogLevelStringValue = logDefaults.string(forKey: "LogLevel"), let intValue = Int(defaultLogLevelStringValue) { 74 | defaultLogLevelInt = intValue 75 | } else { 76 | defaultLogLevelInt = 3 77 | } 78 | 79 | if defaultLogLevelInt >= level.severity { 80 | createDirectory() 81 | let currentTime = generateCurrentTimeStamp() 82 | let logURL = libraryURL?.appendingPathComponent("MUT.log") 83 | let dateLogString = currentTime + " " + logLabel + " " + logString 84 | //NSLog("[INFO ]: Writing to MUT log file: '\(logString)'." 85 | do { 86 | try dateLogString.appendLineToURL(fileURL: logURL!) 87 | } 88 | catch { 89 | //NSLog("[ERROR ]: An error occured while writing to the Log. \(error).") 90 | } 91 | } 92 | } 93 | } 94 | 95 | extension String { 96 | func appendLineToURL(fileURL: URL) throws { 97 | try (self + "\n").appendToURL(fileURL: fileURL) 98 | } 99 | 100 | func appendToURL(fileURL: URL) throws { 101 | let data = self.data(using: String.Encoding.utf8)! 102 | try data.append(fileURL: fileURL) 103 | } 104 | } 105 | 106 | extension Data { 107 | func append(fileURL: URL) throws { 108 | if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) { 109 | defer { 110 | fileHandle.closeFile() 111 | } 112 | fileHandle.seekToEndOfFile() 113 | fileHandle.write(self) 114 | } 115 | else { 116 | try write(to: fileURL, options: .atomic) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /The MUT/MUT Templates.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/mut/a189bc21fbc921d9af3771c94e6f97aabaec3a63/The MUT/MUT Templates.zip -------------------------------------------------------------------------------- /The MUT/MobileDeviceXMLParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MobileDeviceXMLParser.swift 3 | // MUT 4 | // 5 | // Created by Nate Anderson on 8/31/21. 6 | // Copyright © 2021 Levenick Enterprises LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MobileDeviceXMLParser: NSObject, XMLParserDelegate { 12 | 13 | private var currentElement = "" 14 | private var currentElementValue = "" 15 | private var id = "" 16 | 17 | public func getMobileDeviceIdFromResponse(data: Data) -> String { 18 | let parser = XMLParser(data: data) 19 | parser.delegate = self 20 | parser.parse() 21 | return id 22 | } 23 | 24 | // starting tag of element 25 | func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { 26 | currentElement = elementName 27 | } 28 | 29 | // value of element 30 | func parser(_ parser: XMLParser, foundCharacters string: String) { 31 | if currentElement == "id" { 32 | id = string 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /The MUT/Models/JamfProVersionV1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JamfProVersion.swift 3 | // MUT 4 | // 5 | // Created by Nate Anderson on 9/1/21. 6 | // Copyright © 2021 Levenick Enterprises LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Respresents a Jamf Pro Version object in JPAPI. 12 | 13 | struct JamfProVersionV1: Codable { 14 | var version: String? 15 | } 16 | -------------------------------------------------------------------------------- /The MUT/Models/MobileDeviceV2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MobileDevice.swift 3 | // MUT 4 | // 5 | // Created by Nate Anderson on 8/31/21. 6 | // Copyright © 2021 Levenick Enterprises LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Respresents a Mobile Device object in JPAPI. Version (i.e. V2) of the object 12 | // matches the version of the endpoint, as the object may change with an API 13 | // version change. All fields included here so that they may be utilized in a 14 | // future version of MUT where the JPAPI is fully integrated. 15 | struct MobileDeviceV2: Codable { 16 | var name: String? 17 | var enforceName: Bool? 18 | var assetTag: String? 19 | var siteId: String? 20 | var timeZone: String? 21 | var location: Location? 22 | var updatedExtensionAttributes: [UpdatedExtensionAttributes]? 23 | var ios: Ios? 24 | var tvos: Tvos? 25 | } 26 | 27 | struct Location: Codable, Equatable { 28 | var username: String? 29 | var realName: String? 30 | var emailAddress: String? 31 | var position: String? 32 | var phoneNumber: String? 33 | var departmentId: String? 34 | var buildingId: String? 35 | var room: String? 36 | } 37 | 38 | struct UpdatedExtensionAttributes: Codable { 39 | var id: String? 40 | var name: String? 41 | var type: String? 42 | var value: [String] 43 | var extensionAttributeCollectionAllowed: Bool? 44 | } 45 | 46 | struct Ios: Codable, Equatable { 47 | var purchasing: Purchasing? 48 | } 49 | 50 | struct Tvos: Codable, Equatable { 51 | var airplayPassword: String? 52 | var purchasing: Purchasing? 53 | } 54 | 55 | struct Purchasing: Codable, Equatable { 56 | var purchased: Bool? 57 | var leased: Bool? 58 | var poNumber: String? 59 | var vendor: String? 60 | var appleCareId: String? 61 | var purchasePrice: String? 62 | var purchasingAccount: String? 63 | var poDate: String? 64 | var warrantyExpiresDate: String? 65 | var leaseExpiresDate: String? 66 | var lifeExpectancy: Int? 67 | var purchasingContact: String? 68 | } 69 | -------------------------------------------------------------------------------- /The MUT/Settings Menu/InsecureSSLPopover.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsecureSSLPopover.swift 3 | // MUT 4 | // 5 | // Created by Michael Levenick on 12/10/22. 6 | // Copyright © 2022 Levenick Enterprises LLC. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | class InsecureSSLPopOver: NSViewController { 13 | 14 | let logMan = logManager() 15 | 16 | 17 | override func viewDidAppear() { 18 | super.viewDidAppear() 19 | // Forces the window to be the size we want, not resizable 20 | preferredContentSize = NSSize(width: 450, height: 355) 21 | } 22 | 23 | @IBAction func btnAddingCertTokeychain(_ sender: Any) { 24 | if let url = URL(string: "https://support.apple.com/en-is/guide/keychain-access/kyca2431/mac") { 25 | if NSWorkspace.shared.open(url) { 26 | logMan.writeLog(level: .info, logString: "Opening Apple documentation on adding certificates to Keychain.") 27 | } 28 | } 29 | } 30 | 31 | 32 | @IBAction func btnChangingTrustSettings(_ sender: Any) { 33 | if let url = URL(string: "https://support.apple.com/en-is/guide/keychain-access/kyca11871/mac") { 34 | if NSWorkspace.shared.open(url) { 35 | logMan.writeLog(level: .info, logString: "Opening Apple documentation on changing trust settings for certificates.") 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /The MUT/Settings Menu/KeychainDefaultsPopover.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainDefaultsPopover.swift 3 | // MUT 4 | // 5 | // Created by Michael Levenick on 1/24/23. 6 | // Copyright © 2023 Levenick Enterprises LLC. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | class KeychainDefaultsPopover: NSViewController { 13 | 14 | let logMan = logManager() 15 | 16 | override func viewDidAppear() { 17 | super.viewDidAppear() 18 | // Forces the window to be the size we want, not resizable 19 | preferredContentSize = NSSize(width: 450, height: 425) 20 | } 21 | 22 | @IBAction func btnKeychainStorage(_ sender: Any) { 23 | if let url = URL(string: "https://support.apple.com/en-is/guide/security/secb0694df1a/web") { 24 | if NSWorkspace.shared.open(url) { 25 | logMan.writeLog(level: .info, logString: "Opening Apple documentation on Keychain Storage.") 26 | } 27 | } 28 | } 29 | 30 | @IBAction func btnDefaultsStorage(_ sender: Any) { 31 | if let url = URL(string: "https://developer.apple.com/documentation/foundation/userdefaults") { 32 | if NSWorkspace.shared.open(url) { 33 | logMan.writeLog(level: .info, logString: "Opening Apple documentation on Defaults Storage.") 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /The MUT/Settings Menu/MenuController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuController.swift 3 | // MUT 4 | // 5 | // Created by Michael Levenick on 12/4/22. 6 | // Copyright © 2022 Levenick Enterprises LLC. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | class MenuController: NSViewController { 13 | 14 | let logMan = logManager() 15 | 16 | // Set up defaults to be able to save to and restore from 17 | let menuDefaults = UserDefaults.standard 18 | 19 | // This runs when the view loads 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | SwitchTabs(selectedButton: btnGeneralOutlet, tabIdentifier: "General") 23 | } 24 | 25 | override func viewDidAppear() { 26 | super.viewDidAppear() 27 | // Forces the window to be the size we want, not resizable 28 | preferredContentSize = NSSize(width: 550, height: 290) 29 | 30 | restoreGUI() 31 | } 32 | 33 | @IBOutlet weak var tabViewController: NSTabView! 34 | 35 | // Top menu outlets 36 | @IBOutlet weak var btnGeneralOutlet: NSButton! 37 | @IBOutlet weak var btnSecurityOutlet: NSButton! 38 | 39 | // General Menu Outlets 40 | @IBOutlet weak var popLogLevelOutlet: NSPopUpButton! 41 | @IBOutlet weak var chkDelimiterOutlet: NSButton! 42 | @IBOutlet weak var chkUsernameIntOutlet: NSButton! 43 | 44 | // Security Menu Outlets 45 | @IBOutlet weak var chkUntrustedSSLOutlet: NSButton! 46 | @IBOutlet weak var chkStoreURLOutlet: NSButton! 47 | @IBOutlet weak var chkStoreUsernameOutlet: NSButton! 48 | 49 | // Menu Bar Actions 50 | @IBAction func btnGeneral(_ sender: Any) { 51 | SwitchTabs(selectedButton: btnGeneralOutlet, tabIdentifier: "General") 52 | preferredContentSize = NSSize(width: 550, height: 290) 53 | } 54 | 55 | @IBAction func btnSecurity(_ sender: Any) { 56 | SwitchTabs(selectedButton: btnSecurityOutlet, tabIdentifier: "Security") 57 | preferredContentSize = NSSize(width: 550, height: 320) 58 | } 59 | 60 | // General Menu Actions 61 | @IBAction func popLogLevel(_ sender: Any) { 62 | menuDefaults.set(popLogLevelOutlet.selectedTag(), forKey: "LogLevel") 63 | } 64 | 65 | @IBAction func btnOpenLog(_ sender: Any) { 66 | logMan.openLog() 67 | } 68 | 69 | @IBAction func chkDelimiter(_ sender: Any) { 70 | if chkDelimiterOutlet.state == NSControl.StateValue.on { 71 | menuDefaults.set(";", forKey: "Delimiter") 72 | logMan.writeLog(level: .info, logString: "The new delimiter is semi-colon. This delimiter will be stored to defaults.") 73 | } else { 74 | menuDefaults.removeObject(forKey: "Delimiter") 75 | logMan.writeLog(level: .info, logString: "Removing semi-colon delimiter from defaults storage. Comma delimiter will be used.") 76 | } 77 | } 78 | 79 | @IBAction func chkUsernamesInts(_ sender: Any) { 80 | if chkUsernameIntOutlet.state == NSControl.StateValue.on { 81 | menuDefaults.set(true, forKey: "UserInts") 82 | } else { 83 | menuDefaults.removeObject(forKey: "UserInts") 84 | } 85 | } 86 | 87 | // Security Menu Actions 88 | @IBAction func chkAllowUntrusted(_ sender: Any) { 89 | if chkUntrustedSSLOutlet.state == NSControl.StateValue.on { 90 | menuDefaults.set(true, forKey: "Insecure") 91 | } else { 92 | menuDefaults.removeObject(forKey: "Insecure") 93 | } 94 | } 95 | 96 | @IBAction func btnClearKeychain(_ sender: Any) { 97 | if popPrompt().clearKeychain() { 98 | DispatchQueue.global(qos: .background).async { 99 | do { 100 | try KeyChainHelper.delete() 101 | self.logMan.writeLog(level: .info, logString: "Deleting information stored in keychain.") 102 | } catch KeychainError.noPassword { 103 | // No info found in keychain 104 | self.logMan.writeLog(level: .info, logString: "No stored info found in KeyChain.") 105 | } catch KeychainError.unexpectedPasswordData { 106 | // Info found, but it was bad 107 | self.logMan.writeLog(level: .error, logString: "Information was found in KeyChain, but it was somehow corrupt.") 108 | } catch { 109 | // Something else 110 | self.logMan.writeLog(level: .fatal, logString: "Unhandled exception found with extracting KeyChain info.") 111 | } 112 | } 113 | } 114 | } 115 | 116 | @IBAction func chkStoreURL(_ sender: Any) { 117 | if chkStoreURLOutlet.state == NSControl.StateValue.on { 118 | menuDefaults.set(true, forKey: "StoreURL") 119 | } else { 120 | menuDefaults.removeObject(forKey: "StoreURL") 121 | } 122 | } 123 | 124 | @IBAction func chkStoreUsername(_ sender: Any) { 125 | if chkStoreUsernameOutlet.state == NSControl.StateValue.on { 126 | menuDefaults.set(true, forKey: "StoreUsername") 127 | } else { 128 | menuDefaults.removeObject(forKey: "StoreUsername") 129 | } 130 | } 131 | 132 | @IBAction func btnHardReset(_ sender: Any) { 133 | if popPrompt().hardReset() { 134 | 135 | // Clear the keychain 136 | DispatchQueue.global(qos: .background).async { 137 | do { 138 | try KeyChainHelper.delete() 139 | self.logMan.writeLog(level: .info, logString: "Deleting information stored in keychain.") 140 | } catch KeychainError.noPassword { 141 | // No info found in keychain 142 | self.logMan.writeLog(level: .info, logString: "No stored info found in KeyChain.") 143 | } catch KeychainError.unexpectedPasswordData { 144 | // Info found, but it was bad 145 | self.logMan.writeLog(level: .error, logString: "Information was found in KeyChain, but it was somehow corrupt.") 146 | } catch { 147 | // Something else 148 | self.logMan.writeLog(level: .fatal, logString: "Unhandled exception found with extracting KeyChain info.") 149 | } 150 | } 151 | 152 | // Clear all stored defaults 153 | if let bundle = Bundle.main.bundleIdentifier { 154 | UserDefaults.standard.removePersistentDomain(forName: bundle) 155 | } 156 | exit(0) 157 | } 158 | } 159 | 160 | func SwitchTabs(selectedButton: NSButton, tabIdentifier: String){ 161 | // Array of all Button Outlets 162 | let buttons = [btnGeneralOutlet, btnSecurityOutlet] 163 | 164 | for button in buttons { 165 | button?.state = NSControl.StateValue.off 166 | } 167 | 168 | selectedButton.state = NSControl.StateValue.on 169 | tabViewController.selectTabViewItem(withIdentifier: tabIdentifier) 170 | } 171 | 172 | func restoreGUI(){ 173 | if menuDefaults.bool(forKey: "StoreUsername") { 174 | chkStoreUsernameOutlet.state = NSControl.StateValue.on 175 | } 176 | 177 | if menuDefaults.bool(forKey: "StoreURL") { 178 | chkStoreURLOutlet.state = NSControl.StateValue.on 179 | } 180 | 181 | if menuDefaults.bool(forKey: "Insecure") { 182 | chkUntrustedSSLOutlet.state = NSControl.StateValue.on 183 | } 184 | 185 | if menuDefaults.string(forKey: "Delimiter") == ";" { 186 | chkDelimiterOutlet.state = NSControl.StateValue.on 187 | } 188 | 189 | if menuDefaults.bool(forKey: "UserInts") { 190 | chkUsernameIntOutlet.state = NSControl.StateValue.on 191 | } 192 | 193 | if menuDefaults.value(forKey: "LogLevel") != nil { 194 | popLogLevelOutlet.selectItem(withTag: menuDefaults.integer(forKey: "LogLevel")) 195 | } else { 196 | popLogLevelOutlet.selectItem(withTag: 3) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /The MUT/The MUT.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /The MUT/The_MUT.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | The_MUT.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /The MUT/The_MUT.xcdatamodeld/The_MUT.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /The MUT/dataPreparation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManipulation.swift 3 | // The MUT v5 4 | // 5 | // Created by Michael Levenick on 5/24/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class dataPreparation { 12 | 13 | let logMan = logManager() 14 | 15 | // ****************************************** 16 | // Functions to create URLs can be found here 17 | // ****************************************** 18 | 19 | public func generateURL(endpoint: String, identifierType: String, identifier: String, jpapi: Bool, jpapiVersion: String) -> URL { 20 | var instancedURL = Credentials.server! 21 | if !Credentials.server!.contains(".") { 22 | instancedURL = "https://" + Credentials.server! + ".jamfcloud.com/" 23 | } 24 | var versionEndpoint = "" 25 | var encodedURL = NSURL(string: "https://null".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! as URL 26 | if jpapi { 27 | //JPAPI URLS 28 | if jpapiVersion != "nil" { 29 | versionEndpoint = "\(jpapiVersion)/" 30 | } 31 | let concatURL = instancedURL + "/uapi" + versionEndpoint + endpoint 32 | let cleanURL = concatURL.replacingOccurrences(of: "//uapi", with: "/uapi") 33 | encodedURL = NSURL(string: "\(cleanURL)")! as URL 34 | } else { 35 | // CAPI URLS 36 | let concatURL = instancedURL + "/JSSResource/" + endpoint + "/" + identifierType + "/" + identifier 37 | var cleanURL = concatURL.replacingOccurrences(of: "//JSSResource", with: "/JSSResource") 38 | cleanURL = cleanURL.replacingOccurrences(of: "JSSResource//", with: "JSSResource/") 39 | encodedURL = NSURL(string: "\(cleanURL)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! as URL 40 | } 41 | return encodedURL 42 | } 43 | 44 | public func generateJpapiURL(endpoint: String, endpointVersion: String, identifier: String) -> URL { 45 | var instancedURL = Credentials.server! 46 | if !Credentials.server!.contains(".") { 47 | instancedURL = "https://" + Credentials.server! + ".jamfcloud.com/" 48 | } 49 | var encodedURL = NSURL(string: "https://null".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! as URL 50 | var concatURL = instancedURL + "/api/" + endpointVersion + "/" + endpoint 51 | if(!identifier.isEmpty) { 52 | concatURL.append("/" + identifier) 53 | } 54 | let cleanURL = concatURL.replacingOccurrences(of: "//api", with: "/api") 55 | encodedURL = NSURL(string: "\(cleanURL)")! as URL 56 | 57 | return encodedURL 58 | } 59 | 60 | public func generatePrestageURL(endpoint: String, prestageID: String, jpapiVersion: String, httpMethod: String) -> URL { 61 | 62 | var instancedURL = Credentials.server! 63 | if !Credentials.server!.contains(".") { 64 | instancedURL = "https://" + Credentials.server! + ".jamfcloud.com/" 65 | } 66 | var versionEndpoint = "" 67 | 68 | var encodedURL = NSURL(string: "https://null".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! as URL 69 | 70 | versionEndpoint = "\(jpapiVersion)/" 71 | 72 | var concatURL = instancedURL + "/uapi" + "/" + versionEndpoint + endpoint + "/" + prestageID + "/scope" 73 | if httpMethod == "DELETE" && (endpoint == "computer-prestages" || endpoint == "mobile-device-prestages"){ 74 | concatURL = concatURL + "/delete-multiple" 75 | } 76 | let cleanURL = concatURL.replacingOccurrences(of: "//uapi", with: "/uapi") 77 | encodedURL = NSURL(string: "\(cleanURL)")! as URL 78 | return encodedURL 79 | } 80 | 81 | // ****************************************** 82 | // Functions to encode/decode data can be found here 83 | // ****************************************** 84 | 85 | public func base64Credentials(user: String, password: String) -> String { 86 | // Concatenate the credentials and base64 encode the resulting string 87 | let concatCredentials = "\(user):\(password)" 88 | let utf8Credentials = concatCredentials.data(using: String.Encoding.utf8) 89 | let base64Credentials = utf8Credentials?.base64EncodedString() ?? "nil" 90 | return base64Credentials 91 | } 92 | 93 | public func expectedColumns(endpoint: String) -> Int { 94 | switch endpoint { 95 | case "users": 96 | return 9 97 | case "computers": 98 | return 23 99 | case "mobiledevices": 100 | return 22 101 | case "scope": 102 | return 1 103 | default: 104 | return 0 105 | } 106 | } 107 | 108 | public func endpoint(csvArray: [[String]]) -> String { 109 | let headerRow = csvArray[0] 110 | if headerRow.count <= 2 { 111 | return "scope" 112 | } else { 113 | switch headerRow[0] { 114 | case "Current Username": 115 | return "users" 116 | case "Computer Serial": 117 | return "computers" 118 | case "Mobile Device Serial": 119 | return "mobiledevices" 120 | default: 121 | return "Endpoint_Error" 122 | } 123 | } 124 | } 125 | 126 | public func eaIDs(expectedColumns: Int, numberOfColumns: Int, headerRow: [String]) -> [String] { 127 | var ea_ids = [String]() 128 | for i in expectedColumns...(numberOfColumns - 1) { 129 | let clean_ea_id = headerRow[i].replacingOccurrences(of: "EA_", with: "") 130 | ea_ids = ea_ids + [clean_ea_id] 131 | if !clean_ea_id.isInt { 132 | logMan.writeLog(level: .error, logString: "Problem with EA ID field: \(headerRow[i]) in column \(i + 1).") 133 | //print("Problem with EA ID field \(i)") 134 | } 135 | } 136 | return ea_ids 137 | } 138 | 139 | public func eaValues(expectedColumns: Int, numberOfColumns: Int, currentRow: [String]) -> [String] { 140 | var ea_values = [String]() 141 | for i in expectedColumns...(numberOfColumns - 1) { 142 | ea_values = ea_values + [currentRow[i]] 143 | } 144 | return ea_values 145 | } 146 | 147 | //Builds the dictionary for the identifier table on Attributes view 148 | public func buildID (ofArray: [[String]]) -> [[String: String]] { 149 | //print("Beginning buildID...") 150 | var dictID: [[String: String]] = [] 151 | let rows = ofArray.count 152 | var row = 1 153 | //start at second entry in CSV to skip headers 154 | var currentRow: [String] = [] 155 | while row < rows { 156 | currentRow = ofArray[row] 157 | dictID.append(["csvIdentifier" : currentRow[0]]) 158 | row += 1 159 | } 160 | return dictID 161 | } 162 | 163 | //buildScopes is just a duplicate of buildID that puts in "scopeID" as the key instead. Used on Prestages and Groups view 164 | public func buildScopes (ofArray: [[String]]) -> [[String: String]] { 165 | //print("Beginning buildScopes...") 166 | var dictID: [[String: String]] = [] 167 | let rows = ofArray.count 168 | var row = 1 169 | //start at second entry in CSV to skip headers 170 | var currentRow: [String] = [] 171 | while row < rows { 172 | currentRow = ofArray[row] 173 | dictID.append(["scopeID" : currentRow[0]]) 174 | row += 1 175 | } 176 | return dictID 177 | } 178 | 179 | //Comment 180 | //Builds a dictionary of all attributes being modified, pairing key-values for every attribute. 181 | //used for tableMain 182 | public func buildDict(rowToRead: Int, ofArray: [[String]]) -> [[String : String]] { 183 | //print("Beginning buildDict using array: \(ofArray)") 184 | 185 | //reads in the header row for the keys. Would handle any header row. 186 | let headerRow = ofArray[0] 187 | 188 | //how many attributes are there 189 | let columns = headerRow.count 190 | //start at the first attribute 191 | var column = 0 192 | 193 | //Start at first record, skipping header row 194 | var currentEntry = [""] 195 | //Will append to the returnArray throughout the loops 196 | var returnArray: [[ String : String ]] = [] 197 | //print("Number of columns in headerRow: \(columns)") 198 | //start at first column 199 | column = 0 200 | //set row to whatever is input for row to read. Can be hard coded, or we can increment it 201 | currentEntry = ofArray[rowToRead] 202 | //go through each column, pairing headerRow for attribute with the value from the row. 203 | while column < columns { 204 | //print("Current Entry... \(currentEntry[column])") 205 | var builderTwo: [String : String] = [:] 206 | if currentEntry[column] == "" { 207 | builderTwo = ["tableAttribute" : headerRow[column], "tableValue" : "(unchanged)"] 208 | } else if currentEntry[column] == "CLEAR!" { 209 | builderTwo = ["tableAttribute" : headerRow[column], "tableValue" : "WILL BE CLEARED"] 210 | } else { 211 | builderTwo = ["tableAttribute" : headerRow[column], "tableValue" : currentEntry[column]] 212 | } 213 | returnArray.append(builderTwo) 214 | column += 1 215 | } 216 | return returnArray 217 | } 218 | 219 | } 220 | 221 | // This allows us to calculate whether or not EA IDs are actually ints 222 | extension String { 223 | var isInt: Bool { 224 | return Int(self) != nil 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /The MUT/jsonBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // jsonBuilder.swift 3 | // MUT 4 | // 5 | // Created by Michael Levenick on 7/9/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | public class jsonManager { 13 | 14 | let logMan = logManager() 15 | 16 | public func buildScopeUpdatesJson(versionLock: Int, serialNumbers: [String]) -> Data{ 17 | // Array & Dictionary 18 | var jsonToReturn: Data? = "".data(using: .utf8) 19 | let json: JSON = ["serialNumbers": serialNumbers, "versionLock": versionLock] 20 | do { 21 | // Parse the JSON to return token and Expiry 22 | jsonToReturn = try json.rawData() 23 | } catch let error as NSError { 24 | //NSLog("[ERROR ]: Failed to get data from jsonToReturn" + error.debugDescription) 25 | logMan.writeLog(level: .error, logString: "Failed to get data from jsonToReturn" + error.debugDescription) 26 | } 27 | return jsonToReturn ?? "".data(using: .utf8)! 28 | } 29 | 30 | public func buildMobileDeviceUpdatesJson(data: [String]) -> Data { 31 | let jsonEncoder = JSONEncoder() 32 | var encodeMobileDevice: Data? = "".data(using: .utf8) 33 | let mobileDeviceUpdate = getMobileDeviceUpdateObject(data: data) 34 | 35 | do { 36 | encodeMobileDevice = try jsonEncoder.encode(mobileDeviceUpdate) 37 | } catch let error as NSError { 38 | //NSLog("[ERROR ]: Failed to get data from jsonEncoder" + error.debugDescription) 39 | logMan.writeLog(level: .error, logString: "Failed to get data from jsonEncoder" + error.debugDescription) 40 | } 41 | return encodeMobileDevice ?? "".data(using: .utf8)! 42 | } 43 | 44 | func getMobileDeviceUpdateObject(data: [String]) -> MobileDeviceV2 { 45 | var mobileDeviceUpdate = MobileDeviceV2() 46 | 47 | mobileDeviceUpdate.name = data[1].isEmpty ? nil : data[1] 48 | mobileDeviceUpdate.enforceName = Bool(data[2].lowercased()) 49 | 50 | return mobileDeviceUpdate 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /The MUT/loginWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // loginWindow.swift 3 | // The MUT 4 | // 5 | // Created by Michael Levenick on 5/28/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | import SwiftyJSON 12 | 13 | class loginWindow: NSViewController { 14 | 15 | // Declare outlets for use in the interface 16 | @IBOutlet weak var txtURLOutlet: NSTextField! 17 | @IBOutlet weak var txtUserOutlet: NSTextField! 18 | @IBOutlet weak var txtPassOutlet: NSSecureTextField! 19 | 20 | @IBOutlet weak var spinProgress: NSProgressIndicator! 21 | 22 | @IBOutlet weak var btnSubmitOutlet: NSButton! 23 | @IBOutlet weak var chkRememberMe: NSButton! 24 | 25 | @IBOutlet weak var chkAutoLoginOutlet: NSButton! 26 | @IBOutlet weak var lblAutoLogin: NSTextField! 27 | 28 | // Punctuation character set to be used in cleaning up URLs 29 | let punctuation = CharacterSet(charactersIn: ".:/") 30 | 31 | // Set up defaults to be able to save to and restore from 32 | let loginDefaults = UserDefaults.standard 33 | 34 | // Instantiating objects 35 | let tokenMan = tokenManagement() 36 | let dataPrep = dataPreparation() 37 | let logMan = logManager() 38 | 39 | var keyChainLogin = false 40 | 41 | // This runs when the view loads 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | preferredContentSize = NSSize(width: 550, height: 550) 45 | 46 | // Attempt to load info from keychain 47 | do { 48 | try KeyChainHelper.load() 49 | self.txtURLOutlet.stringValue = Credentials.server! 50 | self.txtUserOutlet.stringValue = Credentials.username! 51 | self.txtPassOutlet.stringValue = Credentials.password! 52 | if loginDefaults.bool(forKey: "AutoLogin") { 53 | self.logMan.writeLog(level: .info, logString: "Found credentials stored in KeyChain. Attempting login.") 54 | keyChainLogin = true 55 | self.btnSubmit(self) 56 | } 57 | } catch KeychainError.noPassword { 58 | // No info found in keychain 59 | self.logMan.writeLog(level: .info, logString: "No stored info found in KeyChain.") 60 | disableAutoLogin() 61 | } catch KeychainError.unexpectedPasswordData { 62 | // Info found, but it was bad 63 | self.logMan.writeLog(level: .error, logString: "Information was found in KeyChain, but it was somehow corrupt.") 64 | } catch { 65 | // Something else 66 | self.logMan.writeLog(level: .fatal, logString: "Unhandled exception found with extracting KeyChain info.") 67 | } 68 | 69 | // Restore Remember Me checkbox settings if we have a default stored 70 | if loginDefaults.bool(forKey: "Remember") { 71 | chkRememberMe.state = NSControl.StateValue.on 72 | } else { 73 | chkRememberMe.state = NSControl.StateValue.off 74 | disableAutoLogin() 75 | } 76 | 77 | // Restore Auto Login checkbox settings if we have a default stored 78 | if loginDefaults.bool(forKey: "AutoLogin") { 79 | chkAutoLoginOutlet.state = NSControl.StateValue.on 80 | } else { 81 | chkAutoLoginOutlet.state = NSControl.StateValue.off 82 | } 83 | 84 | if loginDefaults.string(forKey: "InstanceURL") != nil { 85 | self.txtURLOutlet.stringValue = loginDefaults.string(forKey: "InstanceURL")! 86 | } 87 | 88 | if loginDefaults.string(forKey: "UserName") != nil { 89 | self.txtUserOutlet.stringValue = loginDefaults.string(forKey: "UserName")! 90 | } 91 | } 92 | 93 | override func viewDidAppear() { 94 | super.viewDidAppear() 95 | // Forces the window to be the size we want, not resizable 96 | preferredContentSize = NSSize(width: 550, height: 550) 97 | } 98 | 99 | @IBAction func btnSubmit(_ sender: Any) { 100 | 101 | // Clean up whitespace at the beginning and end of the fields, in case of faulty copy/paste 102 | txtURLOutlet.stringValue = txtURLOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces) 103 | txtUserOutlet.stringValue = txtUserOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces) 104 | 105 | // Warn the user if they have failed to enter an instancename AND prem URL 106 | if txtURLOutlet.stringValue == "" { 107 | _ = popPrompt().noServer() 108 | } 109 | 110 | // Warn the user if they have failed to enter a username 111 | if txtUserOutlet.stringValue == "" { 112 | _ = popPrompt().noUser() 113 | } 114 | 115 | // Warn the user if they have failed to enter a password 116 | if txtPassOutlet.stringValue == "" { 117 | _ = popPrompt().noPass() 118 | } 119 | 120 | // Store the credentials information for later use 121 | Credentials.username = txtUserOutlet.stringValue 122 | Credentials.password = txtPassOutlet.stringValue 123 | Credentials.server = txtURLOutlet.stringValue 124 | Credentials.base64Encoded = self.dataPrep.base64Credentials(user: self.txtUserOutlet.stringValue, 125 | password: self.txtPassOutlet.stringValue) 126 | 127 | // Move forward with verification 128 | if txtURLOutlet.stringValue != "" 129 | && txtPassOutlet.stringValue != "" 130 | && txtUserOutlet.stringValue != "" { 131 | 132 | // Change the UI to a running state 133 | guiRunning() 134 | 135 | DispatchQueue.global(qos: .background).async { 136 | // Get our token data from the API class 137 | self.tokenMan.getToken(allowUntrusted: self.loginDefaults.bool(forKey: "Insecure")) 138 | DispatchQueue.main.async { 139 | //print(String(decoding: Token.data!, as: UTF8.self)) // Uncomment for debugging 140 | // Reset the GUI and pop up a warning with the info if we get a fatal error 141 | if String(decoding: Token.data!, as: UTF8.self).contains("FATAL") { 142 | _ = popPrompt().fatalWarning(error: String(decoding: Token.data!, as: UTF8.self)) 143 | self.guiReset() 144 | } else { 145 | // No error found leads you here: 146 | if String(decoding: Token.data!, as: UTF8.self).contains("token") { 147 | // Good credentials here, as told by there being a token 148 | do { 149 | // Parse the JSON to return token and Expiry 150 | let newJson = try JSON(data: Token.data!) 151 | Token.value = newJson["token"].stringValue 152 | 153 | // Get the expiry and attempt to convert to epoch 154 | let expireString = newJson["expires"].stringValue 155 | let dateFormatter = ISO8601DateFormatter() 156 | dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 157 | 158 | // If we can convert successfully, store it to the Token object. Otherwise throw an error. 159 | if let date = dateFormatter.date(from: expireString) { 160 | Token.expiration = Int(date.timeIntervalSince1970 * 1000) 161 | } else { 162 | self.logMan.writeLog(level: .error, logString: "Failed to convert token expiry to epoch. Received \(expireString).") 163 | } 164 | 165 | self.dismiss(self) 166 | } catch let error as NSError { 167 | self.logMan.writeLog(level: .error, logString: "Failed to load: \(error.localizedDescription)") 168 | } 169 | 170 | // Store the username if we should 171 | if self.loginDefaults.bool(forKey: "StoreUsername"){ 172 | self.loginDefaults.set(self.txtUserOutlet.stringValue, forKey: "UserName") 173 | self.loginDefaults.synchronize() 174 | } else { 175 | self.loginDefaults.removeObject(forKey: "UserName") 176 | } 177 | 178 | // Store the URL if we should 179 | if self.loginDefaults.bool(forKey: "StoreURL"){ 180 | self.loginDefaults.set(self.txtURLOutlet.stringValue, forKey: "InstanceURL") 181 | self.loginDefaults.synchronize() 182 | } else { 183 | self.loginDefaults.removeObject(forKey: "InstanceURL") 184 | } 185 | 186 | // Store username if button pressed 187 | if self.loginDefaults.bool(forKey: "Remember") { 188 | 189 | // Attempt to save the information in keychain 190 | self.logMan.writeLog(level: .info, logString: "Remember Me checkbox checked. Storing credentials in KeyChain for later use.") 191 | DispatchQueue.global(qos: .background).async { 192 | do { 193 | try KeyChainHelper.save(username: Credentials.username!, 194 | password: Credentials.password!, 195 | server: Credentials.server!) 196 | } catch { 197 | self.logMan.writeLog(level: .error, logString: "Error writing credentials to keychain. \(error)") 198 | } 199 | } 200 | 201 | } else { 202 | self.loginDefaults.removeObject(forKey: "Remember") 203 | } 204 | self.spinProgress.stopAnimation(self) 205 | self.btnSubmitOutlet.isHidden = false 206 | } else { 207 | // Bad credentials here 208 | if self.keyChainLogin { 209 | DispatchQueue.main.async { 210 | self.guiReset() 211 | // Popup warning of invalid credentials 212 | _ = popPrompt().invalidKeychain() 213 | self.keyChainLogin = false 214 | } 215 | self.deleteKeyChain() 216 | } else { 217 | DispatchQueue.main.async { 218 | self.guiReset() 219 | // Popup warning of invalid credentials 220 | _ = popPrompt().invalidCredentials() 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | @IBAction func btnQuit(_ sender: Any) { 231 | self.dismiss(self) 232 | NSApplication.shared.terminate(self) 233 | } 234 | 235 | @IBAction func chkRememberAction(_ sender: Any) { 236 | if chkRememberMe.state == NSControl.StateValue.on { 237 | loginDefaults.set(true, forKey: "Remember") 238 | enableAutoLogin() 239 | } else { 240 | // Remove both auto login and rememberme from defaults 241 | loginDefaults.removeObject(forKey: "AutoLogin") 242 | loginDefaults.removeObject(forKey: "Remember") 243 | 244 | disableAutoLogin() 245 | 246 | // Clear the keychain, just in case. 247 | deleteKeyChain() 248 | } 249 | } 250 | 251 | @IBAction func chkAutoLoginAction(_ sender: Any) { 252 | if chkAutoLoginOutlet.state == NSControl.StateValue.on { 253 | loginDefaults.set(true, forKey: "AutoLogin") 254 | } else { 255 | loginDefaults.removeObject(forKey: "AutoLogin") 256 | } 257 | } 258 | 259 | func guiRunning() { 260 | btnSubmitOutlet.isHidden = true 261 | spinProgress.startAnimation(self) 262 | } 263 | func guiReset() { 264 | spinProgress.stopAnimation(self) 265 | btnSubmitOutlet.isHidden = false 266 | } 267 | 268 | func deleteKeyChain() { 269 | DispatchQueue.global(qos: .background).async { 270 | do { 271 | try KeyChainHelper.delete() 272 | self.logMan.writeLog(level: .info, logString: "Deleting information stored in keychain.") 273 | } catch KeychainError.noPassword { 274 | // No info found in keychain 275 | self.logMan.writeLog(level: .info, logString: "No stored info found in KeyChain.") 276 | } catch KeychainError.unexpectedPasswordData { 277 | // Info found, but it was bad 278 | self.logMan.writeLog(level: .error, logString: "Information was found in KeyChain, but it was somehow corrupt.") 279 | } catch { 280 | // Something else 281 | self.logMan.writeLog(level: .fatal, logString: "Unhandled exception found with extracting KeyChain info.") 282 | } 283 | } 284 | } 285 | 286 | func disableAutoLogin(){ 287 | // Disable option to auto login if rememberme unchecked 288 | loginDefaults.removeObject(forKey: "AutoLogin") 289 | chkAutoLoginOutlet.state = NSControl.StateValue.off 290 | chkAutoLoginOutlet.isEnabled = false 291 | lblAutoLogin.textColor = .secondaryLabelColor 292 | } 293 | 294 | func enableAutoLogin(){ 295 | // Re-enable option to auto login if rememberme checked 296 | chkAutoLoginOutlet.isEnabled = true 297 | lblAutoLogin.textColor = .labelColor 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /The MUT/popPrompt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // popPrompt.swift 3 | // The MUT v5 4 | // 5 | // Created by Michael Levenick on 5/24/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | public class popPrompt { 13 | 14 | var globalCSVString: String! 15 | 16 | 17 | // Generate a generic warning message for invalid credentials etc 18 | public func generalWarning(question: String, text: String) -> Bool { 19 | let myPopup: NSAlert = NSAlert() 20 | myPopup.messageText = question 21 | myPopup.informativeText = text 22 | myPopup.alertStyle = NSAlert.Style.warning 23 | myPopup.addButton(withTitle: "OK") 24 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 25 | } 26 | 27 | // Generate a specific prompt to ask for credentials 28 | public func selectDelim (question: String, text: String) -> Bool { 29 | let myPopup: NSAlert = NSAlert() 30 | myPopup.messageText = question 31 | myPopup.informativeText = text 32 | myPopup.alertStyle = NSAlert.Style.warning 33 | myPopup.addButton(withTitle: "Use Comma") 34 | myPopup.addButton(withTitle: "Use Semi-Colon") 35 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 36 | } 37 | 38 | // Browse for a CSV File 39 | public func browseForCSV() -> String { 40 | let openPanel = NSOpenPanel() 41 | openPanel.allowsMultipleSelection = false 42 | openPanel.canChooseDirectories = false 43 | openPanel.canCreateDirectories = false 44 | openPanel.canChooseFiles = true 45 | openPanel.allowedFileTypes = ["csv"] 46 | openPanel.begin { (result) in 47 | if result == NSApplication.ModalResponse.OK { 48 | //print(openPanel.URL!) //uncomment for debugging 49 | let globalPathToCSV = openPanel.url! as NSURL? 50 | //print(self.globalPathToCSV.path!) //uncomment for debugging 51 | self.globalCSVString = globalPathToCSV?.path! 52 | } 53 | } 54 | return globalCSVString 55 | } 56 | 57 | public func groupFailoverAsk() -> Int { 58 | let myPopup: NSAlert = NSAlert() 59 | myPopup.messageText = "Update Failed" 60 | myPopup.informativeText = """ 61 | MUT encountered a problem submitting your update to Jamf Pro. Details can be found in the MUT.log 62 | 63 | Would you like to retry the update in Classic Mode? 64 | """ 65 | myPopup.alertStyle = NSAlert.Style.warning 66 | myPopup.addButton(withTitle: "Yes") 67 | myPopup.addButton(withTitle: "No") 68 | myPopup.addButton(withTitle: "More Info") 69 | return myPopup.runModal().rawValue 70 | } 71 | 72 | public func cannotClassic() -> Int { 73 | let myPopup: NSAlert = NSAlert() 74 | myPopup.messageText = "Update Failed" 75 | myPopup.informativeText = """ 76 | MUT encountered a problem submitting your update to Jamf Pro. Details can be found in the MUT.log 77 | 78 | Unfortunately, Classic Mode is not available for "Replace" updates. 79 | """ 80 | myPopup.alertStyle = NSAlert.Style.warning 81 | myPopup.addButton(withTitle: "OK") 82 | myPopup.addButton(withTitle: "More Info") 83 | return myPopup.runModal().rawValue 84 | } 85 | 86 | public func invalidCredentials() -> Bool { 87 | let myPopup: NSAlert = NSAlert() 88 | myPopup.messageText = "Invalid Credentials" 89 | myPopup.informativeText = "The credentials you entered do not seem to have sufficient permissions. This could be due to an incorrect user/password, or possibly from insufficient permissions.\n\nMUT tests this against the user's ability to generate a token for the new JPAPI/UAPI. This token is now required for some tasks that MUT performs." 90 | myPopup.alertStyle = NSAlert.Style.warning 91 | myPopup.addButton(withTitle: "OK") 92 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 93 | } 94 | 95 | public func invalidKeychain() -> Bool { 96 | let myPopup: NSAlert = NSAlert() 97 | myPopup.messageText = "Invalid Keychain Info" 98 | myPopup.informativeText = "The credentials stored in your keychain do not seem to have sufficient permissions. This could be due to an incorrect user/password, or possibly from insufficient permissions.\n\nMUT tests this against the user's ability to generate a token for the new JPAPI/UAPI. This token is now required for some tasks that MUT performs.\n\nMUT will now remove the stored keychain info." 99 | myPopup.alertStyle = NSAlert.Style.warning 100 | myPopup.addButton(withTitle: "OK") 101 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 102 | } 103 | 104 | public func noServer() -> Bool { 105 | let myPopup: NSAlert = NSAlert() 106 | myPopup.messageText = "No Server Info" 107 | myPopup.informativeText = "It appears that you have not entered any information for your Jamf Pro URL. Please enter either a Jamf Cloud instance name, or your full URL if you host your own server." 108 | myPopup.alertStyle = NSAlert.Style.warning 109 | myPopup.addButton(withTitle: "OK") 110 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 111 | } 112 | 113 | public func noUser() -> Bool { 114 | let myPopup: NSAlert = NSAlert() 115 | myPopup.messageText = "No Username Found" 116 | myPopup.informativeText = "It appears that you have not entered a username for MUT to use while accessing Jamf Pro. Please enter your username and password, and try again." 117 | myPopup.alertStyle = NSAlert.Style.warning 118 | myPopup.addButton(withTitle: "OK") 119 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 120 | } 121 | 122 | public func noPass() -> Bool { 123 | let myPopup: NSAlert = NSAlert() 124 | myPopup.messageText = "No Password Found" 125 | myPopup.informativeText = "It appears that you have not entered a password for MUT to use while accessing Jamf Pro. Please enter your username and password, and try again." 126 | myPopup.alertStyle = NSAlert.Style.warning 127 | myPopup.addButton(withTitle: "OK") 128 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 129 | } 130 | 131 | public func fatalWarning(error: String) -> Bool { 132 | let myPopup: NSAlert = NSAlert() 133 | myPopup.messageText = "Fatal Error" 134 | myPopup.informativeText = "The MUT received a fatal error at authentication. The most common cause of this is an incorrect server URL. The full error output is below. \n\n \(error))" 135 | myPopup.alertStyle = NSAlert.Style.warning 136 | myPopup.addButton(withTitle: "OK") 137 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 138 | } 139 | 140 | public func clearKeychain() -> Bool { 141 | let myPopup: NSAlert = NSAlert() 142 | myPopup.messageText = "Clear Keychain?" 143 | myPopup.informativeText = "This will remove your MUT credentials from keychain. You will need to re-enter your credentials when you next log in." 144 | myPopup.alertStyle = NSAlert.Style.warning 145 | myPopup.addButton(withTitle: "Make it so") 146 | myPopup.addButton(withTitle: "Cancel") 147 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 148 | } 149 | 150 | public func hardReset() -> Bool { 151 | let myPopup: NSAlert = NSAlert() 152 | myPopup.messageText = "Perform Hard Reset?" 153 | myPopup.informativeText = "If you are experiencing odd behavior with MUT it may help to perform a hard reset.\n\n This will remove all stored settings, including MUT crednetials stored in keychain, and quit the application immediately." 154 | myPopup.alertStyle = NSAlert.Style.warning 155 | myPopup.addButton(withTitle: "Make it so") 156 | myPopup.addButton(withTitle: "Cancel") 157 | return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /The MUT/tokenManagement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // The MUT v5 4 | // 5 | // Created by Michael Levenick on 5/24/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | public class tokenManagement: NSObject { 13 | 14 | let logMan = logManager() 15 | let sessionHandler = SessionHandler.SharedSessionHandler 16 | let tokenDefaults = UserDefaults.standard 17 | 18 | 19 | // This function can be used to generate a token. Pass in a URL and base64 encoded credentials. 20 | // The credentials are inserted into the header. 21 | public func getToken(allowUntrusted: Bool){ 22 | let dataPrep = dataPreparation() 23 | 24 | // Percent encode special characters that are not allowed in URLs, such as spaces 25 | let encodedURL = "\(Credentials.server!)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://null" 26 | 27 | // Create a URL for getting a token. 28 | let tokenURL = dataPrep.generateJpapiURL(endpoint: "auth/token", endpointVersion: "v1", identifier: "") 29 | 30 | // The semaphore is what allows us to force the code to wait for this request to complete 31 | // Without the semaphore, MUT will queue up a request for every single line of the CSV simultaneously 32 | let semaphore = DispatchSemaphore(value: 0) 33 | let request = NSMutableURLRequest(url: tokenURL) 34 | 35 | // Determine the request type. If we pass this in with a variable, we could use this function for PUT as well. 36 | request.httpMethod = "POST" 37 | 38 | request.addValue("Basic \(Credentials.base64Encoded!)", forHTTPHeaderField: "Authorization" ) 39 | request.addValue("text/xml", forHTTPHeaderField: "Content-Type") 40 | // set session to use 41 | let session = sessionHandler.mySession 42 | sessionHandler.setAllowUntrusted(allowUntrusted: allowUntrusted) 43 | 44 | // Completion handler. This is what ensures that the response is good/bad 45 | // and also what handles the semaphore 46 | let task = session.dataTask(with: request as URLRequest, completionHandler: { 47 | (data, response, error) -> Void in 48 | if let httpResponse = response as? HTTPURLResponse { 49 | if httpResponse.statusCode >= 199 && httpResponse.statusCode <= 299 { 50 | // Good response from API 51 | Token.data = data! 52 | //NSLog("[INFO ]: Successful GET completed by The MUT.app") 53 | self.logMan.writeLog(level: .info, logString: "A new token was successfully generated by MUT. \(httpResponse.statusCode).") 54 | //self.logMan.writeLog(level: .info, logString: String(decoding: token, as: UTF8.self)) 55 | } else { 56 | // Bad Response from API 57 | Token.data = data! 58 | self.logMan.writeLog(level: .error, logString: "MUT Failed to generate a token. \(httpResponse.statusCode).") 59 | self.logMan.writeLog(level: .error, logString: String(decoding: Token.data!, as: UTF8.self)) 60 | } 61 | semaphore.signal() // Signal completion to the semaphore 62 | } 63 | 64 | if error != nil { 65 | let errorString = "[FATAL ]: " + error!.localizedDescription 66 | Token.data = errorString.data(using: .utf8)! 67 | //NSLog("[FATAL ]: " + error!.localizedDescription) 68 | self.logMan.writeLog(level: .fatal, logString: error!.localizedDescription) 69 | semaphore.signal() // Signal completion to the semaphore 70 | } 71 | }) 72 | task.resume() // Kick off the actual GET here 73 | semaphore.wait() // Wait for the semaphore before moving on to the return value 74 | 75 | if String(decoding: Token.data!, as: UTF8.self).contains("FATAL") { 76 | _ = popPrompt().fatalWarning(error: String(decoding: Token.data!, as: UTF8.self)) 77 | } else { 78 | // No error found leads you here: 79 | if String(decoding: Token.data!, as: UTF8.self).contains("token") { 80 | // Good credentials here, as told by there being a token 81 | do { 82 | // Parse the JSON to return token and Expiry 83 | let newJson = try JSON(data: Token.data!) 84 | Token.value = newJson["token"].stringValue 85 | 86 | // Get the expiry and attempt to convert to epoch 87 | let expireString = newJson["expires"].stringValue 88 | let dateFormatter = ISO8601DateFormatter() 89 | dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 90 | 91 | // If we can convert successfully, store it to the Token object. Otherwise throw an error. 92 | if let date = dateFormatter.date(from: expireString) { 93 | Token.expiration = Int(date.timeIntervalSince1970 * 1000) 94 | } else { 95 | self.logMan.writeLog(level: .error, logString: "Failed to convert token expiry to epoch. Received \(expireString).") 96 | } 97 | 98 | } catch let error as NSError { 99 | self.logMan.writeLog(level: .error, logString: "Failed to load: \(error.localizedDescription)") 100 | } 101 | } 102 | } 103 | } 104 | 105 | public func tokenRefresher() { 106 | let currentEpoch = Int(Date().timeIntervalSince1970 * 1000) 107 | 108 | // Find the difference between expiry time and current epoch 109 | let secondsToExpire = (Token.expiration! - currentEpoch)/1000 110 | 111 | if secondsToExpire <= 30 { 112 | logMan.writeLog(level: .info, logString: "Token only has \(secondsToExpire) seconds left to live. Refreshing token.") 113 | getToken(allowUntrusted: self.tokenDefaults.bool(forKey: "Insecure")) 114 | } else { 115 | logMan.writeLog(level: .info, logString: "Token has \(secondsToExpire) seconds left to live. Proceeding with current token.") 116 | } 117 | } 118 | } 119 | 120 | 121 | -------------------------------------------------------------------------------- /The MUT/xmlBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // xmlBuilder.swift 3 | // The MUT 4 | // 5 | // Created by Michael Levenick on 5/24/19. 6 | // Copyright © 2019 Levenick Enterprises, LLC. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Foundation 11 | 12 | public class xmlManager { 13 | // Globally declaring the xml variable to allow the various functions to populate it 14 | var xml: XMLDocument? 15 | let removalValue = "CLEAR!" 16 | let xmlDefaults = UserDefaults.standard 17 | let logMan = logManager() 18 | 19 | public func userObject(username: String, 20 | full_name: String, 21 | email_address: String, 22 | phone_number: String, 23 | position: String, 24 | ldap_server: String, 25 | ea_ids: [String], 26 | ea_values: [String], 27 | site_ident: String, 28 | managedAppleID: String) -> Data { 29 | 30 | // User Object update XML Creation: 31 | 32 | // Variables needed for the rest of the XML Generation 33 | let root = XMLElement(name: "user") 34 | let xml = XMLDocument(rootElement: root) 35 | 36 | // Username 37 | let usernameElement = XMLElement(name: "name", stringValue: username) 38 | populateElement(variableToCheck: username, elementName: "name", elementToAdd: usernameElement, whereToAdd: root) 39 | 40 | // Full Name 41 | let fullNameElement = XMLElement(name: "full_name", stringValue: full_name) 42 | populateElement(variableToCheck: full_name, elementName: "full_name", elementToAdd: fullNameElement, whereToAdd: root) 43 | 44 | // Email Address 45 | let emailElement = XMLElement(name: "email", stringValue: email_address) 46 | let emailAddressElement = XMLElement(name: "email_address", stringValue: email_address) 47 | populateElement(variableToCheck: email_address, elementName: "email", elementToAdd: emailElement, whereToAdd: root) 48 | populateElement(variableToCheck: email_address, elementName: "email_address", elementToAdd: emailAddressElement, whereToAdd: root) 49 | 50 | // Phone Number 51 | let phoneNumberElement = XMLElement(name: "phone_number", stringValue: phone_number) 52 | populateElement(variableToCheck: phone_number, elementName: "phone_number", elementToAdd: phoneNumberElement, whereToAdd: root) 53 | 54 | // Position 55 | let positionElement = XMLElement(name: "position", stringValue: position) 56 | populateElement(variableToCheck: position, elementName: "position", elementToAdd: positionElement, whereToAdd: root) 57 | 58 | // Managed Apple ID 59 | let managedAppleIDElement = XMLElement(name: "managed_apple_id", stringValue: managedAppleID) 60 | populateElement(variableToCheck: managedAppleID, elementName: "managed_apple_id", elementToAdd: managedAppleIDElement, whereToAdd: root) 61 | 62 | // LDAP Server 63 | let ldapServerElement = XMLElement(name: "ldap_server") 64 | var ldapServerIDElement = XMLElement(name: "id", stringValue: ldap_server) // Set LDAP Server ID to -1 to unassign from all. 65 | 66 | if ldap_server == removalValue { 67 | ldapServerIDElement = XMLElement(name: "id", stringValue: "-1") 68 | ldapServerElement.addChild(ldapServerIDElement) 69 | root.addChild(ldapServerElement) 70 | } else if ldap_server != "" { 71 | ldapServerElement.addChild(ldapServerIDElement) 72 | root.addChild(ldapServerElement) 73 | } 74 | 75 | // Site 76 | let sitesElement = XMLElement(name: "sites") 77 | let siteElement = XMLElement(name: "site") 78 | var siteIDElement = XMLElement(name: "id", stringValue: site_ident) 79 | if site_ident == removalValue { 80 | siteIDElement = XMLElement(name: "id", stringValue: "-1") 81 | siteElement.addChild(siteIDElement) 82 | sitesElement.addChild(siteElement) 83 | root.addChild(sitesElement) 84 | } else if site_ident != "" { 85 | if site_ident.isInt { 86 | siteIDElement = XMLElement(name: "id", stringValue: site_ident) 87 | } else { 88 | siteIDElement = XMLElement(name: "name", stringValue: site_ident) 89 | } 90 | siteElement.addChild(siteIDElement) 91 | sitesElement.addChild(siteElement) 92 | root.addChild(sitesElement) 93 | } 94 | 95 | // Extension Attributes 96 | let extensionAttributesElement = XMLElement(name: "extension_attributes") 97 | 98 | if ea_values.count > 0 { 99 | var hasEAs = 0 100 | // Loop through the EA values, adding them to the EA node 101 | for i in 0...(ea_ids.count - 1 ) { 102 | 103 | // Extension Attributes 104 | if ea_values[i] == removalValue { 105 | let currentExtensionAttributesElement = XMLElement(name: "extension_attribute") 106 | currentExtensionAttributesElement.addChild(XMLElement(name: "id", stringValue: ea_ids[i])) 107 | currentExtensionAttributesElement.addChild(XMLElement(name: "value", stringValue: "")) 108 | extensionAttributesElement.addChild(currentExtensionAttributesElement) 109 | hasEAs = hasEAs + 1 110 | } else if ea_values[i] != "" { 111 | let currentExtensionAttributesElement = XMLElement(name: "extension_attribute") 112 | currentExtensionAttributesElement.addChild(XMLElement(name: "id", stringValue: ea_ids[i])) 113 | currentExtensionAttributesElement.addChild(XMLElement(name: "value", stringValue: ea_values[i])) 114 | extensionAttributesElement.addChild(currentExtensionAttributesElement) 115 | hasEAs = hasEAs + 1 116 | } 117 | } 118 | if hasEAs > 0 { 119 | // Add the EA subset to the root element 120 | root.addChild(extensionAttributesElement) 121 | } 122 | } 123 | 124 | // Print the XML 125 | //NSLog(xml.debugDescription) // Uncomment for debugging 126 | return xml.xmlData 127 | } 128 | 129 | public func iosObject(assetTag: String, 130 | username: String, 131 | full_name: String, 132 | email_address: String, 133 | phone_number: String, 134 | position: String, 135 | department: String, 136 | building: String, 137 | room: String, 138 | poNumber: String, 139 | vendor: String, 140 | purchasePrice: String, 141 | poDate: String, 142 | warrantyExpires: String, 143 | isLeased: String, 144 | leaseExpires: String, 145 | appleCareID: String, 146 | airplayPassword: String, 147 | site_ident: String, 148 | ea_ids: [String], 149 | ea_values: [String]) -> Data { 150 | 151 | 152 | // iOS Object update XML Creation: 153 | 154 | let generalStuff = assetTag + airplayPassword + site_ident 155 | let locationStuff = username + full_name + email_address + phone_number + position + department + building + room 156 | let purchasingStuff = poNumber + vendor + poDate + warrantyExpires + leaseExpires + purchasePrice + appleCareID + isLeased 157 | 158 | // Variables needed for the rest of the XML Generation 159 | let root = XMLElement(name: "mobile_device") 160 | let xml = XMLDocument(rootElement: root) 161 | let general = XMLElement(name: "general") 162 | let location = XMLElement(name: "location") 163 | let purchasing = XMLElement(name: "purchasing") 164 | 165 | // ---------------------- 166 | // GENERAL ATTRIBUTES 167 | // ---------------------- 168 | 169 | // Asset Tag 170 | let assetTagElement = XMLElement(name: "asset_tag", stringValue: assetTag) 171 | populateElement(variableToCheck: assetTag, elementName: "asset_tag", elementToAdd: assetTagElement, whereToAdd: general) 172 | 173 | let airplayPasswordElement = XMLElement(name: "airplay_password", stringValue: airplayPassword) 174 | populateElement(variableToCheck: airplayPassword, elementName: "airplay_password", elementToAdd: airplayPasswordElement, whereToAdd: general) 175 | 176 | // Site 177 | let siteElement = XMLElement(name: "site") 178 | var siteIDElement = XMLElement(name: "id", stringValue: site_ident) 179 | if site_ident == removalValue { 180 | siteIDElement = XMLElement(name: "id", stringValue: "-1") 181 | siteElement.addChild(siteIDElement) 182 | general.addChild(siteElement) 183 | } else if site_ident != "" { 184 | if site_ident.isInt { 185 | siteIDElement = XMLElement(name: "id", stringValue: site_ident) 186 | } else { 187 | siteIDElement = XMLElement(name: "name", stringValue: site_ident) 188 | } 189 | siteElement.addChild(siteIDElement) 190 | general.addChild(siteElement) 191 | } 192 | 193 | // ---------------------- 194 | // LOCATION ATTRIBUTES 195 | // ---------------------- 196 | 197 | // Username 198 | let usernameElement = XMLElement(name: "username", stringValue: username) 199 | populateElement(variableToCheck: username, elementName: "username", elementToAdd: usernameElement, whereToAdd: location) 200 | 201 | // Real Name 202 | let realnameElement = XMLElement(name: "realname", stringValue: full_name) 203 | let real_nameElement = XMLElement(name: "real_name", stringValue: full_name) 204 | populateElement(variableToCheck: full_name, elementName: "realname", elementToAdd: realnameElement, whereToAdd: location) 205 | populateElement(variableToCheck: full_name, elementName: "real_name", elementToAdd: real_nameElement, whereToAdd: location) 206 | 207 | 208 | // Email Address 209 | let emailAddressElement = XMLElement(name: "email_address", stringValue: email_address) 210 | populateElement(variableToCheck: email_address, elementName: "email_address", elementToAdd: emailAddressElement, whereToAdd: location) 211 | 212 | // Position 213 | let positionElement = XMLElement(name: "position", stringValue: position) 214 | populateElement(variableToCheck: position, elementName: "position", elementToAdd: positionElement, whereToAdd: location) 215 | 216 | // Phone Number 217 | let phoneElement = XMLElement(name: "phone", stringValue: phone_number) 218 | let phoneNumberElement = XMLElement(name: "phone_number", stringValue: phone_number) 219 | populateElement(variableToCheck: phone_number, elementName: "phone", elementToAdd: phoneElement, whereToAdd: location) 220 | populateElement(variableToCheck: phone_number, elementName: "phone_number", elementToAdd: phoneNumberElement, whereToAdd: location) 221 | 222 | // Department 223 | let departmentElement = XMLElement(name: "department", stringValue: department) 224 | populateElement(variableToCheck: department, elementName: "department", elementToAdd: departmentElement, whereToAdd: location) 225 | 226 | // Building 227 | let buildingElement = XMLElement(name: "building", stringValue: building) 228 | populateElement(variableToCheck: building, elementName: "building", elementToAdd: buildingElement, whereToAdd: location) 229 | 230 | // Room 231 | let roomElement = XMLElement(name: "room", stringValue: room) 232 | populateElement(variableToCheck: room, elementName: "room", elementToAdd: roomElement, whereToAdd: location) 233 | 234 | // ---------------------- 235 | // PURCHASING ATTRIBUTES 236 | // ---------------------- 237 | 238 | // PO Number 239 | let poNumberElement = XMLElement(name: "po_number", stringValue: poNumber) 240 | populateElement(variableToCheck: poNumber, elementName: "po_number", elementToAdd: poNumberElement, whereToAdd: purchasing) 241 | 242 | // Vendor 243 | let vendorElement = XMLElement(name: "vendor", stringValue: vendor) 244 | populateElement(variableToCheck: vendor, elementName: "vendor", elementToAdd: vendorElement, whereToAdd: purchasing) 245 | 246 | // Purchase Price 247 | let purchasePriceElement = XMLElement(name: "purchase_price", stringValue: purchasePrice) 248 | populateElement(variableToCheck: purchasePrice, elementName: "purchase_price", elementToAdd: purchasePriceElement, whereToAdd: purchasing) 249 | 250 | // PO Date 251 | let poDateElement = XMLElement(name: "po_date", stringValue: poDate) 252 | populateElement(variableToCheck: poDate, elementName: "po_date", elementToAdd: poDateElement, whereToAdd: purchasing) 253 | 254 | // Warranty Expires 255 | let warrantyExpiresElement = XMLElement(name: "warranty_expires", stringValue: warrantyExpires) 256 | populateElement(variableToCheck: warrantyExpires, elementName: "warranty_expires", elementToAdd: warrantyExpiresElement, whereToAdd: purchasing) 257 | 258 | // Lease Expires 259 | let leaseExpiresElement = XMLElement(name: "lease_expires", stringValue: leaseExpires) 260 | populateElement(variableToCheck: leaseExpires, elementName: "lease_expires", elementToAdd: leaseExpiresElement, whereToAdd: purchasing) 261 | 262 | // AppleCare ID 263 | let appleCareIDElement = XMLElement(name: "applecare_id", stringValue: appleCareID) 264 | populateElement(variableToCheck: appleCareID, elementName: "applecare_id", elementToAdd: appleCareIDElement, whereToAdd: purchasing) 265 | 266 | // isLeased 267 | let isLeasedIDElement = XMLElement(name: "is_leased", stringValue: isLeased) 268 | populateElement(variableToCheck: isLeased, elementName: "is_leased", elementToAdd: isLeasedIDElement, whereToAdd: purchasing) 269 | 270 | // ---------------------- 271 | // EXTENSION ATTRIBUTES 272 | // ---------------------- 273 | 274 | let extensionAttributesElement = XMLElement(name: "extension_attributes") 275 | 276 | if ea_values.count > 0 { 277 | var hasEAs = 0 278 | // Loop through the EA values, adding them to the EA node 279 | for i in 0...(ea_ids.count - 1 ) { 280 | 281 | // Extension Attributes 282 | if ea_values[i] == removalValue { 283 | let currentExtensionAttributesElement = XMLElement(name: "extension_attribute") 284 | currentExtensionAttributesElement.addChild(XMLElement(name: "id", stringValue: ea_ids[i])) 285 | currentExtensionAttributesElement.addChild(XMLElement(name: "value", stringValue: "")) 286 | extensionAttributesElement.addChild(currentExtensionAttributesElement) 287 | hasEAs = hasEAs + 1 288 | } else if ea_values[i] != "" { 289 | let currentExtensionAttributesElement = XMLElement(name: "extension_attribute") 290 | currentExtensionAttributesElement.addChild(XMLElement(name: "id", stringValue: ea_ids[i])) 291 | currentExtensionAttributesElement.addChild(XMLElement(name: "value", stringValue: ea_values[i])) 292 | extensionAttributesElement.addChild(currentExtensionAttributesElement) 293 | hasEAs = hasEAs + 1 294 | } 295 | } 296 | if hasEAs > 0 { 297 | root.addChild(extensionAttributesElement) 298 | } 299 | 300 | } 301 | 302 | if generalStuff != "" { 303 | root.addChild(general) 304 | } 305 | if locationStuff != "" { 306 | root.addChild(location) 307 | } 308 | if purchasingStuff != "" { 309 | root.addChild(purchasing) 310 | } 311 | 312 | // Print the XML 313 | //NSLog(xml.debugDescription) // Uncomment for debugging 314 | return xml.xmlData 315 | } 316 | 317 | public func macosObject(displayName: String, 318 | assetTag: String, 319 | barcode1: String, 320 | barcode2: String, 321 | username: String, 322 | full_name: String, 323 | email_address: String, 324 | position: String, 325 | phone_number: String, 326 | department: String, 327 | building: String, 328 | room: String, 329 | poNumber: String, 330 | vendor: String, 331 | purchasePrice: String, 332 | poDate: String, 333 | warrantyExpires: String, 334 | isLeased: String, 335 | leaseExpires: String, 336 | appleCareID: String, 337 | site_ident: String, 338 | managed: String, 339 | ea_ids: [String], 340 | ea_values: [String]) -> Data { 341 | 342 | // macOS Object update XML Creation: 343 | var generalStuff = displayName + assetTag + barcode1 + barcode2 + site_ident 344 | let managedValue = managed.lowercased() 345 | if managedValue == "true" || managedValue == "false" { 346 | generalStuff = displayName + assetTag + barcode1 + barcode2 + site_ident + managedValue 347 | } 348 | let locationStuff = username + full_name + email_address + phone_number + position + department + building + room 349 | let purchasingStuff = poNumber + vendor + purchasePrice + poDate + warrantyExpires + leaseExpires + appleCareID + isLeased 350 | 351 | // Variables needed for the rest of the XML Generation 352 | let root = XMLElement(name: "computer") 353 | let xml = XMLDocument(rootElement: root) 354 | let general = XMLElement(name: "general") 355 | let location = XMLElement(name: "location") 356 | let purchasing = XMLElement(name: "purchasing") 357 | 358 | // ---------------------- 359 | // GENERAL ATTRIBUTES 360 | // ---------------------- 361 | 362 | // Device Name 363 | let deviceNameElement = XMLElement(name: "name", stringValue: displayName) 364 | populateElement(variableToCheck: displayName, elementName: "name", elementToAdd: deviceNameElement, whereToAdd: general) 365 | 366 | // Asset Tag 367 | let assetTagElement = XMLElement(name: "asset_tag", stringValue: assetTag) 368 | populateElement(variableToCheck: assetTag, elementName: "asset_tag", elementToAdd: assetTagElement, whereToAdd: general) 369 | 370 | // Barcode 1 371 | let barcode1Element = XMLElement(name: "barcode_1", stringValue: barcode1) 372 | populateElement(variableToCheck: barcode1, elementName: "barcode_1", elementToAdd: barcode1Element, whereToAdd: general) 373 | 374 | // Barcode 2 375 | let barcode2Element = XMLElement(name: "barcode_2", stringValue: barcode2) 376 | populateElement(variableToCheck: barcode2, elementName: "barcode_2", elementToAdd: barcode2Element, whereToAdd: general) 377 | 378 | 379 | // Site 380 | let siteElement = XMLElement(name: "site") 381 | var siteIDElement = XMLElement(name: "id", stringValue: site_ident) 382 | if site_ident == removalValue { 383 | siteIDElement = XMLElement(name: "id", stringValue: "-1") 384 | siteElement.addChild(siteIDElement) 385 | general.addChild(siteElement) 386 | } else if site_ident != "" { 387 | if site_ident.isInt { 388 | siteIDElement = XMLElement(name: "id", stringValue: site_ident) 389 | } else { 390 | siteIDElement = XMLElement(name: "name", stringValue: site_ident) 391 | } 392 | siteElement.addChild(siteIDElement) 393 | general.addChild(siteElement) 394 | } 395 | 396 | // Managed 397 | if managedValue == "true" || managedValue == "false" { 398 | let managedElement = XMLElement(name: "remote_management") 399 | let managedElementValue = XMLElement(name: "managed", stringValue: managedValue) 400 | managedElement.addChild(managedElementValue) 401 | general.addChild(managedElement) 402 | } else if managedValue != "" { 403 | logMan.writeLog(level: .error, logString: "Invalid value found for \"Managed\". Expected \"true\" or \"false\", found \(managedValue).") 404 | logMan.writeLog(level: .error, logString: "Skipping \"Managed\", continuing with PUT command.") 405 | } 406 | 407 | // ---------------------- 408 | // LOCATION ATTRIBUTES 409 | // ---------------------- 410 | 411 | // Username 412 | let usernameElement = XMLElement(name: "username", stringValue: username) 413 | populateElement(variableToCheck: username, elementName: "username", elementToAdd: usernameElement, whereToAdd: location) 414 | 415 | // Real Name 416 | let realnameElement = XMLElement(name: "realname", stringValue: full_name) 417 | let real_nameElement = XMLElement(name: "real_name", stringValue: full_name) 418 | populateElement(variableToCheck: full_name, elementName: "realname", elementToAdd: realnameElement, whereToAdd: location) 419 | populateElement(variableToCheck: full_name, elementName: "real_name", elementToAdd: real_nameElement, whereToAdd: location) 420 | 421 | // Email Address 422 | let emailAddressElement = XMLElement(name: "email_address", stringValue: email_address) 423 | populateElement(variableToCheck: email_address, elementName: "email_address", elementToAdd: emailAddressElement, whereToAdd: location) 424 | 425 | // Position 426 | let positionElement = XMLElement(name: "position", stringValue: position) 427 | populateElement(variableToCheck: position, elementName: "position", elementToAdd: positionElement, whereToAdd: location) 428 | 429 | // Phone Number 430 | let phoneElement = XMLElement(name: "phone", stringValue: phone_number) 431 | let phoneNumberElement = XMLElement(name: "phone_number", stringValue: phone_number) 432 | populateElement(variableToCheck: phone_number, elementName: "phone", elementToAdd: phoneElement, whereToAdd: location) 433 | populateElement(variableToCheck: phone_number, elementName: "phone_number", elementToAdd: phoneNumberElement, whereToAdd: location) 434 | 435 | // Department 436 | let departmentElement = XMLElement(name: "department", stringValue: department) 437 | populateElement(variableToCheck: department, elementName: "department", elementToAdd: departmentElement, whereToAdd: location) 438 | 439 | // Building 440 | let buildingElement = XMLElement(name: "building", stringValue: building) 441 | populateElement(variableToCheck: building, elementName: "building", elementToAdd: buildingElement, whereToAdd: location) 442 | 443 | // Room 444 | let roomElement = XMLElement(name: "room", stringValue: room) 445 | populateElement(variableToCheck: room, elementName: "room", elementToAdd: roomElement, whereToAdd: location) 446 | 447 | // ---------------------- 448 | // PURCHASING ATTRIBUTES 449 | // ---------------------- 450 | 451 | // PO Number 452 | let poNumberElement = XMLElement(name: "po_number", stringValue: poNumber) 453 | populateElement(variableToCheck: poNumber, elementName: "po_number", elementToAdd: poNumberElement, whereToAdd: purchasing) 454 | 455 | // Vendor 456 | let vendorElement = XMLElement(name: "vendor", stringValue: vendor) 457 | populateElement(variableToCheck: vendor, elementName: "vendor", elementToAdd: vendorElement, whereToAdd: purchasing) 458 | 459 | // Purchase Price 460 | let purchasePriceElement = XMLElement(name: "purchase_price", stringValue: purchasePrice) 461 | populateElement(variableToCheck: purchasePrice, elementName: "purchase_price", elementToAdd: purchasePriceElement, whereToAdd: purchasing) 462 | 463 | // PO Date 464 | let poDateElement = XMLElement(name: "po_date", stringValue: poDate) 465 | populateElement(variableToCheck: poDate, elementName: "po_date", elementToAdd: poDateElement, whereToAdd: purchasing) 466 | 467 | // Warranty Expires 468 | let warrantyExpiresElement = XMLElement(name: "warranty_expires", stringValue: warrantyExpires) 469 | populateElement(variableToCheck: warrantyExpires, elementName: "warranty_expires", elementToAdd: warrantyExpiresElement, whereToAdd: purchasing) 470 | 471 | // Lease Expires 472 | let leaseExpiresElement = XMLElement(name: "lease_expires", stringValue: leaseExpires) 473 | populateElement(variableToCheck: leaseExpires, elementName: "lease_expires", elementToAdd: leaseExpiresElement, whereToAdd: purchasing) 474 | 475 | // AppleCare ID 476 | let appleCareIDElement = XMLElement(name: "applecare_id", stringValue: appleCareID) 477 | populateElement(variableToCheck: appleCareID, elementName: "applecare_id", elementToAdd: appleCareIDElement, whereToAdd: purchasing) 478 | 479 | // isLeased 480 | let isLeasedIDElement = XMLElement(name: "is_leased", stringValue: isLeased) 481 | populateElement(variableToCheck: isLeased, elementName: "is_leased", elementToAdd: isLeasedIDElement, whereToAdd: purchasing) 482 | 483 | // ---------------------- 484 | // EXTENSION ATTRIBUTES 485 | // ---------------------- 486 | 487 | let extensionAttributesElement = XMLElement(name: "extension_attributes") 488 | 489 | if ea_values.count > 0 { 490 | var hasEAs = 0 491 | // Loop through the EA values, adding them to the EA node 492 | for i in 0...(ea_ids.count - 1 ) { 493 | 494 | // Extension Attributes 495 | if ea_values[i] == removalValue { 496 | let currentExtensionAttributesElement = XMLElement(name: "extension_attribute") 497 | currentExtensionAttributesElement.addChild(XMLElement(name: "id", stringValue: ea_ids[i])) 498 | currentExtensionAttributesElement.addChild(XMLElement(name: "value", stringValue: "")) 499 | extensionAttributesElement.addChild(currentExtensionAttributesElement) 500 | hasEAs = hasEAs + 1 501 | } else if ea_values[i] != "" { 502 | let currentExtensionAttributesElement = XMLElement(name: "extension_attribute") 503 | currentExtensionAttributesElement.addChild(XMLElement(name: "id", stringValue: ea_ids[i])) 504 | currentExtensionAttributesElement.addChild(XMLElement(name: "value", stringValue: ea_values[i])) 505 | extensionAttributesElement.addChild(currentExtensionAttributesElement) 506 | hasEAs = hasEAs + 1 507 | } 508 | } 509 | if hasEAs > 0 { 510 | root.addChild(extensionAttributesElement) 511 | } 512 | } 513 | 514 | if generalStuff != "" { 515 | root.addChild(general) 516 | } 517 | if locationStuff != "" { 518 | root.addChild(location) 519 | } 520 | if purchasingStuff != "" { 521 | root.addChild(purchasing) 522 | } 523 | 524 | // Print the XML 525 | NSLog(xml.debugDescription) // Uncomment for debugging 526 | return xml.xmlData 527 | } 528 | 529 | public func staticGroup(appendReplaceRemove: String, 530 | objectType: String, 531 | identifiers: [String]) -> Data { 532 | 533 | // Static Group XML Creation: 534 | 535 | // Example of the XML that is generated by this function 536 | /* 537 | 538 | false 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | */ 549 | 550 | if objectType == "computers" { 551 | // Variables needed for the rest of the XML Generation 552 | let root = XMLElement(name: "computer_group") 553 | let xml = XMLDocument(rootElement: root) 554 | let isSmartElement = XMLElement(name: "is_smart", stringValue: "false") 555 | var computersElement = XMLElement(name: "computers") 556 | 557 | if appendReplaceRemove == "append" { 558 | computersElement = XMLElement(name: "computer_additions") 559 | //computersElement = XMLElement(name: "computer_additions") 560 | } 561 | if appendReplaceRemove == "replace" { 562 | computersElement = XMLElement(name: "computers") 563 | //computersElement = XMLElement(name: "computers") 564 | } 565 | if appendReplaceRemove == "remove" { 566 | computersElement = XMLElement(name: "computer_deletions") 567 | //computersElement = XMLElement(name: "computer_deletions") 568 | } 569 | 570 | // Loop 571 | for i in 0...(identifiers.count - 1){ 572 | let computerElement = XMLElement(name: "computer") 573 | let identifier = identifiers[i] 574 | if identifier.isInt { 575 | computerElement.addChild(XMLElement(name: "id", stringValue: identifier.trimmingCharacters(in: CharacterSet.whitespaces))) 576 | } else { 577 | computerElement.addChild(XMLElement(name: "serial_number", stringValue: identifier.trimmingCharacters(in: CharacterSet.whitespaces))) 578 | } 579 | computersElement.addChild(computerElement) 580 | } 581 | root.addChild(isSmartElement) 582 | root.addChild(computersElement) 583 | return xml.xmlData 584 | } 585 | 586 | if objectType == "mobiledevices" { 587 | // Variables needed for the rest of the XML Generation 588 | let root = XMLElement(name: "mobile_device_group") 589 | let xml = XMLDocument(rootElement: root) 590 | let isSmartElement = XMLElement(name: "is_smart", stringValue: "false") 591 | var devicesElement = XMLElement(name: "mobile_devices") 592 | 593 | if appendReplaceRemove == "append" { 594 | devicesElement = XMLElement(name: "mobile_device_additions") 595 | } 596 | if appendReplaceRemove == "replace" { 597 | devicesElement = XMLElement(name: "mobile_devices") 598 | } 599 | if appendReplaceRemove == "remove" { 600 | devicesElement = XMLElement(name: "mobile_device_deletions") 601 | } 602 | 603 | // Loop 604 | for i in 0...(identifiers.count - 1){ 605 | let deviceElement = XMLElement(name: "mobile_device") 606 | let identifier = identifiers[i] 607 | if identifier.isInt { 608 | deviceElement.addChild(XMLElement(name: "id", stringValue: identifier.trimmingCharacters(in: CharacterSet.whitespaces))) 609 | } else { 610 | deviceElement.addChild(XMLElement(name: "serial_number", stringValue: identifier.trimmingCharacters(in: CharacterSet.whitespaces))) 611 | } 612 | devicesElement.addChild(deviceElement) 613 | } 614 | root.addChild(isSmartElement) 615 | root.addChild(devicesElement) 616 | return xml.xmlData 617 | } 618 | 619 | if objectType == "users" { 620 | // Variables needed for the rest of the XML Generation 621 | let root = XMLElement(name: "user_group") 622 | let xml = XMLDocument(rootElement: root) 623 | let isSmartElement = XMLElement(name: "is_smart", stringValue: "false") 624 | var usersElement = XMLElement(name: "users") 625 | 626 | if appendReplaceRemove == "append" { 627 | usersElement = XMLElement(name: "user_additions") 628 | } 629 | if appendReplaceRemove == "replace" { 630 | usersElement = XMLElement(name: "users") 631 | } 632 | if appendReplaceRemove == "remove" { 633 | usersElement = XMLElement(name: "user_deletions") 634 | } 635 | 636 | // Loop 637 | for i in 0...(identifiers.count - 1){ 638 | let userElement = XMLElement(name: "user") 639 | let identifier = identifiers[i] 640 | if identifier.isInt { 641 | if xmlDefaults.value(forKey: "UserInts") != nil { 642 | userElement.addChild(XMLElement(name: "username", stringValue: identifier.trimmingCharacters(in: CharacterSet.whitespaces))) 643 | } else { 644 | userElement.addChild(XMLElement(name: "id", stringValue: identifier.trimmingCharacters(in: CharacterSet.whitespaces))) 645 | } 646 | } else { 647 | userElement.addChild(XMLElement(name: "username", stringValue: identifier.trimmingCharacters(in: CharacterSet.whitespaces))) 648 | } 649 | usersElement.addChild(userElement) 650 | } 651 | root.addChild(isSmartElement) 652 | root.addChild(usersElement) 653 | return xml.xmlData 654 | } 655 | 656 | return Data("nil".utf8) 657 | } 658 | 659 | func populateElement(variableToCheck: String, 660 | elementName: String, 661 | elementToAdd: XMLElement, 662 | whereToAdd: XMLElement) { 663 | // Populate the element as needed 664 | var elementToAdd = XMLElement(name: elementName, stringValue: variableToCheck) 665 | 666 | if variableToCheck == removalValue { 667 | elementToAdd = XMLElement(name: elementName, stringValue: "") 668 | whereToAdd.addChild(elementToAdd) 669 | } else if variableToCheck != "" { 670 | whereToAdd.addChild(elementToAdd) 671 | } 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: mut 5 | description: A Mass Update Tool that allows Jamf Pro admins to update their inventory records. 6 | labels: 7 | jira-key: AEGIS 8 | spec: 9 | type: application 10 | lifecycle: production 11 | owner: Aegis 12 | system: mut 13 | --------------------------------------------------------------------------------