├── .gitignore ├── images ├── jamJAR.png ├── jamJAR-latest.png ├── jamJAR-pending.png ├── jamJAR-installed.png ├── jamJAR-noupdates.png └── jamJAR-macosupdates.gif ├── catalog-info.yaml ├── README.md ├── extension_attribute └── ea-jamJAR.sh ├── .ci └── Jenkinsfile ├── jamf_schema └── uk.co.dataJAR.jamJAR.json ├── LICENSE ├── script └── jamJAR.py └── postflight └── postflight /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /images/jamJAR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamJAR/HEAD/images/jamJAR.png -------------------------------------------------------------------------------- /images/jamJAR-latest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamJAR/HEAD/images/jamJAR-latest.png -------------------------------------------------------------------------------- /images/jamJAR-pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamJAR/HEAD/images/jamJAR-pending.png -------------------------------------------------------------------------------- /images/jamJAR-installed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamJAR/HEAD/images/jamJAR-installed.png -------------------------------------------------------------------------------- /images/jamJAR-noupdates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamJAR/HEAD/images/jamJAR-noupdates.png -------------------------------------------------------------------------------- /images/jamJAR-macosupdates.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/jamJAR/HEAD/images/jamJAR-macosupdates.gif -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | annotations: 5 | sonarqube.org/project-key: com.jamf.team-red-dawg:jamJAR 6 | description: jamJAR is the open source project, that leverages some of the same workflows as Jamf Auto Update. 7 | name: jamjar 8 | tags: 9 | - datajar 10 | - autoupdate 11 | spec: 12 | type: application 13 | lifecycle: production 14 | owner: zubry 15 | system: jamf-auto-update 16 | serviceTier: 5 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | I have a Jamf, I have a Munki... uh... jamJAR! 5 |

