Upgrade to Pro — share decks privately, control downloads, hide ads and more …

A Slack App to Simplify Mac Lookups and Troubleshooting

Jamf
November 13, 2019
150

A Slack App to Simplify Mac Lookups and Troubleshooting

Jamf

November 13, 2019
Tweet

Transcript

  1. © JAMF Software, LLC #JNUC2019 /jamf: A Slack app to

    simplify Mac lookups and troubleshooting The origin story The /jamf workflow How to build your own /jamf /mymac Gotchas and considerations
  2. #JNUC2019 SEEK Ltd SEEK Asia OCC Mundial Catho ( )

    &' *+,-./01 SEEK AP&A (Asia-Pacific & Americas)
  3. #JNUC2019 Our challenge How best to help level 1 and

    2 techs troubleshoot the Macs that they come across effectively and efficiently?
  4. #JNUC2019 6 It’s ‘busy’ and not intuitive for beginners UI

    does not translate well to iOS/mobile Poor iOS experience == tech required to carry their laptops for all desk visits
  5. #JNUC2019 Difficult to use Jamf console + Techs unlikely to

    use it for troubleshooting + Lost opportunity for proactive support = Tech & Mac user
  6. #JNUC2019 Initiated from Slack's message input field Invoked by entering

    /, followed by command name, e.g. /jamf Option to provide parameter values e.g. /jamf taniacomputer What is a Slack Slash Command?
  7. #JNUC2019 urlencoded payload sent to your app ⏳ Slack expects

    response within 3 seconds A Ephemeral response What is a Slack Slash Command?
  8. #JNUC2019 Clicking More Info returns a list of attributes and,

    if applicable, a health status for each.
  9. #JNUC2019 ✅ Easy to use - little training required ✅

    All important Mac attributes efficiently listed ✅ Quick assessment of Mac health
  10. #JNUC2019 Lambda Serverless code execution Natively supports Python, Java, Go,

    Powershell, Node.js, C#, and Ruby Runtime API available for additional languages
  11. #JNUC2019 API Gateway Service for creating, publishing, maintaining, monitoring, and

    securing REST and WebSocket APIs at any scale Allows your lambda function to receive Slacks HTTP POST request
  12. #JNUC2019 Lambda Tasks Respond to slack with statusCode 200 Make

    a Jamf Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction from user
  13. #JNUC2019 Respond to slack with statusCode 200 Make a Jamf

    Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction from user
  14. © JAMF Software, LLC #JNUC2019 Simple Notification Service Fully managed

    push messaging service SNS instance called a topic Allows first lambda function to ‘notify’ second lambda function that it has been invoked, providing it with the same payload information that it receives from Slack
  15. #JNUC2019 Respond to slack with statusCode 200 Make a Jamf

    Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction with user
  16. #JNUC2019 Respond to slack with statusCode 200 Make a Jamf

    Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction from user json
  17. #JNUC2019 Respond to slack with statusCode 200 Make a Jamf

    Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction from user json
  18. #JNUC2019 Respond to slack with statusCode 200 Make a Jamf

    Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction from user json
  19. #JNUC2019 Respond to slack with statusCode 200 Make a Jamf

    Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction from user json
  20. #JNUC2019 Respond to slack with statusCode 200 Make a Jamf

    Pro API Get request for search string Loop through results and format it for Slack, attaching a button for further interaction from user json
  21. #JNUC2019 Identity & Access Management IAM allows you to manage

    access to AWS Services securely Users, Groups, Roles Lambda functions require a role assigned to it that has the required lambda permissions
  22. #JNUC2019 Which Mac attributes? Primary user Hardware warranty Last check-in

    & recon times macOS version Security agents Are they installed, updated, and running? FileVault & SIP Status 32-bit apps and other Catalina considerations Core app versions Are they up to date?
  23. #JNUC2019 How do we display it? ⏭ Order of results

    C Efficiency and Sustainability ⚠ How to communicate client via emoji
  24. #JNUC2019 #!/bin/bash sip_status=$(csrutil status | grep "enabled") if [[ -z

    "$sip_status" ]] then summary="⚠ SIP (System Integrity Protection) is disabled" else summary="✅ SIP (System Integrity Protection) is enabled" fi echo "<result>$summary</result>
  25. #JNUC2019 #!/bin/bash sip_status=$(csrutil status | grep "enabled") if [[ -z

    "$sip_status" ]] then summary="⚠ SIP (System Integrity Protection) is disabled" else summary="✅ SIP (System Integrity Protection) is enabled" fi echo "<result>$summary</result>
  26. #JNUC2019 #!/bin/bash sip_status=$(csrutil status | grep "enabled") if [[ -z

    "$sip_status" ]] then summary="⚠ SIP (System Integrity Protection) is disabled" else summary="✅ SIP (System Integrity Protection) is enabled" fi echo "<result>$summary</result>
  27. #JNUC2019 group_accounts = computer[“groups_accounts"]["computer_group_memberships"] group_string = "" for group in

    group_accounts: if "⚠" in group: group = group.strip().replace("⚠","⚠ Member of *") group_string = group_string + group + "*\n"
  28. #JNUC2019 group_accounts = computer[“groups_accounts"]["computer_group_memberships"] group_string = "" for group in

    group_accounts: if "⚠" in group: group = group.strip().replace("⚠","⚠ Member of *") group_string = group_string + group + "*\n"
  29. #JNUC2019 group_accounts = computer[“groups_accounts"]["computer_group_memberships"] group_string = "" for group in

    group_accounts: if "⚠" in group: group = group.strip().replace("⚠","⚠ Member of *") group_string = group_string + group + "*\n"
  30. #JNUC2019 group_accounts = computer[“groups_accounts"]["computer_group_memberships"] group_string = "" for group in

    group_accounts: if "⚠" in group: group = group.strip().replace("⚠","⚠ Member of *") group_string = group_string + group + "*\n"
  31. #JNUC2019 ext_attributes = computer["extension_attributes"] ea_string = "" for ea in

    ext_attributes: if "slack" in ea_name: ea_name = ea["name"] ea_name = ea_name.replace("slack - ","") ea_value_formatted = ea["value"].replace(".",'\u034F\u002E') ea_value_formatted = ea_value_formatted.strip().replace("\n","\n>") ea_string = ea_string + " *" + ea_name + "*\n>" + ea_value_formatted
  32. #JNUC2019 ext_attributes = computer["extension_attributes"] ea_string = "" for ea in

    ext_attributes: if "slack" in ea_name: ea_name = ea["name"] ea_name = ea_name.replace("slack - ","") ea_value_formatted = ea["value"].replace(".",'\u034F\u002E') ea_value_formatted = ea_value_formatted.strip().replace("\n","\n>") ea_string = ea_string + " *" + ea_name + "*\n>" + ea_value_formatted
  33. #JNUC2019 ext_attributes = computer["extension_attributes"] ea_string = "" for ea in

    ext_attributes: if "slack" in ea_name: ea_name = ea["name"] ea_name = ea_name.replace("slack - ","") ea_value_formatted = ea["value"].replace(".",'\u034F\u002E') ea_value_formatted = ea_value_formatted.strip().replace("\n","\n>") ea_string = ea_string + " *" + ea_name + "*\n>" + ea_value_formatted
  34. #JNUC2019 ext_attributes = computer["extension_attributes"] ea_string = "" for ea in

    ext_attributes: if "slack" in ea_name: ea_name = ea["name"] ea_name = ea_name.replace("slack - ","") ea_value_formatted = ea["value"].replace(".",'\u034F\u002E') ea_value_formatted = ea_value_formatted.strip().replace("\n","\n>") ea_string = ea_string + " *" + ea_name + "*\n>" + ea_value_formatted
  35. #JNUC2019 ext_attributes = computer["extension_attributes"] ea_string = "" for ea in

    ext_attributes: if "slack" in ea_name: ea_name = ea["name"] ea_name = ea_name.replace("slack - ","") ea_value_formatted = ea["value"].replace(".",'\u034F\u002E') ea_value_formatted = ea_value_formatted.strip().replace("\n","\n>") ea_string = ea_string + " *" + ea_name + "*\n>" + ea_value_formatted
  36. #JNUC2019 import json import boto3 from urllib import parse as

    urlparse def lambda_handler(event, context): message = dict(urlparse.parse_qsl(event["body"])) search_string = message["text"] client = boto3.client('sns') trigger = client.publish( TargetArn=topicARN, Message=json.dumps({'default': json.dumps(message)}), MessageStructure='json')
  37. #JNUC2019 import json import boto3 from urllib import parse as

    urlparse def lambda_handler(event, context): message = dict(urlparse.parse_qsl(event["body"])) search_string = message["text"] client = boto3.client('sns') trigger = client.publish( TargetArn=topicARN, Message=json.dumps({'default': json.dumps(message)}), MessageStructure='json')
  38. #JNUC2019 import json import boto3 from urllib import parse as

    urlparse def lambda_handler(event, context): message = dict(urlparse.parse_qsl(event["body"])) search_string = message["text"] client = boto3.client('sns') trigger = client.publish( TargetArn=topicARN, Message=json.dumps({'default': json.dumps(message)}), MessageStructure='json')
  39. #JNUC2019 search_string = message["text"] slack_ready_search_string = search_string.replace("*","\u034F*") response_text = ":female-detective::skin-tone-3:

    looking up _" + slack_ready_search_string + "_..." response_code = 200 return { 'statusCode': response_code, 'body': response_text }
  40. #JNUC2019 search_string = message["text"] slack_ready_search_string = search_string.replace("*","\u034F*") response_text = ":female-detective::skin-tone-3:

    looking up _" + slack_ready_search_string + "_..." response_code = 200 return { 'statusCode': response_code, 'body': response_text } Combining Graphene Joiner
  41. #JNUC2019 search_string = message["text"] slack_ready_search_string = search_string.replace("*","\u034F*") response_text = ":female-detective::skin-tone-3:

    looking up _" + slack_ready_search_string + "_..." response_code = 200 return { 'statusCode': response_code, 'body': response_text }
  42. #JNUC2019 search_string = message["text"] slack_ready_search_string = search_string.replace("*","\u034F*") response_text = ":female-detective::skin-tone-3:

    looking up _" + slack_ready_search_string + "_..." response_code = 200 return { 'statusCode': response_code, 'body': response_text }
  43. #JNUC2019 import json def lambda_handler(event, context): data = event["Records"][0]["Sns"]["Message"] json_loaded

    = json.loads(data) response_url = json_loaded["response_url"] channel = json_loaded["channel_id"] search_string = json_loaded[“text"]
  44. #JNUC2019 import json def lambda_handler(event, context): data = event["Records"][0]["Sns"]["Message"] json_loaded

    = json.loads(data) response_url = json_loaded["response_url"] channel = json_loaded["channel_id"] search_string = json_loaded["text"]
  45. #JNUC2019 import json def lambda_handler(event, context): data = event["Records"][0]["Sns"]["Message"] json_loaded

    = json.loads(data) response_url = json_loaded["response_url"] channel = json_loaded["channel_id"] search_string = json_loaded["text"]
  46. #JNUC2019 import json def lambda_handler(event, context): data = event["Records"][0]["Sns"]["Message"] json_loaded

    = json.loads(data) response_url = json_loaded["response_url"] channel = json_loaded["channel_id"] search_string = json_loaded["text"]
  47. #JNUC2019 from botocore.vendored import requests headers = { 'Accept': 'application/json',

    'Content-type': 'application/json', } search_string = json_loaded["text"] api_url = "https://jamf_pro_url/JSSResource/computers/match/ {}".format(search_string) jamf_response = requests.get(api_url, headers=headers, auth=(jamf_username, jamf_pw))
  48. #JNUC2019 from botocore.vendored import requests headers = { 'Accept': 'application/json',

    'Content-type': 'application/json', } search_string = json_loaded["text"] api_url = "https://jamf_pro_url/JSSResource/computers/match/ {}".format(search_string) jamf_response = requests.get(api_url, headers=headers, auth=(jamf_username, jamf_pw))
  49. #JNUC2019 data = jamf_response.json() computers = data["computers"] total_results = len(computers)

    attachments = [] for computer in computers: name = computer["name"] name_string = “:computer: *" + name + "*\n" computer_summary = name_string attachment = { 'callback_id': “id search", 'text': computer_summary, 'actions' : [{ 'name': 'moreinfo',
  50. #JNUC2019 data = jamf_response.json() computers = data["computers"] total_results = len(computers)

    attachments = [] for computer in computers: name = computer["name"] name_string = “:computer: *" + name + "*\n" computer_summary = name_string attachment = { 'callback_id': “id search", 'text': computer_summary, 'actions' : [{ 'name': 'moreinfo',
  51. #JNUC2019 data = jamf_response.json() computers = data["computers"] total_results = len(computers)

    attachments = [] for computer in computers: name = computer["name"] name_string = “:computer: *" + name + "*\n" computer_summary = name_string attachment = { 'callback_id': “id search", 'text': computer_summary, 'actions' : [{ 'name': 'moreinfo',
  52. #JNUC2019 data = jamf_response.json() computers = data["computers"] total_results = len(computers)

    attachments = [] for computer in computers: name = computer["name"] name_string = “:computer: *" + name + "*\n" computer_summary = name_string attachment = { 'callback_id': “id search", 'text': computer_summary, 'actions' : [{ 'name': 'moreinfo',
  53. #JNUC2019 for computer in computers: name = computer["name"] name_string =

    “:computer: *" + name + "*\n" computer_summary = name_string attachment = { 'callback_id': “id search", 'text': computer_summary, 'actions' : [{ 'name': 'moreinfo', 'text': 'More Info', 'type': "button", 'value': computer_id }] } attachments.append(attachment)
  54. #JNUC2019 for computer in computers: name = computer["name"] name_string =

    “:computer: *" + name + "*\n" computer_summary = name_string attachment = { 'callback_id': “id search", 'text': computer_summary, 'actions' : [{ 'name': 'moreinfo', 'text': 'More Info', 'type': "button", 'value': computer_id }] } attachments.append(attachment)
  55. #JNUC2019 response_url = json_loaded["response_url"] response_json = { 'text': "Total Results:

    *" + str(total_results) + "*\n" 'attachments' : attachments } response = requests.post( response_url, data=json.dumps(response_json), headers={'Content-Type': 'application/json'} )
  56. #JNUC2019 response_url = json_loaded["response_url"] response_json = { 'text': "Total Results:

    *" + str(total_results) + "*\n" 'attachments' : attachments } response = requests.post( response_url, data=json.dumps(response_json), headers={'Content-Type': 'application/json'} )
  57. #JNUC2019 response_url = json_loaded["response_url"] response_json = { 'text': "Total Results:

    *" + str(total_results) + "*\n" 'attachments' : attachments } response = requests.post( response_url, data=json.dumps(response_json), headers={'Content-Type': 'application/json'} )
  58. #JNUC2019 response_url = json_loaded["response_url"] response_json = { 'text': "Total Results:

    *" + str(total_results) + "*\n" 'attachments' : attachments } response = requests.post( response_url, data=json.dumps(response_json), headers={'Content-Type': 'application/json'} )
  59. #JNUC2019 Securing /jamf D Verify Channel ID E Lock down

    Jamf Permissions F Verify Message Authenticity G Stored sensitive information securely
  60. #JNUC2019 taniacomputer_channel_id = "GK0CRKFND" request = event["body"] message = dict(urlparse.parse_qsl(request))

    channel_id = message["channel_id"] if channel_id != taniacomputer_channel_id: response_text = ":warning: `/jamf` can only be used from an authorized channel." response_json["text"] = response_text else: #etc...
  61. #JNUC2019 taniacomputer_channel_id = "GK0CRKFND" request = event["body"] message = dict(urlparse.parse_qsl(request))

    channel_id = message["channel_id"] if channel_id != taniacomputer_channel_id: response_text = ":warning: `/jamf` can only be used from an authorized channel." response_json["text"] = response_text else: #etc...
  62. #JNUC2019 taniacomputer_channel_id = "GK0CRKFND" request = event["body"] message = dict(urlparse.parse_qsl(request))

    channel_id = message["channel_id"] if channel_id != taniacomputer_channel_id: response_text = ":warning: `/jamf` can only be used from an authorized channel." response_json["text"] = response_text else: #etc...
  63. #JNUC2019 Verify Slack Signing Secret “The cooler fresher sibling of

    verification token” Slack gives you a unique ‘signing secret’ string for your app Compute a HMAC-SHA256 hash of each request (using the signing secret as a base) And compare it to the hash value delivered in the request header, event["headers"]['X-Slack-Signature']
  64. #JNUC2019 import hmac import hashlib slack_request_timestamp = event["headers"]['X-Slack-Request- Timestamp'] request_body

    = event["body"] basestring = f”v0:{slack_request_timestamp}: {request_body}".encode('utf-8') slack_signing_secret = bytes(signing_secret_string, 'utf-8') expected_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest() Verify Slack Signing Secret
  65. #JNUC2019 import hmac import hashlib slack_request_timestamp = event["headers"]['X-Slack-Request- Timestamp'] request_body

    = event["body"] basestring = f”v0:{slack_request_timestamp}: {request_body}".encode('utf-8') slack_signing_secret = bytes(signing_secret_string, 'utf-8') expected_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest() Verify Slack Signing Secret
  66. #JNUC2019 import hmac import hashlib slack_request_timestamp = event["headers"]['X-Slack-Request- Timestamp'] request_body

    = event["body"] basestring = f”v0:{slack_request_timestamp}: {request_body}".encode('utf-8') slack_signing_secret = bytes(signing_secret_string, 'utf-8') expected_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest() Verify Slack Signing Secret
  67. #JNUC2019 import hmac import hashlib slack_request_timestamp = event["headers"]['X-Slack-Request- Timestamp'] request_body

    = event["body"] basestring = f”v0:{slack_request_timestamp}: {request_body}".encode('utf-8') slack_signing_secret = bytes(signing_secret_string, 'utf-8') expected_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest() Verify Slack Signing Secret
  68. #JNUC2019 import hmac import hashlib slack_request_timestamp = event["headers"]['X-Slack-Request- Timestamp'] request_body

    = event["body"] basestring = f”v0:{slack_request_timestamp}: {request_body}".encode('utf-8') slack_signing_secret = bytes(signing_secret_string, 'utf-8') expected_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest() Verify Slack Signing Secret
  69. #JNUC2019 expected_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest() delivered_signature =

    event["headers"]['X-Slack-Signature'] if hmac.compare_digest(expected_signature, delivered_sig): # Hooray, request came from Slack else: # Wah, this is an imposter message Verify Slack Signing Secret
  70. #JNUC2019 expected_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest() delivered_signature =

    event["headers"]['X-Slack-Signature'] if hmac.compare_digest(expected_signature, delivered_sig): # Hooray, request came from Slack else: # Wah, this is an imposter message Verify Slack Signing Secret
  71. #JNUC2019 import time slack_request_timestamp = float(event[“headers”]['X-Slack- Request-Timestamp']) current_time = time.time()

    if (current_time - slack_request_timestamp) > 300: response_text = "Message is more than 5 minutes old" response_code = 412 else: #etc... Verify Timestamp
  72. #JNUC2019 import time slack_request_timestamp = float(event[“headers”]['X-Slack- Request-Timestamp']) current_time = time.time()

    if (current_time - slack_request_timestamp) > 300: response_text = "Message is more than 5 minutes old" response_code = 412 else: #etc... Verify Timestamp
  73. #JNUC2019 import time slack_request_timestamp = float(event[“headers”]['X-Slack- Request-Timestamp']) current_time = time.time()

    if (current_time - slack_request_timestamp) > 300: response_text = "Message is more than 5 minutes old" response_code = 412 else: #etc... Verify Timestamp
  74. #JNUC2019 import time slack_request_timestamp = float(event[“headers”]['X-Slack- Request-Timestamp']) current_time = time.time()

    if (current_time - slack_request_timestamp) > 300: response_text = "Message is more than 5 minutes old" response_code = 412 else: #etc... Verify Timestamp
  75. © JAMF Software, LLC #JNUC2019 Secrets Management Instead of hardcoding

    sensitive information into the Lambda code, use Amazon Secrets Manager to store: Jamf API credentials Slack signing secret Any other sensitive information Retrieve the values ‘on the fly’
  76. #JNUC2019 import boto3 from botocore.vendored import requests auth_credentials = get_secret(‘jamf_slackbot_credentials')

    url = "https://myjss.com/JSSResource/computers/name/ {}".format(search_string) response = requests.get(url, headers=headers, auth=(auth_credentials[“username"], auth_credentials["password"]))
  77. #JNUC2019 import boto3 from botocore.vendored import requests auth_credentials = get_secret(‘jamf_slackbot_credentials')

    url = "https://myjss.com/JSSResource/computers/name/ {}".format(search_string) response = requests.get(url, headers=headers, auth=(auth_credentials[“username"], auth_credentials["password"]))
  78. #JNUC2019 import boto3 from botocore.vendored import requests auth_credentials = get_secret(‘jamf_slackbot_credentials')

    url = "https://myjss.com/JSSResource/computers/name/ {}".format(search_string) response = requests.get(url, headers=headers, auth=(auth_credentials[“username"], auth_credentials["password"]))
  79. #JNUC2019 /mymac Customer facing Mac ‘health’ summary, with links to

    Apple articles and Self Service policies jamfselfservice:// Invoked with /mymac No other input required - Slack username used to find assigned Mac
  80. #JNUC2019 #!/bin/bash summary=“" sip_status=$(csrutil status | grep “enabled") if [[

    -z "$sip_status" ]] then summary="$summary ⚠ SIP (System Integrity Protection) is disabled Please contact #ops to resolve ASAP. <https://support.apple.com/en-au/HT204899|About SIP>" else summary="$summary ✅ SIP (System Integrity Protection) is enabled <https://support.apple.com/en-au/HT204899|About SIP>" fi echo "<result>$summary</result>"
  81. #JNUC2019 #!/bin/bash summary=“" sip_status=$(csrutil status | grep “enabled") if [[

    -z "$sip_status" ]] then summary="$summary ⚠ SIP (System Integrity Protection) is disabled Please contact #ops to resolve ASAP. <https://support.apple.com/en-au/HT204899|About SIP>" else summary="$summary ✅ SIP (System Integrity Protection) is enabled <https://support.apple.com/en-au/HT204899|About SIP>" fi echo "<result>$summary</result>"
  82. #JNUC2019 #!/bin/bash summary=“" sip_status=$(csrutil status | grep “enabled") if [[

    -z "$sip_status" ]] then summary="$summary ⚠ SIP (System Integrity Protection) is disabled Please contact #ops to resolve ASAP. <https://support.apple.com/en-au/HT204899|About SIP>" else summary="$summary ✅ SIP (System Integrity Protection) is enabled <https://support.apple.com/en-au/HT204899|About SIP>" fi echo "<result>$summary</result>"
  83. #JNUC2019 #!/bin/bash summary=“" sip_status=$(csrutil status | grep “enabled") if [[

    -z "$sip_status" ]] then summary="$summary ⚠ SIP (System Integrity Protection) is disabled Please contact #ops to resolve ASAP. <https://support.apple.com/en-au/HT204899|About SIP>" else summary="$summary ✅ SIP (System Integrity Protection) is enabled <https://support.apple.com/en-au/HT204899|About SIP>" fi echo "<result>$summary</result>"
  84. #JNUC2019 #!/bin/bash summary=“" sip_status=$(csrutil status | grep “enabled") if [[

    -z "$sip_status" ]] then summary="$summary ⚠ SIP (System Integrity Protection) is disabled Please contact #ops to resolve ASAP. <https://support.apple.com/en-au/HT204899|About SIP>" else summary="$summary ✅ SIP (System Integrity Protection) is enabled <https://support.apple.com/en-au/HT204899|About SIP>" fi echo "<result>$summary</result>"
  85. #JNUC2019 Jamf records epoch time in milliseconds, while Python time.now()

    returns seconds Lambda Function timeout is 3 seconds, but 6 seconds probably best
  86. © JAMF Software, LLC #JNUC2019 Maximum of 100 attachments per

    message, but Slack best practice is 20 response_url can be used up to 5 times within 30 minutes of the command being invoked Provide a helper text for your slash command
  87. #JNUC2019 from urllib import parse as urlparse request = event["body"]

    message = dict(urlparse.parse_qsl(request)) try: search_string = message["text"] except KeyError: search_string = "" if search_string == "" or search_string == "help": response_text = help_text response_code = 200 else: #etc... Helper Text
  88. #JNUC2019 from urllib import parse as urlparse request = event["body"]

    message = dict(urlparse.parse_qsl(request)) try: search_string = message["text"] except KeyError: search_string = "" if search_string == "" or search_string == "help": response_text = help_text response_code = 200 else: #etc... Helper Text
  89. © JAMF Software, LLC #JNUC2019 Thank you for your time!

    www.taniacomputer.com @taniacomputer
  90. © JAMF Software, LLC Thank you for listening! Give us

    feedback by completing the 2-question session survey in the JNUC 2019 app. UP NEXT Build a Second Generation 1-to-1 Student Device Program 2:45 - 3:30 p.m.