├── test_attributes ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── reconForm ├── README.md ├── JLG_Swift ├── JLG_Custom └── Jamf Log Grabber /test_attributes: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "testing jamf pro variables" > $HOME/Desktop/test.txt 4 | echo "4 is $4" >> $HOME/Desktop/test.txt 5 | echo "5 is $5" >> $HOME/Desktop/test.txt 6 | echo "6 is $6" >> $HOME/Desktop/test.txt 7 | echo "7 is $7" >> $HOME/Desktop/test.txt 8 | echo "arg1 is $4 or $ARG1" >> $HOME/Desktop/test.txt 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - MacOS Version 28 | - Execution Method [Self Service, Policy, Terminal] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /reconForm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #This is for troubleshooting Recon issues where you may see errors in the jamf.log file such as 'Error running recon: Invalid Message - The message could not be parsed.' 4 | #BE CAREFUL nesting this inside of policies. You're calling a jamf process inside of a jamf process whether it's self service or via 'sudo jamf police -id ##' and can create MORE trouble. 5 | #Recommend running this on the device as a local file then running log grabber which will pick this up too. 6 | 7 | #Leave default variables if using Jamf Log Grabber in conjunction with this tool 8 | log_folder="$HOME/Desktop" 9 | outputFile="$log_folder/Recon/RawXMLReconForm" 10 | finalOutputFile="$log_folder/Recon/Recon.xml" 11 | 12 | reconForm() { 13 | #Create reconForm 14 | sudo jamf recon -saveFormTo $log_folder/Recon/ 15 | #Finds the file created so it can convert it 16 | logFolderPath=$(find $log_folder/Recon -name '*.reconForm') 17 | #Echo for debugging file path 18 | echo $logFolderPath 19 | #Change to XML format 20 | mv -- "$logFolderPath" "${outputFile%.reconForm}.xml" 21 | #Pretty print XML 22 | xmllint -format "$outputFile".xml > $finalOutputFile 23 | } 24 | 25 | reconForm 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jamf Log Grabber 2 | 3 | > [!CAUTION] 4 | > This script is not intended for using to attach logs to a device record in Jamf Pro. Do not utilize such a workflow as it can lead to severe server performance issues. 5 | 6 | 7 | Jamf Log Grabber is a bash based script that can be deployed manually, via Jamf MDM Policy, or as a Self Service Tool. It creates a zip folder on the user's desktop for upload to your service desk for troubleshooting. 8 | 9 | ## Supported Applications Include 10 | - All Jamf Applications 11 | - Device Compliance 12 | - App Installers 13 | - Multiple client side diagnostics 14 | - Up to 3 custom apps 15 | 16 | 17 | 18 | 19 | ## Features 20 | 21 | - Simplified Customization 22 | - Verbose results file for faster diagnostics 23 | - MDM Communication Information 24 | - Network checks for Jamf Remote Assist and App Installers 25 | - Inventory Troubleshooting: Checks for files left behind during a Jamf Recon command and provides a file name for further investigation 26 | - Jamf Connect License Check and troubleshooting 27 | - 3 Custom apps to configure for your own log gathering 28 | - Jamf Protect Diagnostics: Set Parameter 10 to 'true' in your policy 29 | 30 | ## Installation 31 | 32 | Copy and paste the latest version of Jamf Log Grabber to a scripts payload as outlined below. 33 | 34 | -In Jamf Pro, paste the contents of the script in a new script payload under Settings> Computer Management> Scripts> +New 35 | 36 | -Click on Options and set the names for Parameters 4-9 as follows 37 | 38 | 39 | -Under Computers> Policies, create a new policy that contains the Jamf Log Grabber. If you want to get additional app logs, set the name and file path you want as seen below in the "Parameter Values" section 40 | 41 | 42 | -When the script is ran, there will be a folder with your app name and the logs inside as seen with DEPNotify in this example. 43 | 44 | 45 | 46 | -If you do not see your app folder, it is because the file was not found and the cleanup array removed the empty folder. You can confirm checking the cleanup section of results.html 47 | -------------------------------------------------------------------------------- /JLG_Swift: -------------------------------------------------------------------------------- 1 | // 2 | // JLG_Swift.swift 3 | // 4 | // Created by Zach Propheter on 5 | // Updated by Paull Stanley on 3/22/25. 6 | // Jamf Trust Diag added on 4/9/25 7 | // Device Compliance update added 5/5/25 8 | 9 | import Foundation 10 | 11 | let fileManager = FileManager.default 12 | 13 | // MARK: - Helper Functions 14 | 15 | func runCommand(_ command: String) -> String { 16 | let process = Process() 17 | let pipe = Pipe() 18 | 19 | process.executableURL = URL(fileURLWithPath: "/bin/zsh") 20 | process.arguments = ["-c", command] 21 | process.standardOutput = pipe 22 | process.standardError = pipe 23 | 24 | do { 25 | try process.run() 26 | } catch { 27 | print("❌ Failed to launch command: \(command)\n\(error.localizedDescription)") 28 | return "" 29 | } 30 | 31 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 32 | return String(data: data, encoding: .utf8) ?? "" 33 | } 34 | 35 | func runAndSave(command: String, outputPath: String) { 36 | let output = runCommand(command) 37 | let outputURL = URL(fileURLWithPath: outputPath) 38 | 39 | do { 40 | try output.write(to: outputURL, atomically: true, encoding: .utf8) 41 | print("✅ Saved command output to: \(outputPath)") 42 | } catch { 43 | print("❌ Failed to save output to \(outputPath): \(error.localizedDescription)") 44 | } 45 | } 46 | 47 | func copyItem(from sourcePath: String, to destinationPath: String) { 48 | let sourceURL = URL(fileURLWithPath: sourcePath) 49 | let destinationURL = URL(fileURLWithPath: destinationPath) 50 | 51 | guard fileManager.fileExists(atPath: sourcePath) else { 52 | print("⚠️ Source not found: \(sourcePath)") 53 | return 54 | } 55 | 56 | do { 57 | if fileManager.fileExists(atPath: destinationPath) { 58 | try fileManager.removeItem(at: destinationURL) 59 | } 60 | try fileManager.copyItem(at: sourceURL, to: destinationURL) 61 | print("✅ Copied \(sourcePath) to \(destinationPath)") 62 | } catch { 63 | print("❌ Copy failed from \(sourcePath) to \(destinationPath): \(error.localizedDescription)") 64 | } 65 | } 66 | 67 | func createFolder(at path: String) { 68 | let folderURL = URL(fileURLWithPath: path) 69 | do { 70 | try fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) 71 | print("📁 Created folder: \(path)") 72 | } catch { 73 | print("❌ Failed to create folder at \(path): \(error.localizedDescription)") 74 | } 75 | } 76 | 77 | // MARK: - Parse CLI Arguments 78 | 79 | let arguments = CommandLine.arguments 80 | 81 | let protectDiagnosticsEnabled = arguments.contains("--protectDiagnostics") || arguments.contains("-p") 82 | let sysdiagnoseEnabled = arguments.contains("--sysdiagnose") || arguments.contains("-s") 83 | 84 | print("🚀 Script started with arguments:") 85 | print("- Protect Diagnostics: \(protectDiagnosticsEnabled ? "ENABLED" : "DISABLED")") 86 | print("- Sysdiagnose: \(sysdiagnoseEnabled ? "ENABLED" : "DISABLED")") 87 | 88 | // MARK: - User & Paths Setup 89 | 90 | let loggedInUser = runCommand("stat -f%Su /dev/console").trimmingCharacters(in: .whitespacesAndNewlines) 91 | let userHomeDirectory = "/Users/\(loggedInUser)" 92 | let desktopDirectory = "\(userHomeDirectory)/Desktop" 93 | 94 | // Timestamp & main folder 95 | let formatter = DateFormatter() 96 | formatter.dateFormat = "MM-dd-yyyy" 97 | let dateString = formatter.string(from: Date()) 98 | let mainFolderName = "\(loggedInUser)_\(dateString)_logs" 99 | let mainFolderPath = "\(desktopDirectory)/\(mainFolderName)" 100 | 101 | // Rename existing folder if necessary 102 | if fileManager.fileExists(atPath: mainFolderPath) { 103 | let oldFolderPath = "\(mainFolderPath)_old" 104 | do { 105 | if fileManager.fileExists(atPath: oldFolderPath) { 106 | try fileManager.removeItem(atPath: oldFolderPath) 107 | } 108 | try fileManager.moveItem(atPath: mainFolderPath, toPath: oldFolderPath) 109 | print("📦 Renamed existing folder to: \(oldFolderPath)") 110 | } catch { 111 | print("❌ Failed to rename existing folder: \(error.localizedDescription)") 112 | } 113 | } 114 | 115 | // MARK: - Create Folder Structure 116 | 117 | createFolder(at: mainFolderPath) 118 | 119 | let folders = [ 120 | "Client_Logs", 121 | "Managed_Preferences", 122 | "Client_Logs/SoftwareUpdates", 123 | "Client_Logs/DDM", 124 | "Client_Logs/DiagnosticReports", 125 | "App_Installers", 126 | "Jamf_Security", 127 | "Device_Compliance", 128 | "Connect" 129 | ] 130 | 131 | folders.forEach { 132 | createFolder(at: "\(mainFolderPath)/\($0)") 133 | } 134 | 135 | let clientLogsPath = "\(mainFolderPath)/Client_Logs" 136 | let managedPrefsPath = "\(mainFolderPath)/Managed_Preferences" 137 | let softwareUpdatesPath = "\(clientLogsPath)/SoftwareUpdates" 138 | let ddmPath = "\(clientLogsPath)/DDM" 139 | let jamfSecurityFolder = "\(mainFolderPath)/Jamf_Security" 140 | 141 | // MARK: - Sudo Privileges Check 142 | 143 | let isRoot = (getuid() == 0) 144 | print(isRoot ? "✅ Running with sudo privileges." : "⚠️ Warning: Not running with sudo. Some files may be inaccessible.") 145 | 146 | // MARK: - PROTECTCTL DIAGNOSTICS 147 | 148 | if protectDiagnosticsEnabled { 149 | let protectDiagnosticsFolder = jamfSecurityFolder 150 | print("⏳ Running protectctl diagnostics (this may take a while)...") 151 | 152 | let protectCommand = "protectctl diagnostics -o '\(protectDiagnosticsFolder)' -d 5" 153 | let output = runCommand(protectCommand) 154 | 155 | print("📝 protectctl diagnostics command output: \(output)") 156 | 157 | do { 158 | let contents = try fileManager.contentsOfDirectory(atPath: protectDiagnosticsFolder) 159 | print("📂 protectctl diagnostics output files: \(contents.joined(separator: ", "))") 160 | } catch { 161 | print("❌ Failed to list protectctl diagnostics folder: \(error.localizedDescription)") 162 | } 163 | 164 | print("✅ protectctl diagnostics completed.") 165 | } 166 | 167 | 168 | // MARK: - SYSDIAGNOSE 169 | 170 | if sysdiagnoseEnabled { 171 | let sysdiagnosePath = mainFolderPath 172 | print("⏳ Running sysdiagnose (this may take several minutes)...") 173 | 174 | let sysdiagnoseCommand = "sudo sysdiagnose -u -f'\(sysdiagnosePath)'" 175 | let output = runCommand(sysdiagnoseCommand) 176 | 177 | print("📝 sysdiagnose command output: \(output)") 178 | 179 | print("✅ sysdiagnose completed.") 180 | } 181 | 182 | 183 | // MARK: - File Gathering 184 | 185 | copyItem(from: "/Library/Receipts/InstallHistory.plist", to: "\(clientLogsPath)/InstallHistory.plist") 186 | copyItem(from: "/Library/Managed Preferences", to: managedPrefsPath) 187 | copyItem(from: "\(userHomeDirectory)/Library/Logs/JAMF", to: "\(clientLogsPath)/SelfService") 188 | 189 | let selfServicePlusAppPath = "/Applications/Self Service+.app" 190 | if fileManager.fileExists(atPath: selfServicePlusAppPath) { 191 | let ssPlusLogCommand = """ 192 | /usr/bin/log show --style compact --predicate 'subsystem == "com.jamf.selfserviceplus"' --debug --info --last 1d 193 | """ 194 | runAndSave(command: ssPlusLogCommand, outputPath: "\(clientLogsPath)/SelfService/SelfServicePlus.log") 195 | } else { 196 | print("ℹ️ Self Service+ not installed.") 197 | } 198 | 199 | runAndSave(command: "system_profiler -xml", outputPath: "\(clientLogsPath)/SystemReport.spx") 200 | runAndSave(command: "profiles show -output stdout-xml", outputPath: "\(clientLogsPath)/profiles.xml") 201 | 202 | // Convert Profiles XML to JSON 203 | class XMLToJSONParser: NSObject, XMLParserDelegate { 204 | private var resultStack: [[String: Any]] = [[:]] 205 | private var currentElement = "" 206 | private var currentText = "" 207 | private var currentArrayStack: [[Any]] = [[]] 208 | 209 | func parse(xmlData: Data) -> Data? { 210 | let parser = XMLParser(data: xmlData) 211 | parser.delegate = self 212 | if parser.parse(), let json = resultStack.first { 213 | return try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) 214 | } 215 | return nil 216 | } 217 | 218 | // XMLParser Delegate Methods 219 | func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) { 220 | currentElement = elementName 221 | currentText = "" 222 | 223 | if !attributeDict.isEmpty { 224 | resultStack.append([elementName: attributeDict]) 225 | } else if elementName == "dict" || elementName == "array" { 226 | resultStack.append([:]) 227 | currentArrayStack.append([]) 228 | } 229 | } 230 | 231 | func parser(_ parser: XMLParser, foundCharacters string: String) { 232 | currentText += string.trimmingCharacters(in: .whitespacesAndNewlines) 233 | } 234 | 235 | func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { 236 | let trimmedText = currentText.trimmingCharacters(in: .whitespacesAndNewlines) 237 | 238 | if elementName == "key" { 239 | currentElement = trimmedText 240 | } else if !trimmedText.isEmpty { 241 | if elementName == "integer", let intValue = Int(trimmedText) { 242 | resultStack[resultStack.count - 1][currentElement] = intValue 243 | } else if elementName == "true" { 244 | resultStack[resultStack.count - 1][currentElement] = true 245 | } else if elementName == "false" { 246 | resultStack[resultStack.count - 1][currentElement] = false 247 | } else { 248 | resultStack[resultStack.count - 1][currentElement] = trimmedText 249 | } 250 | } else if elementName == "dict" { 251 | let dict = resultStack.popLast() ?? [:] 252 | if var last = resultStack.popLast() { 253 | if last[currentElement] == nil { 254 | last[currentElement] = dict 255 | } else if var existingArray = last[currentElement] as? [[String: Any]] { 256 | existingArray.append(dict) 257 | last[currentElement] = existingArray 258 | } else { 259 | last[currentElement] = [last[currentElement], dict] 260 | } 261 | resultStack.append(last) 262 | } 263 | } else if elementName == "array" { 264 | let array = currentArrayStack.popLast() ?? [] 265 | if var last = resultStack.popLast() { 266 | last[currentElement] = array 267 | resultStack.append(last) 268 | } 269 | } 270 | currentText = "" 271 | } 272 | } 273 | 274 | 275 | let profilesLogsPath = "\(clientLogsPath)/profiles.xml" 276 | // Function to convert XML plist to JSON 277 | func convertXMLPlistToJSON(filePath: String) { 278 | let fileURL = URL(fileURLWithPath: filePath) 279 | 280 | do { 281 | let xmlData = try Data(contentsOf: fileURL) 282 | var format = PropertyListSerialization.PropertyListFormat.xml 283 | let plistObject = try PropertyListSerialization.propertyList(from: xmlData, options: [], format: &format) 284 | 285 | let jsonData = try JSONSerialization.data(withJSONObject: plistObject, options: .prettyPrinted) 286 | let jsonFilePath = fileURL.deletingPathExtension().appendingPathExtension("json") 287 | try jsonData.write(to: jsonFilePath) 288 | 289 | print("✅ JSON file saved at: \(jsonFilePath.path)") 290 | } catch { 291 | print("❌ Error: \(error.localizedDescription)") 292 | } 293 | } 294 | 295 | // Main execution 296 | convertXMLPlistToJSON(filePath: profilesLogsPath) 297 | 298 | 299 | // MARK: - MDM Client Commands 300 | 301 | let mdmCommands: [(String, String)] = [ 302 | ("/usr/libexec/mdmclient AvailableOSUpdates", "\(softwareUpdatesPath)/AvailableOSUpdates.txt"), 303 | ("/usr/libexec/mdmclient QueryDeviceInformation", "\(clientLogsPath)/QueryDeviceInformation.txt"), 304 | ("/usr/libexec/mdmclient DumpManagementStatus", "\(clientLogsPath)/DumpManagementStatus.txt") 305 | ] 306 | 307 | mdmCommands.forEach { runAndSave(command: $0.0, outputPath: $0.1) } 308 | 309 | // MARK: - Additional Logs & Copies 310 | 311 | runAndSave(command: "launchctl dumpstate", outputPath: "\(clientLogsPath)/launchctl_dumpstate.txt") 312 | runAndSave(command: "systemextensionsctl list", outputPath: "\(clientLogsPath)/system_extensions.txt") 313 | runAndSave(command: "kextstat", outputPath: "\(clientLogsPath)/kextstat.txt") 314 | runAndSave(command: "cat /Library/Application\\ Support/JAMF/.jmf_settings.json", outputPath: "\(clientLogsPath)/restricted_software.json") 315 | 316 | copyItem(from: "/System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/SoftwareUpdateSubscriber.xpc/Contents/Info.plist", to: "\(softwareUpdatesPath)/ClientInfo.plist") 317 | copyItem(from: "/private/var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plist", to: "\(softwareUpdatesPath)/DDM.plist") 318 | 319 | // DDM XPC Info Plist collection 320 | let xpcServicesPath = "/System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices" 321 | if let xpcContents = try? fileManager.contentsOfDirectory(atPath: xpcServicesPath) { 322 | for item in xpcContents where item.hasSuffix(".xpc") && item != "SoftwareUpdateSubscriber.xpc" { 323 | let infoPlistPath = "\(xpcServicesPath)/\(item)/Contents/Info.plist" 324 | let destPlistPath = "\(ddmPath)/\(item)_Info.plist" 325 | copyItem(from: infoPlistPath, to: destPlistPath) 326 | } 327 | } 328 | 329 | copyItem(from: "/Library/Logs/DiagnosticReports", to: "\(clientLogsPath)/DiagnosticReports") 330 | 331 | // MARK: - App Installers & Flagged Installs 332 | 333 | copyItem(from: "/var/db/ConfigurationProfiles/Settings/Managed Applications/Device", to: "\(mainFolderPath)/App_Installers") 334 | 335 | let completedPath = "/var/db/ConfigurationProfiles/Settings/Managed Applications/Device/_completed" 336 | let flaggedFilesPath = "\(mainFolderPath)/App_Installers/Flagged_Installers.txt" 337 | 338 | if fileManager.fileExists(atPath: completedPath) { 339 | do { 340 | let files = try fileManager.contentsOfDirectory(atPath: completedPath) 341 | let flaggedFiles = files.filter { 342 | let installFailedOutput = runCommand("defaults read '\(completedPath)/\($0)' InstallFailed") 343 | return installFailedOutput.trimmingCharacters(in: .whitespacesAndNewlines) == "1" || installFailedOutput.lowercased().contains("true") 344 | } 345 | 346 | let flaggedOutput = flaggedFiles.isEmpty ? "No failed installers found." : flaggedFiles.joined(separator: "\n") 347 | try flaggedOutput.write(toFile: flaggedFilesPath, atomically: true, encoding: .utf8) 348 | 349 | print("✅ Flagged installers report saved at \(flaggedFilesPath)") 350 | } catch { 351 | print("❌ Error scanning _completed folder: \(error.localizedDescription)") 352 | } 353 | } else { 354 | print("⚠️ No _completed folder found at \(completedPath)") 355 | } 356 | 357 | // MARK: - Jamf Security & Device Compliance Logs 358 | 359 | if !runCommand("which protectctl").isEmpty { 360 | runAndSave(command: "protectctl info --verbose", outputPath: "\(mainFolderPath)/Jamf_Security/jamfprotectinfo.log") 361 | } else { 362 | print("⚠️ protectctl not found on this system.") 363 | } 364 | 365 | func runAndStreamToFile(command: String, arguments: [String], outputPath: String) { 366 | let process = Process() 367 | process.executableURL = URL(fileURLWithPath: command) 368 | process.arguments = arguments 369 | 370 | let outputPipe = Pipe() 371 | process.standardOutput = outputPipe 372 | process.standardError = outputPipe 373 | 374 | let fileHandle: FileHandle 375 | do { 376 | FileManager.default.createFile(atPath: outputPath, contents: nil, attributes: nil) 377 | fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: outputPath)) 378 | } catch { 379 | print("Failed to open file for writing: \(error)") 380 | return 381 | } 382 | 383 | let readingHandle = outputPipe.fileHandleForReading 384 | readingHandle.readabilityHandler = { handle in 385 | let data = handle.availableData 386 | if data.isEmpty { 387 | return 388 | } 389 | 390 | // Just write to file — no terminal output 391 | do { 392 | try fileHandle.write(contentsOf: data) 393 | } catch { 394 | print("Error writing to file: \(error)") 395 | } 396 | } 397 | 398 | do { 399 | try process.run() 400 | process.waitUntilExit() 401 | readingHandle.readabilityHandler = nil 402 | try fileHandle.close() 403 | print("Finished writing to \(outputPath)") 404 | } catch { 405 | print("Error running process: \(error)") 406 | } 407 | } 408 | 409 | 410 | runAndStreamToFile( 411 | command: "/usr/bin/log", 412 | arguments: [ 413 | "show", 414 | "--debug", 415 | "--info", 416 | "--predicate", 417 | #"subsystem CONTAINS "jamfAAD" OR subsystem BEGINSWITH "com.apple.AppSSO" OR subsystem BEGINSWITH "com.jamf.backgroundworkflows""#, 418 | "--last", "1d", 419 | ], 420 | outputPath: "\(mainFolderPath)/Device_Compliance/JamfConditionalAccess.log" 421 | ) 422 | 423 | // MARK: - Jamf Connect Logs 424 | 425 | var jamfConnectBinary = runCommand("which jamfconnect").trimmingCharacters(in: .whitespacesAndNewlines) 426 | if jamfConnectBinary.isEmpty, fileManager.fileExists(atPath: "/usr/local/bin/jamfconnect") { 427 | jamfConnectBinary = "/usr/local/bin/jamfconnect" 428 | } 429 | 430 | if !jamfConnectBinary.isEmpty { 431 | _ = runCommand("\(jamfConnectBinary) logs") 432 | 433 | let logsSource = "/Library/Application Support/JamfConnect/Logs" 434 | if let items = try? fileManager.contentsOfDirectory(atPath: logsSource), 435 | let zipFile = items.first(where: { $0.hasSuffix(".zip") }) { 436 | _ = runCommand("unzip -o '\(logsSource)/\(zipFile)' -d '\(mainFolderPath)/Connect'") 437 | print("✅ Extracted Jamf Connect logs.") 438 | } else { 439 | print("⚠️ Jamf Connect logs not found.") 440 | } 441 | } else { 442 | print("⚠️ Jamf Connect binary not found. Skipping.") 443 | } 444 | 445 | // MARK: - Recon Folder for JAMF tmp 446 | 447 | if runCommand("ps -acx | grep 'jamf$'").isEmpty { 448 | createFolder(at: "\(mainFolderPath)/Recon") 449 | copyItem(from: "/Library/Application Support/JAMF/tmp/", to: "\(mainFolderPath)/Recon") 450 | } else { 451 | print("⚠️ Jamf process is running. Skipping Recon.") 452 | } 453 | 454 | // MARK: - Plist Conversion 455 | 456 | func convertPlistToXMLSwift(inputPath: String) { 457 | guard let plistData = fileManager.contents(atPath: inputPath) else { 458 | print("❌ Failed to read plist at \(inputPath)") 459 | return 460 | } 461 | 462 | do { 463 | let plistObject = try PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) 464 | let xmlData = try PropertyListSerialization.data(fromPropertyList: plistObject, format: .xml, options: 0) 465 | try xmlData.write(to: URL(fileURLWithPath: inputPath)) 466 | print("✅ Converted plist to XML: \(inputPath)") 467 | } catch { 468 | print("❌ Error converting plist at \(inputPath): \(error.localizedDescription)") 469 | } 470 | } 471 | 472 | func convertAllPlistsToXMLSwift(atPath path: String) { 473 | guard let enumerator = fileManager.enumerator(atPath: path) else { 474 | print("❌ Failed to enumerate: \(path)") 475 | return 476 | } 477 | 478 | for case let file as String in enumerator where file.hasSuffix(".plist") { 479 | convertPlistToXMLSwift(inputPath: "\(path)/\(file)") 480 | } 481 | } 482 | 483 | [clientLogsPath, managedPrefsPath, softwareUpdatesPath, ddmPath, "\(mainFolderPath)/App_Installers", "\(mainFolderPath)/Connect"] 484 | .forEach { convertAllPlistsToXMLSwift(atPath: $0) } 485 | 486 | // Move Security Plists 487 | // Plist files to look for 488 | let securityPlists = ["com.jamf.trust.plist", "com.jamf.protect.plist"] 489 | 490 | for plist in securityPlists { 491 | let sourcePlistPath = "\(managedPrefsPath)/\(plist)" 492 | let destPlistPath = "\(jamfSecurityFolder)/\(plist)" 493 | 494 | if fileManager.fileExists(atPath: sourcePlistPath) { 495 | do { 496 | try fileManager.copyItem(atPath: sourcePlistPath, toPath: destPlistPath) 497 | print("✅ Copied \(plist) from Managed_Preferences to Jamf_Security.") 498 | } catch { 499 | print("❌ Failed to copy \(plist): \(error.localizedDescription)") 500 | } 501 | } else { 502 | print("⚠️ \(plist) not found in Managed_Preferences.") 503 | } 504 | } 505 | 506 | 507 | // Define constants 508 | let domains = ["map.wandera.com", "jamf.com"] 509 | let ports = [80, 443] 510 | let includeDNSTests = true 511 | let includeCurlTests = true 512 | let includePortTests = true 513 | let includePingTests = true 514 | let pingCount = 4 515 | let curlTimeout: TimeInterval = 5 516 | let portTimeout: TimeInterval = 2 517 | let sleepWait: TimeInterval = 10 518 | 519 | let unixTimeStamp = Int(Date().timeIntervalSince1970) 520 | let outputPath = "\(jamfSecurityFolder)" 521 | var pathToOutput = "\(outputPath)/JamfTrustDiagnostics.txt" 522 | 523 | // Function to run the tests (your existing code) 524 | func jamfTrustDiagnostics() { 525 | func postDateTime() { 526 | let utcDateTime = DateFormatter() 527 | utcDateTime.dateFormat = "yyyy-MM-dd HH:mm:ss" 528 | let dateStr = utcDateTime.string(from: Date()) 529 | try? "\(dateStr)\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 530 | } 531 | 532 | func listSystemDetails() { 533 | try? "\n---------------------------------- System Details ----------------------------------\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 534 | postMacOSVersion() 535 | postNameServers() 536 | postSystemExtensions() 537 | postNetworkServices() 538 | postIfConfig() 539 | postNetStat() 540 | try? "\n---------------------------------- System Details ----------------------------------\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 541 | } 542 | 543 | func postMacOSVersion() { 544 | let macOSVersion = runCommand("sw_vers") 545 | try? "\nMacOS Version:\n\(macOSVersion)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 546 | } 547 | 548 | func postNameServers() { 549 | let nameServers = runCommand("scutil --dns | grep 'nameserver\\[[0-9]*\\]'") 550 | try? "\nNameservers:\n\(nameServers)\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 551 | } 552 | 553 | func postSystemExtensions() { 554 | let extensions = runCommand("systemextensionsctl list") 555 | try? "\nSystem Extensions:\n\(extensions)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 556 | } 557 | 558 | func postNetworkServices() { 559 | let services = runCommand("networksetup -listallnetworkservices") 560 | try? "\nNetwork Services:\n\(services)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 561 | } 562 | 563 | func postIfConfig() { 564 | let ifConfig = runCommand("ifconfig -a -v") 565 | try? "\nifconfig:\n\(ifConfig)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 566 | } 567 | 568 | func postNetStat() { 569 | let netStat = runCommand("netstat -rn") 570 | try? "\nnetstat:\n\(netStat)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 571 | } 572 | 573 | func runCommand(_ command: String) -> String { 574 | let task = Process() 575 | let pipe = Pipe() 576 | 577 | task.executableURL = URL(fileURLWithPath: "/bin/bash") 578 | task.arguments = ["-c", command] 579 | task.standardOutput = pipe 580 | 581 | do { 582 | try task.run() 583 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 584 | return String(data: data, encoding: .utf8) ?? "" 585 | } catch { 586 | return "Error running command: \(command)" 587 | } 588 | } 589 | 590 | func postResults(_ result: String) { 591 | try? result.write(toFile: pathToOutput, atomically: true, encoding: .utf8) 592 | } 593 | 594 | func pingTests(domain: String) { 595 | let pingTest = runCommand("ping -c \(pingCount) \(domain) 2>&1") 596 | let ping6Test = runCommand("ping6 -c \(pingCount) \(domain) 2>&1") 597 | 598 | try? "\nPing (IPV4):\n\(pingTest)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 599 | try? "\nPing (IPV6):\n\(ping6Test)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 600 | } 601 | 602 | func curlTests(domain: String) { 603 | let curlHttpTest = runCommand("curl -L -m \(curlTimeout) --silent --head http://\(domain) | awk '/^HTTP/'") 604 | let curlHttpsTest = runCommand("curl -L -m \(curlTimeout) --silent --head https://\(domain) | awk '/^HTTP/'") 605 | 606 | try? "\n\nHTTP Request:\n\(curlHttpTest)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 607 | try? "\n\nHTTPS Request:\n\(curlHttpsTest)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 608 | } 609 | 610 | func dnsTests(domain: String) { 611 | let result = runCommand("host \(domain) | awk '{print $NF}'") 612 | let dnsServer = runCommand("dig \(domain) | grep ';; SERVER:' | awk '{ $1=$2=\"\"; print $0 }' | sed 's/^[ \\t]*//'") 613 | 614 | try? "Domain: \(domain)\nResults: \(result)\nDNS Server being queried: \(dnsServer)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 615 | } 616 | 617 | func portTests(domain: String) { 618 | try? "\n\nPort Tests:\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 619 | for port in ports { 620 | let portTest = runCommand("nc -z -v -w \(Int(portTimeout)) \(domain) \(port) 2>&1") 621 | try? "\(portTest)".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 622 | } 623 | } 624 | 625 | func doTests() { 626 | for domain in domains { 627 | try? "\n---------------------------------- Tests for \(domain) ----------------------------------\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 628 | 629 | if includeDNSTests { dnsTests(domain: domain) } 630 | if includePingTests { pingTests(domain: domain) } 631 | if includeCurlTests { curlTests(domain: domain) } 632 | if includePortTests { portTests(domain: domain) } 633 | } 634 | } 635 | 636 | func run() { 637 | try? FileManager.default.createDirectory(atPath: outputPath, withIntermediateDirectories: true, attributes: nil) 638 | pathToOutput = "\(outputPath)/JamfTrustDiagnostics.txt" 639 | 640 | try? "\n=============================== Jamf Trust ===============================\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 641 | postDateTime() 642 | doTests() 643 | listSystemDetails() 644 | try? "\n=============================== Jamf Trust ===============================\n".write(toFile: pathToOutput, atomically: true, encoding: .utf8) 645 | 646 | } 647 | 648 | run() 649 | } 650 | 651 | // Check for the presence of the file before running the tests 652 | func checkAndTrustDiagnostics() { 653 | let filePath = "/Library/Managed Preferences/com.jamf.trust.plist" 654 | let fileManager = FileManager.default 655 | 656 | if fileManager.fileExists(atPath: filePath) { 657 | // File exists, run the tests 658 | print("✅ File found. Running the tests...") 659 | jamfTrustDiagnostics() 660 | } else { 661 | // File doesn't exist, print a message 662 | print("❌ File not found at \(filePath). Skipping tests.") 663 | } 664 | } 665 | 666 | // Call the function to check and run tests 667 | checkAndTrustDiagnostics() 668 | 669 | // MDMCheck.txt Creation and output 670 | 671 | // Function to run a shell command and return its output 672 | func runShellCommand(_ command: String, arguments: [String] = []) -> String { 673 | let process = Process() 674 | process.launchPath = command 675 | process.arguments = arguments 676 | 677 | let pipe = Pipe() 678 | process.standardOutput = pipe 679 | process.standardError = pipe 680 | 681 | do { 682 | try process.run() 683 | } catch { 684 | return "Error running command: \(command)" 685 | } 686 | 687 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 688 | return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" 689 | } 690 | 691 | // Paths 692 | 693 | let mdmCheckFilePath = "\(mainFolderPath)/MDMCheck.txt" 694 | 695 | // Start writing the results 696 | var mdmCheckResults = "MDM Check Report\n=================\n" 697 | 698 | // Get the logged-in user 699 | 700 | mdmCheckResults += "Checking \(loggedInUser)'s computer for MDM communication issues:\n" 701 | 702 | // 1. Check MDM status 703 | let mdmLog = runShellCommand("/usr/bin/log", arguments: ["show", "--style", "compact", "--predicate", "(process CONTAINS \"mdmclient\")", "--last", "1d"]) 704 | 705 | if mdmLog.contains("Unable to create MDM identity") { 706 | mdmCheckResults += "- MDM is broken\n" 707 | } else { 708 | mdmCheckResults += "- MDM is communicating\n" 709 | } 710 | 711 | // 2. Check if the MDM Profile is installed 712 | let mdmProfileCheck = runShellCommand("/usr/libexec/mdmclient", arguments: ["QueryInstalledProfiles"]) 713 | if mdmProfileCheck.contains("00000000-0000-0000-A000-4A414D460003") { 714 | mdmCheckResults += "- MDM Profile Installed\n" 715 | } else { 716 | mdmCheckResults += "- MDM Profile Not Installed\n" 717 | } 718 | 719 | // 3. Get the MDM Daemon Status 720 | let apsctlStatus = runShellCommand("/System/Library/PrivateFrameworks/ApplePushService.framework/apsctl", arguments: ["status"]) 721 | let daemonLines = apsctlStatus.components(separatedBy: "\n") 722 | if let daemonLine = daemonLines.first(where: { $0.contains("persistent connection status") }) { 723 | let statusValue = daemonLine.components(separatedBy: ":").last?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown" 724 | mdmCheckResults += "- The MDM Daemon Status is: \(statusValue)\n" 725 | } else { 726 | mdmCheckResults += "- MDM Daemon Status: Not Found\n" 727 | } 728 | 729 | // 4. Get the APNS Topic 730 | let configProfiles = runShellCommand("/usr/sbin/system_profiler", arguments: ["SPConfigurationProfileDataType"]) 731 | let topicLine = configProfiles.components(separatedBy: "\n").first(where: { $0.contains("Topic") }) 732 | 733 | if let topic = topicLine?.components(separatedBy: "\"").dropFirst(1).first { 734 | mdmCheckResults += "- APNS Topic is: \(topic)\n" 735 | } else { 736 | mdmCheckResults += "- No APNS Topic Found\n" 737 | } 738 | 739 | // Write to the MDMCheck.txt file 740 | do { 741 | try mdmCheckResults.write(toFile: mdmCheckFilePath, atomically: true, encoding: .utf8) 742 | print("✅ MDMCheck.txt created at \(mdmCheckFilePath)") 743 | } catch { 744 | print("❌ Failed to write MDMCheck.txt: \(error.localizedDescription)") 745 | } 746 | 747 | print("✅ Log gathering complete! Files are saved to: \(mainFolderPath)") 748 | 749 | 750 | 751 | // Reset all folder permissions so users can delete 752 | 753 | func runCommand(_ command: String, arguments: [String] = []) -> (output: String?, error: String?, exitCode: Int32) { 754 | let process = Process() 755 | process.executableURL = URL(fileURLWithPath: "/usr/bin/env") 756 | process.arguments = [command] + arguments 757 | 758 | let outputPipe = Pipe() 759 | process.standardOutput = outputPipe 760 | 761 | let errorPipe = Pipe() 762 | process.standardError = errorPipe 763 | 764 | do { 765 | try process.run() 766 | process.waitUntilExit() 767 | } catch { 768 | return (nil, error.localizedDescription, -1) 769 | } 770 | 771 | let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() 772 | let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() 773 | 774 | let output = String(data: outputData, encoding: .utf8) 775 | let error = String(data: errorData, encoding: .utf8) 776 | 777 | return (output?.trimmingCharacters(in: .whitespacesAndNewlines), error?.trimmingCharacters(in: .whitespacesAndNewlines), process.terminationStatus) 778 | } 779 | 780 | func makeFilesDeletableByAnyone(at path: String) { 781 | // Set full read/write/execute permissions for all users (directories and files) 782 | let result = runCommand("chmod", arguments: ["-R", "777", path]) 783 | if result.exitCode == 0 { 784 | print("✅ Made files at \(path) accessible and deletable by any user.") 785 | } else { 786 | print("⚠️ Failed to update permissions at \(path): \(result.error ?? "Unknown error")") 787 | } 788 | } 789 | 790 | makeFilesDeletableByAnyone(at: mainFolderPath) 791 | -------------------------------------------------------------------------------- /JLG_Custom: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Jamf Log Grabber is designed to collect any logs associated with Jamf managed devices. 4 | 5 | #Custom arrays are now set for each individual type of log. It is recommended to include all as there are minor dependencies for some arrays. 6 | #This new workflow allows for you to add arrays for additional in house apps like SUPER, DEPNOTIFY, Crowdstrike, or any other commonly used MacOS applications. 7 | 8 | #################################################################################################### 9 | #This script is not intended for using to attach logs to a device record in Jamf Pro. Do not utilize such a workflow as it can lead to severe server performance issues 10 | #################################################################################################### 11 | 12 | # Redistribution and use in source and binary forms, with or without 13 | # modification, are permitted provided that the following conditions are met: 14 | # * Redistributions of source code must retain the above copyright 15 | # notice, this list of conditions and the following disclaimer. 16 | # * Redistributions in binary form must reproduce the above copyright 17 | # notice, this list of conditions and the following disclaimer in the 18 | # documentation and/or other materials provided with the distribution. 19 | # * Neither the name of the JAMF Software, LLC nor the 20 | # names of its contributors may be used to endorse or promote products 21 | # derived from this software without specific prior written permission. 22 | # THIS SOFTWARE IS PROVIDED BY JAMF SOFTWARE, LLC "AS IS" AND ANY 23 | # EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | # DISCLAIMED. IN NO EVENT SHALL JAMF SOFTWARE, LLC BE LIABLE FOR ANY 26 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | #################################################################################################### 34 | 35 | #DATE FOR LOG FOLDER ZIP CREATION 36 | current_date=$(date +"%Y-%m-%d") 37 | #HARD CODED VARIABLES, DO NOT CHANGE 38 | loggedInUser=$( echo "show State:/Users/ConsoleUser" | /usr/sbin/scutil | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }' ) 39 | #### Error check to make sure environment variables are correctly set as multiple recent reports in early 2024 had this broken 40 | echo "HOME is $HOME" 41 | if [[ $HOME == "" ]]; then 42 | HOME="/Users/$loggedInUser" 43 | fi 44 | echo $HOME 45 | #### End of HOME check 46 | log_folder=$HOME/Desktop/"$loggedInUser"_"$current_date"_logs 47 | results=$log_folder/Results.html 48 | JSS=$log_folder/Client_Logs 49 | security=$log_folder/Jamf_Security 50 | connect=$log_folder/Connect 51 | managed_preferences=$log_folder/Managed_Preferences 52 | recon=$log_folder/Recon 53 | self_service=$log_folder/Self_Service 54 | Device_Compliance=$log_folder/Device_Compliance 55 | JRA=$log_folder/JRA 56 | App_Installers=$log_folder/App_Installers 57 | jamfLog=$JSS/jamf.log 58 | 59 | reconleftovers=$(ls /Library/Application\ Support/JAMF/tmp/ 2> /dev/null) 60 | #runProtectDiagnostics="${10}" 61 | #for testing 62 | #runProtectDiagnostics="true" 63 | protectDiagnostics=$(ls "$HOME/Desktop/" | grep "JamfProtectDiagnostics") 64 | 65 | 66 | 67 | #DATE AND TIME FOR RESULTS.TXT INFORMATION 68 | #currenttime=$(date +"%D %T") 69 | currenttime() { 70 | date +"%D %T" 71 | } 72 | currenttime1=$(echo "$(currenttime)" | awk '{print $2}') 73 | 74 | #################################################################################################### 75 | #You can add custom app log grabbing using the following rubric, just continue numbering the appnames or renaming them to fit your needs 76 | #You can pass jamf script variables as part of a policy to get your additional apps 77 | 78 | #CustomApp1Name=$4 79 | CustomApp1Folder=$log_folder/$CustomApp1Name 80 | #CustomApp1LogSource="$5" 81 | #Now go down to CustomApp1Array and put in the files you want to grab 82 | #CustomApp2Name="$6" 83 | CustomApp2Folder=$log_folder/$CustomApp2Name 84 | #CustomApp2LogSource="$7" 85 | #Now go down to CustomApp2Array and put in the files you want to grab 86 | #CustomApp3Name="$8" 87 | CustomApp3Folder=$log_folder/$CustomApp3Name 88 | #CustomApp3LogSource="$9" 89 | #Now go down to CustomApp2Array and put in the files you want to grab 90 | #################################################################################################### 91 | 92 | #Build a results file in HTML 93 | buildHTMLResults() { 94 | printf ' 95 | 96 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |

