From 26111a3941179509154fd7639763c69aa3b822c1 Mon Sep 17 00:00:00 2001 From: Tuxudo Date: Sat, 27 Feb 2021 08:42:52 -0500 Subject: [PATCH] Update module YAML widget, JSON client tab, report on bundle version (addresses PR #11) Contains new client script and migrations --- README.md | 5 +- applications_controller.php | 59 +++--- applications_model.php | 175 ++++++++---------- locales/en.json | 3 +- ...000001_applications_add_bundle_version.php | 27 +++ scripts/applications.py | 29 +-- views/applications_32_bit_apps_widget.php | 33 ---- views/applications_32_bit_apps_widget.yml | 10 + views/applications_listing.php | 37 ++-- views/applications_tab.php | 137 +++++++++----- 10 files changed, 284 insertions(+), 231 deletions(-) mode change 100644 => 100755 applications_controller.php mode change 100644 => 100755 applications_model.php mode change 100644 => 100755 locales/en.json create mode 100755 migrations/2021_02_26_000001_applications_add_bundle_version.php delete mode 100644 views/applications_32_bit_apps_widget.php create mode 100755 views/applications_32_bit_apps_widget.yml mode change 100644 => 100755 views/applications_listing.php mode change 100644 => 100755 views/applications_tab.php diff --git a/README.md b/README.md index 3c6799a..ea622a5 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ Shows information about applications on the client. Data can be viewed under the Applications tab on the client details page or using the Applications listing view -Database: + +Table Schema +------ * name - varchar(255) - name of the application * path - TEXT - application's path * last_modified - BIGINT - date application was last modified (epoch) @@ -15,3 +17,4 @@ Database: * info - TEXT - info about the application * has64bit - int - 0/1 does application contain 64-bit code * signed_by - varchar(255) - code signing of application +* bundle_version - varchar(255) - the application's bundle version from the CFBundleVersion key \ No newline at end of file diff --git a/applications_controller.php b/applications_controller.php old mode 100644 new mode 100755 index 3a37e84..0100aa0 --- a/applications_controller.php +++ b/applications_controller.php @@ -16,55 +16,46 @@ function __construct() $this->module_path = dirname(__FILE__); } - /** + /** * Default method * @author tuxudo * **/ - function index() + function index() { echo "You've loaded the applications module!"; } - /** - * Retrieve data in json format + /** + * Retrieve data in json format for widget * **/ - public function get_data($serial_number = '') + public function get_32_bit_apps() { - $obj = new View(); + $sql = "SELECT COUNT(CASE WHEN name <> '' AND has64bit = 0 THEN 1 END) AS count, name + FROM applications + LEFT JOIN reportdata USING (serial_number) + WHERE has64bit = 0 + ".get_machine_group_filter('AND')." + GROUP BY name + ORDER BY count DESC"; - if (! $this->authorized()) { - $obj->view('json', array('msg' => 'Not authorized')); - } - $queryobj = new Applications_model(); - - $sql = "SELECT name, path, lastModified, obtained_from, runtime_environment, version, info, signed_by, has64BitIntelCode - FROM applications - WHERE serial_number = '$serial_number'"; - - $applications_tab = $queryobj->query($sql); - - $applications = new Applications_model; - $obj->view('json', array('msg' => current(array('msg' => $applications_tab)))); - } + jsonView($queryobj->query($sql)); + } - /** - * Retrieve data in json format for widget + /** + * Retrieve data in json format * **/ - public function get_32_bit_apps() - { - $obj = new View(); + public function get_data($serial_number = '') + { + $sql = "SELECT name, path, last_modified, obtained_from, runtime_environment, version, bundle_version, info, signed_by, has64bit + FROM applications + WHERE serial_number = '$serial_number'"; - if (! $this->authorized()) { - $obj->view('json', array('msg' => array('error' => 'Not authenticated'))); - return; - } - - $apps_32 = new Applications_model; - $obj->view('json', array('msg' => $apps_32->get_32_bit_apps())); - } + $queryobj = new Applications_model(); + jsonView($queryobj->query($sql)); + } -} // END class Applications_controller +} // End class Applications_controller diff --git a/applications_model.php b/applications_model.php old mode 100644 new mode 100755 index 3e6fc27..e7ba1d5 --- a/applications_model.php +++ b/applications_model.php @@ -4,117 +4,100 @@ class Applications_model extends \Model { - function __construct($serial='') - { - parent::__construct('id', 'applications'); //primary key, tablename - $this->rs['id'] = ''; - $this->rs['serial_number'] = $serial; - $this->rs['name'] = ''; - $this->rs['path'] = ''; - $this->rs['last_modified'] = 0; - $this->rs['obtained_from'] = ''; - $this->rs['runtime_environment'] = ''; - $this->rs['version'] = ''; - $this->rs['info'] = ''; - $this->rs['signed_by'] = ''; - $this->rs['has64bit'] = 0; // True or False + function __construct($serial='') + { + parent::__construct('id', 'applications'); //primary key, tablename + $this->rs['id'] = ''; + $this->rs['serial_number'] = $serial; + $this->rs['name'] = ''; + $this->rs['path'] = ''; + $this->rs['last_modified'] = 0; + $this->rs['obtained_from'] = ''; + $this->rs['runtime_environment'] = ''; + $this->rs['version'] = ''; + $this->rs['info'] = ''; + $this->rs['signed_by'] = ''; + $this->rs['has64bit'] = 0; // True or False + $this->rs['bundle_version'] = ''; - $this->serial_number = $serial; - } + $this->serial_number = $serial; + } - // ------------------------------------------------------------------------ - - /** - * Retrieve data in json format for widget - * - **/ - public function get_32_bit_apps() - { - $out = array(); - $sql = "SELECT COUNT(CASE WHEN name <> '' AND has64bit = 0 THEN 1 END) AS count, name - FROM applications - LEFT JOIN reportdata USING (serial_number) - WHERE has64bit = 0 - ".get_machine_group_filter('AND')." - GROUP BY name - ORDER BY count DESC"; - - $out = $this->query($sql); - return $out; - } - - /** - * Process data sent by postflight - * - * @param string data - * @author tuxudo - **/ - function process($plist) - { - - if ( ! $plist){ - throw new Exception("Error Processing Request: No property list found", 1); - } - - // Delete previous set - $this->deleteWhere('serial_number=?', $this->serial_number); + // ------------------------------------------------------------------------ + + /** + * Process data sent by postflight + * + * @param string data + * @author tuxudo + **/ + function process($plist) + { + // Check if we have data + if ( ! $plist){ + throw new Exception("Error Processing Request: No property list found", 1); + } + + // Delete previous set + $this->deleteWhere('serial_number=?', $this->serial_number); + + $parser = new CFPropertyList(); + $parser->parse($plist, CFPropertyList::FORMAT_XML); + $myList = $parser->toArray(); + + $typeList = array( + 'name' => '', + 'last_modified' => '', + 'obtained_from' => 'unknown', + 'path' => '', + 'runtime_environment' => '', + 'version' => '', + 'info' => '', + 'signed_by' => '', + 'has64bit' => 0, // Yes or No + 'bundle_version' => '' // Yes or No + ); - $parser = new CFPropertyList(); - $parser->parse($plist, CFPropertyList::FORMAT_XML); - $myList = $parser->toArray(); - - $typeList = array( - 'name' => '', - 'last_modified' => '', - 'obtained_from' => 'unknown', - 'path' => '', - 'runtime_environment' => '', - 'version' => '', - 'info' => '', - 'signed_by' => '', - 'has64bit' => 0 // Yes or No - ); - // List of paths to ignore $bundlepath_ignorelist = is_array(conf('bundlepath_ignorelist')) ? conf('bundlepath_ignorelist') : array(); $path_regex = ':^'.implode('|', $bundlepath_ignorelist).'$:'; - - - foreach ($myList as $app) { - // Check if we have a name - if( ! array_key_exists("name", $app)){ - continue; - } - + + // Process each app + foreach ($myList as $app) { + // Check if we have a name + if( ! array_key_exists("name", $app)){ + continue; + } + // Skip path - if (preg_match($path_regex, $app['path'])) { - continue; - } - + if (preg_match($path_regex, $app['path'])) { + continue; + } + // Fix signed_by entries if (array_key_exists("signed_by",$app)) { $app['signed_by'] = str_replace(array('Developer ID Application: '), array(''), $app['signed_by']); } - + // Fix last_modified date if (array_key_exists("last_modified",$app)) { $temptime = $app['last_modified']; $date = new DateTime("@$temptime"); $app['last_modified'] = $date->format('U'); } - + // Process each app for saving - foreach ($typeList as $key => $value) { - $this->rs[$key] = $value; - if(array_key_exists($key, $app)) - { - $this->rs[$key] = $app[$key]; - } - } - - // Save application - $this->id = ''; - $this->save(); - } - } -} + foreach ($typeList as $key => $value) { + $this->rs[$key] = $value; + if(array_key_exists($key, $app)) + { + $this->rs[$key] = $app[$key]; + } + } + + // Save application + $this->id = ''; + $this->save(); + } + } +} \ No newline at end of file diff --git a/locales/en.json b/locales/en.json old mode 100644 new mode 100755 index 004f7c8..bfc86ec --- a/locales/en.json +++ b/locales/en.json @@ -9,5 +9,6 @@ "identified_developer": "Identified Developer", "applications": "Applications", "no_32_bit": "No 32-bit Applications", - "32_bit_apps": "32-bit Applications" + "32_bit_apps": "32-bit Applications", + "bundle_version": "Bundle Version" } diff --git a/migrations/2021_02_26_000001_applications_add_bundle_version.php b/migrations/2021_02_26_000001_applications_add_bundle_version.php new file mode 100755 index 0000000..886d710 --- /dev/null +++ b/migrations/2021_02_26_000001_applications_add_bundle_version.php @@ -0,0 +1,27 @@ +table($this->tableName, function (Blueprint $table) { + $table->string('bundle_version')->nullable(); + + $table->index('bundle_version'); + }); + } + + public function down() + { + $capsule = new Capsule(); + $capsule::schema()->table($this->tableName, function (Blueprint $table) { + $table->dropColumn('bundle_version'); + }); + } +} diff --git a/scripts/applications.py b/scripts/applications.py index dd7a266..a73e407 100755 --- a/scripts/applications.py +++ b/scripts/applications.py @@ -7,6 +7,20 @@ import plistlib import sys +sys.path.insert(0, '/usr/local/munki') +sys.path.insert(0, '/usr/local/munkireport') + +from munkilib import FoundationPlist + +def get_app_bundle_version(app_path): + '''Return the CFBundleVersion of the app based on its path''' + + try: + info_plist = FoundationPlist.readPlist(app_path+"/Contents/Info.plist") + return info_plist['CFBundleVersion'] + + except Exception: + return "" def get_applications_info(): '''Uses system profiler to get applications for this machine.''' @@ -41,6 +55,7 @@ def flatten_applications_info(array): device['obtained_from'] = obj[item] elif item == 'path': device['path'] = obj[item] + device['bundle_version'] = get_app_bundle_version(obj[item]) elif item == 'runtime_environment': device['runtime_environment'] = obj[item] elif item == 'version': @@ -60,17 +75,6 @@ def flatten_applications_info(array): def main(): """Main""" - # Create cache dir if it does not exist - cachedir = '%s/cache' % os.path.dirname(os.path.realpath(__file__)) - if not os.path.exists(cachedir): - os.makedirs(cachedir) - - # Skip manual check - if len(sys.argv) > 1: - if sys.argv[1] == 'manualcheck': - print 'Manual check: skipping' - exit(0) - # Set the encoding reload(sys) sys.setdefaultencoding('utf8') @@ -79,8 +83,9 @@ def main(): result = dict() info = get_applications_info() result = flatten_applications_info(info) - + # Write applications results to cache + cachedir = '%s/cache' % os.path.dirname(os.path.realpath(__file__)) output_plist = os.path.join(cachedir, 'applications.plist') plistlib.writePlist(result, output_plist) #print plistlib.writePlistToString(result) diff --git a/views/applications_32_bit_apps_widget.php b/views/applications_32_bit_apps_widget.php deleted file mode 100644 index ddafa6c..0000000 --- a/views/applications_32_bit_apps_widget.php +++ /dev/null @@ -1,33 +0,0 @@ -
-
-
-

- - -

-
-
-
-
- - - diff --git a/views/applications_32_bit_apps_widget.yml b/views/applications_32_bit_apps_widget.yml new file mode 100755 index 0000000..570d80b --- /dev/null +++ b/views/applications_32_bit_apps_widget.yml @@ -0,0 +1,10 @@ +type: scrollbox +widget_id: 32-bit-apps-widget +api_url: /module/applications/get_32_bit_apps +i18n_title: applications.32_bit_apps +icon: fa-code +listing_link: /show/listing/applications/applications +search_key: name +url_type: client_detail +i18nEmptyResult: applications.no_32_bit +badge: count diff --git a/views/applications_listing.php b/views/applications_listing.php old mode 100644 new mode 100755 index 14652d3..7d26f9a --- a/views/applications_listing.php +++ b/views/applications_listing.php @@ -14,6 +14,7 @@ + @@ -26,7 +27,7 @@ - + @@ -105,23 +106,37 @@ $('td:eq(0)', nRow).html(link); // Localize Obtained From - var obtained_from=$('td:eq(5)', nRow).html(); + var obtained_from=$('td:eq(6)', nRow).html(); obtained_from = obtained_from == 'unknown' ? i18n.t('unknown') : obtained_from = obtained_from == 'mac_app_store' ? i18n.t('applications.mac_app_store') : obtained_from = obtained_from == 'apple' ? "Apple": (obtained_from === 'identified_developer' ? i18n.t('applications.identified_developer') : obtained_from) - $('td:eq(5)', nRow).html(obtained_from) + $('td:eq(6)', nRow).html(obtained_from) // Format date - var event = parseInt($('td:eq(6)', nRow).html()); + var event = parseInt($('td:eq(7)', nRow).html()); var date = new Date(event * 1000); - $('td:eq(6)', nRow).html(''+ moment(date).format('llll')+''); - - // 64-bit Yes/No - var bit64=$('td:eq(7)', nRow).html(); - bit64 = bit64 == '1' ? i18n.t('Yes') : - (bit64 === '0' ? i18n.t('No') : '') - $('td:eq(7)', nRow).html(bit64) + $('td:eq(7)', nRow).html(''+ moment(date).format('llll')+''); + + // runtime_environment + var colbit=$('td:eq(8)', nRow).html(); + var colvar=$('td:eq(9)', nRow).html(); + colvar = colvar == 'arch_x86' && colbit == '1' ? 'Intel 64-bit' : + colvar = colvar == 'arch_x86' && colbit == '0' ? 'Intel 32-bit' : + colvar = colvar == 'arch_i64' ? 'Intel 64-bit' : + colvar = colvar == 'arch_i32_i64' ? 'Intel 32/64-bit' : + colvar = colvar == 'arch_i32' ? 'Intel 32-bit' : + colvar = colvar == 'arch_arm_i64' ? 'Universal 2' : + colvar = colvar == 'arch_ios' ? 'Apple Silicon' : + colvar = colvar == 'arch_arm' ? 'Apple Silicon' : + (colvar == 'arch_arm' ? 'Apple Silicon' : colvar) + $('td:eq(9)', nRow).html(colvar) + + // has64bit + var colvar=$('td:eq(8)', nRow).html(); + colvar = colvar == '1' ? i18n.t('yes') : + (colvar == '0' ? i18n.t('no') : '') + $('td:eq(8)', nRow).html(colvar) } }); diff --git a/views/applications_tab.php b/views/applications_tab.php old mode 100644 new mode 100755 index a39e3c0..eedd96e --- a/views/applications_tab.php +++ b/views/applications_tab.php @@ -1,51 +1,102 @@

+
-

- - - - - - - - - - - - - - - - - retrieveMany('serial_number=?', array($serial_number)) as $item): ?> - name)); ?> + +
+
+ + + + + + + + + + + + + + + - - - - - - - - - + - + +
name; ?>version; ?>signed_by; ?>obtained_from); ?>last_modified); ?>has64bit); ?>runtime_environment); ?>path; ?>info; ?>
+ - - + $(document).on('appReady', function(e, lang) { + + // Get applications data + $.getJSON( appUrl + '/module/applications/get_data/' + serialNumber, function( data ) { + if( ! data ){ + $('#applications-msg').text(i18n.t('no_data')); + $('#applications-cnt').text(0); + + } else { + // Hide + $('#applications-msg').text(''); + $('#applications-table-view').removeClass('hide'); + + // Set count of applications + $('#applications-cnt').text(data.length); + + $('#applications-history-table').DataTable({ + data: data, + order: [[0,'asc']], + autoWidth: false, + columns: [ + { data: 'name' }, + { data: 'version' }, + { data: 'bundle_version' }, + { data: 'signed_by' }, + { data: 'obtained_from' }, + { data: 'last_modified' }, + { data: 'has64bit' }, + { data: 'runtime_environment' }, + { data: 'path' }, + { data: 'info' } + ], + createdRow: function( nRow, aData, iDataIndex ) { + // Localize Obtained From + var obtained_from=$('td:eq(4)', nRow).html(); + obtained_from = obtained_from == 'unknown' ? i18n.t('unknown') : + obtained_from = obtained_from == 'mac_app_store' ? i18n.t('applications.mac_app_store') : + obtained_from = obtained_from == 'apple' ? "Apple": + (obtained_from == 'identified_developer' ? i18n.t('applications.identified_developer') : obtained_from) + $('td:eq(4)', nRow).html(obtained_from) + + // Format date + var event = parseInt($('td:eq(5)', nRow).html()); + var date = new Date(event * 1000); + $('td:eq(5)', nRow).html(''+moment(date).format('llll')+''); + + // runtime_environment + var colbit=$('td:eq(6)', nRow).html(); + var colvar=$('td:eq(7)', nRow).html(); + colvar = colvar == 'arch_x86' && colbit == '1' ? 'Intel 64-bit' : + colvar = colvar == 'arch_x86' && colbit == '0' ? 'Intel 32-bit' : + colvar = colvar == 'arch_i64' ? 'Intel 64-bit' : + colvar = colvar == 'arch_i32_i64' ? 'Intel 32/64-bit' : + colvar = colvar == 'arch_i32' ? 'Intel 32-bit' : + colvar = colvar == 'arch_arm_i64' ? 'Universal 2' : + colvar = colvar == 'arch_ios' ? 'Apple Silicon' : + colvar = colvar == 'arch_arm' ? 'Apple Silicon' : + (colvar == 'arch_arm' ? 'Apple Silicon' : colvar) + $('td:eq(7)', nRow).html(colvar) + + // has64bit + var colvar=$('td:eq(6)', nRow).html(); + colvar = colvar == '1' ? i18n.t('yes') : + (colvar == '0' ? i18n.t('no') : '') + $('td:eq(6)', nRow).html(colvar) + } + }); + } + }); + }); + \ No newline at end of file