├── 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 |
%s
%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 '%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%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 '%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'%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:%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:%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 '%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 '%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 '%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 '%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 '%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 '%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 '%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+="| HOSTNAME | Reverse DNS | IP Address | Port | Protocol | Accessible | SSL Error | Available | ' 659 | #Test for SSL Inspection 660 | if [[ ${PORT} == "80" ]]; then 661 | #http traffic no ssl inspection issues 662 | SSL_STATUS='N/A | ' 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='N/A | ' 678 | else 679 | SSL_STATUS="Unexpected Certificate: ${SSL_ISSUER} | " 680 | fi 681 | 682 | else 683 | 684 | SSL_STATUS='Successful | ' 685 | fi 686 | else 687 | SSL_STATUS='N/A | ' 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='Unavailable | ' 693 | SSL_STATUS='Not checked | ' 694 | fi 695 | 696 | # Done. Stick the row of info into the HTML var... 697 | HOST_TEST_TABLES+="
|---|---|---|---|---|---|---|
| ${HOSTNAME} | ${REVERSE_DNS} | ${IP_ADDRESS} | ${PORT} | ${PROTOCOL} | ${AVAILBILITY_STATUS}${SSL_STATUS}
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 | 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+="| HOSTNAME | Reverse DNS | IP Address | Port | Protocol | Accessible | SSL Error | Available | ' 842 | #Test for SSL Inspection 843 | if [[ ${PORT} == "80" ]]; then 844 | #http traffic no ssl inspection issues 845 | SSL_STATUS='N/A | ' 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='N/A | ' 861 | else 862 | SSL_STATUS="Unexpected Certificate: ${SSL_ISSUER} | " 863 | fi 864 | 865 | else 866 | 867 | SSL_STATUS='Successful | ' 868 | fi 869 | else 870 | SSL_STATUS='N/A | ' 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='Unavailable | ' 876 | SSL_STATUS='Not checked | ' 877 | fi 878 | 879 | # Done. Stick the row of info into the HTML var... 880 | HOST_TEST_TABLES+="
|---|---|---|---|---|---|---|
| ${HOSTNAME} | ${REVERSE_DNS} | ${IP_ADDRESS} | ${PORT} | ${PROTOCOL} | ${AVAILBILITY_STATUS}${SSL_STATUS}
%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 '%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 '%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 '%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 '%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)'%s
%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 '%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%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 '%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'%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:%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:%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 '%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 '%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 '%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 '%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 '%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 '%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 '%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+="| HOSTNAME | Reverse DNS | IP Address | Port | Protocol | Accessible | SSL Error | Available | ' 653 | #Test for SSL Inspection 654 | if [[ ${PORT} == "80" ]]; then 655 | #http traffic no ssl inspection issues 656 | SSL_STATUS='N/A | ' 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='N/A | ' 672 | else 673 | SSL_STATUS="Unexpected Certificate: ${SSL_ISSUER} | " 674 | fi 675 | 676 | else 677 | 678 | SSL_STATUS='Successful | ' 679 | fi 680 | else 681 | SSL_STATUS='N/A | ' 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='Unavailable | ' 687 | SSL_STATUS='Not checked | ' 688 | fi 689 | 690 | # Done. Stick the row of info into the HTML var... 691 | HOST_TEST_TABLES+="
|---|---|---|---|---|---|---|
| ${HOSTNAME} | ${REVERSE_DNS} | ${IP_ADDRESS} | ${PORT} | ${PROTOCOL} | ${AVAILBILITY_STATUS}${SSL_STATUS}
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 | 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+="| HOSTNAME | Reverse DNS | IP Address | Port | Protocol | Accessible | SSL Error | Available | ' 836 | #Test for SSL Inspection 837 | if [[ ${PORT} == "80" ]]; then 838 | #http traffic no ssl inspection issues 839 | SSL_STATUS='N/A | ' 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='N/A | ' 855 | else 856 | SSL_STATUS="Unexpected Certificate: ${SSL_ISSUER} | " 857 | fi 858 | 859 | else 860 | 861 | SSL_STATUS='Successful | ' 862 | fi 863 | else 864 | SSL_STATUS='N/A | ' 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='Unavailable | ' 870 | SSL_STATUS='Not checked | ' 871 | fi 872 | 873 | # Done. Stick the row of info into the HTML var... 874 | HOST_TEST_TABLES+="
|---|---|---|---|---|---|---|
| ${HOSTNAME} | ${REVERSE_DNS} | ${IP_ADDRESS} | ${PORT} | ${PROTOCOL} | ${AVAILBILITY_STATUS}${SSL_STATUS}
%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 '%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 '%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 '%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 '%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)'