diff --git a/.codacy.yml b/.codacy.yml index 0701063..ecc3b91 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,6 +1,5 @@ --- exclude_paths: - 'aiojss/**' - - 'tools/**' - 'scripts/**' - 'extension_attributes/**' diff --git a/.gitignore b/.gitignore index ed50ea2..0666066 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ extension_attributes/* !extension_attributes/Last User/ scripts/* !scripts/Install Software Updates/ +!scripts/templates +!extension_attributes/templates +tools/ci_tests/computers.json diff --git a/README.md b/README.md index 597bf78..52a7023 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # git2jss -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d9d618c32e93436ea67102fd3d3f5b21)](https://www.codacy.com/app/adam-furbee/git2jss?utm_source=github.com&utm_medium=referral&utm_content=BadStreff/git2jss&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/c49c0bd6a88d4f1e8c6808455171178e)](https://app.codacy.com/gh/rustymyers/git2jss/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) A fast asynchronous python library for syncing your scripts in git with your JSS easily. This allows admins to keep their script in a version control system for easy updating rather than googling and copy-pasting from resources that they find online. @@ -26,16 +26,16 @@ Optional flags for `sync.py`: - `--verbose` to add additional logging - `--update_all` to upload all resources in `./extension_attributes` and `./scripts` - `--jenkins` to write a Jenkins file:`jenkins.properties` with `$scripts` and `$eas` and compare `$GIT_PREVIOUS_COMMIT` with `$GIT_COMMIT` - -### [ConfigParser](https://docs.python.org/3/library/configparser.html) (Optional): - -A config file can be created in the project root or the users home folder. When a config file exists, the script will not promt for a password. - - A jamfapi.cfg file can provide the following variables: - - - username - - password - - url + +### [ConfigParser](https://docs.python.org/3/library/configparser.html) (Optional): + +A config file can be created in the project root or the users home folder. When a config file exists, the script will not promt for a password. + + A jamfapi.cfg file can provide the following variables: + +- username +- password +- url ### Prerequisites git2jss requires [Python 3.6](https://www.python.org/downloads/) and the python modules listed in `requirements.txt` diff --git a/extension_attributes/templates/Last User/ea.sh b/extension_attributes/templates/Last User/ea.sh new file mode 100644 index 0000000..059b4d3 --- /dev/null +++ b/extension_attributes/templates/Last User/ea.sh @@ -0,0 +1,7 @@ +#!/bin/sh +lastUser=`defaults read /Library/Preferences/com.apple.loginwindow lastUserName` + +if [ $lastUser == "" ]; then + echo "No logins" +else + echo "$lastUser" \ No newline at end of file diff --git a/extension_attributes/templates/Last User/ea.xml b/extension_attributes/templates/Last User/ea.xml new file mode 100644 index 0000000..8195681 --- /dev/null +++ b/extension_attributes/templates/Last User/ea.xml @@ -0,0 +1,13 @@ + + + Last User + true + This attribute displays the last user to log in. This attribute applies to both Mac and Windows. + String + + script + Mac + diff --git a/sync.py b/sync.py index f39f433..7005c8d 100755 --- a/sync.py +++ b/sync.py @@ -4,7 +4,6 @@ import os from os.path import dirname, join, realpath import sys -import xml.etree.ElementTree as ET import getpass import argparse import logging @@ -14,8 +13,7 @@ import uvloop import configparser import requests -import configparser -import requests +from defusedxml import ElementTree as eTree logging.basicConfig( level=logging.DEBUG, @@ -40,7 +38,7 @@ def get_uapi_token(): fetches api token """ jamf_test_url = url + "/api/v1/auth/token" - response = requests.post(url=jamf_test_url, auth=(username, password)) + response = requests.post(url=jamf_test_url, auth=(username, password), timeout=5) response_json = response.json() return response_json["token"] @@ -51,27 +49,7 @@ def invalidate_uapi_token(uapi_token): """ jamf_test_url = url + "/api/v1/auth/invalidate-token" headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token} - _ = requests.post(url=jamf_test_url, headers=headers) - - -# https://github.com/lazymutt/Jamf-Pro-API-Sampler/blob/5f8efa92911271248f527e70bd682db79bc600f2/jamf_duplicate_detection.py#L99 -def get_uapi_token(): - """ - fetches api token - """ - jamf_test_url = url + "/api/v1/auth/token" - response = requests.post(url=jamf_test_url, auth=(username, password)) - response_json = response.json() - return response_json["token"] - - -def invalidate_uapi_token(uapi_token): - """ - invalidates api token - """ - jamf_test_url = url + "/api/v1/auth/invalidate-token" - headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token} - _ = requests.post(url=jamf_test_url, headers=headers) + _ = requests.post(url=jamf_test_url, headers=headers, timeout=5) def check_for_changes(): @@ -208,7 +186,7 @@ async def upload_extension_attribute(session, url, user, passwd, ext_attr, semap if has_script and data: template.find("input_type/script").text = data if args.verbose: - print(ET.tostring(template)) + print(eTree.tostring(template)) print("response status initial get: ", resp.status) if resp.status == 200: put_url = ( @@ -217,12 +195,12 @@ async def upload_extension_attribute(session, url, user, passwd, ext_attr, semap + template.find("name").text ) resp = await session.put( - put_url, data=ET.tostring(template), headers=headers + put_url, data=eTree.tostring(template), headers=headers ) else: post_url = url + "/JSSResource/computerextensionattributes/id/0" resp = await session.post( - post_url, data=ET.tostring(template), headers=headers + post_url, data=eTree.tostring(template), headers=headers ) if args.verbose: print("response status: ", resp.status) @@ -248,7 +226,7 @@ async def get_ea_template(session, url, user, passwd, ext_attr): with open( join(sync_path, "extension_attributes", ext_attr, xml_file[0]), "r" ) as file: - template = ET.fromstring(file.read()) + template = eTree.parse(file.read()) except IndexError: with async_timeout.timeout(args.timeout): headers = { @@ -268,15 +246,17 @@ async def get_ea_template(session, url, user, passwd, ext_attr): + ext_attr, headers=headers, ) as response: - template = ET.fromstring(await response.text()) + template = eTree.fromstring(await response.text()) else: - template = ET.parse(join(sync_path, "templates/ea.xml")).getroot() + template = eTree.parse( + join(sync_path, "templates/ea.xml") + ).getroot() # name is mandatory, so we use the foldername if nothing is set in # a template if args.verbose: - print(ET.tostring(template)) + print(eTree.tostring(template)) if template.find("category") and template.find("category").text not in CATEGORIES: - ET.SubElement(template, "category").text = "None" + eTree.SubElement(template, "category").text = "None" if args.verbose: c = template.find("category").text print( @@ -284,7 +264,7 @@ async def get_ea_template(session, url, user, passwd, ext_attr): setting to None""" ) if template.find("name") is None: - ET.SubElement(template, "name").text = ext_attr + eTree.SubElement(template, "name").text = ext_attr elif not template.find("name").text or template.find("name").text is None: template.find("name").text = ext_attr return template @@ -344,12 +324,12 @@ async def upload_script(session, url, user, passwd, script, semaphore): url + "/JSSResource/scripts/name/" + template.find("name").text ) resp = await session.put( - put_url, data=ET.tostring(template), headers=headers + put_url, data=eTree.tostring(template), headers=headers ) else: post_url = url + "/JSSResource/scripts/id/0" resp = await session.post( - post_url, data=ET.tostring(template), headers=headers + post_url, data=eTree.tostring(template), headers=headers ) if resp.status in (201, 200): print("Uploaded script: %s" % template.find("name").text) @@ -369,7 +349,7 @@ async def get_script_template(session, url, user, passwd, script): ] try: with open(join(sync_path, "scripts", script, xml_file[0]), "r") as file: - template = ET.fromstring(file.read()) + template = eTree.fromstring(file.read()) except IndexError: with async_timeout.timeout(args.timeout): headers = { @@ -384,14 +364,14 @@ async def get_script_template(session, url, user, passwd, script): async with session.get( url + "/JSSResource/scripts/name/" + script, headers=headers ) as response: - template = ET.fromstring(await response.text()) + template = eTree.fromstring(await response.text()) else: - template = ET.parse( + template = eTree.parse( join(sync_path, "templates/script.xml") ).getroot() # name is mandatory, so we use the filename if nothing is set in a template if args.verbose: - print(ET.tostring(template)) + print(eTree.tostring(template)) if ( template.find("category") is not None and template.find("category").text not in CATEGORIES @@ -404,7 +384,7 @@ async def get_script_template(session, url, user, passwd, script): setting to None""" ) if template.find("name") is None: - ET.SubElement(template, "name").text = script + eTree.SubElement(template, "name").text = script elif not template.find("name").text or template.find("name").text is None: template.find("name").text = script return template @@ -427,7 +407,7 @@ async def get_existing_categories(session, url, user, passwd, semaphore): c.find("name").text for c in [ e - for e in ET.fromstring(await resp.text()).findall( + for e in eTree.fromstring(await resp.text()).findall( "category" ) ] @@ -490,26 +470,23 @@ async def main(): CONFIG_FILE = config_path if CONFIG_FILE != "": - try: - # Get config - CONFPARSER.read(CONFIG_FILE) - except: - print("Can't read config file") + # Get config + CONFPARSER.read(CONFIG_FILE) try: username = CONFPARSER.get("jss", "username") - except: + except configparser.NoOptionError: print("Can't find username in configfile") try: password = CONFPARSER.get("jss", "password") - except: + except configparser.NoOptionError: print("Can't find password in configfile") try: url = CONFPARSER.get("jss", "server") - except: + except configparser.NoOptionError: print("Can't find url in configfile") try: sync_path = CONFPARSER.get("jss", "sync_path") - except: + except configparser.NoOptionError: print("Can't find sync_path in config") # Ask for password if not supplied via command line args @@ -537,3 +514,6 @@ async def main(): warnings.simplefilter("always", ResourceWarning) loop.run_until_complete(main()) + + # Remove token + invalidate_uapi_token(token) diff --git a/tools/ci_tests/validate_files_and_folders.sh b/tools/ci_tests/validate_files_and_folders.sh old mode 100644 new mode 100755 index 7433be8..81d113c --- a/tools/ci_tests/validate_files_and_folders.sh +++ b/tools/ci_tests/validate_files_and_folders.sh @@ -5,20 +5,20 @@ #Load up some variables #Define scripts and templates folders -scripts=$(ls -p scripts | grep -v '/$' | sed -e 's/\..*$//') -scripts_templates=$(ls -p scripts/templates/| sed -e 's/\..*$//') +scripts=$(ls -1 scripts | grep -v "templates") +scripts_templates=$(ls -1 scripts/templates/) #Define EA and templates -extensionattributes=$(ls -p extension_attributes | grep -v '/$' | sed -e 's/\..*$//') -extensionattributes_templates=$(ls -p extension_attributes/templates/| sed -e 's/\..*$//') +extensionattributes=$(ls -1 extension_attributes | grep -v "templates") +extensionattributes_templates=$(ls -1 extension_attributes/templates/) #Validate both the Script and the Template for the Script exist. -echo "Making sure files exist in both places in scripts and scripts/Templates" +echo "Making sure files with same names exist in both places in scripts and scripts/Templates" scriptcompare=$(sdiff -bBWsw 75 <(echo "$scripts") <(echo "$scripts_templates" )) if [ "$scriptcompare" == "" ]; then - echo "Script and Script Template Exist All good in the hood!" + echo "Script and Script Template Exist in both folders!" else echo "Errors! occurred please correct the below" echo " Scripts | Templates" @@ -31,10 +31,10 @@ fi #Valate both the EA and the Template for the EA exist. -echo "Making sure files exist in both places extension_attributes and extension_attributes/Templates" +echo "Making sure files with same names exist in both places extension_attributes and extension_attributes/Templates" eacompare=$(sdiff -bBWsw 75 <(echo "$extensionattributes") <(echo "$extensionattributes_templates")) if [ "$eacompare" == "" ]; then - echo "EA and EA Template Exist All good in the hood!" + echo "EA and EA Template Exist in both folders!" else echo "Errors! occurred please correct the below" echo " Extension Attributes | Templates" diff --git a/tools/ci_tests/validatexml.sh b/tools/ci_tests/validatexml.sh old mode 100644 new mode 100755 index b20eaeb..78f203b --- a/tools/ci_tests/validatexml.sh +++ b/tools/ci_tests/validatexml.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/zsh ################################################################################### ## Validates XML for proper formatting ################################################################################### @@ -9,10 +9,10 @@ function scripts() { printf "\033[31m Working on Scripts\n" printf "\033[31m---------------------------------------------------------------------------------\n" printf "\033[0m" -scriptfolders=$(ls -ltr ./scripts | cut -c52- | awk 'NR>1') +scriptfolders=$(ls -1 ./scripts | grep -v templates) while read folder ; do echo "$folder" - xmllint --noout ./scripts/"$folder"/*.xml + xmllint --noout ./scripts/"$folder"/*.xml done <<< "$scriptfolders" } @@ -20,7 +20,7 @@ scriptfolders=$(ls -ltr ./scripts | cut -c52- | awk 'NR>1') function ea(){ - eafolders=$(ls -ltr ./extension_attributes | cut -c52- | awk 'NR>1') + eafolders=$(ls -1 ./extension_attributes | grep -v templates) printf "\033[31m---------------------------------------------------------------------------------\n" printf "\033[31m Working on Extension Attributes\n" @@ -30,7 +30,7 @@ function ea(){ echo "$folder" - xmllint --noout ./extension_attributes/"$folder"/*.xml + xmllint --noout ./extension_attributes/"$folder"/*.xml done <<< "$eafolders" } diff --git a/tools/ci_tests/verifyEA.py b/tools/ci_tests/verifyEA.py old mode 100644 new mode 100755 index 8e40a16..43825f9 --- a/tools/ci_tests/verifyEA.py +++ b/tools/ci_tests/verifyEA.py @@ -1,60 +1,119 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import requests -from xml.etree import ElementTree as ET +from defusedxml import ElementTree as eTree import os import getpass import json +import configparser # Use this script to validate that EA values aren't changing as a result of syncing # Overwrite computers.json overwrite = False +smart_group = "1018" # Constants -url = 'https://your.jss.com' -username = getpass.getuser() -password = getpass.getpass() +# Get configs from files +CONFIG_FILE_LOCATIONS = ["jamfapi.cfg", os.path.expanduser("~/jamfapi.cfg")] +CONFIG_FILE = "" +# Parse Config File +CONFPARSER = configparser.ConfigParser() +for config_path in CONFIG_FILE_LOCATIONS: + if os.path.exists(config_path): + print("Found Config: {0}".format(config_path)) + CONFIG_FILE = config_path + +if CONFIG_FILE != "": + # Get config + CONFPARSER.read(CONFIG_FILE) + try: + username = CONFPARSER.get("jss", "username") + password = CONFPARSER.get("jss", "password") + url = CONFPARSER.get("jss", "server") + smart_group = CONFPARSER.get("verifyEA", "smart_group") + except configparser.NoOptionError: + print("Can't find configs in configfile") + except configparser.NoSectionError: + print("Can't find sections in configfile") +else: + url = "https://your.jss.com" + username = getpass.getuser() + password = getpass.getpass() + smart_group = input("Enter smart_group") + + +def get_uapi_token(): + """ + fetches api token + """ + jamf_test_url = url + "/api/v1/auth/token" + response = requests.post(url=jamf_test_url, auth=(username, password), timeout=5) + response_json = response.json() + return response_json["token"] + + +def invalidate_uapi_token(uapi_token): + """ + invalidates api token + """ + jamf_test_url = url + "/api/v1/auth/invalidate-token" + headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token} + _ = requests.post(url=jamf_test_url, headers=headers, timeout=5) -def overwrite_file(): - print('Overwriting File: computers.json...') - with open('computers.json', 'w') as f: +def overwrite_file(file_path): + print("Overwriting File: computers.json...") + with open(file_path, "w") as f: f.write(json.dumps(computers)) -def read_file(): - print('Reading cached data from disk...') - with open('computers.json', 'r') as f: + +def read_file(file_path): + print("Reading cached data from disk...") + with open(file_path, "r") as f: computers_from_disk = json.load(f) return computers_from_disk -def build_computers_data_object(): - # Get IDs for computers - print('Communicating with the Jamf Pro Server...') + +def build_computers_data_object(token, group_id): + """Builds computer data into local file + params: token, group_id + returns: computers objects json + """ + print("Communicating with the Jamf Pro Server...") computers = {} - r = requests.get(url + '/JSSResource/computergroups/id/810', - auth = (username, password), - headers= {'Content-Type': 'application/xml'}) + r = requests.get( + url + "/JSSResource/computergroups/id/{0}".format(group_id), + headers={"Content-Type": "application/xml", "Authorization": "Bearer " + token}, + timeout=5 + ) - tree = ET.fromstring(r.content) - resource_ids = [ e.text for e in tree.findall('computers/computer/id') ] + tree = eTree.fromstring(r.content) + resource_ids = [e.text for e in tree.findall("computers/computer/id")] # Download each resource and save to disk for resource_id in resource_ids: - # Get detailed information about the record - r = requests.get(url + '/JSSResource/computers/id/%s' % (resource_id), - auth = (username, password), - headers={'Content-Type': 'application/json'}) - - # Parse xml - tree = ET.fromstring(r.content) - ea_values = [ e.text for e in tree.findall('extension_attributes/extension_attribute/value') ] - ea_names = [ e.text for e in tree.findall('extension_attributes/extension_attribute/name') ] - - # Build the json for the comparison + r = requests.get( + url + "/JSSResource/computers/id/{0}".format(resource_id), + headers={"Content-Type": "application/json", "Authorization": "Bearer " + token}, + timeout=5 + ) + + # Parse xml + tree = eTree.fromstring(r.content) + ea_values = [ + e.text + for e in tree.findall("extension_attributes/extension_attribute/value") + ] + ea_names = [ + e.text + for e in tree.findall("extension_attributes/extension_attribute/name") + ] + + # Build the json for the comparison computers[resource_id] = {} - for k,v in zip(ea_names,ea_values): + for k, v in zip(ea_names, ea_values): computers[resource_id][k] = v return computers @@ -67,25 +126,35 @@ def compare_computer(computer_id): print("Processing Computer ID: %s" % computer_id) for key in computers[computer_id].keys(): if computers[computer_id][key] != computers_from_disk[computer_id][key]: - print("Value Change Found\n\tEA Name:\t{}\n\tOriginal Value:\t{}\n\tNew Value:\t{}".format(key,computers[computer_id][key],computers_from_disk[computer_id][key])) + print( + "Value Change Found\n\tEA Name:\t{}\n\tOriginal Value:\t{}\n\tNew Value:\t{}".format( + key, + computers[computer_id][key], + computers_from_disk[computer_id][key], + ) + ) + # Is this the first time it was run? mypath = os.path.dirname(os.path.realpath(__file__)) -if os.path.exists(os.path.join(mypath,'computers.json')): - computers_from_disk = read_file() +myfile = os.path.join(mypath, "computers.json") +if os.path.exists(myfile): + computers_from_disk = read_file(myfile) else: - print('No cached data found, writing new data to computers.json') + print("No cached data found, writing new data to computers.json") overwrite = True -# Get computers information from JSS -computers = build_computers_data_object() +token = get_uapi_token() -# Overwrite local file? -if overwrite == True: - overwrite_file() +# Get computers information from JSS smart group +computers = build_computers_data_object(token, smart_group) +# Overwrite local file? +if overwrite: + overwrite_file(myfile) + print("Computer data staged for comparison with future runs.") else: # Compare each computer - print('Analyzing the results...') + print("Analyzing the results...") for computer_id in list(computers.keys()): compare_computer(computer_id) diff --git a/tools/download.py b/tools/download.py index 6c7545b..02ef580 100755 --- a/tools/download.py +++ b/tools/download.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import getpass import requests -from xml.etree import ElementTree as ET +from defusedxml import ElementTree as eTree from xml.dom import minidom import os import argparse @@ -18,7 +18,7 @@ def get_uapi_token(): fetches api token """ jamf_test_url = url + "/api/v1/auth/token" - response = requests.post(url=jamf_test_url, auth=(username, password)) + response = requests.post(url=jamf_test_url, auth=(username, password), timeout=5) response_json = response.json() return response_json["token"] @@ -29,7 +29,7 @@ def invalidate_uapi_token(uapi_token): """ jamf_test_url = url + "/api/v1/auth/invalidate-token" headers = {"Accept": "*/*", "Authorization": "Bearer " + uapi_token} - _ = requests.post(url=jamf_test_url, headers=headers) + _ = requests.post(url=jamf_test_url, headers=headers, timeout=5) def download_scripts( @@ -79,6 +79,7 @@ def download_scripts( "Authorization": "Bearer " + token, }, verify=args.do_not_verify_ssl, + timeout=5, ) # Basic error handling @@ -90,7 +91,7 @@ def download_scripts( % r.status_code ) exit(1) - tree = ET.fromstring(r.content) + tree = eTree.fromstring(r.content) resource_ids = [e.text for e in tree.findall(".//id")] # Download each resource and save to disk @@ -105,8 +106,9 @@ def download_scripts( "Authorization": "Bearer " + token, }, verify=args.do_not_verify_ssl, + timeout=5, ) - tree = ET.fromstring(r.content) + tree = eTree.fromstring(r.content) if mode == "ea": if tree.find("input_type/type").text != "script": @@ -132,7 +134,7 @@ def download_scripts( # Create script string, and determine the file extension if get_script: - xmlstr = ET.tostring( + xmlstr = eTree.tostring( tree.find(script_xml), encoding="unicode", method="text" ).replace("\r", "") if xmlstr.startswith("#!/bin/sh"): @@ -166,11 +168,11 @@ def download_scripts( tree.remove(tree.find("id")) tree.remove(tree.find("script_contents_encoded")) tree.remove(tree.find("filename")) - except: + except TypeError: pass xmlstr = minidom.parseString( - ET.tostring(tree, encoding="unicode", method="xml") + eTree.tostring(tree, encoding="unicode", method="xml") ).toprettyxml(indent=" ") with open(os.path.join(resource_path, "%s.xml" % mode), "w") as f: f.write(xmlstr) @@ -202,26 +204,23 @@ def download_scripts( CONFIG_FILE = config_path if CONFIG_FILE != "": - try: - # Get config - CONFPARSER.read(CONFIG_FILE) - except: - print("Can't read config file") + # Get config + CONFPARSER.read(CONFIG_FILE) try: username = CONFPARSER.get("jss", "username") - except: + except configparser.NoOptionError: print("Can't find username in configfile") try: password = CONFPARSER.get("jss", "password") - except: + except configparser.NoOptionError: print("Can't find password in configfile") try: url = CONFPARSER.get("jss", "server") - except: + except configparser.NoOptionError: print("Can't find url in configfile") try: export_path = CONFPARSER.get("jss", "export_path") - except: + except configparser.NoOptionError: print("Can't find export_path in config") # Ask for password if not supplied via command line args