6 | 7 | ## Introduction 8 | 9 | jamJAR is a solution that applies out of the box thinking & lean methodologies to macOS "patch management". 10 | 11 | This holistic approach synergises jamf, autopkg & munki into an aggregated convergence that cherry-picks functionality from each products core competency to create an innovative, scalable & modular update framework. 12 | 13 | For more information, please visit the [wiki.](https://github.com/dataJAR/jamJAR/wiki) 14 | 15 | ## Videos 16 | 17 | University of Utah, MacAdmins Meeting. April 2017 - https://stream.lib.utah.edu/index.php?c=details&id=12885 18 | 19 | macad.uk. Feb 2017 - https://www.youtube.com/watch?v=SiDWCqwUSIM&feature=youtu.be 20 | 21 | jamJAR: Three years on - Oct 2020 - https://www.youtube.com/watch?v=JGU48-S-unE 22 | -------------------------------------------------------------------------------- /extension_attribute/ea-jamJAR.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #################################################################################################### 4 | # 5 | # Copyright (c) 2023, dataJAR Ltd. All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # * Neither data JAR Ltd nor the 15 | # names of its contributors may be used to endorse or promote products 16 | # derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY DATA JAR LTD "AS IS" AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL DATA JAR LTD BE LIABLE FOR ANY 22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | # 29 | #################################################################################################### 30 | # 31 | # SUPPORT FOR THIS PROGRAM 32 | # 33 | # This program is distributed "as is" by DATA JAR LTD. 34 | # For more information or support, please utilise the following resources: 35 | # 36 | # http://www.datajar.co.uk 37 | # 38 | #################################################################################################### 39 | # 40 | # DESCRIPTION 41 | # Returns the last line of the Auto-Update log file 42 | # 43 | #################################################################################################### 44 | # 45 | # CHANGE LOG 46 | # 2.1 - 2023-07-01 - Double quoted "${logFilePath}", to allow for spaces 47 | # 2.0 - 2021-10-02 - Rewritten in bash, to stop python prompts on macOS 12+ 48 | # 49 | #################################################################################################### 50 | 51 | if [ -f "/Library/Preferences/uk.co.dataJAR.jamJAR.plist" ] 52 | then 53 | logDir=$(/usr/bin/defaults read "/Library/Managed Preferences/uk.co.dataJAR.jamJAR.plist" log_file_dir 2> /dev/null) 54 | logFileName=$(/usr/bin/defaults read "/Library/Managed Preferences/uk.co.dataJAR.jamJAR.plist" log_file_name 2> /dev/null) 55 | fi 56 | 57 | if [ -f "/Library/Managed Preferences/uk.co.dataJAR.jamJAR.plist" ] 58 | then 59 | logDir=$(/usr/bin/defaults read "/Library/Managed Preferences/uk.co.dataJAR.jamJAR.plist" log_file_dir 2> /dev/null) 60 | logFileName=$(/usr/bin/defaults read "/Library/Managed Preferences/uk.co.dataJAR.jamJAR.plist" log_file_name 2> /dev/null) 61 | fi 62 | 63 | if [ -z "${logDir-unset}" ] 64 | then 65 | logDir="/var/log/" 66 | fi 67 | 68 | if [ -z "${logFileName-unset}" ] 69 | then 70 | logFileName="jamJAR" 71 | fi 72 | 73 | if [[ "${logDir}" == */ ]] 74 | then 75 | logFilePath="${logDir}""${logFileName}"".log" 76 | else 77 | logFilePath="${logDir}""/""${logFileName}"".log" 78 | fi 79 | 80 | if [ -f "${logFilePath}" ] 81 | then 82 | /bin/echo "$(/usr/bin/tail -n -1 "${logFilePath}")" 83 | else 84 | /bin/echo "" 85 | fi -------------------------------------------------------------------------------- /.ci/Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | @Library('tools') _ 4 | import com.jamf.jenkins.KanikoBuild 5 | import java.text.SimpleDateFormat 6 | 7 | pipeline { 8 | agent { 9 | kubernetes { 10 | defaultContainer 'sonar' 11 | yaml ''' 12 | apiVersion: v1 13 | kind: Pod 14 | spec: 15 | serviceAccountName: ecr-ci-sa 16 | containers: 17 | - name: sonar 18 | image: docker.jamf.build/sonarsource/sonar-scanner-cli:latest 19 | tty: true 20 | command: 21 | - cat 22 | ''' 23 | } 24 | } 25 | options { 26 | buildDiscarder(logRotator(numToKeepStr: '6')) // Keeps only the last 6 builds 27 | } 28 | 29 | environment { 30 | PROJECT = 'jamJAR' 31 | SONARPROJECT = "com.jamf.team-red-dawg:jamJAR" 32 | SONAR_URL = "https://sonarqube.jamf.build" 33 | } 34 | 35 | stages { 36 | //Sonarqube Analysis 37 | stage('SonarQube Analysis') { 38 | when { 39 | beforeAgent true 40 | anyOf { 41 | branch 'main' 42 | branch 'dev' 43 | not { changeRequest() } 44 | } 45 | } 46 | steps { 47 | container('sonar') { 48 | script { 49 | performSonarQubeAnalysis() 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | post { 57 | always { 58 | publishHTML([ 59 | allowMissing: true, 60 | alwaysLinkToLastBuild: false, 61 | reportDir: 'build/reports/tests/test', 62 | reportFiles: 'index.html', 63 | reportName: 'Gradle Build Report', 64 | keepAll: true 65 | ]) 66 | } 67 | } 68 | } 69 | 70 | 71 | // Define all custom Jenkins utility functions 72 | 73 | // Define a reusable function for time logging 74 | def logCurrentTime(String context = "Time Log") { 75 | def sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z") 76 | echo "${context}: ${sdf.format(new Date())}" 77 | } 78 | 79 | // Define a reusable function Git Setup 80 | def setupGitConfig() { 81 | echo 'Setting up git configuration' 82 | env.GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=no' 83 | sh """ 84 | git config --global user.email "${env.GIT_COMMITTER_EMAIL}" 85 | git config --global user.name "${env.GIT_COMMITTER_NAME}" 86 | """ 87 | } 88 | 89 | // Define a reusable function for repo setup 90 | def cloneRepository(String repoDirectory) { 91 | try { 92 | echo "Cloning repository from ${env.HELM_MANIFEST_REPO} into ${repoDirectory}" 93 | 94 | sh """ 95 | set -x 96 | git clone --single-branch --branch ${env.HELM_REPO_BRANCH} ${env.HELM_MANIFEST_REPO} ${repoDirectory} 97 | """ 98 | 99 | echo "Repository cloned successfully into ${repoDirectory}" 100 | } catch (Exception e) { 101 | echo "Error while cloning repository: ${e.getMessage()}" 102 | throw e // Rethrow the exception to handle it at a higher level or fail the build 103 | } 104 | } 105 | 106 | // Define a reusable function for SonarQube Analysis 107 | def performSonarQubeAnalysis() { 108 | withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_AUTH_TOKEN')]) { 109 | def gitUrl = sh(script: 'git config --get remote.origin.url', returnStdout: true).trim() 110 | echo "Git URL: $gitUrl" 111 | 112 | sh ''' 113 | sonar-scanner \ 114 | -Dsonar.host.url="${SONAR_URL}" \ 115 | -Dsonar.login="${SONAR_AUTH_TOKEN}" \ 116 | -Dsonar.branch.name="${BRANCH_NAME}" \ 117 | -Dsonar.projectKey="${SONARPROJECT}" \ 118 | -Dsonar.python.version="3" \ 119 | -Dsonar.sources=. 120 | ''' 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /jamf_schema/uk.co.dataJAR.jamJAR.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "uk.co.dataJAR.jamJAR", 3 | "description": "JamJAR", 4 | "properties": { 5 | "notifier_path": { 6 | "title": "notifier_path", 7 | "description": "The path of the application sending the notifications. Currently only terminal-notifier & Jamf Pro's Management Action have been tested. If this is defined within jamJAR preferences but is missing, then no notifications are sent when a user is logged in. The only notifications will be from Munki status over the loginwindow. Defaults to - string: /Library/Application Support/JAMF/bin/Management Action.app/Contents/MacOS/Management Action", 8 | "property_order": 5, 9 | "anyOf": [ 10 | {"type": "null", "title": "Not Configured"}, 11 | { 12 | "title": "Configured", 13 | "type": "string" 14 | } 15 | ] 16 | }, 17 | "no_msg_category": { 18 | "title": "no_msg_category", 19 | "description": "Titles installed by jamJAR with the category defined within this key will not post a notification.", 20 | "property_order": 10, 21 | "anyOf": [ 22 | {"type": "null", "title": "Not Configured"}, 23 | { 24 | "title": "Configured", 25 | "type": "string" 26 | } 27 | ] 28 | }, 29 | "notifier_msg_installed": { 30 | "title": "notifier_msg_installed", 31 | "description": "The notification text to display when an item is installed, this requires 2 string placeholders (%s). These are then passed the following: Item Name (i.e. Google Chrome). Item Version (i.e. 56.0.2924.87). %s %s has been installed", 32 | "property_order": 15, 33 | "anyOf": [ 34 | {"type": "null", "title": "Not Configured"}, 35 | { 36 | "title": "Configured", 37 | "type": "string" 38 | } 39 | ] 40 | }, 41 | "notifier_msg_title": { 42 | "title": "notifier_msg_title", 43 | "description": "The notification texts title. Defaults to jamJAR", 44 | "property_order": 20, 45 | "anyOf": [ 46 | {"type": "null", "title": "Not Configured"}, 47 | { 48 | "title": "Configured", 49 | "type": "string" 50 | } 51 | ] 52 | }, 53 | "datajar_notifier_logout_button": { 54 | "title": "datajar_notifier_logout_button", 55 | "description": "Requires jamJAR 2.0+ and datajar_notifier to be set to True Sets the title for the logout button shown in the Alert notifications. Defaults to - string: Logout", 56 | "property_order": 25, 57 | "anyOf": [ 58 | {"type": "null", "title": "Not Configured"}, 59 | { 60 | "title": "Configured", 61 | "type": "string" 62 | } 63 | ] 64 | }, 65 | "datajar_notifier": { 66 | "title": "datajar_notifier", 67 | "description": "Boolean that determines if jamjar should use the datajar notifier for notifications. With jamJAR 2.0+ this changes the notifications sent. If any pending notification is to be sent for either macOS updates or other updates , a persistent Alert notification is sent. Other notifications will be the banner, non-persistent notification. (NOTE: If using Notifier you can set all notifications to Alert or Banner via a notifications configuration profile) Defaults to - boolean: False", 68 | "property_order": 30, 69 | "anyOf": [ 70 | {"type": "null", "title": "Not Configured"}, 71 | { 72 | "title": "Configured", 73 | "type": "boolean" 74 | } 75 | ] 76 | }, 77 | "notifier_msg_uptodate": { 78 | "title": "notifier_msg_uptodate", 79 | "description": "The notification text to display when a jamJAR policy is initiated via Self Service & the title attempted to install is up-to-date. This requires 1 string placeholders (%s), which defaults to Item Name (i.e. Google Chrome). Latest version of %s is installed.", 80 | "property_order": 35, 81 | "anyOf": [ 82 | {"type": "null", "title": "Not Configured"}, 83 | { 84 | "title": "Configured", 85 | "type": "string" 86 | } 87 | ] 88 | }, 89 | "log_file_name": { 90 | "title": "log_file_name", 91 | "description": "Requires jamJAR 2.0+ Name for the log file created in the dir specified in log_file_dir, .log is appended to the name. Defaults to - string: jamJAR", 92 | "property_order": 40, 93 | "anyOf": [ 94 | {"type": "null", "title": "Not Configured"}, 95 | { 96 | "title": "Configured", 97 | "type": "string" 98 | } 99 | ] 100 | }, 101 | "notifier_msg_pending": { 102 | "title": "notifier_msg_pending", 103 | "description": "The notification text to display when there are updates pending. Currently blocking apps or apps requiring a restart action will all trigger this notification. Defaults to - string: Logout to complete pending updates", 104 | "property_order": 45, 105 | "anyOf": [ 106 | {"type": "null", "title": "Not Configured"}, 107 | { 108 | "title": "Configured", 109 | "type": "string" 110 | } 111 | ] 112 | }, 113 | "log_file_dir": { 114 | "title": "log_file_dir", 115 | "description": "This is the directory in which the log file is created, this is created via the jamJAR postflight after every run. Prior to jamJAR 2.0: The log rotates at midnight & appends the date to the old file. As of jamJAR 2.0+: The log rotates every 10MB, not archiving older versions. Defaults to - string: /var/log", 116 | "property_order": 50, 117 | "anyOf": [ 118 | {"type": "null", "title": "Not Configured"}, 119 | { 120 | "title": "Configured", 121 | "type": "string" 122 | } 123 | ] 124 | }, 125 | "notifier_msg_nopending": { 126 | "title": "notifier_msg_nopending", 127 | "description": "The notification text to display when all pending updates have been installed. Defaults to - string: No updates pending", 128 | "property_order": 55, 129 | "anyOf": [ 130 | {"type": "null", "title": "Not Configured"}, 131 | { 132 | "title": "Configured", 133 | "type": "string" 134 | } 135 | ] 136 | }, 137 | "notifier_msg_osupdatespending": { 138 | "title": "notifier_msg_osupdatespending", 139 | "description": "Requires jamJAR 2.0+ The notification text to display when macOS updates are pending. This leverages Manual Apple Updates in Munki 5 Also requires that the datajar_notifier is set to True and is deployed to macOS clients and that Munki has InstallAppleSoftwareUpdates set to True Defaults to - string: macOS Updates available. Click here for more details", 140 | "property_order": 60, 141 | "anyOf": [ 142 | {"type": "null", "title": "Not Configured"}, 143 | { 144 | "title": "Configured", 145 | "type": "string" 146 | } 147 | ] 148 | }, 149 | "delete_secure_auth": { 150 | "title": "delete_secure_auth", 151 | "description": "Requires jamJAR 2.0+ If set to True, deletes the AdditionalHttpHeaders key in /private/var/root/Library/Preferences/ManagedInstalls.plist Defaults to - boolean: False", 152 | "property_order": 65, 153 | "anyOf": [ 154 | {"type": "null", "title": "Not Configured"}, 155 | { 156 | "title": "Configured", 157 | "type": "boolean" 158 | } 159 | ] 160 | }, 161 | "notifier_sender_id": { 162 | "title": "notifier_sender_id", 163 | "description": "The bundle ID of the application that sent the notification, this is only used for terminal-notifier, but seems that Jamf Pro's Management Action ignores this if sent. If this preference is not configured, com.jamfsoftware.selfservice is used.", 164 | "property_order": 70, 165 | "anyOf": [ 166 | {"type": "null", "title": "Not Configured"}, 167 | { 168 | "title": "Configured", 169 | "type": "string" 170 | } 171 | ] 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /script/jamJAR.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/munki/munki-python 2 | # encoding: utf-8 3 | # pylint: disable = invalid-name 4 | ''' 5 | Copyright (c) 2023, dataJAR Ltd. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither data JAR Ltd nor the names of its contributors may be used to 15 | endorse or promote products derived from this software without specific 16 | prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY DATA JAR LTD "AS IS" AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL DATA JAR LTD BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | SUPPORT FOR THIS PROGRAM 30 | 31 | This program is distributed "as is" by DATA JAR LTD. 32 | For more information or support, please utilise the following resources: 33 | 34 | https://macadmins.slack.com/messages/jamjar 35 | https://macadmins.slack.com/messages/datajar 36 | https://github.com/dataJAR/jamJAR 37 | ''' 38 | 39 | 40 | # Version 41 | __version__ = '2.1' 42 | 43 | 44 | # Standard imports 45 | import os 46 | import subprocess 47 | import sys 48 | # pylint: disable = import-error,no-name-in-module 49 | from CoreFoundation import (CFPreferencesCopyAppValue, 50 | CFPreferencesGetAppIntegerValue) 51 | # pylint: disable = import-error,no-name-in-module 52 | from SystemConfiguration import SCDynamicStoreCopyConsoleUser 53 | 54 | 55 | def main(): 56 | ''' 57 | Check parameters & update manifest as needed, send alert if ran via Self Service & 58 | up-to-date. Force install if wanted. 59 | ''' 60 | 61 | # Get items details of items in the manifest 62 | #jamjar_installs = process_managed_installs() 63 | #jamjar_uninstalls = process_managed_uninstalls() 64 | jamjar_installs, jamjar_uninstalls= process_manifest() 65 | 66 | # Processes parameters 67 | jamjar_installs, jamjar_uninstalls, yolo_mode = process_parameters( 68 | jamjar_installs, jamjar_uninstalls) 69 | 70 | # Get count of any warning items 71 | warning_count = process_warnings() 72 | 73 | # Get integer values of pending items in the ManagedInstalls & uk.co.dataJAR.jamJAR plists 74 | pending_count = CFPreferencesGetAppIntegerValue('PendingUpdateCount', 75 | 'ManagedInstalls', None)[0] 76 | 77 | # Update manifest 78 | update_client_manifest(jamjar_installs, jamjar_uninstalls) 79 | 80 | # Preflight policy text 81 | print(f"Preflight: Contains {len(jamjar_installs)} installs {jamjar_installs}, " 82 | f"{len(jamjar_uninstalls)} uninstalls {jamjar_uninstalls}, {pending_count} pending, " 83 | f"{warning_count} warnings") 84 | 85 | # Run managedsoftwareupdate --installonly 86 | if yolo_mode: 87 | print("WARNING: YOLO mode engaged") 88 | run_managedsoftwareupdate_yolo() 89 | else: 90 | run_managedsoftwareupdate_auto() 91 | 92 | # If running under Self Service, then USERNAME is None but we'll have a USER 93 | if (not os.environ.get('USERNAME') 94 | and os.environ.get('USER') 95 | and os.environ.get('USER') != 'root'): 96 | # Give feedback that items are uptodate 97 | process_uptodate() 98 | 99 | # Update counts after installs 100 | update_counts() 101 | 102 | 103 | # Other functions 104 | def process_managed_install_report(): 105 | ''' 106 | Processes ManagedInstallReport.plist 107 | ''' 108 | 109 | # Var declaration 110 | managed_install_report = {} 111 | 112 | # Generate path to ManagedInstallReport 113 | install_report_plist = f'{MANAGED_INSTALL_DIR}/ManagedInstallReport.plist' 114 | 115 | # If the path exists 116 | if os.path.exists(install_report_plist): 117 | # Read in the plist 118 | managed_install_report = FoundationPlist.readPlist(install_report_plist) 119 | 120 | # Return contents of ManagedInstallReport, if exists 121 | return managed_install_report 122 | 123 | 124 | def process_manifest(): 125 | ''' 126 | Read in manifest, returning managed_installs and managed_uninstalls. 127 | ''' 128 | 129 | # Var declaration 130 | client_manifest = {} 131 | jamjar_installs = [] 132 | jamjar_uninstalls = [] 133 | 134 | # If LocalOnlyManifest is declared, but does not exist exit. 135 | if os.path.exists(f'{MANAGED_INSTALL_DIR}/manifests/{MANIFEST}'): 136 | # If LocalOnlyManifest exists, try to read it 137 | try: 138 | client_manifest = FoundationPlist.readPlist( 139 | f'{MANAGED_INSTALL_DIR}/manifests/{MANIFEST}') 140 | except FoundationPlist.NSPropertyListSerializationException: 141 | print("ERROR: Cannot read f'{MANAGED_INSTALL_DIR}/manifests/{MANIFEST}") 142 | sys.exit(1) 143 | 144 | # If any items are in the managed_installs array 145 | if client_manifest.get('managed_installs'): 146 | # Add each item to the jamjar_installs list 147 | for managed_install in client_manifest.get('managed_installs'): 148 | jamjar_installs.append(managed_install) 149 | 150 | # If any items are in the managed_uninstalls array 151 | if client_manifest.get('managed_uninstalls'): 152 | # Add each item to the jamjar_uninstalls list 153 | for managed_uninstall in client_manifest.get('managed_uninstalls'): 154 | jamjar_uninstalls.append(managed_uninstall) 155 | 156 | # Return jamjar_installs and jamjar_installs 157 | return jamjar_installs, jamjar_uninstalls 158 | 159 | 160 | def process_parameters(jamjar_installs, jamjar_uninstalls): 161 | ''' 162 | Try & get parameters $4, $7, $6, $7, $8 & assign. 163 | ''' 164 | 165 | # Var declaration 166 | installs_to_add = None 167 | installs_to_remove = None 168 | uninstalls_to_add = None 169 | uninstalls_to_remove = None 170 | yolo_mode = None 171 | 172 | # If something has been passed to $4 173 | if sys.argv[4] != '': 174 | # Split at , 175 | installs_to_add = sys.argv[4] 176 | # Process to add to jamjar_installs 177 | jamjar_installs = process_parameter_4(installs_to_add, jamjar_installs) 178 | 179 | # If something has been passed to $5 180 | if sys.argv[5] != '': 181 | # Split at , 182 | installs_to_remove = sys.argv[5] 183 | # Process to add to jamjar_installs 184 | jamjar_installs = process_parameter_5(installs_to_remove, jamjar_installs) 185 | 186 | # If something has been passed to $6 187 | if sys.argv[6] != '': 188 | # Split at , 189 | uninstalls_to_add = sys.argv[6] 190 | # Process to add to jamjar_uninstalls 191 | jamjar_uninstalls = process_parameter_6(jamjar_uninstalls, uninstalls_to_add) 192 | 193 | # If something has been passed to $7 194 | if sys.argv[7] != '': 195 | # Split at , 196 | uninstalls_to_remove = sys.argv[7] 197 | # Process to add to jamjar_uninstalls 198 | jamjar_uninstalls = process_parameter_7(jamjar_uninstalls, uninstalls_to_remove) 199 | 200 | # Set yolo_mode, if ENGAGE passed to $8 201 | if sys.argv[8] == 'ENGAGE': 202 | yolo_mode = 'ENGAGE' 203 | 204 | # Check that we have some values passed to the parameters.. exit if not 205 | if (installs_to_add is None and installs_to_remove is None and uninstalls_to_add is None 206 | and uninstalls_to_remove is None and yolo_mode is None): 207 | print("Nothing assigned to $4, $7, $6, $7 or $8... exiting...") 208 | sys.exit(1) 209 | 210 | # Values for the processed variables 211 | return jamjar_installs, jamjar_uninstalls, yolo_mode 212 | 213 | 214 | def process_parameter_4(installs_to_add, jamjar_installs): 215 | ''' 216 | Processes items passed to $4. 217 | ''' 218 | 219 | # For each item in installs_to_add 220 | for install_to_add in installs_to_add.split(','): 221 | jamjar_installs.append(install_to_add) 222 | print(f"Adding {install_to_add} to installs") 223 | 224 | # Returns a list of managed_installs, with dupes removed 225 | return list(set(jamjar_installs)) 226 | 227 | 228 | def process_parameter_5(installs_to_remove, jamjar_installs): 229 | ''' 230 | Processes items passed to $5. 231 | ''' 232 | 233 | # For each item in installs_to_remove 234 | for install_to_remove in installs_to_remove.split(','): 235 | # Try to remove 236 | try: 237 | jamjar_installs.remove(install_to_remove) 238 | print(f"Removed {install_to_remove} from installs") 239 | except ValueError: 240 | print(f"{install_to_remove} not in installs") 241 | 242 | # Returns a list of managed_installs, with dupes removed 243 | return list(set(jamjar_installs)) 244 | 245 | 246 | def process_parameter_6(jamjar_uninstalls, uninstalls_to_add): 247 | ''' 248 | Processes items passed to $6. 249 | ''' 250 | 251 | # For each item in uninstalls_to_add 252 | for uninstall_to_add in uninstalls_to_add.split(','): 253 | jamjar_uninstalls.append(uninstall_to_add) 254 | print(f"Adding {uninstall_to_add} to uninstalls") 255 | 256 | # Returns a list of managed_uninstalls, with dupes removed 257 | return list(set(jamjar_uninstalls)) 258 | 259 | 260 | def process_parameter_7(jamjar_uninstalls, uninstalls_to_remove): 261 | ''' 262 | Processes items passed to $7. 263 | ''' 264 | 265 | # For each item in uninstalls_to_remove 266 | for uninstall_to_remove in uninstalls_to_remove.split(','): 267 | # Try to remove 268 | try: 269 | jamjar_uninstalls.remove(uninstall_to_remove) 270 | print(f"Removed {uninstall_to_remove} from uninstalls") 271 | except ValueError: 272 | print(f"{uninstall_to_remove} not in uninstalls") 273 | 274 | # Returns a list of managed_uninstalls, with dupes removed 275 | return list(set(jamjar_uninstalls)) 276 | 277 | 278 | def process_uptodate(): 279 | ''' 280 | Give feedback that items are up-to-date. 281 | ''' 282 | 283 | # Get the latest version of ManagedInstallReport (if exists) 284 | managed_install_report = process_managed_install_report() 285 | 286 | # If items have been installed 287 | if managed_install_report: 288 | # If an item has updated, & Munki doesn't have a newer item. 289 | # The item will be added to InstalledItems 290 | if managed_install_report.get('InstalledItems'): 291 | # Check each item 292 | for installed_item in managed_install_report.get('InstalledItems'): 293 | # Check the name against items in the ManagedInstalls array 294 | for managed_install in managed_install_report.get('ManagedInstalls'): 295 | # If we have a match, notify the user 296 | if managed_install['name'] == installed_item: 297 | send_installed_uptodate(managed_install['display_name']) 298 | 299 | 300 | def process_warnings(): 301 | ''' 302 | Return number of warnings. 303 | ''' 304 | 305 | # Get the latest version of ManagedInstallReport (if exists) 306 | managed_install_report = process_managed_install_report() 307 | 308 | # Return number of warning items 309 | return len(managed_install_report.get('Warnings', [])) 310 | 311 | 312 | def run_managedsoftwareupdate_auto(): 313 | ''' 314 | Run managedsoftwareupdate. Called with these flags it will show status over 315 | loginwindow, or if logged in postflight will do it's thing. 316 | ''' 317 | 318 | # Command to run 319 | cmd_args = ['/usr/local/munki/managedsoftwareupdate', '--auto', '-m'] 320 | 321 | # Run managedsoftwareupdate, in it's auto mode, logging if an issue is encountered 322 | try: 323 | subprocess.call(cmd_args, stdout=subprocess.DEVNULL) 324 | except subprocess.CalledProcessError as err_msg: 325 | print(f"ERROR: {cmd_args} failed with: ", err_msg) 326 | sys.exit(1) 327 | 328 | 329 | def run_managedsoftwareupdate_yolo(): 330 | ''' 331 | Run managedsoftwareupdate --checkonly then --installonly. 332 | ''' 333 | 334 | # Command to run 335 | cmd_args = ['/usr/local/munki/managedsoftwareupdate', '--checkonly'] 336 | 337 | # Check 1st to cache installs, logging if an issue is encountered 338 | try: 339 | subprocess.call(cmd_args, stdout=subprocess.DEVNULL) 340 | except subprocess.CalledProcessError as err_msg: 341 | print(f"ERROR: {cmd_args} failed with: ", err_msg) 342 | sys.exit(1) 343 | 344 | # Command to run 345 | cmd_args = ['/usr/local/munki/managedsoftwareupdate', '--installonly'] 346 | 347 | # Install all items, including pending, logging if an issue is encountered 348 | try: 349 | subprocess.call(cmd_args, stdout=subprocess.DEVNULL) 350 | except subprocess.CalledProcessError as err_msg: 351 | print(f"ERROR: {cmd_args} failed with: ", err_msg) 352 | sys.exit(1) 353 | 354 | 355 | def send_installed_uptodate(item_display_name): 356 | ''' 357 | Notify if item that install was requested for is up-to-date, check username again incase 358 | user logged out during execution. 359 | ''' 360 | 361 | # The username of the logged in user 362 | username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0] 363 | 364 | # If we have no value for the above, or we're at the loginwindow.. set username to None 365 | if username in ("", "loginwindow"): 366 | username = None 367 | 368 | # If we have the wanted notifier app installed, and we're logged in 369 | if os.path.exists(NOTIFIER_PATH) and username: 370 | # item_name - example: OracleJava8 371 | # item_display_name - example: Oracle Java 8 372 | # item_version - example: 1.8.111.14 373 | if DATAJAR_NOTIFIER: 374 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 375 | f'--messageaction "{NOTIFIER_SENDER_ID}" ' 376 | f'--message "{NOTIFIER_MSG_UPTODATE % (item_display_name)}" ' 377 | f'--title "{NOTIFIER_MSG_TITLE}" ' 378 | f'--type banner'] 379 | else: 380 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 381 | f'-sender "{NOTIFIER_SENDER_ID}" ' 382 | f'-message "{NOTIFIER_MSG_UPTODATE % (item_display_name)}" ' 383 | f'-title "{NOTIFIER_MSG_TITLE}"'] 384 | 385 | # Send notification 386 | subprocess.call(notifier_args, close_fds=True) 387 | 388 | 389 | def update_client_manifest(jamjar_installs, jamjar_uninstalls): 390 | ''' 391 | Update manifest, leaving only items that have not installed/uninstalled yet. 392 | ''' 393 | 394 | # var declaration 395 | updated_client_manifest = {} 396 | 397 | # Get installs 398 | updated_client_manifest['managed_installs'] = jamjar_installs 399 | 400 | # Get uninstalls 401 | updated_client_manifest['managed_uninstalls'] = jamjar_uninstalls 402 | 403 | # Write to plist 404 | FoundationPlist.writePlist(updated_client_manifest, 405 | f'{MANAGED_INSTALL_DIR}/manifests/{MANIFEST}') 406 | 407 | 408 | def update_counts(): 409 | ''' 410 | Update counts for policy log. 411 | ''' 412 | 413 | # Get items details of items in the manifest 414 | #jamjar_installs = process_managed_installs() 415 | #jamjar_uninstalls = process_managed_uninstalls() 416 | jamjar_installs, jamjar_uninstalls= process_manifest() 417 | 418 | # Get integer values of pending items in the ManagedInstalls & uk.co.dataJAR.jamJAR plists 419 | pending_count = CFPreferencesGetAppIntegerValue('PendingUpdateCount', 'ManagedInstalls', 420 | None)[0] 421 | 422 | # Get number of warning items 423 | warning_count = process_warnings() 424 | 425 | # Postflight policy text 426 | print(f"Postflight: Contains {len(jamjar_installs)} installs {jamjar_installs}, " 427 | f"{len(jamjar_uninstalls)} uninstalls {jamjar_uninstalls}, {pending_count} pending, " 428 | f"{warning_count} warnings") 429 | 430 | 431 | if __name__ == "__main__": 432 | 433 | # Make sure we're root 434 | if os.geteuid() != 0: 435 | print('ERROR: This script must be run as root') 436 | sys.exit(1) 437 | 438 | # Try to locate jamf binary 439 | if not os.path.exists('/usr/local/jamf/bin/jamf'): 440 | print('ERROR: Cannot find jamf binary') 441 | sys.exit(1) 442 | 443 | # Import FoundationPlist from munki, exit if errors 444 | sys.path.append("/usr/local/munki") 445 | try: 446 | from munkilib import FoundationPlist 447 | except ImportError: 448 | print('ERROR: Cannot import FoundationPlist') 449 | sys.exit(1) 450 | 451 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#datajar_notifier 452 | DATAJAR_NOTIFIER = CFPreferencesCopyAppValue('datajar_notifier', 'uk.co.dataJAR.jamJAR') 453 | if DATAJAR_NOTIFIER is None: 454 | DATAJAR_NOTIFIER = False 455 | 456 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_msg_title 457 | NOTIFIER_MSG_TITLE = CFPreferencesCopyAppValue('notifier_msg_title', 'uk.co.dataJAR.jamJAR') 458 | if NOTIFIER_MSG_TITLE is None: 459 | NOTIFIER_MSG_TITLE = 'jamJAR' 460 | 461 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_msg_uptodate 462 | NOTIFIER_MSG_UPTODATE = CFPreferencesCopyAppValue('notifier_msg_uptodate', 463 | 'uk.co.dataJAR.jamJAR') 464 | if NOTIFIER_MSG_UPTODATE is None: 465 | NOTIFIER_MSG_UPTODATE = 'Latest version of %s is installed.' 466 | 467 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_path 468 | NOTIFIER_PATH = CFPreferencesCopyAppValue('notifier_path', 'uk.co.dataJAR.jamJAR') 469 | if NOTIFIER_PATH is None: 470 | NOTIFIER_PATH = ('/Library/Application Support/JAMF/bin/Management ' 471 | 'Action.app/Contents/MacOS/Management Action') 472 | 473 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_sender_id 474 | NOTIFIER_SENDER_ID = CFPreferencesCopyAppValue('notifier_sender_id', 'uk.co.dataJAR.jamJAR') 475 | if NOTIFIER_SENDER_ID is None: 476 | NOTIFIER_SENDER_ID = 'com.jamfsoftware.selfservice' 477 | 478 | # Get location of the Managed Installs directory, exit if not found 479 | MANAGED_INSTALL_DIR = CFPreferencesCopyAppValue('ManagedInstallDir', 'ManagedInstalls') 480 | if MANAGED_INSTALL_DIR is None: 481 | print('ERROR: Cannot get Managed Installs directory...') 482 | sys.exit(1) 483 | 484 | # Make sure a LocalOnlyManifest is specified, exit if not declared 485 | MANIFEST = CFPreferencesCopyAppValue('LocalOnlyManifest', 'ManagedInstalls') 486 | if MANIFEST is None: 487 | print("ERROR: No LocalOnlyManifest declared...") 488 | sys.exit(1) 489 | 490 | 491 | # Gimme some main 492 | main() 493 | -------------------------------------------------------------------------------- /postflight/postflight: -------------------------------------------------------------------------------- 1 | #!/usr/local/munki/munki-python 2 | # encoding: utf-8 3 | ''' 4 | Copyright (c) 2023, dataJAR Ltd. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither data JAR Ltd nor the names of its contributors may be used to 14 | endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY DATA JAR LTD "AS IS" AND ANY 18 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL DATA JAR LTD BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | SUPPORT FOR THIS PROGRAM 29 | 30 | This program is distributed "as is" by DATA JAR LTD. 31 | For more information or support, please utilise the following resources: 32 | 33 | https://macadmins.slack.com/messages/jamjar 34 | https://macadmins.slack.com/messages/datajar 35 | https://github.com/dataJAR/jamJAR 36 | ''' 37 | 38 | 39 | # Version 40 | __version__ = '2.1' 41 | 42 | 43 | # Standard imports 44 | import logging 45 | import logging.handlers 46 | import os 47 | import platform 48 | import subprocess 49 | import sys 50 | import time 51 | # pylint: disable = import-error, no-name-in-module 52 | from CoreFoundation import (CFPreferencesAppSynchronize, 53 | CFPreferencesCopyAppValue, 54 | CFPreferencesGetAppIntegerValue, 55 | CFPreferencesSetValue, 56 | CFPreferencesSetAppValue, 57 | kCFPreferencesAnyHost, 58 | kCFPreferencesAnyUser) 59 | # pylint: disable = import-error, no-name-in-module 60 | from SystemConfiguration import SCDynamicStoreCopyConsoleUser 61 | 62 | 63 | def main(): 64 | ''' 65 | Main function. Check ManagedInstallReport to see if items installed, check against 66 | mainfest & alert if something installed or if we have installs pending. 67 | ''' 68 | 69 | # Var declaration 70 | jamjar_installs = [] 71 | jamjar_uninstalls = [] 72 | installed_items = 0 73 | removed_items = 0 74 | pending_apple_updates, pending_items = get_pending_items() 75 | 76 | # Get macOS version, borrowed from: 77 | # https://github.com/munki/munki/blob/master/code/client/munkilib/osutils.py#L47-L60 78 | os_version_tuple = tuple(map(int, platform.mac_ver()[0].split('.'))) 79 | 80 | # If macOS 10.13 or below then sum PENDING_UPDATE_COUNT & PENDING_APPLE_UPDATES_COUNT 81 | # Borrowed from: 82 | # https://github.com/munki/munki/blob/code/client/munkilib/appleupdates/au.py#L497-L518 83 | if os_version_tuple <= (10, 13): 84 | pending_items += pending_apple_updates 85 | else: 86 | # 10.14+, process apple updates now so inherent delay before other notifications etc 87 | process_pending_apple_updates(pending_apple_updates) 88 | 89 | # Get items in the LocalOnlyManifests managed_installs array 90 | if CLIENT_MANIFEST.get('managed_installs'): 91 | for install_item in CLIENT_MANIFEST.get('managed_installs'): 92 | jamjar_installs.append(install_item) 93 | 94 | # Get items in the LocalOnlyManifests managed_uninstalls array 95 | if CLIENT_MANIFEST.get('managed_uninstalls'): 96 | for uninstall_item in CLIENT_MANIFEST.get('managed_uninstalls'): 97 | jamjar_uninstalls.append(uninstall_item) 98 | 99 | # Process installs, uninstalls & pending items.. via their functions & log 100 | jamjar_installs, installed_items = process_installs(jamjar_installs, installed_items) 101 | jamjar_uninstalls, removed_items = process_uninstalls(jamjar_uninstalls, removed_items) 102 | process_pending(pending_items) 103 | warning_count = process_warnings() 104 | # add macOS updates to pending items count if macOS 10.14+ for log count 105 | if os_version_tuple >= (10, 14): 106 | pending_items += pending_apple_updates 107 | 108 | # Log status of run 109 | log_status(installed_items, removed_items, pending_items, warning_count) 110 | 111 | # Edit manifest leaving only items that have not installed yet 112 | if UPDATE_MANIFEST is True: 113 | update_client_manifest(jamjar_installs, jamjar_uninstalls) 114 | 115 | # Delete auth if set 116 | if DELETE_SECURE_AUTH is True: 117 | delete_auth() 118 | 119 | # If we have something to notify about, update inventory 120 | if installed_items != 0 or removed_items != 0: 121 | update_inventory() 122 | 123 | 124 | # Other functions 125 | def delete_auth(): 126 | ''' 127 | Delete the AdditionalHttpHeaders key in 128 | /private/var/root/Library/Preferences/ManagedInstalls.plist 129 | ''' 130 | 131 | # Delete the pref key 132 | CFPreferencesSetAppValue('AdditionalHttpHeaders', None, 'ManagedInstalls') 133 | 134 | # Force a sync to update 135 | CFPreferencesAppSynchronize('ManagedInstalls') 136 | 137 | 138 | def get_pending_items(): 139 | ''' 140 | Return pending items. 141 | ''' 142 | 143 | # Var declaration 144 | pending_apple_updates = 0 145 | pending_items = 0 146 | 147 | # Get the pending updates from InstallInfo.plist if exists, 148 | # therefore not including pending macOS updates. 149 | # Borrowed from: 150 | # https://github.com/munki/munki/blob/master/code/client/munkilib/installinfo.py#L104-L106 151 | install_info_plist = f'{MANAGED_INSTALL_DIR}/InstallInfo.plist' 152 | if os.path.exists(install_info_plist): 153 | install_info = {} 154 | install_info = FoundationPlist.readPlist(install_info_plist) 155 | # Process pending as per below, h/t @TSPARR: 156 | # https://github.com/munki/munki/blob/master/code/client/munkilib/installinfo.py#L104-L106 157 | pending_items = (len(install_info['managed_installs']) + 158 | len(install_info['removals'])) 159 | 160 | # Get the pending updates from AppleUpdates.plist if exists. 161 | apple_updates_plist = f'{MANAGED_INSTALL_DIR}/AppleUpdates.plist' 162 | if os.path.exists(apple_updates_plist): 163 | apple_updates = {} 164 | apple_updates = FoundationPlist.readPlist(apple_updates_plist) 165 | pending_apple_updates = len(apple_updates['AppleUpdates']) 166 | 167 | # Return the items macOS updates pending install, and the number of pending items 168 | return pending_apple_updates, pending_items 169 | 170 | 171 | def get_username(): 172 | ''' 173 | Gets the logged in users username, or returns None 174 | ''' 175 | 176 | # Get the username 177 | username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0] 178 | 179 | # If we have no value for the above, or we're at the loginwindow.. set username to None 180 | if username in ("", "loginwindow"): 181 | username = None 182 | 183 | # Return the logged in users username, or None 184 | return username 185 | 186 | 187 | def log_status(installed_items, removed_items, pending_items, warning_count): 188 | ''' 189 | It's big it's heavy it's wood... 190 | ''' 191 | 192 | # Log this runs status 193 | logging.info('%s installed, %s removed, %s pending, %s warnings', 194 | installed_items, removed_items, pending_items, warning_count) 195 | 196 | 197 | def process_installs(jamjar_installs, installed_items): 198 | ''' 199 | Process installs. 200 | ''' 201 | 202 | # Check to make sure that we have installs pending, notify if installed & remove 203 | # from jamjar_installs, & increment count 204 | if jamjar_installs: 205 | # If items have been installed 206 | if MANAGED_INSTALL_REPORT.get('InstallResults'): 207 | # For each item in the MANAGED_INSTALL_REPORT's InstallResults array 208 | for installed_item in MANAGED_INSTALL_REPORT.get('InstallResults'): 209 | # If the item exists in jamjar and was successfully installed 210 | if installed_item['status'] == 0 and installed_item['name'] in jamjar_installs: 211 | # Notify installed 212 | send_installed_notification(installed_item['display_name'], 213 | installed_item['version']) 214 | # Remove from jamjar_installs 215 | jamjar_installs.remove(installed_item['name']) 216 | # Update count 217 | installed_items += 1 218 | 219 | # If Munki doesn't have a newer item, it can be found in InstalledItems. 220 | # Remove from jamjar_installs but do not notify 221 | if MANAGED_INSTALL_REPORT.get('InstalledItems'): 222 | # For each item in the MANAGED_INSTALL_REPORT's InstallItems array 223 | for some_item in MANAGED_INSTALL_REPORT.get('InstalledItems'): 224 | # If the item exists in jamjar_installs 225 | if some_item in jamjar_installs: 226 | # Remove from jamjar_installs 227 | jamjar_installs.remove(some_item) 228 | 229 | # Return the items pending install, and the number of items installed 230 | return (jamjar_installs, installed_items) 231 | 232 | 233 | def process_pending_apple_updates(pending_apple_updates): 234 | ''' 235 | Process pending Apple updates for macOS 10.14+ only. 236 | ''' 237 | 238 | # The username of the logged in user 239 | username = get_username() 240 | 241 | # Get integer values of pending items in the uk.co.dataJAR.jamJAR plist 242 | last_apple_pending_count = CFPreferencesGetAppIntegerValue('apple_update_pending_count', 243 | 'uk.co.dataJAR.jamJAR', None)[0] 244 | 245 | # Get any unattended apple updates from ManagedInstalls 246 | unattended_apple_updates = CFPreferencesCopyAppValue('UnattendedAppleUpdates', 247 | 'ManagedInstalls') 248 | 249 | # Only process if UnattendedAppleUpdates is set to true, we're logged 250 | # and using the dataJAR notifier 251 | if DATAJAR_NOTIFIER and unattended_apple_updates and username: 252 | # If we have more no pending updates & did last time, tidy up 253 | if (pending_apple_updates == 0 and last_apple_pending_count > 0): 254 | send_pending_apple_update_notification(True) 255 | # If we have more pending updates than last run, post notification 256 | elif pending_apple_updates > last_apple_pending_count: 257 | send_pending_apple_update_notification(False) 258 | # If we're to notify whenever updates are pending 259 | elif pending_apple_updates > 0 and DATAJAR_NOTIFIER_ALWAYS_PENDING_NOTIFY: 260 | send_pending_apple_update_notification(False) 261 | 262 | # Update pending_count in /Library/Preferences/uk.co.dataJAR.jamJAR.plist 263 | CFPreferencesSetValue('apple_update_pending_count', pending_apple_updates, 264 | 'uk.co.dataJAR.jamJAR', kCFPreferencesAnyUser, 265 | kCFPreferencesAnyHost) 266 | 267 | 268 | def process_pending(pending_items): 269 | ''' 270 | Process pending. 271 | ''' 272 | 273 | # The username of the logged in user 274 | username = get_username() 275 | 276 | # Get the count of pending items from last run 277 | last_pending_count = CFPreferencesGetAppIntegerValue('pending_count', 278 | 'uk.co.dataJAR.jamJAR', None)[0] 279 | 280 | # If we have more pending updates than last run 281 | if pending_items > last_pending_count: 282 | # Post notification, don't clear prior 283 | send_pending_notification(False) 284 | # Create /private/tmp/com.googlecode.munki.installatlogout if missing, to trigger 285 | # munki t install when at the loginwindow 286 | if not os.path.exists('/private/tmp/com.googlecode.munki.installatlogout'): 287 | with open('/private/tmp/com.googlecode.munki.installatlogout', 'w', 288 | encoding='utf-8') as trigger_file: 289 | trigger_file.close() 290 | # If we have more no pending updates & did last time, tidy up 291 | elif pending_items == 0 and last_pending_count > pending_items: 292 | # Wait befor next steps 293 | time.sleep(10) 294 | # Post notification, clearing prior 295 | send_pending_notification(True) 296 | # Delete /private/tmp/com.googlecode.munki.installatlogout, if exists 297 | if os.path.exists('/private/tmp/com.googlecode.munki.installatlogout'): 298 | os.unlink('/private/tmp/com.googlecode.munki.installatlogout') 299 | # If we have more no pending updates, tidy up 300 | elif pending_items == 0: 301 | # Delete /private/tmp/com.googlecode.munki.installatlogout, if exists 302 | if os.path.exists('/private/tmp/com.googlecode.munki.installatlogout'): 303 | os.unlink('/private/tmp/com.googlecode.munki.installatlogout') 304 | # If we have pending updates, we're to notify whenever there is anything pending & 305 | # are using the dataJAR Notifier and we're logged in, then post a notification 306 | elif (pending_items > 0 and DATAJAR_NOTIFIER and username and 307 | DATAJAR_NOTIFIER_ALWAYS_PENDING_NOTIFY): 308 | # Post notification, don't clear prior 309 | send_pending_notification(False) 310 | # Create /private/tmp/com.googlecode.munki.installatlogout if missing, to trigger 311 | # munki t install when at the loginwindow 312 | if not os.path.exists('/private/tmp/com.googlecode.munki.installatlogout'): 313 | with open('/private/tmp/com.googlecode.munki.installatlogout', 'w', 314 | encoding='utf-8') as trigger_file: 315 | trigger_file.close() 316 | # If we're to only notify on pending when more items are pending that prior 317 | elif (pending_items > last_pending_count and not DATAJAR_NOTIFIER_ALWAYS_PENDING_NOTIFY): 318 | # Post notification, don't clear prior 319 | send_pending_notification(False) 320 | # Create /private/tmp/com.googlecode.munki.installatlogout if missing, to trigger 321 | # munki t install when at the loginwindow 322 | if not os.path.exists('/private/tmp/com.googlecode.munki.installatlogout'): 323 | with open('/private/tmp/com.googlecode.munki.installatlogout', 'w', 324 | encoding='utf-8') as trigger_file: 325 | trigger_file.close() 326 | # Create munki trigger file as we have pending items 327 | else: 328 | # If we're logged in 329 | if username: 330 | # Create /private/tmp/com.googlecode.munki.installatlogout if missing, to trigger 331 | # munki t install when at the loginwindow 332 | if not os.path.exists('/private/tmp/com.googlecode.munki.installatlogout'): 333 | with open('/private/tmp/com.googlecode.munki.installatlogout', 'w', 334 | encoding='utf-8') as trigger_file: 335 | trigger_file.close() 336 | 337 | # Update pending_count in /Library/Preferences/uk.co.dataJAR.jamJAR.plist 338 | CFPreferencesSetValue('pending_count', pending_items, 'uk.co.dataJAR.jamJAR', 339 | kCFPreferencesAnyUser, kCFPreferencesAnyHost) 340 | 341 | 342 | def process_uninstalls(jamjar_uninstalls, removed_items): 343 | ''' 344 | Process uninstalls. 345 | ''' 346 | 347 | # Check to make sure that we have uninstalls pending, if items uninstalled remove 348 | # from jamjar_uninstalls & increment removals 349 | if jamjar_uninstalls: 350 | if MANAGED_INSTALL_REPORT.get('RemovalResults'): 351 | for item in MANAGED_INSTALL_REPORT.get('RemovalResults'): 352 | if item['status'] == 0: 353 | if item['name'] in jamjar_uninstalls: 354 | jamjar_uninstalls.remove(item['name']) 355 | removed_items += 1 356 | # If an item has otherwise been removed 357 | if MANAGED_INSTALL_REPORT.get('RemovedItems'): 358 | for item in MANAGED_INSTALL_REPORT.get('RemovedItems'): 359 | if item in jamjar_uninstalls: 360 | jamjar_uninstalls.remove(item) 361 | removed_items += 1 362 | 363 | # Return the items pending uninstall, and the number of items uninstalled 364 | return (jamjar_uninstalls, removed_items) 365 | 366 | 367 | def process_warnings(): 368 | ''' 369 | Return number of warnings. 370 | ''' 371 | 372 | # Return number of warning items 373 | return len(MANAGED_INSTALL_REPORT.get('Warnings', [])) 374 | 375 | 376 | def send_installed_notification(item_display_name, item_version): 377 | ''' 378 | Check if the defined notifier app exists, & some is logged in 379 | before trying to send a notification. Only sent when installed. 380 | ''' 381 | 382 | # The username of the logged in user 383 | username = get_username() 384 | 385 | # If we have the wanted notifier app installed, and we're logged in 386 | if os.path.exists(NOTIFIER_PATH) and username: 387 | # item_name - example: OracleJava8 388 | # item_display_name - example: Oracle Java 8 389 | # item_version - example: 1.8.111.14 390 | if DATAJAR_NOTIFIER and sys.argv[1] != 'manualcheck': 391 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 392 | f'--messageaction "{NOTIFIER_SENDER_ID}" ' 393 | f'--message "{NOTIFIER_MSG_INSTALLED % (item_display_name, item_version.strip())}" ' 394 | f'--title "{NOTIFIER_MSG_TITLE}" ' 395 | f'--type banner'] 396 | else: 397 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 398 | f'-sender "{NOTIFIER_SENDER_ID}" ' 399 | f'-message "{NOTIFIER_MSG_INSTALLED % (item_display_name,item_version.strip())}" ' 400 | f'-title "{NOTIFIER_MSG_TITLE}"'] 401 | 402 | # Send notification 403 | subprocess.call(notifier_args, close_fds=True) 404 | 405 | 406 | def send_pending_apple_update_notification(clear_prior_msg): 407 | ''' 408 | Check if the defined notifier app exists, & some is logged in 409 | before trying to send a notification. Only sent when something pending 410 | ''' 411 | 412 | # Get the username 413 | username = get_username() 414 | 415 | # If using the dataJAR notifier and someone is logged in 416 | if os.path.exists(NOTIFIER_PATH) and username: 417 | # item_name - example: OracleJava8 418 | # item_display_name - example: Oracle Java 8 419 | # item_version - example: 1.8.111.14 420 | 421 | # If using the dataJAR notifier 422 | if DATAJAR_NOTIFIER: 423 | # If we're to clear the prior message 424 | if clear_prior_msg: 425 | # Build the notifier_args array to clear 426 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 427 | f'--messageaction "{NOTIFIER_SENDER_ID}" ' 428 | f'--message "{NOTIFIER_MSG_OSUPDATESPENDING}" ' 429 | f'--title "{NOTIFIER_MSG_TITLE}" ' 430 | '--type alert ' 431 | '--remove prior'] 432 | # Clear prior message 433 | subprocess.call(notifier_args, close_fds=True) 434 | else: 435 | # Display the macOS updates pending items notification 436 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 437 | f'--messageaction "{NOTIFIER_SENDER_ID}" ' 438 | f'--message "{NOTIFIER_MSG_OSUPDATESPENDING}" ' 439 | f'--title "{NOTIFIER_MSG_TITLE}" ' 440 | '--type alert '] 441 | # If not clearing, just post pending 442 | else: 443 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 444 | f'-sender "{NOTIFIER_SENDER_ID}" ' 445 | f'-message "{NOTIFIER_MSG_OSUPDATESPENDING}" ' 446 | f'-title "{NOTIFIER_MSG_TITLE}"'] 447 | # Send notification 448 | subprocess.call(notifier_args, close_fds=True) 449 | 450 | 451 | def send_pending_notification(clear_prior_msg): 452 | ''' 453 | Check if the defined notifier app exists, & some is logged in 454 | before trying to send a notification. Only sent when something pending. 455 | ''' 456 | 457 | # Get the username 458 | username = get_username() 459 | 460 | # If the wanted notifier app exists, and we're logged in as a user 461 | if os.path.exists(NOTIFIER_PATH) and username: 462 | # item_name - example: OracleJava8 463 | # item_display_name - example: Oracle Java 8 464 | # item_version - example: 1.8.111.14 465 | 466 | # If using the dataJAR notifier 467 | if DATAJAR_NOTIFIER: 468 | # If we're to clear the prior message 469 | if clear_prior_msg: 470 | # Build the notifier_args array to clear 471 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 472 | f'--messageaction "{NOTIFIER_SENDER_ID}" ' 473 | f'--message "{NOTIFIER_MSG_PENDING}" ' 474 | f'--title "{NOTIFIER_MSG_TITLE}" ' 475 | f'--messagebutton "{DATAJAR_NOTIFIER_LOGOUT_BUTTON}" ' 476 | '--messagebuttonaction logout ' 477 | '--type alert ' 478 | '--remove prior'] 479 | # Clear prior message 480 | subprocess.call(notifier_args, close_fds=True) 481 | # Display the no pending items notification 482 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 483 | f'--messageaction "{NOTIFIER_SENDER_ID}" ' 484 | f'--message "{NOTIFIER_MSG_NOPENDING}" ' 485 | f'--title "{NOTIFIER_MSG_TITLE}" ' 486 | '--type banner '] 487 | # If not clearing, just post pending 488 | else: 489 | # Build the notifier_args array to notify of pending 490 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 491 | f'--messageaction "{NOTIFIER_SENDER_ID}" ' 492 | f'--message "{NOTIFIER_MSG_PENDING}" ' 493 | f'--title "{NOTIFIER_MSG_TITLE}" ' 494 | f'--messagebutton "{DATAJAR_NOTIFIER_LOGOUT_BUTTON}" ' 495 | '--messagebuttonaction logout ' 496 | '--type alert '] 497 | # If not using the dataJAR Notifier 498 | else: 499 | notifier_args = ['/usr/bin/su', '-l', username, '-c', f'"{NOTIFIER_PATH}" ' 500 | f'-sender "{NOTIFIER_SENDER_ID}" ' 501 | f'-message "{NOTIFIER_MSG_PENDING}" ' 502 | f'-title "{NOTIFIER_MSG_TITLE}"'] 503 | 504 | # Send notification 505 | subprocess.call(notifier_args, close_fds=True) 506 | 507 | 508 | def update_client_manifest(jamjar_installs, jamjar_uninstalls): 509 | ''' 510 | Update manifest, leaving only items that have not installed/uninstalled yet. 511 | ''' 512 | 513 | # var declaration 514 | updated_client_manifest = {} 515 | 516 | # Get installs 517 | updated_client_manifest['managed_installs'] = jamjar_installs 518 | 519 | # Get uninstalls 520 | updated_client_manifest['managed_uninstalls'] = jamjar_uninstalls 521 | 522 | # Write to plist 523 | FoundationPlist.writePlist(updated_client_manifest, 524 | f'{MANAGED_INSTALL_DIR}/manifests/{MANIFEST}') 525 | 526 | 527 | def update_inventory(): 528 | ''' 529 | If we have something to notify about, are not in Munki mode & 530 | have a jamf binary, update inventory. 531 | ''' 532 | 533 | # Command to run 534 | cmd_args = ['/usr/local/jamf/bin/jamf', 'recon'] 535 | 536 | # Run command 537 | subprocess.call(cmd_args, stdout=subprocess.DEVNULL) 538 | 539 | 540 | if __name__ == "__main__": 541 | 542 | # Make sure we're root 543 | if os.geteuid() != 0: 544 | print('ERROR: This script must be run as root') 545 | sys.exit(1) 546 | 547 | # Try to locate jamf binary 548 | if not os.path.exists('/usr/local/jamf/bin/jamf'): 549 | logging.error('Cannot find jamf binary') 550 | sys.exit(1) 551 | 552 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#log_file_dir 553 | LOG_FILE_DIR = CFPreferencesCopyAppValue('log_file_dir', 'uk.co.dataJAR.jamJAR') 554 | if LOG_FILE_DIR is None: 555 | LOG_FILE_DIR = '/var/log/' 556 | 557 | # Create LOG_FILE_DIR if doesn't exist 558 | if not os.path.exists(LOG_FILE_DIR): 559 | os.makedirs(LOG_FILE_DIR) 560 | 561 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#log_file_name 562 | LOG_FILE_NAME = CFPreferencesCopyAppValue('log_file_name', 'uk.co.dataJAR.jamJAR') 563 | if LOG_FILE_NAME is None: 564 | LOG_FILE_NAME = 'jamJAR' 565 | 566 | # Configure logging 567 | LOGGER = logging.getLogger() 568 | LOGGER.setLevel(logging.INFO) 569 | HANDLER = logging.handlers.RotatingFileHandler(os.path.join(LOG_FILE_DIR, 570 | LOG_FILE_NAME + '.log'), 571 | mode='a', maxBytes=10000000, backupCount=1) 572 | FORMATTER = logging.Formatter('%(asctime)s %(levelname)s %(message)s', 573 | datefmt='%Y-%m-%d %H:%M:%S') 574 | HANDLER.setFormatter(FORMATTER) 575 | LOGGER.addHandler(HANDLER) 576 | 577 | # Import FoundationPlist from munki, exit if errors 578 | sys.path.append("/usr/local/munki") 579 | try: 580 | from munkilib import FoundationPlist 581 | except ImportError: 582 | logging.error('Cannot import FoundationPlist') 583 | sys.exit(1) 584 | 585 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#datajar_notifier 586 | DATAJAR_NOTIFIER = CFPreferencesCopyAppValue('datajar_notifier', 'uk.co.dataJAR.jamJAR') 587 | if DATAJAR_NOTIFIER is None: 588 | DATAJAR_NOTIFIER = False 589 | 590 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#datajar_notifier_always_pending_notify 591 | DATAJAR_NOTIFIER_ALWAYS_PENDING_NOTIFY = CFPreferencesCopyAppValue( 592 | 'datajar_notifier_always_pending_notify', 593 | 'uk.co.dataJAR.jamJAR') 594 | if DATAJAR_NOTIFIER_ALWAYS_PENDING_NOTIFY is None: 595 | DATAJAR_NOTIFIER_ALWAYS_PENDING_NOTIFY = True 596 | 597 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#datajar_notifier_logout_button 598 | DATAJAR_NOTIFIER_LOGOUT_BUTTON = CFPreferencesCopyAppValue('datajar_notifier_logout_button', 599 | 'uk.co.dataJAR.jamJAR') 600 | if DATAJAR_NOTIFIER_LOGOUT_BUTTON is None: 601 | DATAJAR_NOTIFIER_LOGOUT_BUTTON = 'Logout' 602 | 603 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#delete_secure_auth 604 | DELETE_SECURE_AUTH = CFPreferencesCopyAppValue('delete_secure_auth', 'uk.co.dataJAR.jamJAR') 605 | if DELETE_SECURE_AUTH is None: 606 | DELETE_SECURE_AUTH = False 607 | 608 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_msg_installed 609 | NOTIFIER_MSG_INSTALLED = CFPreferencesCopyAppValue('notifier_msg_installed', 610 | 'uk.co.dataJAR.jamJAR') 611 | if NOTIFIER_MSG_INSTALLED is None: 612 | NOTIFIER_MSG_INSTALLED = '%s %s has been installed' 613 | 614 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_msg_nopending 615 | NOTIFIER_MSG_NOPENDING = CFPreferencesCopyAppValue('notifier_msg_nopending', 616 | 'uk.co.dataJAR.jamJAR') 617 | if NOTIFIER_MSG_NOPENDING is None: 618 | NOTIFIER_MSG_NOPENDING = 'No updates pending' 619 | 620 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_msg_osupdatespending 621 | NOTIFIER_MSG_OSUPDATESPENDING = CFPreferencesCopyAppValue('notifier_msg_osupdatespending', 622 | 'uk.co.dataJAR.jamJAR') 623 | if NOTIFIER_MSG_OSUPDATESPENDING is None: 624 | NOTIFIER_MSG_OSUPDATESPENDING = 'macOS Updates available. Click here for more details' 625 | 626 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_msg_pending 627 | NOTIFIER_MSG_PENDING = CFPreferencesCopyAppValue('notifier_msg_pending', 'uk.co.dataJAR.jamJAR') 628 | if NOTIFIER_MSG_PENDING is None: 629 | NOTIFIER_MSG_PENDING = 'Logout to complete pending updates' 630 | 631 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_msg_title 632 | NOTIFIER_MSG_TITLE = CFPreferencesCopyAppValue('notifier_msg_title', 'uk.co.dataJAR.jamJAR') 633 | if NOTIFIER_MSG_TITLE is None: 634 | NOTIFIER_MSG_TITLE = 'jamJAR' 635 | 636 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_path 637 | NOTIFIER_PATH = CFPreferencesCopyAppValue('notifier_path', 'uk.co.dataJAR.jamJAR') 638 | if NOTIFIER_PATH is None: 639 | NOTIFIER_PATH = ('/Library/Application Support/JAMF/bin/Management ' 640 | 'Action.app/Contents/MacOS/Management Action') 641 | 642 | # https://github.com/dataJAR/jamJAR/wiki/jamJAR-Preferences#notifier_sender_id 643 | NOTIFIER_SENDER_ID = CFPreferencesCopyAppValue('notifier_sender_id', 'uk.co.dataJAR.jamJAR') 644 | if NOTIFIER_SENDER_ID is None: 645 | NOTIFIER_SENDER_ID = 'com.jamfsoftware.selfservice' 646 | 647 | # Get location of the Managed Installs directory, exit if not found 648 | MANAGED_INSTALL_DIR = CFPreferencesCopyAppValue('ManagedInstallDir', 'ManagedInstalls') 649 | if MANAGED_INSTALL_DIR is None: 650 | logging.error('Cannot get Managed Installs directory...') 651 | sys.exit(1) 652 | 653 | # Check if ManagedInstallReport exists 654 | INSTALL_REPORT_PLIST = f'{MANAGED_INSTALL_DIR}/ManagedInstallReport.plist' 655 | if not os.path.exists(INSTALL_REPORT_PLIST): 656 | logging.info('ManagedInstallReport is missing, nothing to process. Exiting...') 657 | sys.exit(0) 658 | else: 659 | MANAGED_INSTALL_REPORT = {} 660 | MANAGED_INSTALL_REPORT = FoundationPlist.readPlist(INSTALL_REPORT_PLIST) 661 | 662 | # Make sure a LocalOnlyManifest is specified, then grab the name 663 | MANIFEST = CFPreferencesCopyAppValue('LocalOnlyManifest', 'ManagedInstalls') 664 | 665 | # Var declaration 666 | CLIENT_MANIFEST = {} 667 | UPDATE_MANIFEST = True 668 | 669 | # If no LocalOnlyManifest, then look for CLIENT_MANIFEST. Try to read it. Error 670 | # out if cannot 671 | if MANIFEST is None: 672 | # Set manifest path 673 | MANIFEST = f'{MANAGED_INSTALL_DIR}/manifests/CLIENT_MANIFEST.plist' 674 | # Set to false 675 | UPDATE_MANIFEST = False 676 | # If the manifest is missing, exit 677 | if not os.path.exists(MANIFEST): 678 | logging.error('Cannot find any client manifests') 679 | sys.exit(1) 680 | # If it exists 681 | else: 682 | # Try to read the manifest 683 | try: 684 | CLIENT_MANIFEST = FoundationPlist.readPlist(MANIFEST) 685 | # If reading the manifest fails, exit 686 | except FoundationPlist.NSPropertyListSerializationException: 687 | logging.error('Cannot read any client manifests') 688 | sys.exit(1) 689 | # If LocalOnlyManifest is declared, but does not exist exit. 690 | elif MANIFEST is not None and not os.path.exists(f'{MANAGED_INSTALL_DIR}/manifests/{MANIFEST}'): 691 | logging.warning('LocalOnlyManifest (%s) declared, but is missing', MANIFEST) 692 | sys.exit(0) 693 | else: 694 | # If LocalOnlyManifest exists, try to read it 695 | try: 696 | CLIENT_MANIFEST = FoundationPlist.readPlist( 697 | f'{MANAGED_INSTALL_DIR}/manifests/{MANIFEST}') 698 | except FoundationPlist.NSPropertyListSerializationException: 699 | logging.error('Cannot read LocalOnlyManifest') 700 | sys.exit(1) 701 | 702 | # Gimme some main 703 | main() 704 | --------------------------------------------------------------------------------