├── .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 |
--------------------------------------------------------------------------------