├── Apache-Script-Runner ├── WebhookScript.sh └── script_runner.php ├── Email-on-Enroll-and-Un-Enroll ├── email_on_enroll_unenroll.py └── requirements.txt ├── HipChat-JSS-Upgrade-Notification ├── jss_upgraded_notification.py └── requirements.txt ├── HipChat-Patch-Notification ├── patch_notification.py └── requirements.txt ├── PHP-Webhook-Processing ├── JSSWebhook.php └── Usage.sample.php ├── README.md ├── Slack-REST-API-Change-Notification └── api_change_notification.rb └── images └── basic_webhook_integration_diagram.png /Apache-Script-Runner/WebhookScript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This example file only reads and prints out the contents of the saved XML from script_runner.php 3 | # Use this as a starting point for a full script to parse the XML and take action on the event 4 | 5 | # An optional path to output log entries as shown below 6 | log_path='/tmp/script_runner.log' 7 | 8 | # Read the contents of the temporary file with the XML data 9 | data=$(cat $1) 10 | 11 | echo "Running my script" >> $log_path 12 | if [ "$data" ]; then 13 | # Only execute this block if the $data has contents 14 | echo "Contents of body:" >> $log_path 15 | echo "$data" >> $log_path 16 | else 17 | # Execute this block if $data is empty 18 | echo "Body not found!" >> $log_path 19 | fi 20 | 21 | exit 0 22 | -------------------------------------------------------------------------------- /Apache-Script-Runner/script_runner.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Email-on-Enroll-and-Un-Enroll/email_on_enroll_unenroll.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from flask_mail import Mail, Message 3 | from threading import Thread 4 | import datetime 5 | import os 6 | 7 | JSS_ADDRESS = 'https://your.jss.org' 8 | DESTINATION_EMAIL = 'recipient-address@your.org' 9 | 10 | 11 | class Configuration(object): 12 | MAIL_SERVER = 'smtp.your.org' 13 | MAIL_PORT = 587 # SSL/TLS port 14 | MAIL_USE_TLS = True 15 | MAIL_USE_SSL = False # Disable for POODLE 16 | MAIL_USERNAME = 'sender@your.org' 17 | MAIL_PASSWORD = 'secret_password' 18 | MAIL_DEFAULT_SENDER = 'Your Notifications ' 19 | 20 | 21 | app = flask.Flask(__name__) 22 | app.config.from_object(Configuration) 23 | email = Mail(app) 24 | 25 | 26 | @app.route('/MobileDeviceEnrolled', methods=['POST']) 27 | def mobile_device_enrolled(): 28 | device = flask.request.get_json() 29 | if not device: 30 | return '', 400 31 | 32 | send_email(device['event'], enrolled=True) 33 | return '', 200 34 | 35 | 36 | @app.route('/MobileDeviceUnEnrolled', methods=['POST']) 37 | def mobile_device_unenrolled(): 38 | device = flask.request.get_json() 39 | if not device: 40 | return '', 400 41 | 42 | send_email(device['event'], enrolled=False) 43 | return '', 200 44 | 45 | 46 | def build_email_body(data): 47 | header = 'A mobile device has been enrolled:' if data['enrolled'] else 'A mobile device has been un-enrolled:' 48 | txt = '''{0}, 49 | 50 | Notification received: {1} 51 | URL to device: {2} 52 | 53 | Device name: {3} 54 | Device model: {4} 55 | Serial number: {5} 56 | '''.format(header, str(data['time']), data['url'], data['name'], data['model'], data['serial']) 57 | 58 | html = '''

{0}