Jamf Log Grabber Results

' > $results 123 | } 124 | 125 | #################################################################################################### 126 | #Array for Jamf Logs 127 | Jamf() { 128 | printf '

Client Logs

' >> $results 129 | mkdir -p $log_folder/Client_Logs 130 | #GET SYSTEM PROFILE REPORT- This spits out a couple errors due to KEXT stuff that's deprecated 131 | system_profiler -xml > $JSS/SystemReport.spx 2>/dev/null 132 | #ADD JAMF CLIENT LOGS TO LOG FOLDER 133 | if [ -e /private/var/log/jamf.log ]; then cp "/private/var/log/jamf.log" $JSS 134 | grep "Error" $JSS/jamf.log > $JSS/jamferror.log 135 | else 136 | printf '

%s

' "red" "Jamf Client Logs not found" >> $results 137 | fi 138 | #CHECK FOR JAMF INSTALL LOGS 139 | if [ -e /var/log/install.log ]; then cp "/var/log/install.log" $JSS 140 | else 141 | printf '%s
' "red" "Install Logs not found" >> $results 142 | fi 143 | #CHECK FOR JAMF SYSTEM LOGS 144 | if [ -e /var/log/system.log ]; then cp "/var/log/system.log" $JSS 145 | else 146 | printf '%s
' "red" "System Logs not found" >> $results 147 | fi 148 | #CHECK FOR JAMF SYSTEM LOGS 149 | if [ -e /Library/Logs/MCXTools.log ]; then cp "/Library/Logs/MCXTools.log" $JSS 150 | else 151 | printf '%s
' "red" "System Logs not found" >> $results 152 | fi 153 | #FIND AND COPY JAMF SOFTWARE PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 154 | #COPY DEBUG LOG 155 | if [ -e /Library/Preferences/com.jamfsoftware.jamf.plist ]; then cp "/Library/Preferences/com.jamfsoftware.jamf.plist" "$JSS/com.jamfsoftware.jamf.plist" | plutil -convert xml1 "$JSS/com.jamfsoftware.jamf.plist" 156 | else 157 | printf '%s
' "red" "Jamf Software plist not found" >> $results 158 | fi 159 | mkdir -p $log_folder/Self_Service 160 | #Checks what versions of self service are installed 161 | if [ -e /Applications/Self\ Service.app ]; then 162 | og="true" 163 | else 164 | og="false" 165 | fi 166 | echo "$og" 167 | if [[ -e /Applications/Self\ Service+.app ]]; then 168 | ssPlus="true" 169 | else 170 | ssPlus="false" 171 | fi 172 | echo "$ssPlus" 173 | #fun little logic to update selfServiceStatus variable to pull logs according to reporting 174 | if [ $og == "true" ] && [ $ssPlus == "true" ]; then 175 | selfServiceStatus="ogPlus" 176 | echo "true/true" 177 | echo $selfServiceStatus 178 | elif [ $og == "true" ] && [ $ssPlus == "false" ]; then 179 | selfServiceStatus="ogSS" 180 | echo "true/false" 181 | elif [ $og == "false" ] && [ $ssPlus == "true" ]; then 182 | selfServiceStatus="ssPlus" 183 | echo "false/true" 184 | else 185 | selfServiceStatus="notInstalled" 186 | echo "not installed" 187 | fi 188 | #everything put together to pull SS, SS+, or both apps logs 189 | case $selfServiceStatus in 190 | ogPlus) 191 | echo "Self Service and Self Service+ are installed on this machine" 192 | cp -r "$HOME/Library/Logs/JAMF/" $self_service 193 | log show --style compact --predicate 'subsystem == "com.jamf.selfserviceplus"' --debug --info > $self_service/SelfServicePlus.log 194 | ;; 195 | ssplus) 196 | echo "Self Service+ is installed on this machine" 197 | log show --style compact --predicate 'subsystem == "com.jamf.selfserviceplus"' --debug --info > $self_service/SelfServicePlus.log 198 | ;; 199 | ogSS) 200 | echo "Self Service is installed on this machine" 201 | cp -r "$HOME/Library/Logs/JAMF/" $self_service 202 | ;; 203 | *) 204 | echo "Self Service and Self Service+ are not installed on this machine" 205 | esac 206 | #Checks current MacOS Version against GDMF feed and flags if not a current release 207 | currentMacOS=$(sw_vers --buildVersion) 208 | checkIfSupportedOS=$(curl -s https://gdmf.apple.com/v2/pmv | grep -c $currentMacOS) 209 | if [[ $checkIfSupportedOS == 1 ]]; then 210 | printf '%s
' "white" "MacOS build $currentMacOS installed" >> $results 211 | else 212 | printf '%s
' "red" "MacOS build $currentMacOS installed. Unable to locate in GDMF feed." >> $results 213 | fi 214 | #Check if account is a Mobile Account and report if so 215 | NETACCLIST=$(dscl . list /Users OriginalNodeName | awk '{print $1}' 2>/dev/null) 216 | if [ "$NETACCLIST" == "" ]; then 217 | printf '%s
' "white" "No mobile accounts on device." >> $results 218 | else 219 | printf '%s
' "red" "The following are mobile accounts:" >> $results 220 | for account in $NETACCLIST; do 221 | printf ' '$account'
' >> $results 222 | done 223 | fi 224 | ############################################# 225 | # Software Update Stuff # 226 | ############################################# 227 | #Show Secure Token enabled users 228 | checkForSecureTokenUsers=$(fdesetup list | sed -E 's/,[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}//g') 229 | if [[ $checkForSecureTokenUsers == "" ]]; then 230 | printf '%s
' "red" "No Secure Token Users Found" >> $results 231 | else 232 | printf '%s
' "white" "Secure Token Users are: $checkForSecureTokenUsers" >> $results 233 | fi 234 | #Get info.plist that would be relayed to server for comparison 235 | mkdir -p $JSS/SoftwareUpdates 236 | if [[ -e /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/SoftwareUpdateSubscriber.xpc/Contents/Info.plist ]]; then 237 | cp /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/SoftwareUpdateSubscriber.xpc/Contents/Info.plist $JSS/SoftwareUpdates/ClientInfo.plist 238 | else 239 | printf '%s
' "red" "Unable to find SoftwareUpdate info.plist" >> $results 240 | fi 241 | if [[ -e /private/var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plist ]]; then 242 | /private/var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plist > $JSS/SoftwareUpdates/DDM.plist 243 | else 244 | printf '%s
' "red" "Unable to find Software Update DDM plist" >> $results 245 | fi 246 | ############################################# 247 | # DDM Info # 248 | ############################################# 249 | #Copy the current declaration info.plists for reference 250 | DDMInfoPlists=$(ls /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/ | grep -Ewv 'SoftwareUpdateSubscriber.xpc') 251 | mkdir -p $JSS/DDM 252 | for file in $DDMInfoPlists; do 253 | if [[ -e /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/$file/Contents/info.plist ]]; then 254 | cp /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/$file/Contents/info.plist $JSS/DDM/"$file"_info.plist 255 | fi 256 | done 257 | #Parse through all agents and deamons for any running keyword "jamf" and are not a part of standard Jamf applications. If none are found, they are still printed 258 | AgentsAndDaemons=$(grep -r "jamf" /Users/$loggedInUser/Library/LaunchAgents/ /Library/LaunchAgents/ /Library/LaunchDaemons/ /System/Library/LaunchAgents/ /System/Library/LaunchDaemons/) 259 | printf '%s
' "white" "A search for custom Agents and Daemons containing 'jamf' keywords has been ran and a copy of the results can be found in the Client Logs folder." >> $results 260 | echo -e "$AgentsAndDaemons" > $JSS/AgentsAndDaemons.txt 261 | #read blocked applications in jamf 262 | sudo cat /Library/Application\ Support/JAMF/.jmf_settings.json > $JSS/restricted_software.json 263 | #show installed profiles and output to xml. Use this to compare profile settings against actual settings in Managed Preferences Folder 264 | sudo profiles show -output $JSS/profiles.xml stdout-xml 265 | /usr/libexec/mdmclient AvailableOSUpdates > $JSS/SoftwareUpdates/AvailableOSUpdates.txt 266 | /usr/libexec/mdmclient QueryDeviceInformation > $JSS/QueryDeviceInformation.txt 267 | /usr/libexec/mdmclient DumpManagementStatus > $JSS/DumpManagementStatus.txt 268 | launchctl dumpstate > $JSS/launchctl_dumpstate.txt 269 | systemextensionsctl list > $JSS/system_extensions.txt 270 | kextstat > $JSS/kextstat.txt 271 | cp /Library/Receipts/InstallHistory.plist $JSS 272 | if [ -e /Library/Logs/DiagnosticReports/ ]; then mkdir -p $JSS/DiagnosticReports && cp -r /Library/Logs/DiagnosticReports/ "$JSS/DiagnosticReports" 273 | #SLEEP TO ALLOW COPY TO FINISH PROCESSING ALL FILES 274 | sleep 5 275 | else 276 | printf '%s
' "red" "No crash reports found." >> $results 277 | fi 278 | } 279 | 280 | 281 | #################################################################################################### 282 | #Array for Jamf Connect Logs 283 | Connect() { 284 | printf '

Jamf Connect

' >> $results 285 | printf '%s
' "white" "Checking for Jamf Connect" >> $results 286 | if [ -e /Library/Managed\ Preferences/com.jamf.connect.plist ]; then 287 | printf '%s
' "white" "Jamf Connect installed, collecting Jamf Connect logs..." >>$results 288 | connectInstalled="True" 289 | mkdir -p $log_folder/Connect 290 | #OUTPUT ALL HISTORICAL JAMF CONNECT LOGS, THIS WILL ALWAYS GENERATE A LOG FILE EVEN IF CONNECT IS NOT INSTALLED 291 | log show --style compact --predicate 'subsystem == "com.jamf.connect"' --debug > $connect/JamfConnect.log 292 | #OUTPUT ALL HISTORICAL JAMF CONNECT LOGIN LOGS 293 | log show --style compact --predicate 'subsystem == "com.jamf.connect.login"' --debug > $connect/jamfconnect.login.log 294 | kerblist=$("klist" 2>/dev/null) 295 | if [[ "$kerblist" == "" ]];then 296 | printf '%s
' "white" "-No Kerberos Ticket for Current Logged in User $loggedInUser" > $connect/klist_manuallyCollected.txt; else 297 | echo $kerblist > $connect/klist_manuallyCollected.txt; 298 | fi 299 | #CHECK FOR JAMF CONNECT LOGIN LOGS AND PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 300 | if [ -e /tmp/jamf_login.log ]; then cp "/tmp/jamf_login.log" $connect/jamf_login_tmp.log 301 | else 302 | printf '%s
' "orange" "-Jamf Login /tmp file not found
-This usually only exists on recent installs.
-Don't worry if you don't see anything. We're just being thorough." >> $results 303 | fi 304 | 305 | if [ -e /Library/Managed\ Preferences/com.jamf.connect.login.plist ]; then cp "/Library/Managed Preferences/com.jamf.connect.login.plist" "$connect/com.jamf.connect.login_managed.plist" | plutil -convert xml1 "$connect/com.jamf.connect.login_managed.plist" | log show --style compact --predicate 'subsystem == "com.jamf.connect.login"' --debug > "$connect/com.jamf.connect.login.log" 306 | else 307 | printf '%s
' "red" "-Jamf Connect Login plist not found" >> $results 308 | fi 309 | 310 | #CHECK FOR JAMF CONNECT LICENSE, THEN COPY AND CONVERT TO A READABLE FORMAT 311 | printf '

License Check