59 | Notification received: {1} 60 |
URL to device: {2} 61 |
62 |
Device name: {3} 63 |
Device model: {4} 64 |
Serial number: {5} 65 | '''.format(header, str(data['time']), data['url'], data['name'], data['model'], data['serial']) 66 | 67 | return txt, html 68 | 69 | 70 | def send_async_email(msg): 71 | with app.app_context(): 72 | email.send(msg) 73 | 74 | 75 | def send_email(device_data, enrolled=True): 76 | subject = 'Mobile Device Enrolled' if enrolled else 'Mobile Device Un-Enrolled' 77 | email_data = { 78 | 'enrolled': enrolled, 79 | 'name': device_data['deviceName'], 80 | 'model': device_data['model'], 81 | 'serial': device_data['serialNumber'], 82 | 'url': os.path.join('{}/mobileDevices.html?id={}'.format(JSS_ADDRESS, device_data['jssID'])), 83 | 'time': datetime.datetime.utcnow() 84 | } 85 | txt, html = build_email_body(email_data) 86 | msg = Message( 87 | subject, 88 | recipients=[DESTINATION_EMAIL] 89 | ) 90 | msg.body = txt 91 | msg.html = html 92 | thr = Thread(target=send_async_email, args=[msg]) 93 | thr.start() 94 | 95 | 96 | if __name__ == '__main__': 97 | app.run('0.0.0.0', debug=True) 98 | -------------------------------------------------------------------------------- /Email-on-Enroll-and-Un-Enroll/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.11.1 2 | Flask-Mail==0.9.1 3 | -------------------------------------------------------------------------------- /HipChat-JSS-Upgrade-Notification/jss_upgraded_notification.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from flask.ext.sqlalchemy import SQLAlchemy 3 | import json 4 | import os 5 | import requests 6 | from distutils.version import LooseVersion 7 | import urllib 8 | 9 | HIPCHAT_API_KEY = '' 10 | HIPCHAT_ROOM_NAME = urllib.quote('') 11 | VERIFY_SSL = False 12 | 13 | 14 | class Configuration(object): 15 | APPLICATION_DIR = os.path.dirname(os.path.realpath(__file__)) 16 | DEBUG = True 17 | SQLALCHEMY_DATABASE_URI = 'sqlite:///{}/jss_upgrade.db'.format(APPLICATION_DIR) 18 | SQLALCHEMY_TRACK_MODIFICATIONS = False 19 | 20 | 21 | app = flask.Flask('jss-upgrade') 22 | app.config.from_object(Configuration) 23 | db = SQLAlchemy(app) 24 | 25 | 26 | # This object represents the JSS in the database 27 | class JSSRecord(db.Model): 28 | id = db.Column(db.Integer, primary_key=True) 29 | url = db.Column(db.String, index=True, unique=True, nullable=False) 30 | version = db.Column(db.String, nullable=False) 31 | 32 | def __repr__(self): 33 | return ''.format(self.url) 34 | 35 | 36 | # This is the route for the inbound webhook: http://localhost:5000/jss_upgraded 37 | @app.route('/jss_upgraded', methods=['POST']) 38 | def jss_upgraded(): 39 | startup_event = flask.request.get_json() 40 | if not startup_event and not startup_event['jssUrl']: 41 | return '', 400 42 | 43 | jss = jss_lookup(startup_event['jssUrl']) 44 | 45 | print('Getting JSS version for: {}'.format(jss.url)) 46 | startup_version = get_jss_version(jss.url) 47 | 48 | if LooseVersion(startup_version) > LooseVersion(jss.version): 49 | notify_hipchat(jss.version, startup_version, jss.url) 50 | update_jss_version(jss, startup_version) 51 | else: 52 | print("Reported version for JSS '{}' matches version on record".format(jss.url)) 53 | 54 | return '', 200 55 | 56 | 57 | # Find the JSS that sent the inbound webhook in the database and create it if it does not yet exists 58 | def jss_lookup(jss_url): 59 | result = JSSRecord.query.filter(JSSRecord.url == jss_url).first() 60 | if not result: 61 | print("Creating new JSS entry for: {}".format(jss_url)) 62 | try: 63 | new_jss = JSSRecord(url=jss_url, version=get_jss_version(jss_url)) 64 | db.session.add(new_jss) 65 | db.session.commit() 66 | except Exception as e: 67 | print("Unable to create new JSS record:\n{}".format(e)) 68 | raise 69 | 70 | return new_jss 71 | else: 72 | return result 73 | 74 | 75 | # This method scrapes the version from the login page 76 | def get_jss_version(jss_url): 77 | r = requests.get(jss_url, verify=VERIFY_SSL) 78 | for line in str(r.text).split('\n'): 79 | if line.startswith(' ').split('=')[-1] 81 | 82 | 83 | # If the JSS that has reported in is at a newer version save that to the database record 84 | def update_jss_version(jss, new_version): 85 | print("Updating JSS record '{}' with new version: {}".format(jss.url, new_version)) 86 | jss.version = new_version 87 | db.session.add(jss) 88 | db.session.commit() 89 | 90 | 91 | # Send a message into the chat room using the personal API token 92 | def notify_hipchat(previous_version, new_version, jss_url): 93 | message = "Your JSS has been upgraded from version {} to version {}".format(previous_version, new_version) 94 | d = { 95 | 'color': 'green', 96 | 'from': 'JSS Upgrade: {}'.format(jss_url), 97 | 'notify': True, 98 | 'message': message 99 | } 100 | 101 | print('Sending HipChat notification') 102 | r = requests.post( 103 | 'https://api.hipchat.com/v2/room/{}/notification?auth_token={}'.format(HIPCHAT_ROOM_NAME, HIPCHAT_API_KEY), 104 | headers={'Content-Type': 'application/json'}, 105 | data=json.dumps(d) 106 | ) 107 | if r.status_code != 204: 108 | print('There was an error communicating with HipChat: {}\m{}'.format(r.status_code, r.json())) 109 | 110 | 111 | if __name__ == '__main__': 112 | if not os.path.exists(Configuration.SQLALCHEMY_DATABASE_URI): 113 | # This will create the database file if it does not exist 114 | db.create_all() 115 | 116 | # Run the Flask app over port 5000 on the localhost 117 | app.run('0.0.0.0', debug=True) 118 | -------------------------------------------------------------------------------- /HipChat-JSS-Upgrade-Notification/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.11.1 2 | Flask-SQLAlchemy==2.1 3 | -------------------------------------------------------------------------------- /HipChat-Patch-Notification/patch_notification.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import datetime 3 | import errno 4 | import json 5 | import os 6 | import requests 7 | import shutil 8 | import subprocess 9 | import urllib 10 | 11 | HIPCHAT_API_KEY = '' 12 | HIPCHAT_ROOM_NAME = urllib.quote('') 13 | 14 | BASEDIR = os.path.abspath(os.path.dirname(__file__)) 15 | SOURCE_DIRECTORY = os.path.join(BASEDIR, 'SOURCES') 16 | PACKAGE_DIRECTORY = os.path.join(BASEDIR, 'PACKAGES') 17 | 18 | app = flask.Flask('patch-notification') 19 | 20 | 21 | # This is the route for the inbound webhook: 22 | # http://localhost:5000/patchupdate 23 | # http://localhost:5000/PatchUpdate 24 | # You can define multiple routes to send to one function 25 | @app.route('/patchupdate', methods=['POST']) 26 | @app.route('/PatchUpdate', methods=['POST']) 27 | def patch_update(): 28 | patch = flask.request.get_json() 29 | notify_new_patch_hipchat(patch['name'], patch['latestVersion'], patch['reportUrl']) 30 | 31 | if patch['name'] == 'Firefox': 32 | filename = download_firefox(patch['latestVersion']) 33 | if filename: 34 | notify_patch_downloaded_hipchat(patch['name'], filename) 35 | print('Download successful') 36 | package_name = package_firefox(filename, patch['latestVersion']) 37 | if package_name: 38 | notify_patch_packaged_hipchat(patch['name'], package_name) 39 | else: 40 | print('The package failed to build') 41 | else: 42 | print('The file failed to download') 43 | 44 | return '', 200 45 | 46 | 47 | # Send a message into the chat room that a new patch is available 48 | def notify_new_patch_hipchat(name, version, url): 49 | message = "

Your JSS has received a new Patch Update (Click here to view the report)

" \ 50 | "
    " \ 51 | "
  • Name: {}
  • " \ 52 | "
  • Version {}
  • " \ 53 | "

".format(url, name, version) 54 | 55 | data = { 56 | 'color': 'red', 57 | 'from': 'JSS New Patch Notification', 58 | 'notify': True, 59 | 'message_format': 'html', 60 | 'message': message 61 | } 62 | 63 | hipchat_room_notification(data) 64 | 65 | 66 | # Send a message into the chat room that the new version of Firefox has been downloaded 67 | def notify_patch_downloaded_hipchat(name, filename): 68 | message = "

'{}' has been downloaded to your SOURCES directory on the notification server:

" \ 69 | "{}".format(name, filename) 70 | 71 | data = { 72 | 'color': 'yellow', 73 | 'from': 'JSS Patch Downloaded', 74 | 'notify': True, 75 | 'message_format': 'html', 76 | 'message': message 77 | } 78 | 79 | hipchat_room_notification(data) 80 | 81 | 82 | # Send a message into the chat room that the Firefox package has been built 83 | def notify_patch_packaged_hipchat(name, filename): 84 | message = "

The package for '{}' has been built in your PACKAGES directory on the notification server:

" \ 85 | "{}".format(name, filename) 86 | 87 | data = { 88 | 'color': 'green', 89 | 'from': 'JSS Patch Package Ready', 90 | 'notify': True, 91 | 'message_format': 'html', 92 | 'message': message 93 | } 94 | 95 | hipchat_room_notification(data) 96 | 97 | 98 | # Send a message into the chat room using the personal API token 99 | def hipchat_room_notification(message): 100 | print('Sending HipChat notification') 101 | r = requests.post( 102 | 'https://api.hipchat.com/v2/room/{}/notification?auth_token={}'.format(HIPCHAT_ROOM_NAME, HIPCHAT_API_KEY), 103 | headers={'Content-Type': 'application/json'}, 104 | data=json.dumps(message) 105 | ) 106 | if r.status_code != 204: 107 | print('There was an error communicating with HipChat: {}\m{}'.format(r.status_code, r.json())) 108 | 109 | 110 | def get_firefox_download_url(): 111 | r = requests.get('https://www.mozilla.org/en-US/firefox/new/') 112 | return [u for u in [l for l in r.text.split() if 'https://download.mozilla.org' in l] if 'os=osx' in u][-1].split( 113 | 'data-direct-link=')[-1].replace('"', '') 114 | 115 | 116 | # This downloads the latest version of Firefox when executed 117 | def download_firefox(version): 118 | firefox_url = get_firefox_download_url() 119 | firefox_file = os.path.join(SOURCE_DIRECTORY, 'Firefox_{}_{}.dmg'.format(version, timestamp_now())) 120 | 121 | resp = requests.head(firefox_url, allow_redirects=True) 122 | urllib.urlretrieve(firefox_url, firefox_file) 123 | 124 | return firefox_file if os.stat(firefox_file).st_size == int(resp.headers['content-length']) else False 125 | 126 | 127 | # A basic copy function in Python 128 | def copy(src, dest): 129 | try: 130 | shutil.copytree(src, dest) 131 | except OSError as e: 132 | # If the error was caused because the source wasn't a directory 133 | if e.errno == errno.ENOTDIR: 134 | shutil.copy(src, dest) 135 | else: 136 | print('Directory not copied. Error: %s' % e) 137 | 138 | 139 | # This will convert the downloaded DMG of Firefox into an installable package 140 | def package_firefox(dmg_file, version): 141 | tmp_dir = '/tmp/firefox_package' 142 | mnt_dir = '/tmp/firefox_dmg' 143 | root_dir = os.path.join(tmp_dir, 'root') 144 | app_dir = os.path.join(root_dir, 'Applications') 145 | 146 | os.makedirs(tmp_dir) 147 | os.makedirs(app_dir, mode=0777) 148 | 149 | mnt_cmd = ['/usr/bin/hdiutil', 'attach', '-mountpoint', mnt_dir, '-quiet', '-nobrowse', dmg_file] 150 | print("Mounting the DMG: {}".format(dmg_file)) 151 | print("Command string: {}".format(' '.join(mnt_cmd))) 152 | subprocess.call(mnt_cmd) 153 | print("Copying '{}' to package root".format(os.path.join(mnt_dir, 'Firefox.app'))) 154 | copy(os.path.join(mnt_dir, 'Firefox.app'), os.path.join(app_dir, 'Firefox.app')) 155 | os.chown(app_dir, 0, 80) 156 | 157 | umnt_cmd = ['/usr/bin/hdiutil', 'detach', '-quiet', mnt_dir] 158 | print("Unmounting the DMG: {}".format(mnt_dir)) 159 | print("Command string: {}".format(' '.join(umnt_cmd))) 160 | subprocess.call(umnt_cmd) 161 | 162 | package_file = os.path.join(PACKAGE_DIRECTORY, 'Firefox_{}_{}.pkg'.format(version, timestamp_now())) 163 | pkg_cmd = ['/usr/bin/pkgbuild', 164 | '--root', root_dir, 165 | '--identifier', 'com.jamfsw.firefox', '--version', version, 166 | '--install-location', "/", 167 | package_file] 168 | print("Building the package: {}".format(package_file)) 169 | print("Command string: {}".format(' '.join(pkg_cmd))) 170 | subprocess.call(pkg_cmd) 171 | 172 | shutil.rmtree(tmp_dir) 173 | 174 | return package_file 175 | 176 | 177 | def timestamp_now(): 178 | """Returns a Unix UTC timestamp""" 179 | return int((datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)).total_seconds()) 180 | 181 | 182 | if __name__ == '__main__': 183 | # Run the Flask app over port 5000 on the localhost 184 | app.run('0.0.0.0', debug=True) 185 | -------------------------------------------------------------------------------- /HipChat-Patch-Notification/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.11.1 2 | requests==2.10.0 3 | -------------------------------------------------------------------------------- /PHP-Webhook-Processing/JSSWebhook.php: -------------------------------------------------------------------------------- 1 | evaluateCallbacks($Details); 18 | } 19 | 20 | /* 21 | * evaluateCallbacks will loop through all stored Callbacks and detect 22 | * if the current Hook will match all triggers of a Callback. 23 | * At first all Base infos are evaluated - maybe we have a ComputerCheckIn 24 | * Callback defined only. 25 | */ 26 | private function evaluateCallbacks(array $Details) 27 | { 28 | // Loop through the Callbacks 29 | foreach($this->Callbacks as $Callback) 30 | { 31 | // Loop through their Base Definitions 32 | foreach($Callback["Details"]["webhook"] as $key => $value) 33 | { 34 | // Check if the defined Variables match 35 | if($Details["webhook"][$key] != $value) 36 | { 37 | // If they don't, try the Next Callback 38 | continue 2; 39 | } 40 | 41 | } 42 | // Loop through their Detail/Event Definitions 43 | foreach($Callback["Details"]["event"] as $key => $value) 44 | { 45 | // Check if the defined Variables match 46 | if($Details["event"][$key] != $value) 47 | { 48 | // If they don't, try the Next Callback 49 | continue 2; 50 | } 51 | } 52 | // All Requirements fullfilled, time to execute the Callback. 53 | $this->runCallback($Callback, $Details); 54 | } 55 | 56 | } 57 | 58 | /* 59 | * registerCallback will Register a Callback based on Object/Method/Detail Combination 60 | * of course only if the object and method exists. 61 | */ 62 | public function registerCallback($Object, $Method, $HookDetails) 63 | { 64 | // Check if the Object and Method exists 65 | if(is_object($Object) && method_exists($Object, $Method)) { 66 | // If yes, add the Callback 67 | $this->Callbacks[] = [ 68 | "Object" => $Object, 69 | "Method" => $Method, 70 | "Details" => $HookDetails 71 | ]; 72 | } 73 | } 74 | 75 | /* 76 | * runCallback is the Method which will run the actual Callback. 77 | * After checking if the Object and Method still exists. 78 | */ 79 | private function runCallback($Callback, $Details) 80 | { 81 | // Check if the Object and Method exists 82 | if(is_object($Callback["Object"]) && method_exists($Callback["Object"], $Callback["Method"])) { 83 | // If yes, call the Callback. 84 | call_user_func([$Callback["Object"], $Callback["Method"]], $Details); 85 | } 86 | } 87 | } 88 | 89 | ?> -------------------------------------------------------------------------------- /PHP-Webhook-Processing/Usage.sample.php: -------------------------------------------------------------------------------- 1 | registerCallback($this, "test", [ 11 | "webhook" => [ 12 | ], 13 | "event" => [ 14 | ] 15 | ]); 16 | } 17 | public function test($Details) 18 | { 19 | var_dump($Details); 20 | } 21 | } 22 | new test(); 23 | $JSSWH->processHook('{ 24 | "webhook": { 25 | "id": 1, 26 | "name": "ComputerAdded Webhook", 27 | "webhookEvent": "ComputerAdded" 28 | }, 29 | "event": { 30 | "udid": "", 31 | "deviceName": "", 32 | "model": "", 33 | "macAddress": "", 34 | "alternateMacAddress": "", 35 | "serialNumber": "", 36 | "osVersion": "", 37 | "osBuild": "", 38 | "userDirectoryID": "-1", 39 | "username": "", 40 | "realName": "", 41 | "emailAddress": "", 42 | "phone": "", 43 | "position": "", 44 | "department": "", 45 | "building": "", 46 | "room": "", 47 | "jssID": 1 48 | } 49 | }'); 50 | 51 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example-JSS-Webhooks 2 | A collection of examples for using Webhooks with v9.93 of the JSS 3 | 4 | Webhooks are outbound HTTP POST requests from a JSS containing a JSON or XML payload with data related to the event that was triggered. A webhook integration would run on a separate server from the JSS and receive these incoming requests to take action on. 5 | 6 | This diagram is a basic overview of how these interactions work: 7 | 8 | ![Basic Webhoook Integration Diagram](/images/basic_webhook_integration_diagram.png) 9 | 10 | Activity in the JSS (such as device check-ins shown above) will routinely trigger events in the Events API. If you have configured a webhook for one of these events it will send that event as JSON or XML to the URL you have specified. 11 | 12 | At this point what your integration does with the data is up to you. 13 | 14 | In the diagram you can see that the integration may perform calls to third party or external services *(directory services such as LDAP or OpenDirectory, chat apps like HipChat and Slack, or project management tools like JIRA and Trello)* and read or write to them over their APIs based upon the event or criteria the event matches that you have coded. 15 | 16 | You can also have your integration take basic identifiers from the event *(e.g. event was triggered on an action of a computer or mobile device and contains the JSS ID / serial number / UUID)* and make return REST API calls to the JSS to modify the record or read in additional data before writing to another service *(e.g. real time integration to other management or inventory tools)*. 17 | 18 | 19 | See [The Unofficial JSS API Docs: Webhooks API](https://unofficial-jss-api-docs.atlassian.net/wiki/display/JRA/Webhooks+API) for a full reference of all webhook events in both JSON and XML format. 20 | 21 | # Email on Mobile Device Enroll and Un-Enroll 22 | 23 | This is a webhooks version of a JSS Events API example demonstrated at the [2012 JNUC](https://www.youtube.com/watch?v=QGxMJ1r8_Lg) with the addition of listening for both **MobileDeviceEnrolled** and **MobileDeviceUnEnrolled** events. 24 | 25 | When the event is received the integration parses the information for the mobile device and creates the email body in both plain text and HTML formats. 26 | 27 | This email notification method can be adapted to any event within webhooks. Additional logic can be added to handle sending the alert to different email recipients depending upon the user assigned to the device. Another enhancement would be to tie into a directory service to look up the user's manager and then set the manager's email as the recipient and other required parties as CCs. 28 | 29 | 30 | # Apache Script Runner 31 | 32 | This is a very basic example of executing a shell script on every inbound request using Apache and PHP5. 33 | 34 | In this basic example a POST sent to http://your.integration/script_runner.php will save the JSON or XML data to a temporary file, execute a script passing the filename as a parameter, the script reads the file and processes it (in this case the file contents are printed to a log file only) and then the temporary file is deleted. 35 | 36 | While not as robust as an integration written using a web framework (see the Flask examples below) this is a quick and easy solution to trigger scripts upon events. 37 | 38 | # HipChat JSS Upgrade Notification 39 | 40 | This app listens for the JSSStartup event and notifies a chat room in HipChat when the JSS is reporting a newer version than during the last recorded startup. 41 | 42 | A small database is used to track the version of the JSS. Some integrations you create will require local data stores to track changes over time. In this example I am using a library called SQLAlchemy for the interface to a SQL database. 43 | 44 | In order to determine if a notification needs to be sent, the integration is going back out to the JSS to read the login page to obtain the version. An integration is able to perform multiple conditional actions on an inbound webhook to determine whether or not the automation should trigger. These can be local or external factors that require the integration to communicate with other services (not just the JSS). 45 | 46 | # HipChat Patch Workflow Notification 47 | 48 | This app is an example workflow automation. The integration notifies a HipChat room on every inbound PatchSoftwareTitleUpdated event. When the inbound patch title is "Firefox" that triggers additional actions to download the latest available version, notify the room when the download is complete, and then create a package ready for deployment to clients. 49 | 50 | Consider similar workflows where an inbound event matching your criteria triggers automations with the integration or the integration triggers workflows in other services/apps. 51 | 52 | # Slack REST API Change Notification 53 | 54 | This tiny Ruby example uses the [Sinatra micro-framework](http://www.sinatrarb.com/) to create a simple webserver to handle WebHook calls from the JSS. In particular, it handles the "RestAPIOperation" event as JSON, but other handlers can be added easily. 55 | 56 | The JSON sent from the JSS is parsed for info about the API operation, ignoring any GET (read) operations, and then a message is built about the change that was made via the API. The message is then sent to a slack channel or user via the "slacktee" command, available from [https://github.com/course-hero/slacktee](https://github.com/course-hero/slacktee). The message could also easily be sent via email or logged to a file. 57 | 58 | To use it, just make the script executable, and run it. Then create a webhook in the JSS sending RestAPIOperation events as JSON to http://hostname.company.com:8000/rest_api_operation where hostname.company.com is the host where the script is running. 59 | 60 | See the code comments for more details. Note: To install sinatra, try `sudo gem install sinatra` 61 | 62 | # PHP Webhook Processing 63 | 64 | If you want to have one Endpoint for all you Webhooks this may be the right solution for you. You'll just point the JSON-Body to JSSWebHook->processHook($JSON) and all Callbacks you registered before for this Webhook will be executed. 65 | 66 | Please study the Code and the Usage.sample Code before you actually use this, it will be better explained inthere ;) 67 | 68 | Filtering based on all Data within the Webhook request itself is possible, so you could just listen to any webhook containing your computer "Charly's iMac". The less filters you define the more webhook events you get. 69 | 70 | This is free to use, edit, redistribute. But there may be bugs, you got the code, fix them yourself (and please report back). -------------------------------------------------------------------------------- /Slack-REST-API-Change-Notification/api_change_notification.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | # This example uses the Sinatra micro-web-framework (http://www.sinatrarb.com/) 4 | # to implement a simple web server for handling HTTP POST requests from the 5 | # JSS WebHooks API. To install Sinatra, try 'sudo gem install sinatra' 6 | # 7 | # This example handles only the "RestAPIOPeration" event sent as JSON to the 8 | # URL http://hostname.company.com:8000/rest_api_operation 9 | # 10 | # The handler for that URL parses the incoming JSON data about the event 11 | # and constructs a message about it, which is then sent to a Slack channel via 12 | # the "slacktee" command, available from https://github.com/course-hero/slacktee 13 | # 14 | # Be sure to set the SLACKTEE and SLACK_RECIPIENT constants as appropriate. 15 | # 16 | # Here's an example Slack message: 17 | # 18 | # Casper user 'jeauxbleaux' just used the JSS API to create the 19 | # JSS Static Computer Group named 'Foobar' (id 14) 20 | # 21 | # To make it do something else with the message, change the last line of the 22 | # `post '/rest_api_operation' do` block below. 23 | # 24 | # NOTE: For simplicity's sake, this example does NOT use a secure connection. 25 | # If you don't trust the network between your JSS and the server running this, 26 | # code (and you shouldn't) make sure to use a server with up-to-date SSL 27 | # capabilities and be sure to use them. It is beyond the scope of this example 28 | # to go into the many possibilities. 29 | # 30 | # To test this withouth a JSS, make this code executable, and run it. 31 | # then in another terminal window on the same machine, use this curl command 32 | # 33 | # curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST --data '{"webhook":{"id":16,"name":"RestAPIOperationWebhook","webhookEvent":"RestAPIOperation"},"event":{"operationSuccessful":true,"objectID":1,"objectName":"Foobar","objectTypeName":"Static Computer Group","authorizedUsername":"jeauxbleaux","restAPIOperationType":"POST"}}' http://localhost:8000/rest_api_operation 34 | # 35 | # 36 | 37 | require 'sinatra/base' 38 | require 'json' 39 | 40 | # Set up a Sinatra web server to handle our hooks. 41 | class WebHookHandler < Sinatra::Base 42 | 43 | ############ Constants ############ 44 | 45 | SLACKTEE = "/usr/local/bin/slacktee" 46 | 47 | SLACK_RECIPIENT = "#jss-notify" 48 | 49 | ############ Sinatra configuration ############ 50 | 51 | configure do 52 | set :bind, '0.0.0.0' 53 | set :port, 8000 54 | set :server, :webrick 55 | end # configure 56 | 57 | ############ Routes ############ 58 | # Each of these handles an HTTP POST request to the 59 | # desired URL 60 | 61 | # https://servername.company.com:8000/rest_api_operation 62 | post '/rest_api_operation' do 63 | 64 | # the body is an IO stream, so ensure its at the start. 65 | request.body.rewind 66 | 67 | # Parse the JSON and extract the event data 68 | posted_json = JSON.parse(request.body.read, symbolize_names: true) 69 | event = posted_json[:event] 70 | 71 | # API operation types are GET, PUT, POST, and DELETE 72 | # Ignore GET operations, we only care about changes. 73 | action = case event[:restAPIOperationType] 74 | when "GET" 75 | return 76 | when "PUT" 77 | "update" 78 | when "POST" 79 | "create" 80 | when "DELETE" 81 | "delete" 82 | end 83 | 84 | # Use the event data to compose a message 85 | message = "Casper user '#{event[:authorizedUsername]}' just used the JSS API to #{action} the JSS #{event[:objectTypeName]} named '#{event[:objectName]}' (id #{event[:objectID]})" 86 | 87 | # send the message to the SLACK_RECEIPIENT via SLACKTEE 88 | system "echo \"#{message}\" | '#{SLACKTEE}' -p -c '#{SLACK_RECIPIENT}'" 89 | 90 | end # post '/rest_api_operation' 91 | 92 | end # class 93 | 94 | # Run the server 95 | WebHookHandler.run! 96 | -------------------------------------------------------------------------------- /images/basic_webhook_integration_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brysontyrrell/Example-JSS-Webhooks/991ce59162342440e54a4daf7fd72562983b059f/images/basic_webhook_integration_diagram.png --------------------------------------------------------------------------------