' >> $results 312 | LicensefromLogin=$(defaults read /Library/Managed\ Preferences/com.jamf.connect.login.plist LicenseFile 2>/dev/null) 313 | LicensefromMenubar=$(defaults read /Library/Managed\ Preferences/com.jamf.connect.plist LicenseFile 2>/dev/null) 314 | if [[ "$LicensefromLogin" != "" ]]; then 315 | (echo "$LicensefromLogin" | base64 -d) > $connect/license.txt 316 | elif [[ "$LicensefromMenubar" != "" ]]; then 317 | (echo "$LicensefromMenubar" | base64 -d) > $connect/license.txt 318 | else 319 | file="" 320 | fi 321 | 322 | #CHECK FOR JAMF CONNECT STATE PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 323 | State_plist=$(defaults read /Users/$loggedInUser/Library/Preferences/com.jamf.connect.state.plist 2>/dev/null) 324 | if [[ "$State_plist" == "" ]]; then 325 | printf '%s
' "red" "-A Jamf Connect State list was not found because no user is logged into Menu Bar" >> $results; 326 | else cp $HOME/Library/Preferences/com.jamf.connect.state.plist "$connect/com.jamf.connect.state.plist" | plutil -convert xml1 $connect/com.jamf.connect.state.plist 327 | fi 328 | 329 | #CHECK FOR JAMF CONNECT MENU BAR PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 330 | if [ -e /Library/Managed\ Preferences/com.jamf.connect.plist ]; then cp "/Library/Managed Preferences/com.jamf.connect.plist" "$connect/com.jamf.connect_managed.plist" | plutil -convert xml1 "$connect/com.jamf.connect_managed.plist" | log show --style compact --predicate 'subsystem == "com.jamf.connect"' --debug > "$connect/com.jamf.connect.log" 331 | else 332 | printf '%s
' "red" "Jamf Connect plist not found" >> $results 333 | fi 334 | 335 | #LIST AUTHCHANGER SETTIGNS 336 | if [ -e /usr/local/bin/authchanger ]; then 337 | /usr/local/bin/authchanger -print > "$connect/authchanger_manuallyCollected.txt" 338 | : 339 | else 340 | printf '%s
' "white" "-No Authchanger settings found" >> $results 341 | fi 342 | else 343 | printf '%s
' "white" "-No Jamf Connect Installed, doing nothing" >> $results 344 | connectInstalled="False" 345 | fi 346 | #CHECK THE JAMF CONNECT LICENSE FILE AND LOOK FOR OTHER PROFILES THAT MAY HAVE A JAMF CONNECT LICENSE 347 | connectLicenseInstalled=$(defaults read $managed_preferences/com.jamf.connect.plist LicenseFile) 348 | connectLoginLicenseInstalled=$(defaults read $managed_preferences/com.jamf.connect.login.plist LicenseFile) 349 | profilesWithConnectLicense=$(grep -A 1 "LicenseFile" $JSS/profiles.xml | awk -F'' '{print $2}' | sed 's/string>//g' | sed 's/<.*//;/^$/d') 350 | connectLicenseExpiration=$(grep -A 1 "ExpirationDate" $connect/license.txt | awk '{ print $1 }' | sed 's///g' | sed 's/<.*//;/^$/d' | tr -d "-") 351 | connectDateCompare=$(date +%Y%m%d) 352 | 353 | #REPORT JAMF CONNECT PRIVILEGE ELEVATION LOGS 354 | log show --style compact --predicate '(subsystem == "com.jamf.connect.daemon") && (category == "PrivilegeElevation")' >> $connect/Connect_Daemon_Elevation_Logs.txt 355 | log show --style compact --predicate '(subsystem == "com.jamf.connect") && (category == "PrivilegeElevation")' >> $connect/Connect_Menubar_Elevation_Logs.txt 356 | 357 | connectLicenseArray() { 358 | if [[ "$connectLicenseInstalled" == "" ]]; then 359 | printf '%s
' "red" "No License found for com.jamf.connect" >> $results 360 | else 361 | for i in $profilesWithConnectLicense; do 362 | if [[ "$i" == $connectLicenseInstalled ]]; then 363 | printf '%s
' "lime" "--Matching Profile found for installed Connect License (com.jamf.connect)." >> $results 364 | if [[ $connectDateCompare -ge $connectLicenseExpiration ]]; then 365 | printf '%s
' "red" "--Currently installed Jamf Connect License is expired." >> $results 366 | else 367 | printf '%s
' "lime" "--Currently installed Jamf Connect License is valid." >> $results 368 | fi 369 | else 370 | printf '%s
' "red" "--Mismatch between installed Connect license found in com.jamf.connect.login plist and an installed profile. Search the profiles.xml file for this license string to see which one profile is attempting to install this license string:
" 371 | printf '%s
' "white" "$i " >> $results 372 | fi 373 | done 374 | fi 375 | } 376 | 377 | connectLoginLicenseArray() { 378 | if [[ "$connectLoginLicenseInstalled" == "" ]]; then 379 | printf '%s
' "red" "No License found for com.jamf.connect.login" >> $results 380 | else 381 | for i in $profilesWithConnectLicense; do 382 | if [[ "$i" == $connectLoginLicenseInstalled ]]; then 383 | printf '%s
' "lime" "--Matching Profile found for installed Connect License (com.jamf.connect.login)." >> $results 384 | if [ $connectDateCompare -ge $connectLicenseExpiration ]; then 385 | printf '%s
' "red" "--Currently installed Jamf Connect License is expired." >> $results 386 | else 387 | printf '%s
' "lime" "--Currently installed Jamf Connect License is valid." >> $results 388 | fi 389 | else 390 | printf '%s
' "red" "--Mismatch between installed Connect license found in com.jamf.connect.login plist and an installed profile. Search the profiles.xml file for this license string to see which one profile is attempting to install this license string:
" 391 | printf '%s
' "white" "$i" >> $results 392 | fi 393 | done 394 | fi 395 | } 396 | 397 | connectLicenseFormatCheck() { 398 | PI110629Check=$(awk '/string>PD94/ {count++} END {print count}' "$JSS/profiles.xml") 399 | if [[ $PI110629Check -ge "1" ]]; then 400 | printf '%s
' "red" "Key value assigned to LicenseFile uses string tag and appears to be affected by PI110629" >> $results 401 | elif [[ $PI110629Check -le "0" ]]; then 402 | printf '%s
' "lime" "Key value assigned to LicenseFile uses data tag and does not appear to be affected by PI110629" >> $results 403 | fi 404 | } 405 | if [[ $connectInstalled = "True" ]]; then 406 | connectLicenseArray 407 | connectLoginLicenseArray 408 | connectLicenseFormatCheck 409 | fi 410 | } 411 | 412 | 413 | #################################################################################################### 414 | #Array for Jamf Protect Logs 415 | Protect() { 416 | #MAKE DIRECTORY FOR ALL JAMF SECURITY RELATED FILES 417 | mkdir -p $log_folder/Jamf_Security 418 | printf '

Jamf Protect

' >> $results 419 | shopt -s nocasematch 420 | 421 | case $runProtectDiagnostics in 422 | true) 423 | if [[ $protectDiagnostics == "" ]]; then 424 | echo "Protect Diagnostics enabled but no existing file found, creating file, please wait up to 5 minutes" 425 | protectctl diagnostics -o $HOME/Desktop/ -d 5 426 | #need to re-eval protect diagnostics so it sees the file 427 | protectDiagnostics=$(ls "$HOME/Desktop/" | grep "JamfProtectDiagnostics") 428 | cp "$HOME/Desktop/$protectDiagnostics" "$security" 429 | printf '%s
' "red" "Jamf Protect diagnostic files created. Please check Jamf_Security Folder for files." >> $results 430 | else 431 | echo "Protect Diagnostics found, copying to Jamf Log Grabber" 432 | cp "$HOME/Desktop/$protectDiagnostics" "$security" 433 | printf '%s
' "red" "Jamf Protect diagnostic files found. A 'protectctl diagnostics' command has been previously ran and the diagnostics folder was found on the desktop of this device." >> $results 434 | fi 435 | ;; 436 | *) 437 | if [[ $protectDiagnostics != "" ]]; then 438 | echo "Protect Diagnostics found, copying to Jamf Log Grabber" 439 | cp "$HOME/Desktop/$protectDiagnostics" "$security" 440 | printf '%s
' "red" "Jamf Protect diagnostic files found. A 'protectctl diagnostics' command has been previously ran and the diagnostics folder was found on the desktop of this device." >> $results 441 | else 442 | echo "Protect Diagnostics disabled and no copy found on desktop" 443 | fi 444 | ;; 445 | esac 446 | #CHECK FOR JAMF PROTECT PLIST, THEN COPY AND CONVERT TO READABLE FORMAT 447 | if [ -e /Library/Managed\ Preferences/com.jamf.protect.plist ]; then cp "/Library/Managed Preferences/com.jamf.protect.plist" "$security/com.jamf.protect.plist" 448 | printf '%s
' "white" "Jamf Protect plist found" >> $results 449 | plutil -convert xml1 "$security/com.jamf.protect.plist" 450 | protectctl info --verbose > $security/jamfprotectinfo.log 451 | 452 | else 453 | printf '%s
' "orange" "Jamf Protect plist not found" >> $results 454 | fi 455 | #CHECK FOR JAMF TRUST PLIST, THEN COPY AND CONVERT TO READABLE FORMAT 456 | if [ -e /Library/Managed\ Preferences/com.jamf.trust.plist ]; then cp "/Library/Managed Preferences/com.jamf.trust.plist" "$security/com.jamf.trust.plist" 457 | plutil -convert xml1 "$security/com.jamf.trust.plist" 458 | else 459 | printf '

Jamf Trust

' >> $results 460 | printf '%s
' "orange" "Jamf Trust plist not found" >> $results 461 | fi 462 | } 463 | 464 | #################################################################################################### 465 | #Array for Recon Troubleshoot 466 | Recon_Troubleshoot() { 467 | mkdir -p $log_folder/Recon 468 | #check for Jamf Recon leftovers 469 | if [[ $reconleftovers == "" ]]; then 470 | : 471 | else 472 | printf '

Recon Troubleshoot

' >> $results 473 | echo $reconleftovers > $recon/Leftovers.txt 474 | #DIAGNOSTIC INFORMATION FOR RECON RESULTS. FOLLOWING THESE STEPS WILL HELP IDENTIFY PROBLEMATIC EXTENSION ATTRIBUTES AND/OR INVENTORY CHECK IN PROBLEMS 475 | echo -e "\nRecon leftovers found and listed above\nTo remediate, take the following steps:\n1. Open the other files in this folder\n2.Find the Extension Attribute that matches the script in this file\n3.Remove or remediate the associate Extension Attribute Script\n4.Confirm by running a 'Sudo Jamf Recon' and verifying the files do not return.\n" >> $recon/Leftovers.txt 476 | #REPORT IN RESULTS FILE THAT LEFTOVERS WERE FOUND 477 | printf '%s
' "white" "Recon Troubleshoot found files in the /tmp directory that should not be there. A report of these files as well as next actions can be found in the Leftovers.txt file in the Recon Directory." >> $results 478 | #copy all files in tmp folder to recon results folder 479 | cp -r /Library/Application\ Support/Jamf/tmp/ $recon/ 480 | timefound=`grep -E -i '[0-9]+:[0-9]+' ${jamfLog} | awk '{print $4}' | tail -1` 481 | echo $timefound > /dev/null 482 | timeFoundNoSeconds=$(echo "${timefound:0:5}${timefound:8:3}") 483 | currentTimeNoSeconds=$(echo "${currenttime1:0:5}${currenttime1:8:3}") 484 | echo $timeFoundNoSeconds > /dev/null 485 | echo $currentTimeNoSeconds > /dev/null 486 | if [[ "$timeFoundNoSeconds" == "$currentTimeNoSeconds" ]]; then 487 | printf '%s
' "Orange" "JLG appears to be running via policy, results in Recon directory may be inaccurate as files are stored there while policies are running." >> $results 488 | else 489 | printf '%s
' "Orange" "JLG appears to have been manually run. Results in Recon directory should be examined closely." >> $results 490 | fi 491 | fi 492 | } 493 | #################################################################################################### 494 | #Array for MDM Communication Check 495 | #IF A DEVICE IS NOT COMMUNICATING WITH MDM, THIS WILL GIVE ITEMS TO LOOK INTO 496 | MDMCommunicationCheck() { 497 | touch $log_folder/MDMCheck.txt 498 | #WRITE TO LOGS WHAT WE ARE DOING NEXT 499 | echo -e "Checking $loggedInUser's computer for MDM communication issues:" >> $log_folder/MDMCheck.txt 500 | #CHECK MDM STATUS AND ADVISE IF IT IS COMMUNICATING 501 | result=$(log show --style compact --predicate '(process CONTAINS "mdmclient")' --last 1d | grep "Unable to create MDM identity") 502 | if [[ $result == '' ]]; then 503 | echo -e "-MDM is communicating" >> $log_folder/MDMCheck.txt 504 | else 505 | echo -e "-MDM is broken" >> $log_folder/MDMCheck.txt 506 | fi 507 | #CHECK FOR THE MDM PROFILE TO BE INSTALLED 508 | mdmProfile=$(/usr/libexec/mdmclient QueryInstalledProfiles | grep "00000000-0000-0000-A000-4A414D460003") 509 | if [[ $mdmProfile == "" ]]; then 510 | echo -e "-MDM Profile Not Installed" >> $log_folder/MDMCheck.txt 511 | else 512 | echo -e "-MDM Profile Installed" >> $log_folder/MDMCheck.txt 513 | fi 514 | #TELL THE STATUS OF THE MDM DAEMON 515 | mdmDaemonStatus=$(/System/Library/PrivateFrameworks/ApplePushService.framework/apsctl status | grep -A 18 com.apple.aps.mdmclient.daemon.push.production | awk -F':' '/persistent connection status/ {print $NF}' | sed 's/^ *//g') 516 | echo -e "-The MDM Daemon Status is:$mdmDaemonStatus" >> $log_folder/MDMCheck.txt 517 | #WRITE THE APNS TOPIC TO THE RESULTS FILE IF IT EXISTS 518 | profileTopic=$(system_profiler SPConfigurationProfileDataType | grep "Topic" | awk -F '"' '{ print $2 }'); 519 | if [ "$profileTopic" != "" ]; then 520 | echo -e "-APNS Topic is: $profileTopic\n" >> $log_folder/MDMCheck.txt 521 | else 522 | echo -e "-No APNS Topic Found\n" >> $log_folder/MDMCheck.txt 523 | fi 524 | } 525 | #################################################################################################### 526 | #Array for Managed Preferences Collection 527 | Managed_Preferences_Array() { 528 | #mkdir -p $log_folder/Managed\ Preferences 529 | #CHECK FOR MANAGED PREFERENCE PLISTS, THEN COPY AND CONVERT THEM TO A READABLE FORMAT 530 | if [ -e /Library/Managed\ Preferences/ ]; then cp -r /Library/Managed\ Preferences $managed_preferences 531 | #SLEEP TO ALLOW COPY TO FINISH PROCESSING ALL FILES 532 | sleep 5 533 | #UNABLE TO CHECK FOLDER FOR WILDCARD PLIST LIKE *.PLIST 534 | plutil -convert xml1 $managed_preferences/*.plist 535 | plutil -convert xml1 $managed_preferences/$loggedInUser/*.plist 536 | printf '

Managed Preferences

' >> $results 537 | checkManagedNotifications=$(plutil -extract NotificationSettings xml1 -o - /Library/Managed\ Preferences/com.apple.notificationsettings.plist | grep "BundleIdentifier" -A 1 | grep "" | sed -E 's|(.*)|\1|') 538 | printf '%s
' "white" "Managed notifications found for the following applications:" >> $results 539 | for app in $checkManagedNotifications; do 540 | printf '\t%s\n
' "white" "- $app" >> $results 541 | done 542 | else 543 | printf '

Managed Preferences

' >> $results 544 | printf '%s
' "red" "No Managed Preferences plist files found" >> $results 545 | fi 546 | } 547 | 548 | #################################################################################################### 549 | #Array for Device Compliance 550 | DeviceCompliance() { 551 | mkdir -p $log_folder/Device_Compliance 552 | log show --debug --info --predicate 'subsystem CONTAINS "jamfAAD" OR subsystem BEGINSWITH "com.apple.AppSSO" OR subsystem BEGINSWITH "com.jamf.backgroundworkflows"' > $Device_Compliance/JamfConditionalAccess.log 553 | if [ -e /Library/Logs/Microsoft/Intune/ ]; then cp /Library/Logs/Microsoft/Intune/*.log $Device_Compliance 554 | else 555 | printf '

Device Compliance

' >> $results 556 | printf '%s
' "orange" "Device Compliance system logs not found" >> $results 557 | fi 558 | if [ -e /$loggedInUser/Logs/Microsoft/Intune/ ]; then cp /Library/Logs/Microsoft/Intune/*.log $Device_Compliance 559 | else 560 | printf '%s
' "orange" "Device Compliance user logs not found" >> $results 561 | fi 562 | 563 | } 564 | 565 | #################################################################################################### 566 | #Array for Device Compliance 567 | Remote_Assist() { 568 | JRA3Check=$(defaults read /Library/Application\ Support/JAMF/Remote\ Assist/jamfRemoteAssistConnectorUI.app/Contents/Info.plist CFBundleVersion 2>/dev/null) 569 | printf '

Jamf Remote Assist

' >> $results 570 | if [[ $JRA3Check = "" ]]; then 571 | printf '%s
' "white" "Jamf Remote Assist not installed, skipping version and network check." >> $results 572 | else 573 | #ADD JAMF Remote Assist Log Folder 574 | printf '%s
' "white" "Jamf Remote Assist Version: $JRA3Check" >> $results 575 | function createNetworkCheckTableJRA () { 576 | 577 | /bin/cat << EOF >> "$results" 578 |

579 |

Network access to the following hostnames are required for using Jamf Remote Assist. These hostnames are a small sample as the Relay hostnames can increment up to 100 currently.

580 | ${HOST_TEST_TABLES} 581 | EOF 582 | } 583 | 584 | 585 | function CalculateHostInfoTablesJRA () { 586 | echo "[step] Checking URLS" 587 | lastCategory="zzzNone" # Some fake category so we recognize that the first host is the start of a new category 588 | firstServer="yes" # Flag for the first host so we don't try to close the preceding table -- there won't be one. 589 | HOST_TEST_TABLES='' # This is the var we will insert into the HTML 590 | for SERVER in "${JRA_URL_ARRAY[@]}"; do 591 | #split the record info fields 592 | HOSTNAME=$(echo ${SERVER} | cut -d ',' -f1) 593 | PORT=$(echo ${SERVER} | cut -d ',' -f2) 594 | PROTOCOL=$(echo ${SERVER} | cut -d ',' -f3) 595 | CATEGORY=$(echo ${SERVER} | cut -d ',' -f4) 596 | # We have categories of hosts... enrollment, software update, etc. We'll put them in separate tables 597 | # If the category for this host is different than the last one and is not blank... 598 | if [[ "${lastCategory}" != "${CATEGORY}" ]] && [[ ! -z "${CATEGORY}" ]]; then 599 | # If this is not the first server, close up the table from the previous category before moving on to the next. 600 | echo "Starting Category : ${CATEGORY}" 601 | if [[ "${firstServer}" != "yes" ]]; then 602 | #We've already started the table html so no need to do it again. 603 | HOST_TEST_TABLES+=" ${NL}" 604 | fi 605 | firstServer="no" 606 | lastCategory="${CATEGORY}" 607 | HOST_TEST_TABLES+="

${CATEGORY}

${NL}" 608 | HOST_TEST_TABLES+=" ${NL}" 609 | HOST_TEST_TABLES+=" ${NL}" 610 | fi # End of table start and end logic. 611 | 612 | echo " > Checking connectivity to: ${HOSTNAME} ${PORT} ${PROTOCOL}" 613 | 614 | # Now print the info for this host... 615 | #Perform Host nslookup to get reported IP 616 | IP_ADDRESS=$(/usr/bin/nslookup ${HOSTNAME} | /usr/bin/grep "Address:" | /usr/bin/awk '{print$2}' | /usr/bin/tail -1) 617 | 618 | 619 | #Get Reverse DNS record 620 | REVERSE_DNS=$(/usr/bin/dig -x ${IP_ADDRESS} +short | /usr/bin/sed 's/.$//') 621 | 622 | # Using nc, if proxy defined then adding in proxy flag 623 | if [[ ${PROTOCOL} == "TCP" ]]; then 624 | if [[ -f "${LOCAL_PROXY_PAC_FILE}" ]]; then 625 | PROXY_PARSE_DATA=$(GetProxyHostFromPac ${HOSTNAME} ${PORT}) 626 | PROXY_HOST=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f1) 627 | PROXY_PORT=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f2) 628 | fi 629 | 630 | #Check if Proxy set 631 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 632 | #no proxy set 633 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 634 | else 635 | echo " > ${PROXY_HOST}:${PROXY_PORT} to be used for ${HOSTNAME}:${PORT}" 636 | STATUS=$(/usr/bin/nc -z -G 1 -x ${PROXY_HOST}:${PROXY_PORT} -X connect ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 637 | fi 638 | 639 | elif [[ ${PROTOCOL} == "TCP - non-proxied" ]]; then 640 | #for non proxy aware urls we will be using netcat aka nc 641 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 642 | else 643 | # UDP goes direct... not proxied. 644 | STATUS=$(/usr/bin/nc -u -z ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 645 | 646 | fi 647 | 648 | #Based on Status will set Availability Value 649 | if [[ ${STATUS} =~ "succeeded" ]]; then 650 | AVAILBILITY="succeeded" 651 | 652 | else 653 | AVAILBILITY="failed" 654 | fi 655 | 656 | 657 | if [[ "${AVAILBILITY}" == "succeeded" ]]; then 658 | AVAILBILITY_STATUS='' 659 | #Test for SSL Inspection 660 | if [[ ${PORT} == "80" ]]; then 661 | #http traffic no ssl inspection issues 662 | SSL_STATUS='' 663 | else 664 | if [[ ${PROTOCOL} == "TCP" ]]; then 665 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 666 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer ) 667 | 668 | else 669 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -proxy "${PROXY_HOST}:${PROXY_PORT}" -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer) 670 | fi 671 | 672 | if [[ ${CERT_STATUS} != *"Apple Inc"* ]] && [[ "${CERT_STATUS}" != *"Akamai Technologies"* ]] && [[ "${CERT_STATUS}" != *"Amazon"* ]] && [[ "${CERT_STATUS}" != *"DigiCert"* ]] && [[ "${CERT_STATUS}" != *"Microsoft"* ]] && [[ "${CERT_STATUS}" != *"COMODO"* ]] && [[ "${CERT_STATUS}" != *"QuoVadis"* ]]; then 673 | 674 | SSL_ISSUER=$(echo ${CERT_STATUS} | awk -F'O=|/OU' '{print $2}') 675 | 676 | if [[ ${HOSTNAME} == *"jcdsdownloads.services.jamfcloud.com" ]];then 677 | SSL_STATUS='' 678 | else 679 | SSL_STATUS="" 680 | fi 681 | 682 | else 683 | 684 | SSL_STATUS='' 685 | fi 686 | else 687 | SSL_STATUS='' 688 | fi 689 | fi 690 | else 691 | # nc did not connect. There is no point in trying the SSL cert subject test. 692 | AVAILBILITY_STATUS='' 693 | SSL_STATUS='' 694 | fi 695 | 696 | # Done. Stick the row of info into the HTML var... 697 | HOST_TEST_TABLES+=" ${AVAILBILITY_STATUS}${SSL_STATUS}${NL}" 698 | done 699 | # Close up the html for the final table 700 | HOST_TEST_TABLES+="
HOSTNAMEReverse DNSIP AddressPortProtocolAccessibleSSL Error
AvailableN/AN/AUnexpected Certificate: ${SSL_ISSUER}SuccessfulN/AUnavailableNot checked
${HOSTNAME}${REVERSE_DNS}${IP_ADDRESS}${PORT}${PROTOCOL}
${NL}" 701 | } 702 | 703 | JRA_URL_ARRAY=( 704 | #Device setup 705 | "us.jra.services.jamfcloud.com,443,TCP,Connection Results" 706 | "files.jra.services.jamfcloud.com,443,TCP" 707 | "download.jra.services.jamfcloud.com,443,TCP" 708 | "relay-1.us.jra.services.jamfcloud.com,443,TCP" 709 | "socket.us.jra.services.jamfcloud.com,5555,UDP" 710 | ) 711 | CalculateHostInfoTablesJRA 712 | createNetworkCheckTableJRA 713 | 714 | log show --style compact --predicate 'subsystem BEGINSWITH "com.jamf.remoteassist"' --debug > $JRA/JRA_debug.log 715 | fi 716 | } 717 | 718 | #################################################################################################### 719 | #Array for App Installers Directory 720 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 721 | AppInstallers() { 722 | mkdir -p $App_Installers 723 | if [ -e /var/db/ConfigurationProfiles/Settings/Managed\ Applications/Device/ ]; then cp -r /var/db/ConfigurationProfiles/Settings/Managed\ Applications/Device/ directory $App_Installers 724 | function createNetworkCheckTableAppInstallers () { 725 | /bin/cat << EOF >> "$results" 726 |

727 |

App Installers

728 | EOF 729 | #Gross for loop that checks for the first report of a failed install and sets the value to true. Avoids a blank return saying App Installer failures found but nothing listed. It is redundant but my limited scripting knowledge only allows for this workaround currently. 730 | for file in $App_Installers/_Completed/*; do 731 | failedInstall=$(defaults read "$file" InstallFailed 2> /dev/null) 732 | if [[ $failedInstall == 1 ]]; then 733 | checkForAllFailedInstalls="True" 734 | break 735 | else 736 | checkForAllFailedInstalls="False" 737 | fi 738 | done 739 | if [[ $checkForAllFailedInstalls = "True" ]]; then 740 | /bin/cat << EOF >> "$results" 741 |

Failed App Installer Check

742 |

The following files were found in the app installer logs and show failed installations. If you are troubleshooting failed App Installers, please examine the following files.

743 | EOF 744 | printf '%s
' "red" "Here's a list of failed app installer logs" >> $results 745 | touch $App_Installers/commandUUID.txt 746 | for file in $App_Installers/_Completed/*; do 747 | failedInstall=$(defaults read "$file" InstallFailed 2> /dev/null) 748 | commandUUID=$(defaults read "$file" InstallUUID 2> /dev/null) 749 | if [[ $failedInstall == 1 ]]; then 750 | printf '%s
' "yellow" "-$file" >> $results 751 | echo "$commandUUID," >> $App_Installers/commandUUID.txt 752 | fi 753 | done 754 | cat $App_Installers/commandUUID.txt | tr '\n' ' ' > $App_Installers/commandUUID.txt 755 | else 756 | /bin/cat << EOF >> "$results" 757 |

Failed App Installer Check

758 |

No failed App Installer files found.

759 | EOF 760 | fi 761 | 762 | /bin/cat << EOF >> "$results" 763 |

764 |

Network access to the following hostname is required for using Jamf's App Installers.

765 | ${HOST_TEST_TABLES} 766 | EOF 767 | } 768 | function CalculateHostInfoTablesAppInstallers () { 769 | echo "[step] Checking URLS" 770 | lastCategory="zzzNone" # Some fake category so we recognize that the first host is the start of a new category 771 | firstServer="yes" # Flag for the first host so we don't try to close the preceding table -- there won't be one. 772 | HOST_TEST_TABLES='' # This is the var we will insert into the HTML 773 | for SERVER in "${APP_Installer_URL_ARRAY[@]}"; do 774 | #split the record info fields 775 | HOSTNAME=$(echo ${SERVER} | cut -d ',' -f1) 776 | PORT=$(echo ${SERVER} | cut -d ',' -f2) 777 | PROTOCOL=$(echo ${SERVER} | cut -d ',' -f3) 778 | CATEGORY=$(echo ${SERVER} | cut -d ',' -f4) 779 | # We have categories of hosts... enrollment, software update, etc. We'll put them in separate tables 780 | # If the category for this host is different than the last one and is not blank... 781 | if [[ "${lastCategory}" != "${CATEGORY}" ]] && [[ ! -z "${CATEGORY}" ]]; then 782 | # If this is not the first server, close up the table from the previous category before moving on to the next. 783 | echo "Starting Category : ${CATEGORY}" 784 | if [[ "${firstServer}" != "yes" ]]; then 785 | #We've already started the table html so no need to do it again. 786 | HOST_TEST_TABLES+=" ${NL}" 787 | fi 788 | firstServer="no" 789 | lastCategory="${CATEGORY}" 790 | HOST_TEST_TABLES+="

${CATEGORY}

${NL}" 791 | HOST_TEST_TABLES+=" ${NL}" 792 | HOST_TEST_TABLES+=" ${NL}" 793 | fi # End of table start and end logic. 794 | 795 | echo " > Checking connectivity to: ${HOSTNAME} ${PORT} ${PROTOCOL}" 796 | 797 | # Now print the info for this host... 798 | #Perform Host nslookup to get reported IP 799 | IP_ADDRESS=$(/usr/bin/nslookup ${HOSTNAME} | /usr/bin/grep "Address:" | /usr/bin/awk '{print$2}' | /usr/bin/tail -1) 800 | 801 | 802 | #Get Reverse DNS record 803 | REVERSE_DNS=$(/usr/bin/dig -x ${IP_ADDRESS} +short | /usr/bin/sed 's/.$//') 804 | 805 | # Using nc, if proxy defined then adding in proxy flag 806 | if [[ ${PROTOCOL} == "TCP" ]]; then 807 | if [[ -f "${LOCAL_PROXY_PAC_FILE}" ]]; then 808 | PROXY_PARSE_DATA=$(GetProxyHostFromPac ${HOSTNAME} ${PORT}) 809 | PROXY_HOST=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f1) 810 | PROXY_PORT=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f2) 811 | fi 812 | 813 | #Check if Proxy set 814 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 815 | #no proxy set 816 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 817 | else 818 | echo " > ${PROXY_HOST}:${PROXY_PORT} to be used for ${HOSTNAME}:${PORT}" 819 | STATUS=$(/usr/bin/nc -z -G 1 -x ${PROXY_HOST}:${PROXY_PORT} -X connect ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 820 | fi 821 | 822 | elif [[ ${PROTOCOL} == "TCP - non-proxied" ]]; then 823 | #for non proxy aware urls we will be using netcat aka nc 824 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 825 | else 826 | # UDP goes direct... not proxied. 827 | STATUS=$(/usr/bin/nc -u -z ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 828 | 829 | fi 830 | 831 | #Based on Status will set Availability Value 832 | if [[ ${STATUS} =~ "succeeded" ]]; then 833 | AVAILBILITY="succeeded" 834 | 835 | else 836 | AVAILBILITY="failed" 837 | fi 838 | 839 | 840 | if [[ "${AVAILBILITY}" == "succeeded" ]]; then 841 | AVAILBILITY_STATUS='' 842 | #Test for SSL Inspection 843 | if [[ ${PORT} == "80" ]]; then 844 | #http traffic no ssl inspection issues 845 | SSL_STATUS='' 846 | else 847 | if [[ ${PROTOCOL} == "TCP" ]]; then 848 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 849 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer ) 850 | 851 | else 852 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -proxy "${PROXY_HOST}:${PROXY_PORT}" -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer) 853 | fi 854 | 855 | if [[ ${CERT_STATUS} != *"Apple Inc"* ]] && [[ "${CERT_STATUS}" != *"Akamai Technologies"* ]] && [[ "${CERT_STATUS}" != *"Amazon"* ]] && [[ "${CERT_STATUS}" != *"DigiCert"* ]] && [[ "${CERT_STATUS}" != *"Microsoft"* ]] && [[ "${CERT_STATUS}" != *"COMODO"* ]] && [[ "${CERT_STATUS}" != *"QuoVadis"* ]]; then 856 | 857 | SSL_ISSUER=$(echo ${CERT_STATUS} | awk -F'O=|/OU' '{print $2}') 858 | 859 | if [[ ${HOSTNAME} == *"jcdsdownloads.services.jamfcloud.com" ]];then 860 | SSL_STATUS='' 861 | else 862 | SSL_STATUS="" 863 | fi 864 | 865 | else 866 | 867 | SSL_STATUS='' 868 | fi 869 | else 870 | SSL_STATUS='' 871 | fi 872 | fi 873 | else 874 | # nc did not connect. There is no point in trying the SSL cert subject test. 875 | AVAILBILITY_STATUS='' 876 | SSL_STATUS='' 877 | fi 878 | 879 | # Done. Stick the row of info into the HTML var... 880 | HOST_TEST_TABLES+=" ${AVAILBILITY_STATUS}${SSL_STATUS}${NL}" 881 | done 882 | # Close up the html for the final table 883 | HOST_TEST_TABLES+="
HOSTNAMEReverse DNSIP AddressPortProtocolAccessibleSSL Error
AvailableN/AN/AUnexpected Certificate: ${SSL_ISSUER}SuccessfulN/AUnavailableNot checked
${HOSTNAME}${REVERSE_DNS}${IP_ADDRESS}${PORT}${PROTOCOL}
${NL}" 884 | } 885 | APP_Installer_URL_ARRAY=( 886 | #Device setup 887 | "appinstallers-packages.services.jamfcloud.com,443,TCP, Connection Results" 888 | ) 889 | CalculateHostInfoTablesAppInstallers 890 | createNetworkCheckTableAppInstallers 891 | else 892 | printf '

App Installers

' >> $results 893 | printf '%s
' "orange" "App Installer Directory not found, device is not in scope for any App Installers or is not receiving the App Installer command from Jamf." >> $results 894 | fi 895 | } 896 | 897 | #################################################################################################### 898 | #Array for App Named in Dynamic Variables 899 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 900 | CustomApp1Array() { 901 | mkdir -p $log_folder/$CustomApp1Name 902 | if [ -e $CustomApp1LogSource ] && [ $CustomApp1LogSource != "/" ]; then cp -r $CustomApp1LogSource $CustomApp1Folder 903 | else 904 | printf '

'$CustomApp1Name'

' >> $results 905 | printf '%s
' "orange" "$CustomApp1Name does not have a log file available to grab or was set to an invalid path." >> $results 906 | fi 907 | } 908 | 909 | #################################################################################################### 910 | #Array for App Named in Dynamic Variables 911 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 912 | CustomApp2Array() { 913 | mkdir -p $log_folder/$CustomApp2Name 914 | if [ -e $CustomApp2LogSource ] && [ $CustomApp2LogSource != "/" ]; then cp -r $CustomApp2LogSource $CustomApp2Folder 915 | else 916 | printf '

'$CustomApp2Name'

' >> $results 917 | printf '%s
' "orange" "$CustomApp2Name does not have a log file available to grab or was set to an invalid path." >> $results 918 | fi 919 | } 920 | 921 | #################################################################################################### 922 | #Array for App Named in Dynamic Variables 923 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 924 | CustomApp3Array() { 925 | mkdir -p $log_folder/$CustomApp3Name 926 | if [ -e $CustomApp3LogSource ] && [ $CustomApp3LogSource != "/" ]; then cp -r $CustomApp3LogSource $CustomApp3Folder 927 | else 928 | printf '

'$CustomApp3Name'

' >> $results 929 | printf '%s
' "orange" "$CustomApp3Name does not have a log file available to grab or was set to an invalid path." >> $results 930 | fi 931 | } 932 | 933 | #################################################################################################### 934 | 935 | #Array for folder cleanup 936 | Cleanup() { 937 | #IF AN ARRAY IS NOT SET TO RUN, REMOVE THE FOLDER NAME FOR IT BELOW TO AVOID ERRORS WITH THE CLEANUP FUNCTION AT THE END OF THE SCRIPT 938 | cleanup=("Client_Logs Recon Self_Service Connect Jamf_Security Managed_Preferences Device_Compliance JRA App_Installers $CustomApp1Name $CustomApp2Name $CustomApp3Name") 939 | #CLEANS OUT EMPTY FOLDERS TO AVOID CONFUSION 940 | printf '

Cleanup Results

' >> $results 941 | printf '%s
' "white" "The following folders contained no files and were removed:" >> $results 942 | for emptyfolder in $cleanup 943 | do 944 | if [ -z "$(ls -A /$log_folder/$emptyfolder)" ]; then 945 | printf '%s
' "yellow" "-$emptyfolder" >> $results 946 | rm -r $log_folder/$emptyfolder 947 | else 948 | : 949 | fi 950 | done 951 | printf '%s
' "white" "Completed Log Grabber on '$(currenttime)'" >> $results 952 | } 953 | 954 | #################################################################################################### 955 | Zip_Folder() { 956 | cd $HOME/Desktop 957 | #NAME ZIPPED FOLDER WITH LOGGED IN USER 958 | zip "$loggedInUser"_"$current_date"_logs.zip -r "$loggedInUser"_"$current_date"_logs 959 | rm -r $log_folder 960 | } 961 | #################################################################################################### 962 | # Set the Arrays you want to grab. 963 | # Default Array is logsToGrab=("Jamf" "Managed_Preferences" "Protect" "Connect" "Recon_Troubleshoot" "MDM_Communication_Check" "Device_Compliance" "App_Installers" "Remote_Assist" "$CustomApp1Name" "$CustomApp2Name" "$CustomApp3Name") 964 | 965 | declare -a logsToGrab=("Jamf" "Managed_Preferences" "Protect" "Connect" "Recon_Troubleshoot" "MDM_Communication_Check" "Device_Compliance" "App_Installers" "Remote_Assist" "$CustomApp1Name" "$CustomApp2Name" "$CustomApp3Name") 966 | 967 | #################################################################################################### 968 | # Put it all together in the Master Array 969 | 970 | logGrabberMasterArray() { 971 | #CLEAR OUT PREVIOUS RESULTS 972 | if [ -e $log_folder ] ;then rm -r $log_folder 973 | fi 974 | #CREATE A FOLDER TO SAVE ALL LOGS 975 | mkdir -p $log_folder 976 | #CREATE A LOG FILE FOR SCRIPT AND SAVE TO LOGS DIRECTORY SO ADMINS CAN SEE WHAT LOGS WERE NOT GATHERED 977 | touch $results 978 | buildHTMLResults 979 | #SET A TIME AND DATE STAMP FOR WHEN THE LOG GRABBER WAS RAN 980 | printf '%s
' "white" "Log Grabber was started at '$(currenttime)'
" >> $results 981 | ## now loop through the above array 982 | for logs in "${logsToGrab[@]}" 983 | do 984 | echo "$logs" 985 | case $logs in 986 | Jamf) 987 | Jamf 988 | ;; 989 | Protect) 990 | Protect 991 | ;; 992 | Connect) 993 | Connect 994 | ;; 995 | Recon_Troubleshoot) 996 | Recon_Troubleshoot 997 | ;; 998 | MDM_Communication_Check) 999 | MDMCommunicationCheck 1000 | ;; 1001 | Managed_Preferences) 1002 | Managed_Preferences_Array 1003 | ;; 1004 | Device_Compliance) 1005 | DeviceCompliance 1006 | ;; 1007 | Remote_Assist) 1008 | Remote_Assist 1009 | ;; 1010 | App_Installers) 1011 | AppInstallers 1012 | ;; 1013 | "$CustomApp1Name") 1014 | #Add or Remove comment from line below to disable or enable the array for the custom app 1015 | #CustomApp1Array 1016 | ;; 1017 | "$CustomApp2Name") 1018 | #Add or Remove comment from line below to disable or enable the array for the custom app 1019 | #CustomApp2Array 1020 | ;; 1021 | "$CustomApp3Name") 1022 | #Add or Remove comment from line below to disable or enable the array for the custom app 1023 | #CustomApp3Array 1024 | ;; 1025 | *) 1026 | echo "$logs is an invalid variable for the array. Check your spelling or add it to the case argument with your own array" >> $results 1027 | ;; 1028 | esac 1029 | done 1030 | 1031 | } 1032 | 1033 | #Runs the Log Grabber as configured 1034 | logGrabberMasterArray 1035 | #Run cleanup Array to remove empty folders 1036 | Cleanup 1037 | #Zips Results- Comment out or remove the line below to leave the folder unzipped 1038 | Zip_Folder 1039 | -------------------------------------------------------------------------------- /Jamf Log Grabber: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Jamf Log Grabber is designed to collect any logs associated with Jamf managed devices. 4 | 5 | #Custom arrays are now set for each individual type of log. It is recommended to include all as there are minor dependencies for some arrays. 6 | #This new workflow allows for you to add arrays for additional in house apps like SUPER, DEPNOTIFY, Crowdstrike, or any other commonly used MacOS applications. 7 | 8 | #################################################################################################### 9 | #This script is not intended for using to attach logs to a device record in Jamf Pro. Do not utilize such a workflow as it can lead to severe server performance issues 10 | #################################################################################################### 11 | 12 | # Redistribution and use in source and binary forms, with or without 13 | # modification, are permitted provided that the following conditions are met: 14 | # * Redistributions of source code must retain the above copyright 15 | # notice, this list of conditions and the following disclaimer. 16 | # * Redistributions in binary form must reproduce the above copyright 17 | # notice, this list of conditions and the following disclaimer in the 18 | # documentation and/or other materials provided with the distribution. 19 | # * Neither the name of the JAMF Software, LLC nor the 20 | # names of its contributors may be used to endorse or promote products 21 | # derived from this software without specific prior written permission. 22 | # THIS SOFTWARE IS PROVIDED BY JAMF SOFTWARE, LLC "AS IS" AND ANY 23 | # EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | # DISCLAIMED. IN NO EVENT SHALL JAMF SOFTWARE, LLC BE LIABLE FOR ANY 26 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 29 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | #################################################################################################### 34 | 35 | #DATE FOR LOG FOLDER ZIP CREATION 36 | current_date=$(date +"%Y-%m-%d") 37 | #HARD CODED VARIABLES, DO NOT CHANGE 38 | loggedInUser=$( echo "show State:/Users/ConsoleUser" | /usr/sbin/scutil | /usr/bin/awk '/Name :/ && ! /loginwindow/ { print $3 }' ) 39 | #### Error check to make sure environment variables are correctly set as multiple recent reports in early 2024 had this broken 40 | echo "HOME is $HOME" 41 | if [[ $HOME == "" ]]; then 42 | HOME="/Users/$loggedInUser" 43 | fi 44 | echo $HOME 45 | #### End of HOME check 46 | log_folder=$HOME/Desktop/"$loggedInUser"_"$current_date"_logs 47 | results=$log_folder/Results.html 48 | JSS=$log_folder/Client_Logs 49 | security=$log_folder/Jamf_Security 50 | connect=$log_folder/Connect 51 | managed_preferences=$log_folder/Managed_Preferences 52 | recon=$log_folder/Recon 53 | self_service=$log_folder/Self_Service 54 | Device_Compliance=$log_folder/Device_Compliance 55 | JRA=$log_folder/JRA 56 | App_Installers=$log_folder/App_Installers 57 | jamfLog=$JSS/jamf.log 58 | 59 | reconleftovers=$(ls /Library/Application\ Support/JAMF/tmp/ 2> /dev/null) 60 | runProtectDiagnostics="${10}" 61 | #for testing 62 | #runProtectDiagnostics="true" 63 | protectDiagnostics=$(ls "$HOME/Desktop/" | grep "JamfProtectDiagnostics") 64 | 65 | 66 | 67 | #DATE AND TIME FOR RESULTS.TXT INFORMATION 68 | #currenttime=$(date +"%D %T") 69 | currenttime() { 70 | date +"%D %T" 71 | } 72 | currenttime1=$(echo "$(currenttime)" | awk '{print $2}') 73 | 74 | #################################################################################################### 75 | #You can add custom app log grabbing using the following rubric, just continue numbering the appnames or renaming them to fit your needs 76 | #You can pass jamf script variables as part of a policy to get your additional apps 77 | 78 | #CustomApp1Name=$4 79 | CustomApp1Folder=$log_folder/$CustomApp1Name 80 | #CustomApp1LogSource="$5" 81 | #Now go down to CustomApp1Array and put in the files you want to grab 82 | #CustomApp2Name="$6" 83 | CustomApp2Folder=$log_folder/$CustomApp2Name 84 | #CustomApp2LogSource="$7" 85 | #Now go down to CustomApp2Array and put in the files you want to grab 86 | #CustomApp3Name="$8" 87 | CustomApp3Folder=$log_folder/$CustomApp3Name 88 | #CustomApp3LogSource="$9" 89 | #Now go down to CustomApp2Array and put in the files you want to grab 90 | #################################################################################################### 91 | 92 | #Build a results file in HTML 93 | buildHTMLResults() { 94 | printf ' 95 | 96 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |

Jamf Log Grabber Results

' > $results 123 | } 124 | 125 | #################################################################################################### 126 | #Array for Jamf Logs 127 | Jamf() { 128 | printf '

Client Logs

' >> $results 129 | mkdir -p $log_folder/Client_Logs 130 | #GET SYSTEM PROFILE REPORT- This spits out a couple errors due to KEXT stuff that's deprecated 131 | system_profiler -xml > $JSS/SystemReport.spx 2>/dev/null 132 | #ADD JAMF CLIENT LOGS TO LOG FOLDER 133 | if [ -e /private/var/log/jamf.log ]; then cp "/private/var/log/jamf.log" $JSS 134 | grep "Error" $JSS/jamf.log > $JSS/jamferror.log 135 | else 136 | printf '

%s

' "red" "Jamf Client Logs not found" >> $results 137 | fi 138 | #CHECK FOR JAMF INSTALL LOGS 139 | if [ -e /var/log/install.log ]; then cp "/var/log/install.log" $JSS 140 | else 141 | printf '%s
' "red" "Install Logs not found" >> $results 142 | fi 143 | #CHECK FOR JAMF SYSTEM LOGS 144 | if [ -e /var/log/system.log ]; then cp "/var/log/system.log" $JSS 145 | else 146 | printf '%s
' "red" "System Logs not found" >> $results 147 | fi 148 | #CHECK FOR JAMF SYSTEM LOGS 149 | if [ -e /Library/Logs/MCXTools.log ]; then cp "/Library/Logs/MCXTools.log" $JSS 150 | else 151 | printf '%s
' "red" "System Logs not found" >> $results 152 | fi 153 | #FIND AND COPY JAMF SOFTWARE PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 154 | #COPY DEBUG LOG 155 | if [ -e /Library/Preferences/com.jamfsoftware.jamf.plist ]; then cp "/Library/Preferences/com.jamfsoftware.jamf.plist" "$JSS/com.jamfsoftware.jamf.plist" | plutil -convert xml1 "$JSS/com.jamfsoftware.jamf.plist" 156 | else 157 | printf '%s
' "red" "Jamf Software plist not found" >> $results 158 | fi 159 | mkdir -p $log_folder/Self_Service 160 | #Checks what versions of self service are installed 161 | if [ -e /Applications/Self\ Service.app ]; then 162 | og="true" 163 | else 164 | og="false" 165 | fi 166 | if [[ -e /Applications/Self\ Service+.app ]]; then 167 | ssPlus="true" 168 | else 169 | ssPlus="false" 170 | fi 171 | #fun little logic to update selfServiceStatus variable to pull logs according to reporting 172 | if [ $og == "true" ] && [ $ssPlus == "true" ]; then 173 | selfServiceStatus="ogPlus" 174 | elif [ $og == "true" ] && [ $ssPlus == "false" ]; then 175 | selfServiceStatus="ogSS" 176 | elif [ $og == "false" ] && [ $ssPlus == "true" ]; then 177 | selfServiceStatus="ssPlus" 178 | else 179 | selfServiceStatus="notInstalled" 180 | fi 181 | #everything put together to pull SS, SS+, or both apps logs 182 | case $selfServiceStatus in 183 | ogPlus) 184 | printf '%s
' "white" "Self Service and Self Service+ are installed on this machine" >> $results 185 | cp -r "$HOME/Library/Logs/JAMF/" $self_service 186 | log show --style compact --predicate 'subsystem == "com.jamf.selfserviceplus"' --debug --info > $self_service/SelfServicePlus.log 187 | ;; 188 | ssplus) 189 | printf '%s
' "white" "Self Service+ is installed on this machine" >> $results 190 | log show --style compact --predicate 'subsystem == "com.jamf.selfserviceplus"' --debug --info > $self_service/SelfServicePlus.log 191 | ;; 192 | ogSS) 193 | printf '%s
' "white" "Self Service is installed on this machine" >> $results 194 | cp -r "$HOME/Library/Logs/JAMF/" $self_service 195 | ;; 196 | *) 197 | printf '%s
' "white" "Self Service and Self Service+ are not installed on this machine" >> $results 198 | esac 199 | #Checks current MacOS Version against GDMF feed and flags if not a current release 200 | currentMacOS=$(sw_vers --buildVersion) 201 | checkIfSupportedOS=$(curl -s https://gdmf.apple.com/v2/pmv | grep -c $currentMacOS) 202 | if [[ $checkIfSupportedOS == 1 ]]; then 203 | printf '%s
' "white" "MacOS build $currentMacOS installed" >> $results 204 | else 205 | printf '%s
' "red" "MacOS build $currentMacOS installed. Unable to locate in GDMF feed." >> $results 206 | fi 207 | #Check if account is a Mobile Account and report if so 208 | NETACCLIST=$(dscl . list /Users OriginalNodeName | awk '{print $1}' 2>/dev/null) 209 | if [ "$NETACCLIST" == "" ]; then 210 | printf '%s
' "white" "No mobile accounts on device." >> $results 211 | else 212 | printf '%s
' "red" "The following are mobile accounts:" >> $results 213 | for account in $NETACCLIST; do 214 | printf ' '$account'
' >> $results 215 | done 216 | fi 217 | ############################################# 218 | # Software Update Stuff # 219 | ############################################# 220 | #Show Secure Token enabled users 221 | checkForSecureTokenUsers=$(fdesetup list | sed -E 's/,[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}//g') 222 | if [[ $checkForSecureTokenUsers == "" ]]; then 223 | printf '%s
' "red" "No Secure Token Users Found" >> $results 224 | else 225 | printf '%s
' "white" "Secure Token Users are: $checkForSecureTokenUsers" >> $results 226 | fi 227 | #Get info.plist that would be relayed to server for comparison 228 | mkdir -p $JSS/SoftwareUpdates 229 | if [[ -e /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/SoftwareUpdateSubscriber.xpc/Contents/Info.plist ]]; then 230 | cp /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/SoftwareUpdateSubscriber.xpc/Contents/Info.plist $JSS/SoftwareUpdates/ClientInfo.plist 231 | else 232 | printf '%s
' "red" "Unable to find SoftwareUpdate info.plist" >> $results 233 | fi 234 | if [[ -e /private/var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plist ]]; then 235 | cp /private/var/db/softwareupdate/SoftwareUpdateDDMStatePersistence.plist $JSS/SoftwareUpdates/DDM.plist 236 | else 237 | printf '%s
' "red" "Unable to find Software Update DDM plist" >> $results 238 | fi 239 | ############################################# 240 | # DDM Info # 241 | ############################################# 242 | #Copy the current declaration info.plists for reference 243 | DDMInfoPlists=$(ls /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/ | grep -Ewv 'SoftwareUpdateSubscriber.xpc') 244 | mkdir -p $JSS/DDM 245 | for file in $DDMInfoPlists; do 246 | if [[ -e /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/$file/Contents/info.plist ]]; then 247 | cp /System/Library/PrivateFrameworks/RemoteManagement.framework/XPCServices/$file/Contents/info.plist $JSS/DDM/"$file"_info.plist 248 | fi 249 | done 250 | #Parse through all agents and deamons for any running keyword "jamf" and are not a part of standard Jamf applications. If none are found, they are still printed 251 | AgentsAndDaemons=$(grep -r "jamf" /Users/$loggedInUser/Library/LaunchAgents/ /Library/LaunchAgents/ /Library/LaunchDaemons/ /System/Library/LaunchAgents/ /System/Library/LaunchDaemons/) 252 | printf '%s
' "white" "A search for custom Agents and Daemons containing 'jamf' keywords has been ran and a copy of the results can be found in the Client Logs folder." >> $results 253 | echo -e "$AgentsAndDaemons" > $JSS/AgentsAndDaemons.txt 254 | #read blocked applications in jamf 255 | sudo cat /Library/Application\ Support/JAMF/.jmf_settings.json > $JSS/restricted_software.json 256 | #show installed profiles and output to xml. Use this to compare profile settings against actual settings in Managed Preferences Folder 257 | sudo profiles show -output $JSS/profiles.xml stdout-xml 258 | /usr/libexec/mdmclient AvailableOSUpdates > $JSS/SoftwareUpdates/AvailableOSUpdates.txt 259 | /usr/libexec/mdmclient QueryDeviceInformation > $JSS/QueryDeviceInformation.txt 260 | /usr/libexec/mdmclient QueryInstalledApps > $JSS/QueryDeviceApplications.txt 261 | /usr/libexec/mdmclient DumpManagementStatus > $JSS/DumpManagementStatus.txt 262 | launchctl dumpstate > $JSS/launchctl_dumpstate.txt 263 | systemextensionsctl list > $JSS/system_extensions.txt 264 | kextstat > $JSS/kextstat.txt 265 | cp /Library/Receipts/InstallHistory.plist $JSS 266 | if [ -e /Library/Logs/DiagnosticReports/ ]; then mkdir -p $JSS/DiagnosticReports && cp -r /Library/Logs/DiagnosticReports/ "$JSS/DiagnosticReports" 267 | #SLEEP TO ALLOW COPY TO FINISH PROCESSING ALL FILES 268 | sleep 5 269 | else 270 | printf '%s
' "red" "No crash reports found." >> $results 271 | fi 272 | } 273 | 274 | 275 | #################################################################################################### 276 | #Array for Jamf Connect Logs 277 | Connect() { 278 | printf '

Jamf Connect

' >> $results 279 | printf '%s
' "white" "Checking for Jamf Connect" >> $results 280 | if [ -e /Library/Managed\ Preferences/com.jamf.connect.plist ]; then 281 | printf '%s
' "white" "Jamf Connect installed, collecting Jamf Connect logs..." >>$results 282 | connectInstalled="True" 283 | mkdir -p $log_folder/Connect 284 | #OUTPUT ALL HISTORICAL JAMF CONNECT LOGS, THIS WILL ALWAYS GENERATE A LOG FILE EVEN IF CONNECT IS NOT INSTALLED 285 | log show --style compact --predicate 'subsystem == "com.jamf.connect"' --debug > $connect/JamfConnect.log 286 | #OUTPUT ALL HISTORICAL JAMF CONNECT LOGIN LOGS 287 | log show --style compact --predicate 'subsystem == "com.jamf.connect.login"' --debug > $connect/jamfconnect.login.log 288 | kerblist=$("klist" 2>/dev/null) 289 | if [[ "$kerblist" == "" ]];then 290 | printf '%s
' "white" "-No Kerberos Ticket for Current Logged in User $loggedInUser" > $connect/klist_manuallyCollected.txt; else 291 | echo $kerblist > $connect/klist_manuallyCollected.txt; 292 | fi 293 | #CHECK FOR JAMF CONNECT LOGIN LOGS AND PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 294 | if [ -e /tmp/jamf_login.log ]; then cp "/tmp/jamf_login.log" $connect/jamf_login_tmp.log 295 | else 296 | printf '%s
' "orange" "-Jamf Login /tmp file not found
-This usually only exists on recent installs.
-Don't worry if you don't see anything. We're just being thorough." >> $results 297 | fi 298 | 299 | if [ -e /Library/Managed\ Preferences/com.jamf.connect.login.plist ]; then cp "/Library/Managed Preferences/com.jamf.connect.login.plist" "$connect/com.jamf.connect.login_managed.plist" | plutil -convert xml1 "$connect/com.jamf.connect.login_managed.plist" | log show --style compact --predicate 'subsystem == "com.jamf.connect.login"' --debug > "$connect/com.jamf.connect.login.log" 300 | else 301 | printf '%s
' "red" "-Jamf Connect Login plist not found" >> $results 302 | fi 303 | 304 | #CHECK FOR JAMF CONNECT LICENSE, THEN COPY AND CONVERT TO A READABLE FORMAT 305 | printf '

License Check

' >> $results 306 | LicensefromLogin=$(defaults read /Library/Managed\ Preferences/com.jamf.connect.login.plist LicenseFile 2>/dev/null) 307 | LicensefromMenubar=$(defaults read /Library/Managed\ Preferences/com.jamf.connect.plist LicenseFile 2>/dev/null) 308 | if [[ "$LicensefromLogin" != "" ]]; then 309 | (echo "$LicensefromLogin" | base64 -d) > $connect/license.txt 310 | elif [[ "$LicensefromMenubar" != "" ]]; then 311 | (echo "$LicensefromMenubar" | base64 -d) > $connect/license.txt 312 | else 313 | file="" 314 | fi 315 | 316 | #CHECK FOR JAMF CONNECT STATE PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 317 | State_plist=$(defaults read /Users/$loggedInUser/Library/Preferences/com.jamf.connect.state.plist 2>/dev/null) 318 | if [[ "$State_plist" == "" ]]; then 319 | printf '%s
' "red" "-A Jamf Connect State list was not found because no user is logged into Menu Bar" >> $results; 320 | else cp $HOME/Library/Preferences/com.jamf.connect.state.plist "$connect/com.jamf.connect.state.plist" | plutil -convert xml1 $connect/com.jamf.connect.state.plist 321 | fi 322 | 323 | #CHECK FOR JAMF CONNECT MENU BAR PLIST, THEN COPY AND CONVERT TO A READABLE FORMAT 324 | if [ -e /Library/Managed\ Preferences/com.jamf.connect.plist ]; then cp "/Library/Managed Preferences/com.jamf.connect.plist" "$connect/com.jamf.connect_managed.plist" | plutil -convert xml1 "$connect/com.jamf.connect_managed.plist" | log show --style compact --predicate 'subsystem == "com.jamf.connect"' --debug > "$connect/com.jamf.connect.log" 325 | else 326 | printf '%s
' "red" "Jamf Connect plist not found" >> $results 327 | fi 328 | 329 | #LIST AUTHCHANGER SETTIGNS 330 | if [ -e /usr/local/bin/authchanger ]; then 331 | /usr/local/bin/authchanger -print > "$connect/authchanger_manuallyCollected.txt" 332 | : 333 | else 334 | printf '%s
' "white" "-No Authchanger settings found" >> $results 335 | fi 336 | else 337 | printf '%s
' "white" "-No Jamf Connect Installed, doing nothing" >> $results 338 | connectInstalled="False" 339 | fi 340 | #CHECK THE JAMF CONNECT LICENSE FILE AND LOOK FOR OTHER PROFILES THAT MAY HAVE A JAMF CONNECT LICENSE 341 | connectLicenseInstalled=$(defaults read $managed_preferences/com.jamf.connect.plist LicenseFile) 342 | connectLoginLicenseInstalled=$(defaults read $managed_preferences/com.jamf.connect.login.plist LicenseFile) 343 | profilesWithConnectLicense=$(grep -A 1 "LicenseFile" $JSS/profiles.xml | awk -F'' '{print $2}' | sed 's/string>//g' | sed 's/<.*//;/^$/d') 344 | connectLicenseExpiration=$(grep -A 1 "ExpirationDate" $connect/license.txt | awk '{ print $1 }' | sed 's///g' | sed 's/<.*//;/^$/d' | tr -d "-") 345 | connectDateCompare=$(date +%Y%m%d) 346 | 347 | #REPORT JAMF CONNECT PRIVILEGE ELEVATION LOGS 348 | log show --style compact --predicate '(subsystem == "com.jamf.connect.daemon") && (category == "PrivilegeElevation")' >> $connect/Connect_Daemon_Elevation_Logs.txt 349 | log show --style compact --predicate '(subsystem == "com.jamf.connect") && (category == "PrivilegeElevation")' >> $connect/Connect_Menubar_Elevation_Logs.txt 350 | 351 | connectLicenseArray() { 352 | if [[ "$connectLicenseInstalled" == "" ]]; then 353 | printf '%s
' "red" "No License found for com.jamf.connect" >> $results 354 | else 355 | for i in $profilesWithConnectLicense; do 356 | if [[ "$i" == $connectLicenseInstalled ]]; then 357 | printf '%s
' "lime" "--Matching Profile found for installed Connect License (com.jamf.connect)." >> $results 358 | if [[ $connectDateCompare -ge $connectLicenseExpiration ]]; then 359 | printf '%s
' "red" "--Currently installed Jamf Connect License is expired." >> $results 360 | else 361 | printf '%s
' "lime" "--Currently installed Jamf Connect License is valid." >> $results 362 | fi 363 | else 364 | printf '%s
' "red" "--Mismatch between installed Connect license found in com.jamf.connect.login plist and an installed profile. Search the profiles.xml file for this license string to see which one profile is attempting to install this license string:
" 365 | printf '%s
' "white" "$i " >> $results 366 | fi 367 | done 368 | fi 369 | } 370 | 371 | connectLoginLicenseArray() { 372 | if [[ "$connectLoginLicenseInstalled" == "" ]]; then 373 | printf '%s
' "red" "No License found for com.jamf.connect.login" >> $results 374 | else 375 | for i in $profilesWithConnectLicense; do 376 | if [[ "$i" == $connectLoginLicenseInstalled ]]; then 377 | printf '%s
' "lime" "--Matching Profile found for installed Connect License (com.jamf.connect.login)." >> $results 378 | if [ $connectDateCompare -ge $connectLicenseExpiration ]; then 379 | printf '%s
' "red" "--Currently installed Jamf Connect License is expired." >> $results 380 | else 381 | printf '%s
' "lime" "--Currently installed Jamf Connect License is valid." >> $results 382 | fi 383 | else 384 | printf '%s
' "red" "--Mismatch between installed Connect license found in com.jamf.connect.login plist and an installed profile. Search the profiles.xml file for this license string to see which one profile is attempting to install this license string:
" 385 | printf '%s
' "white" "$i" >> $results 386 | fi 387 | done 388 | fi 389 | } 390 | 391 | connectLicenseFormatCheck() { 392 | PI110629Check=$(awk '/string>PD94/ {count++} END {print count}' "$JSS/profiles.xml") 393 | if [[ $PI110629Check -ge "1" ]]; then 394 | printf '%s
' "red" "Key value assigned to LicenseFile uses string tag and appears to be affected by PI110629" >> $results 395 | elif [[ $PI110629Check -le "0" ]]; then 396 | printf '%s
' "lime" "Key value assigned to LicenseFile uses data tag and does not appear to be affected by PI110629" >> $results 397 | fi 398 | } 399 | if [[ $connectInstalled = "True" ]]; then 400 | connectLicenseArray 401 | connectLoginLicenseArray 402 | connectLicenseFormatCheck 403 | fi 404 | } 405 | 406 | 407 | #################################################################################################### 408 | #Array for Jamf Protect Logs 409 | Protect() { 410 | #MAKE DIRECTORY FOR ALL JAMF SECURITY RELATED FILES 411 | mkdir -p $log_folder/Jamf_Security 412 | printf '

Jamf Protect

' >> $results 413 | shopt -s nocasematch 414 | 415 | case $runProtectDiagnostics in 416 | true) 417 | if [[ $protectDiagnostics == "" ]]; then 418 | echo "Protect Diagnostics enabled but no existing file found, creating file, please wait up to 5 minutes" 419 | protectctl diagnostics -o $HOME/Desktop/ -d 5 420 | #need to re-eval protect diagnostics so it sees the file 421 | protectDiagnostics=$(ls "$HOME/Desktop/" | grep "JamfProtectDiagnostics") 422 | cp "$HOME/Desktop/$protectDiagnostics" "$security" 423 | printf '%s
' "red" "Jamf Protect diagnostic files created. Please check Jamf_Security Folder for files." >> $results 424 | else 425 | echo "Protect Diagnostics found, copying to Jamf Log Grabber" 426 | cp "$HOME/Desktop/$protectDiagnostics" "$security" 427 | printf '%s
' "red" "Jamf Protect diagnostic files found. A 'protectctl diagnostics' command has been previously ran and the diagnostics folder was found on the desktop of this device." >> $results 428 | fi 429 | ;; 430 | *) 431 | if [[ $protectDiagnostics != "" ]]; then 432 | echo "Protect Diagnostics found, copying to Jamf Log Grabber" 433 | cp "$HOME/Desktop/$protectDiagnostics" "$security" 434 | printf '%s
' "red" "Jamf Protect diagnostic files found. A 'protectctl diagnostics' command has been previously ran and the diagnostics folder was found on the desktop of this device." >> $results 435 | else 436 | echo "Protect Diagnostics disabled and no copy found on desktop" 437 | fi 438 | ;; 439 | esac 440 | #CHECK FOR JAMF PROTECT PLIST, THEN COPY AND CONVERT TO READABLE FORMAT 441 | if [ -e /Library/Managed\ Preferences/com.jamf.protect.plist ]; then cp "/Library/Managed Preferences/com.jamf.protect.plist" "$security/com.jamf.protect.plist" 442 | printf '%s
' "white" "Jamf Protect plist found" >> $results 443 | plutil -convert xml1 "$security/com.jamf.protect.plist" 444 | protectctl info --verbose > $security/jamfprotectinfo.log 445 | 446 | else 447 | printf '%s
' "orange" "Jamf Protect plist not found" >> $results 448 | fi 449 | #CHECK FOR JAMF TRUST PLIST, THEN COPY AND CONVERT TO READABLE FORMAT 450 | if [ -e /Library/Managed\ Preferences/com.jamf.trust.plist ]; then cp "/Library/Managed Preferences/com.jamf.trust.plist" "$security/com.jamf.trust.plist" 451 | plutil -convert xml1 "$security/com.jamf.trust.plist" 452 | else 453 | printf '

Jamf Trust

' >> $results 454 | printf '%s
' "orange" "Jamf Trust plist not found" >> $results 455 | fi 456 | } 457 | 458 | #################################################################################################### 459 | #Array for Recon Troubleshoot 460 | Recon_Troubleshoot() { 461 | mkdir -p $log_folder/Recon 462 | #check for Jamf Recon leftovers 463 | if [[ $reconleftovers == "" ]]; then 464 | : 465 | else 466 | printf '

Recon Troubleshoot

' >> $results 467 | echo $reconleftovers > $recon/Leftovers.txt 468 | #DIAGNOSTIC INFORMATION FOR RECON RESULTS. FOLLOWING THESE STEPS WILL HELP IDENTIFY PROBLEMATIC EXTENSION ATTRIBUTES AND/OR INVENTORY CHECK IN PROBLEMS 469 | echo -e "\nRecon leftovers found and listed above\nTo remediate, take the following steps:\n1. Open the other files in this folder\n2.Find the Extension Attribute that matches the script in this file\n3.Remove or remediate the associate Extension Attribute Script\n4.Confirm by running a 'Sudo Jamf Recon' and verifying the files do not return.\n" >> $recon/Leftovers.txt 470 | #REPORT IN RESULTS FILE THAT LEFTOVERS WERE FOUND 471 | printf '%s
' "white" "Recon Troubleshoot found files in the /tmp directory that should not be there. A report of these files as well as next actions can be found in the Leftovers.txt file in the Recon Directory." >> $results 472 | #copy all files in tmp folder to recon results folder 473 | cp -r /Library/Application\ Support/Jamf/tmp/ $recon/ 474 | timefound=`grep -E -i '[0-9]+:[0-9]+' ${jamfLog} | awk '{print $4}' | tail -1` 475 | echo $timefound > /dev/null 476 | timeFoundNoSeconds=$(echo "${timefound:0:5}${timefound:8:3}") 477 | currentTimeNoSeconds=$(echo "${currenttime1:0:5}${currenttime1:8:3}") 478 | echo $timeFoundNoSeconds > /dev/null 479 | echo $currentTimeNoSeconds > /dev/null 480 | if [[ "$timeFoundNoSeconds" == "$currentTimeNoSeconds" ]]; then 481 | printf '%s
' "Orange" "JLG appears to be running via policy, results in Recon directory may be inaccurate as files are stored there while policies are running." >> $results 482 | else 483 | printf '%s
' "Orange" "JLG appears to have been manually run. Results in Recon directory should be examined closely." >> $results 484 | fi 485 | fi 486 | } 487 | #################################################################################################### 488 | #Array for MDM Communication Check 489 | #IF A DEVICE IS NOT COMMUNICATING WITH MDM, THIS WILL GIVE ITEMS TO LOOK INTO 490 | MDMCommunicationCheck() { 491 | touch $log_folder/MDMCheck.txt 492 | #WRITE TO LOGS WHAT WE ARE DOING NEXT 493 | echo -e "Checking $loggedInUser's computer for MDM communication issues:" >> $log_folder/MDMCheck.txt 494 | #CHECK MDM STATUS AND ADVISE IF IT IS COMMUNICATING 495 | result=$(log show --style compact --predicate '(process CONTAINS "mdmclient")' --last 1d | grep "Unable to create MDM identity") 496 | if [[ $result == '' ]]; then 497 | echo -e "-MDM is communicating" >> $log_folder/MDMCheck.txt 498 | else 499 | echo -e "-MDM is broken" >> $log_folder/MDMCheck.txt 500 | fi 501 | #CHECK FOR THE MDM PROFILE TO BE INSTALLED 502 | mdmProfile=$(/usr/libexec/mdmclient QueryInstalledProfiles | grep "00000000-0000-0000-A000-4A414D460003") 503 | if [[ $mdmProfile == "" ]]; then 504 | echo -e "-MDM Profile Not Installed" >> $log_folder/MDMCheck.txt 505 | else 506 | echo -e "-MDM Profile Installed" >> $log_folder/MDMCheck.txt 507 | fi 508 | #TELL THE STATUS OF THE MDM DAEMON 509 | mdmDaemonStatus=$(/System/Library/PrivateFrameworks/ApplePushService.framework/apsctl status | grep -A 18 com.apple.aps.mdmclient.daemon.push.production | awk -F':' '/persistent connection status/ {print $NF}' | sed 's/^ *//g') 510 | echo -e "-The MDM Daemon Status is:$mdmDaemonStatus" >> $log_folder/MDMCheck.txt 511 | #WRITE THE APNS TOPIC TO THE RESULTS FILE IF IT EXISTS 512 | profileTopic=$(system_profiler SPConfigurationProfileDataType | grep "Topic" | awk -F '"' '{ print $2 }'); 513 | if [ "$profileTopic" != "" ]; then 514 | echo -e "-APNS Topic is: $profileTopic\n" >> $log_folder/MDMCheck.txt 515 | else 516 | echo -e "-No APNS Topic Found\n" >> $log_folder/MDMCheck.txt 517 | fi 518 | } 519 | #################################################################################################### 520 | #Array for Managed Preferences Collection 521 | Managed_Preferences_Array() { 522 | #mkdir -p $log_folder/Managed\ Preferences 523 | #CHECK FOR MANAGED PREFERENCE PLISTS, THEN COPY AND CONVERT THEM TO A READABLE FORMAT 524 | if [ -e /Library/Managed\ Preferences/ ]; then cp -r /Library/Managed\ Preferences $managed_preferences 525 | #SLEEP TO ALLOW COPY TO FINISH PROCESSING ALL FILES 526 | sleep 5 527 | #UNABLE TO CHECK FOLDER FOR WILDCARD PLIST LIKE *.PLIST 528 | plutil -convert xml1 $managed_preferences/*.plist 529 | plutil -convert xml1 $managed_preferences/$loggedInUser/*.plist 530 | printf '

Managed Preferences

' >> $results 531 | checkManagedNotifications=$(plutil -extract NotificationSettings xml1 -o - /Library/Managed\ Preferences/com.apple.notificationsettings.plist | grep "BundleIdentifier" -A 1 | grep "" | sed -E 's|(.*)|\1|') 532 | printf '%s
' "white" "Managed notifications found for the following applications:" >> $results 533 | for app in $checkManagedNotifications; do 534 | printf '\t%s\n
' "white" "- $app" >> $results 535 | done 536 | else 537 | printf '

Managed Preferences

' >> $results 538 | printf '%s
' "red" "No Managed Preferences plist files found" >> $results 539 | fi 540 | } 541 | 542 | #################################################################################################### 543 | #Array for Device Compliance 544 | DeviceCompliance() { 545 | mkdir -p $log_folder/Device_Compliance 546 | log show --debug --info --predicate 'subsystem CONTAINS "jamfAAD" OR subsystem BEGINSWITH "com.apple.AppSSO" OR subsystem BEGINSWITH "com.jamf.backgroundworkflows"' > $Device_Compliance/JamfConditionalAccess.log 547 | if [ -e /Library/Logs/Microsoft/Intune/ ]; then cp /Library/Logs/Microsoft/Intune/*.log $Device_Compliance 548 | else 549 | printf '

Device Compliance

' >> $results 550 | printf '%s
' "orange" "Device Compliance system logs not found" >> $results 551 | fi 552 | if [ -e /$loggedInUser/Logs/Microsoft/Intune/ ]; then cp /Library/Logs/Microsoft/Intune/*.log $Device_Compliance 553 | else 554 | printf '%s
' "orange" "Device Compliance user logs not found" >> $results 555 | fi 556 | 557 | } 558 | 559 | #################################################################################################### 560 | #Array for Device Compliance 561 | Remote_Assist() { 562 | JRA3Check=$(defaults read /Library/Application\ Support/JAMF/Remote\ Assist/jamfRemoteAssistConnectorUI.app/Contents/Info.plist CFBundleVersion 2>/dev/null) 563 | printf '

Jamf Remote Assist

' >> $results 564 | if [[ $JRA3Check = "" ]]; then 565 | printf '%s
' "white" "Jamf Remote Assist not installed, skipping version and network check." >> $results 566 | else 567 | #ADD JAMF Remote Assist Log Folder 568 | printf '%s
' "white" "Jamf Remote Assist Version: $JRA3Check" >> $results 569 | function createNetworkCheckTableJRA () { 570 | 571 | /bin/cat << EOF >> "$results" 572 |

573 |

Network access to the following hostnames are required for using Jamf Remote Assist. These hostnames are a small sample as the Relay hostnames can increment up to 100 currently.

574 | ${HOST_TEST_TABLES} 575 | EOF 576 | } 577 | 578 | 579 | function CalculateHostInfoTablesJRA () { 580 | echo "[step] Checking URLS" 581 | lastCategory="zzzNone" # Some fake category so we recognize that the first host is the start of a new category 582 | firstServer="yes" # Flag for the first host so we don't try to close the preceding table -- there won't be one. 583 | HOST_TEST_TABLES='' # This is the var we will insert into the HTML 584 | for SERVER in "${JRA_URL_ARRAY[@]}"; do 585 | #split the record info fields 586 | HOSTNAME=$(echo ${SERVER} | cut -d ',' -f1) 587 | PORT=$(echo ${SERVER} | cut -d ',' -f2) 588 | PROTOCOL=$(echo ${SERVER} | cut -d ',' -f3) 589 | CATEGORY=$(echo ${SERVER} | cut -d ',' -f4) 590 | # We have categories of hosts... enrollment, software update, etc. We'll put them in separate tables 591 | # If the category for this host is different than the last one and is not blank... 592 | if [[ "${lastCategory}" != "${CATEGORY}" ]] && [[ ! -z "${CATEGORY}" ]]; then 593 | # If this is not the first server, close up the table from the previous category before moving on to the next. 594 | echo "Starting Category : ${CATEGORY}" 595 | if [[ "${firstServer}" != "yes" ]]; then 596 | #We've already started the table html so no need to do it again. 597 | HOST_TEST_TABLES+=" ${NL}" 598 | fi 599 | firstServer="no" 600 | lastCategory="${CATEGORY}" 601 | HOST_TEST_TABLES+="

${CATEGORY}

${NL}" 602 | HOST_TEST_TABLES+=" ${NL}" 603 | HOST_TEST_TABLES+=" ${NL}" 604 | fi # End of table start and end logic. 605 | 606 | echo " > Checking connectivity to: ${HOSTNAME} ${PORT} ${PROTOCOL}" 607 | 608 | # Now print the info for this host... 609 | #Perform Host nslookup to get reported IP 610 | IP_ADDRESS=$(/usr/bin/nslookup ${HOSTNAME} | /usr/bin/grep "Address:" | /usr/bin/awk '{print$2}' | /usr/bin/tail -1) 611 | 612 | 613 | #Get Reverse DNS record 614 | REVERSE_DNS=$(/usr/bin/dig -x ${IP_ADDRESS} +short | /usr/bin/sed 's/.$//') 615 | 616 | # Using nc, if proxy defined then adding in proxy flag 617 | if [[ ${PROTOCOL} == "TCP" ]]; then 618 | if [[ -f "${LOCAL_PROXY_PAC_FILE}" ]]; then 619 | PROXY_PARSE_DATA=$(GetProxyHostFromPac ${HOSTNAME} ${PORT}) 620 | PROXY_HOST=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f1) 621 | PROXY_PORT=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f2) 622 | fi 623 | 624 | #Check if Proxy set 625 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 626 | #no proxy set 627 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 628 | else 629 | echo " > ${PROXY_HOST}:${PROXY_PORT} to be used for ${HOSTNAME}:${PORT}" 630 | STATUS=$(/usr/bin/nc -z -G 1 -x ${PROXY_HOST}:${PROXY_PORT} -X connect ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 631 | fi 632 | 633 | elif [[ ${PROTOCOL} == "TCP - non-proxied" ]]; then 634 | #for non proxy aware urls we will be using netcat aka nc 635 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 636 | else 637 | # UDP goes direct... not proxied. 638 | STATUS=$(/usr/bin/nc -u -z ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 639 | 640 | fi 641 | 642 | #Based on Status will set Availability Value 643 | if [[ ${STATUS} =~ "succeeded" ]]; then 644 | AVAILBILITY="succeeded" 645 | 646 | else 647 | AVAILBILITY="failed" 648 | fi 649 | 650 | 651 | if [[ "${AVAILBILITY}" == "succeeded" ]]; then 652 | AVAILBILITY_STATUS='' 653 | #Test for SSL Inspection 654 | if [[ ${PORT} == "80" ]]; then 655 | #http traffic no ssl inspection issues 656 | SSL_STATUS='' 657 | else 658 | if [[ ${PROTOCOL} == "TCP" ]]; then 659 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 660 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer ) 661 | 662 | else 663 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -proxy "${PROXY_HOST}:${PROXY_PORT}" -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer) 664 | fi 665 | 666 | if [[ ${CERT_STATUS} != *"Apple Inc"* ]] && [[ "${CERT_STATUS}" != *"Akamai Technologies"* ]] && [[ "${CERT_STATUS}" != *"Amazon"* ]] && [[ "${CERT_STATUS}" != *"DigiCert"* ]] && [[ "${CERT_STATUS}" != *"Microsoft"* ]] && [[ "${CERT_STATUS}" != *"COMODO"* ]] && [[ "${CERT_STATUS}" != *"QuoVadis"* ]]; then 667 | 668 | SSL_ISSUER=$(echo ${CERT_STATUS} | awk -F'O=|/OU' '{print $2}') 669 | 670 | if [[ ${HOSTNAME} == *"jcdsdownloads.services.jamfcloud.com" ]];then 671 | SSL_STATUS='' 672 | else 673 | SSL_STATUS="" 674 | fi 675 | 676 | else 677 | 678 | SSL_STATUS='' 679 | fi 680 | else 681 | SSL_STATUS='' 682 | fi 683 | fi 684 | else 685 | # nc did not connect. There is no point in trying the SSL cert subject test. 686 | AVAILBILITY_STATUS='' 687 | SSL_STATUS='' 688 | fi 689 | 690 | # Done. Stick the row of info into the HTML var... 691 | HOST_TEST_TABLES+=" ${AVAILBILITY_STATUS}${SSL_STATUS}${NL}" 692 | done 693 | # Close up the html for the final table 694 | HOST_TEST_TABLES+="
HOSTNAMEReverse DNSIP AddressPortProtocolAccessibleSSL Error
AvailableN/AN/AUnexpected Certificate: ${SSL_ISSUER}SuccessfulN/AUnavailableNot checked
${HOSTNAME}${REVERSE_DNS}${IP_ADDRESS}${PORT}${PROTOCOL}
${NL}" 695 | } 696 | 697 | JRA_URL_ARRAY=( 698 | #Device setup 699 | "us.jra.services.jamfcloud.com,443,TCP,Connection Results" 700 | "files.jra.services.jamfcloud.com,443,TCP" 701 | "download.jra.services.jamfcloud.com,443,TCP" 702 | "relay-1.us.jra.services.jamfcloud.com,443,TCP" 703 | "socket.us.jra.services.jamfcloud.com,5555,UDP" 704 | ) 705 | CalculateHostInfoTablesJRA 706 | createNetworkCheckTableJRA 707 | 708 | log show --style compact --predicate 'subsystem BEGINSWITH "com.jamf.remoteassist"' --debug > $JRA/JRA_debug.log 709 | fi 710 | } 711 | 712 | #################################################################################################### 713 | #Array for App Installers Directory 714 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 715 | AppInstallers() { 716 | mkdir -p $App_Installers 717 | if [ -e /var/db/ConfigurationProfiles/Settings/Managed\ Applications/Device/ ]; then cp -r /var/db/ConfigurationProfiles/Settings/Managed\ Applications/Device/ directory $App_Installers 718 | function createNetworkCheckTableAppInstallers () { 719 | /bin/cat << EOF >> "$results" 720 |

721 |

App Installers

722 | EOF 723 | #Gross for loop that checks for the first report of a failed install and sets the value to true. Avoids a blank return saying App Installer failures found but nothing listed. It is redundant but my limited scripting knowledge only allows for this workaround currently. 724 | for file in $App_Installers/_Completed/*; do 725 | failedInstall=$(defaults read "$file" InstallFailed 2> /dev/null) 726 | if [[ $failedInstall == 1 ]]; then 727 | checkForAllFailedInstalls="True" 728 | break 729 | else 730 | checkForAllFailedInstalls="False" 731 | fi 732 | done 733 | if [[ $checkForAllFailedInstalls = "True" ]]; then 734 | /bin/cat << EOF >> "$results" 735 |

Failed App Installer Check

736 |

The following files were found in the app installer logs and show failed installations. If you are troubleshooting failed App Installers, please examine the following files.

737 | EOF 738 | printf '%s
' "red" "Here's a list of failed app installer logs" >> $results 739 | touch $App_Installers/commandUUID.txt 740 | for file in $App_Installers/_Completed/*; do 741 | failedInstall=$(defaults read "$file" InstallFailed 2> /dev/null) 742 | commandUUID=$(defaults read "$file" InstallUUID 2> /dev/null) 743 | if [[ $failedInstall == 1 ]]; then 744 | printf '%s
' "yellow" "-$file" >> $results 745 | echo "$commandUUID," >> $App_Installers/commandUUID.txt 746 | fi 747 | done 748 | cat $App_Installers/commandUUID.txt | tr '\n' ' ' > $App_Installers/commandUUID.txt 749 | else 750 | /bin/cat << EOF >> "$results" 751 |

Failed App Installer Check

752 |

No failed App Installer files found.

753 | EOF 754 | fi 755 | 756 | /bin/cat << EOF >> "$results" 757 |

758 |

Network access to the following hostname is required for using Jamf's App Installers.

759 | ${HOST_TEST_TABLES} 760 | EOF 761 | } 762 | function CalculateHostInfoTablesAppInstallers () { 763 | echo "[step] Checking URLS" 764 | lastCategory="zzzNone" # Some fake category so we recognize that the first host is the start of a new category 765 | firstServer="yes" # Flag for the first host so we don't try to close the preceding table -- there won't be one. 766 | HOST_TEST_TABLES='' # This is the var we will insert into the HTML 767 | for SERVER in "${APP_Installer_URL_ARRAY[@]}"; do 768 | #split the record info fields 769 | HOSTNAME=$(echo ${SERVER} | cut -d ',' -f1) 770 | PORT=$(echo ${SERVER} | cut -d ',' -f2) 771 | PROTOCOL=$(echo ${SERVER} | cut -d ',' -f3) 772 | CATEGORY=$(echo ${SERVER} | cut -d ',' -f4) 773 | # We have categories of hosts... enrollment, software update, etc. We'll put them in separate tables 774 | # If the category for this host is different than the last one and is not blank... 775 | if [[ "${lastCategory}" != "${CATEGORY}" ]] && [[ ! -z "${CATEGORY}" ]]; then 776 | # If this is not the first server, close up the table from the previous category before moving on to the next. 777 | echo "Starting Category : ${CATEGORY}" 778 | if [[ "${firstServer}" != "yes" ]]; then 779 | #We've already started the table html so no need to do it again. 780 | HOST_TEST_TABLES+=" ${NL}" 781 | fi 782 | firstServer="no" 783 | lastCategory="${CATEGORY}" 784 | HOST_TEST_TABLES+="

${CATEGORY}

${NL}" 785 | HOST_TEST_TABLES+=" ${NL}" 786 | HOST_TEST_TABLES+=" ${NL}" 787 | fi # End of table start and end logic. 788 | 789 | echo " > Checking connectivity to: ${HOSTNAME} ${PORT} ${PROTOCOL}" 790 | 791 | # Now print the info for this host... 792 | #Perform Host nslookup to get reported IP 793 | IP_ADDRESS=$(/usr/bin/nslookup ${HOSTNAME} | /usr/bin/grep "Address:" | /usr/bin/awk '{print$2}' | /usr/bin/tail -1) 794 | 795 | 796 | #Get Reverse DNS record 797 | REVERSE_DNS=$(/usr/bin/dig -x ${IP_ADDRESS} +short | /usr/bin/sed 's/.$//') 798 | 799 | # Using nc, if proxy defined then adding in proxy flag 800 | if [[ ${PROTOCOL} == "TCP" ]]; then 801 | if [[ -f "${LOCAL_PROXY_PAC_FILE}" ]]; then 802 | PROXY_PARSE_DATA=$(GetProxyHostFromPac ${HOSTNAME} ${PORT}) 803 | PROXY_HOST=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f1) 804 | PROXY_PORT=$(echo ${PROXY_PARSE_DATA} | /usr/bin/awk '{print $2}' | /usr/bin/tr -d "';" | /usr/bin/cut -d: -f2) 805 | fi 806 | 807 | #Check if Proxy set 808 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 809 | #no proxy set 810 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 811 | else 812 | echo " > ${PROXY_HOST}:${PROXY_PORT} to be used for ${HOSTNAME}:${PORT}" 813 | STATUS=$(/usr/bin/nc -z -G 1 -x ${PROXY_HOST}:${PROXY_PORT} -X connect ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 814 | fi 815 | 816 | elif [[ ${PROTOCOL} == "TCP - non-proxied" ]]; then 817 | #for non proxy aware urls we will be using netcat aka nc 818 | STATUS=$(/usr/bin/nc -z -G 1 ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 819 | else 820 | # UDP goes direct... not proxied. 821 | STATUS=$(/usr/bin/nc -u -z ${HOSTNAME} ${PORT} 2>&1 | /usr/bin/awk '{print $7}') 822 | 823 | fi 824 | 825 | #Based on Status will set Availability Value 826 | if [[ ${STATUS} =~ "succeeded" ]]; then 827 | AVAILBILITY="succeeded" 828 | 829 | else 830 | AVAILBILITY="failed" 831 | fi 832 | 833 | 834 | if [[ "${AVAILBILITY}" == "succeeded" ]]; then 835 | AVAILBILITY_STATUS='' 836 | #Test for SSL Inspection 837 | if [[ ${PORT} == "80" ]]; then 838 | #http traffic no ssl inspection issues 839 | SSL_STATUS='' 840 | else 841 | if [[ ${PROTOCOL} == "TCP" ]]; then 842 | if [[ ${PROXY_HOST} == "" ]] && [[ ${PROXY_PORT} == "" ]];then 843 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer ) 844 | 845 | else 846 | CERT_STATUS=$(echo | /usr/bin/openssl s_client -showcerts -proxy "${PROXY_HOST}:${PROXY_PORT}" -connect "${HOSTNAME}:${PORT}" -servername "${HOSTNAME}" 2>/dev/null | /usr/bin/openssl x509 -noout -issuer) 847 | fi 848 | 849 | if [[ ${CERT_STATUS} != *"Apple Inc"* ]] && [[ "${CERT_STATUS}" != *"Akamai Technologies"* ]] && [[ "${CERT_STATUS}" != *"Amazon"* ]] && [[ "${CERT_STATUS}" != *"DigiCert"* ]] && [[ "${CERT_STATUS}" != *"Microsoft"* ]] && [[ "${CERT_STATUS}" != *"COMODO"* ]] && [[ "${CERT_STATUS}" != *"QuoVadis"* ]]; then 850 | 851 | SSL_ISSUER=$(echo ${CERT_STATUS} | awk -F'O=|/OU' '{print $2}') 852 | 853 | if [[ ${HOSTNAME} == *"jcdsdownloads.services.jamfcloud.com" ]];then 854 | SSL_STATUS='' 855 | else 856 | SSL_STATUS="" 857 | fi 858 | 859 | else 860 | 861 | SSL_STATUS='' 862 | fi 863 | else 864 | SSL_STATUS='' 865 | fi 866 | fi 867 | else 868 | # nc did not connect. There is no point in trying the SSL cert subject test. 869 | AVAILBILITY_STATUS='' 870 | SSL_STATUS='' 871 | fi 872 | 873 | # Done. Stick the row of info into the HTML var... 874 | HOST_TEST_TABLES+=" ${AVAILBILITY_STATUS}${SSL_STATUS}${NL}" 875 | done 876 | # Close up the html for the final table 877 | HOST_TEST_TABLES+="
HOSTNAMEReverse DNSIP AddressPortProtocolAccessibleSSL Error
AvailableN/AN/AUnexpected Certificate: ${SSL_ISSUER}SuccessfulN/AUnavailableNot checked
${HOSTNAME}${REVERSE_DNS}${IP_ADDRESS}${PORT}${PROTOCOL}
${NL}" 878 | } 879 | APP_Installer_URL_ARRAY=( 880 | #Device setup 881 | "appinstallers-packages.services.jamfcloud.com,443,TCP, Connection Results" 882 | ) 883 | CalculateHostInfoTablesAppInstallers 884 | createNetworkCheckTableAppInstallers 885 | else 886 | printf '

App Installers

' >> $results 887 | printf '%s
' "orange" "App Installer Directory not found, device is not in scope for any App Installers or is not receiving the App Installer command from Jamf." >> $results 888 | fi 889 | } 890 | 891 | #################################################################################################### 892 | #Array for App Named in Dynamic Variables 893 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 894 | CustomApp1Array() { 895 | mkdir -p $log_folder/$CustomApp1Name 896 | if [ -e $CustomApp1LogSource ] && [ $CustomApp1LogSource != "/" ]; then cp -r $CustomApp1LogSource $CustomApp1Folder 897 | else 898 | printf '

'$CustomApp1Name'

' >> $results 899 | printf '%s
' "orange" "$CustomApp1Name does not have a log file available to grab or was set to an invalid path." >> $results 900 | fi 901 | } 902 | 903 | #################################################################################################### 904 | #Array for App Named in Dynamic Variables 905 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 906 | CustomApp2Array() { 907 | mkdir -p $log_folder/$CustomApp2Name 908 | if [ -e $CustomApp2LogSource ] && [ $CustomApp2LogSource != "/" ]; then cp -r $CustomApp2LogSource $CustomApp2Folder 909 | else 910 | printf '

'$CustomApp2Name'

' >> $results 911 | printf '%s
' "orange" "$CustomApp2Name does not have a log file available to grab or was set to an invalid path." >> $results 912 | fi 913 | } 914 | 915 | #################################################################################################### 916 | #Array for App Named in Dynamic Variables 917 | #When done, remove the associated array comment/# inside the Case command inside the logGrabberMasterArray 918 | CustomApp3Array() { 919 | mkdir -p $log_folder/$CustomApp3Name 920 | if [ -e $CustomApp3LogSource ] && [ $CustomApp3LogSource != "/" ]; then cp -r $CustomApp3LogSource $CustomApp3Folder 921 | else 922 | printf '

'$CustomApp3Name'

' >> $results 923 | printf '%s
' "orange" "$CustomApp3Name does not have a log file available to grab or was set to an invalid path." >> $results 924 | fi 925 | } 926 | 927 | #################################################################################################### 928 | 929 | #Array for folder cleanup 930 | Cleanup() { 931 | #IF AN ARRAY IS NOT SET TO RUN, REMOVE THE FOLDER NAME FOR IT BELOW TO AVOID ERRORS WITH THE CLEANUP FUNCTION AT THE END OF THE SCRIPT 932 | cleanup=("Client_Logs Recon Self_Service Connect Jamf_Security Managed_Preferences Device_Compliance JRA App_Installers $CustomApp1Name $CustomApp2Name $CustomApp3Name") 933 | #CLEANS OUT EMPTY FOLDERS TO AVOID CONFUSION 934 | printf '

Cleanup Results

' >> $results 935 | printf '%s
' "white" "The following folders contained no files and were removed:" >> $results 936 | for emptyfolder in $cleanup 937 | do 938 | if [ -z "$(ls -A /$log_folder/$emptyfolder)" ]; then 939 | printf '%s
' "yellow" "-$emptyfolder" >> $results 940 | rm -r $log_folder/$emptyfolder 941 | else 942 | : 943 | fi 944 | done 945 | printf '%s
' "white" "Completed Log Grabber on '$(currenttime)'" >> $results 946 | } 947 | 948 | #################################################################################################### 949 | Zip_Folder() { 950 | cd $HOME/Desktop 951 | #NAME ZIPPED FOLDER WITH LOGGED IN USER 952 | zip "$loggedInUser"_"$current_date"_logs.zip -r "$loggedInUser"_"$current_date"_logs 953 | rm -r $log_folder 954 | } 955 | #################################################################################################### 956 | # Set the Arrays you want to grab. 957 | # Default Array is logsToGrab=("Jamf" "Managed_Preferences" "Protect" "Connect" "Recon_Troubleshoot" "MDM_Communication_Check" "Device_Compliance" "App_Installers" "Remote_Assist" "$CustomApp1Name" "$CustomApp2Name" "$CustomApp3Name") 958 | 959 | declare -a logsToGrab=("Jamf" "Managed_Preferences" "Protect" "Connect" "Recon_Troubleshoot" "MDM_Communication_Check" "Device_Compliance" "App_Installers" "Remote_Assist" "$CustomApp1Name" "$CustomApp2Name" "$CustomApp3Name") 960 | 961 | #################################################################################################### 962 | # Put it all together in the Master Array 963 | 964 | logGrabberMasterArray() { 965 | #CLEAR OUT PREVIOUS RESULTS 966 | if [ -e $log_folder ] ;then rm -r $log_folder 967 | fi 968 | #CREATE A FOLDER TO SAVE ALL LOGS 969 | mkdir -p $log_folder 970 | #CREATE A LOG FILE FOR SCRIPT AND SAVE TO LOGS DIRECTORY SO ADMINS CAN SEE WHAT LOGS WERE NOT GATHERED 971 | touch $results 972 | buildHTMLResults 973 | #SET A TIME AND DATE STAMP FOR WHEN THE LOG GRABBER WAS RAN 974 | printf '%s
' "white" "Log Grabber was started at '$(currenttime)'
" >> $results 975 | ## now loop through the above array 976 | for logs in "${logsToGrab[@]}" 977 | do 978 | echo "$logs" 979 | case $logs in 980 | Jamf) 981 | Jamf 982 | ;; 983 | Protect) 984 | Protect 985 | ;; 986 | Connect) 987 | Connect 988 | ;; 989 | Recon_Troubleshoot) 990 | Recon_Troubleshoot 991 | ;; 992 | MDM_Communication_Check) 993 | MDMCommunicationCheck 994 | ;; 995 | Managed_Preferences) 996 | Managed_Preferences_Array 997 | ;; 998 | Device_Compliance) 999 | DeviceCompliance 1000 | ;; 1001 | Remote_Assist) 1002 | Remote_Assist 1003 | ;; 1004 | App_Installers) 1005 | AppInstallers 1006 | ;; 1007 | "$CustomApp1Name") 1008 | #Add or Remove comment from line below to disable or enable the array for the custom app 1009 | #CustomApp1Array 1010 | ;; 1011 | "$CustomApp2Name") 1012 | #Add or Remove comment from line below to disable or enable the array for the custom app 1013 | #CustomApp2Array 1014 | ;; 1015 | "$CustomApp3Name") 1016 | #Add or Remove comment from line below to disable or enable the array for the custom app 1017 | #CustomApp3Array 1018 | ;; 1019 | *) 1020 | echo "$logs is an invalid variable for the array. Check your spelling or add it to the case argument with your own array" >> $results 1021 | ;; 1022 | esac 1023 | done 1024 | 1025 | } 1026 | 1027 | #Runs the Log Grabber as configured 1028 | logGrabberMasterArray 1029 | #Run cleanup Array to remove empty folders 1030 | Cleanup 1031 | #Zips Results- Comment out or remove the line below to leave the folder unzipped 1032 | Zip_Folder 1033 | --------------------------------------------------------------------------------