diff -Nru click-reviewers-tools-0.7/bin/click-check-content-hub click-reviewers-tools-0.8/bin/click-check-content-hub --- click-reviewers-tools-0.7/bin/click-check-content-hub 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/click-check-content-hub 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +'''click-check-content-hub: perform click content-hub checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function +import sys + +import clickreviews.cr_common as cr_common +import clickreviews.cr_content_hub as cr_content_hub + +if __name__ == "__main__": + if len(sys.argv) < 2: + cr_common.error("Must give path to click package") + + review = cr_content_hub.ClickReviewContentHub(sys.argv[1]) + review.do_checks() + rc = review.do_report() + sys.exit(rc) diff -Nru click-reviewers-tools-0.7/bin/click-check-online-accounts click-reviewers-tools-0.8/bin/click-check-online-accounts --- click-reviewers-tools-0.7/bin/click-check-online-accounts 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/click-check-online-accounts 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +'''click-check-online-accounts: perform click online accounts checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function +import sys + +import clickreviews.cr_common as cr_common +import clickreviews.cr_online_accounts as cr_online_accounts + +if __name__ == "__main__": + if len(sys.argv) < 2: + cr_common.error("Must give path to click package") + + review = cr_online_accounts.ClickReviewAccounts(sys.argv[1]) + review.do_checks() + rc = review.do_report() + sys.exit(rc) diff -Nru click-reviewers-tools-0.7/bin/click-check-push-helper click-reviewers-tools-0.8/bin/click-check-push-helper --- click-reviewers-tools-0.7/bin/click-check-push-helper 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/click-check-push-helper 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +'''click-check-push-helper: perform click push-helper checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function +import sys + +import clickreviews.cr_common as cr_common +import clickreviews.cr_push_helper as cr_push_helper + +if __name__ == "__main__": + if len(sys.argv) < 2: + cr_common.error("Must give path to click package") + + review = cr_push_helper.ClickReviewPushHelper(sys.argv[1]) + review.do_checks() + rc = review.do_report() + sys.exit(rc) diff -Nru click-reviewers-tools-0.7/bin/click-check-scope click-reviewers-tools-0.8/bin/click-check-scope --- click-reviewers-tools-0.7/bin/click-check-scope 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/click-check-scope 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +'''click-check-scope: perform click scope checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function +import sys + +import clickreviews.cr_common as cr_common +import clickreviews.cr_scope as cr_scope + +if __name__ == "__main__": + if len(sys.argv) < 2: + cr_common.error("Must give path to click package") + + review = cr_scope.ClickReviewScope(sys.argv[1]) + review.do_checks() + rc = review.do_report() + sys.exit(rc) diff -Nru click-reviewers-tools-0.7/bin/click-check-url-dispatcher click-reviewers-tools-0.8/bin/click-check-url-dispatcher --- click-reviewers-tools-0.7/bin/click-check-url-dispatcher 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/click-check-url-dispatcher 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +'''click-check-url_dispatcher: perform click url dispatcher checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function +import sys + +import clickreviews.cr_common as cr_common +import clickreviews.cr_url_dispatcher as cr_url_dispatcher + +if __name__ == "__main__": + if len(sys.argv) < 2: + cr_common.error("Must give path to click package") + + review = cr_url_dispatcher.ClickReviewUrlDispatcher(sys.argv[1]) + review.do_checks() + rc = review.do_report() + sys.exit(rc) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/apparmor_policy.py click-reviewers-tools-0.8/bin/clickreviews/apparmor_policy.py --- click-reviewers-tools-0.7/bin/clickreviews/apparmor_policy.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/apparmor_policy.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,37 @@ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import clickreviews.remote + +USER_DATA_FILE = os.path.join(clickreviews.remote.DATA_DIR, + 'apparmor-easyprof-ubuntu.json') + +# XXX: This is a hack and will be gone, as soon as myapps has an API for this. +AA_POLICY_DATA_URL = \ + "http://bazaar.launchpad.net/~click-reviewers/click-reviewers-tools/trunk/view/head:/data/apparmor-easyprof-ubuntu.json" + + +def get_policy_file(fn): + if fn is None: + fn = USER_DATA_FILE + clickreviews.remote.get_remote_file(fn, AA_POLICY_DATA_URL) + + +class ApparmorPolicy(object): + def __init__(self, local_copy_fn=None): + self.policy = clickreviews.remote.read_cr_file(USER_DATA_FILE, + AA_POLICY_DATA_URL, + local_copy_fn) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_common.py click-reviewers-tools-0.8/bin/clickreviews/cr_common.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_common.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_common.py 2014-07-25 13:47:22.000000000 +0000 @@ -60,6 +60,10 @@ self.click_package = fn self._check_path_exists() if not self.click_package.endswith(".click"): + if self.click_package.endswith(".deb"): + error("filename does not end with '.click', but '.deb' " + "instead. See http://askubuntu.com/a/485544/94326 for " + "how click packages are different.") error("filename does not end with '.click'") self.review_type = review_type diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_content_hub.py click-reviewers-tools-0.8/bin/clickreviews/cr_content_hub.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_content_hub.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_content_hub.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,115 @@ +'''cr_content_hub.py: click content-hub checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import json +import os + + +class ClickReviewContentHub(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "content_hub") + + self.valid_keys = ['destination', 'share', 'source'] + + self.content_hub_files = dict() # click-show-files and tests + self.content_hub = dict() + for app in self.manifest['hooks']: + if 'content-hub' not in self.manifest['hooks'][app]: + # msg("Skipped missing content-hub hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['content-hub'], str): + error("manifest malformed: hooks/%s/urls is not str" % app) + (full_fn, jd) = self._extract_content_hub(app) + self.content_hub_files[app] = full_fn + self.content_hub[app] = jd + + def _extract_content_hub(self, app): + '''Get content-hub hook content''' + c = self.manifest['hooks'][app]['content-hub'] + fn = os.path.join(self.unpack_dir, c) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + fh = open_file_read(fn) + contents = "" + for line in fh.readlines(): + contents += line + fh.close() + + try: + jd = json.loads(contents) + except Exception as e: + error("content-hub json unparseable: %s (%s):\n%s" % (bn, + str(e), contents)) + + if not isinstance(jd, dict): + error("content-hub json is malformed: %s:\n%s" % (bn, contents)) + + return (fn, jd) + + def check_valid(self): + '''Check validity of content-hub entries''' + for app in sorted(self.content_hub): + for k in self.content_hub[app].keys(): + t = "info" + n = "valid_%s_%s" % (app, k) + s = "OK" + + if not isinstance(self.content_hub[app][k], list): + t = "error" + s = "'%s' is not a list" % k + elif len(self.content_hub[app][k]) < 1: + t = "error" + s = "'%s' is empty" % k + self._add_result(t, n, s) + if t == "error": + continue + + for v in self.content_hub[app][k]: + t = "info" + n = "valid_%s_%s_value" % (app, k) + s = "OK" + if not isinstance(v, str): + t = "error" + s = "'%s' is not a string" % k + elif v == "": + t = "error" + s = "'%s' is empty" % k + self._add_result(t, n, s) + + def check_unknown_keys(self): + '''Check unknown''' + for app in sorted(self.content_hub): + unknown = [] + t = "info" + n = "unknown_%s" % app + s = "OK" + for key in self.content_hub[app].keys(): + if key not in self.valid_keys: + unknown.append(key) + if len(unknown) == 1: + t = "warn" + s = "Unknown field '%s'" % unknown[0] + elif len(unknown) > 1: + t = "warn" + s = "Unknown fields '%s'" % ", ".join(unknown) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_desktop.py click-reviewers-tools-0.8/bin/clickreviews/cr_desktop.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_desktop.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_desktop.py 2014-07-25 13:47:22.000000000 +0000 @@ -16,7 +16,7 @@ from __future__ import print_function -from clickreviews.cr_common import ClickReview, error, open_file_read +from clickreviews.cr_common import ClickReview, error, open_file_read, msg import glob import json import os @@ -36,7 +36,15 @@ self.desktop_hook_entries = 0 for app in self.manifest['hooks']: if 'desktop' not in self.manifest['hooks'][app]: - error("could not find desktop hook for '%s'" % app) + if 'scope' in self.manifest['hooks'][app]: + # msg("Skipped missing desktop hook for scope '%s'" % app) + continue + if 'push-helper' in self.manifest['hooks'][app]: + # msg("Skipped missing desktop hook for push-helper '%s'" % + # app) + continue + else: + error("could not find desktop hook for '%s'" % app) if not isinstance(self.manifest['hooks'][app]['desktop'], str): error("manifest malformed: hooks/%s/desktop is not str" % app) self.desktop_hook_entries += 1 @@ -102,19 +110,13 @@ def check_desktop_file(self): '''Check desktop file''' t = 'info' - n = 'files_available' - s = 'OK' - if len(self._get_desktop_files().keys()) < 1: - t = 'error' - s = 'No .desktop files available.' - self._add_result(t, n, s) - - t = 'info' n = 'files_usable' s = 'OK' if len(self._get_desktop_files().keys()) != self.desktop_hook_entries: t = 'error' s = 'Could not use all specified .desktop files' + elif self.desktop_hook_entries == 0: + s = 'Skipped: could not find any desktop files' self._add_result(t, n, s) def check_desktop_file_valid(self): diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_functional.py click-reviewers-tools-0.8/bin/clickreviews/cr_functional.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_functional.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_functional.py 2014-07-25 13:47:22.000000000 +0000 @@ -167,3 +167,24 @@ l = "http://askubuntu.com/questions/417342/what-does-functional-qml-application-uses-qtwebkit-mean/417343" self._add_result(t, n, s, l) + + def check_friends(self): + '''Check that QML applications don't use deprecated Friends API''' + t = 'info' + n = 'qml_application_uses_friends' + s = "OK" + l = None + + qmls = [] + pat_mv = re.compile(r'\n\s*import\s+Friends') + for i in self.qml_files: + qml = open_file_read(i).read() + if pat_mv.search(qml): + qmls.append(os.path.relpath(i, self.unpack_dir)) + + if len(qmls) > 0: + t = 'error' + s = "Found files that use deprecated Friends API: %s" % " ,".join(qmls) + l = "http://askubuntu.com/questions/497551/what-does-functional-qml-application-uses-friends-mean" + + self._add_result(t, n, s, l) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_lint.py click-reviewers-tools-0.8/bin/clickreviews/cr_lint.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_lint.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_lint.py 2014-07-25 13:47:22.000000000 +0000 @@ -21,6 +21,7 @@ import os import re +from clickreviews.frameworks import Frameworks from clickreviews.cr_common import ClickReview, open_file_read, cmd CONTROL_FILE_NAMES = ["control", "manifest", "md5sums", "preinst"] @@ -60,6 +61,20 @@ self._list_all_compiled_binaries() + self.known_hooks = ['account-application', + 'account-provider', + 'account-qml-plugin', + 'account-service', + 'apparmor', + 'content-hub', + 'desktop', + 'pay-ui', + 'push-helper', + 'scope', + 'urls'] + + self.redflagged_hooks = ['pay-ui'] + def _list_control_files(self): '''List all control files with their full path.''' for i in CONTROL_FILE_NAMES: @@ -267,11 +282,20 @@ # Some checks are already handled in # cr_common.py:_verify_manifest_structure() - # We don't support multiple apps in 13.10 - if len(self.manifest['hooks'].keys()) != 1: - self._add_result('error', 'hooks', - "more than one app key specified in hooks") - return + # While we support multiple apps in the hooks db, we don't support + # multiple apps specifying desktop hooks. Eg, it is ok to specify a + # scope, an app and a push-helper, but it isn't ok to specify two apps + t = 'info' + n = 'hooks_multiple_apps' + s = 'OK' + count = 0 + for app in self.manifest['hooks']: + if "desktop" in self.manifest['hooks'][app]: + count += 1 + if count > 1: + t = 'error' + s = 'more than one desktop app specified in hooks' + self._add_result(t, n, s) # Verify keys are well-formatted for app in self.manifest['hooks']: @@ -284,22 +308,78 @@ self._add_result(t, n, s) # Verify we have the required hooks - required = ['apparmor', 'desktop'] + required = ['apparmor'] for f in required: for app in self.manifest['hooks']: t = 'info' n = 'hooks_%s_%s' % (app, f) s = "OK" - if f == "apparmor": + if f in list(filter(lambda a: a.startswith('account-'), + self.known_hooks)): + s = "OK (run check-online-accounts for more checks)" + elif f == "apparmor": s = "OK (run check-security for more checks)" + elif f == "content-hub": + s = "OK (run check-content-hub for more checks)" elif f == "desktop": s = "OK (run check-desktop for more checks)" + elif f == "scope": + s = "OK (run check-scope for more checks)" + elif f == "urls": + s = "OK (run check-url-dispatcher for more checks)" if f not in self.manifest['hooks'][app]: t = 'error' s = "'%s' hook not found for '%s'" % (f, app) self._add_result(t, n, s) + mutually_exclusive = ['scope', 'desktop'] + for app in self.manifest['hooks']: + found = [] + for i in mutually_exclusive: + if i in self.manifest['hooks'][app]: + found.append(i) + if len(found) > 1: + t = 'error' + s = "'%s' hooks should not be used together" % ", ".join(found) + self._add_result(t, n, s) + + def check_hooks_unknown(self): + '''Check if have any unknown hooks''' + t = 'info' + n = 'unknown hooks' + s = 'OK' + + # Verify keys are well-formatted + for app in self.manifest['hooks']: + for hook in self.manifest['hooks'][app]: + t = 'info' + n = 'hooks_%s_%s_known' % (app, hook) + s = "OK" + if hook not in self.known_hooks: + t = 'warn' + s = "unknown hook '%s' in %s" % (hook, app) + self._add_result(t, n, s) + + def check_hooks_redflagged(self): + '''Check if have any redflagged hooks''' + t = 'info' + n = 'redflagged hooks' + s = 'OK' + + for app in self.manifest['hooks']: + found = [] + t = 'info' + n = 'hooks_redflag_%s' % (app) + s = "OK" + for hook in self.manifest['hooks'][app]: + if hook in self.redflagged_hooks: + found.append(hook) + if len(found) > 0: + t = 'error' + s = "(MANUAL REVIEW) '%s' not allowed" % ", ".join(found) + self._add_result(t, n, s) + def check_external_symlinks(self): '''Check if symlinks in the click package go out to the system.''' t = 'info' @@ -447,6 +527,9 @@ t = 'info' s = "OK (email '%s' long, but special case of core apps " \ "'com.ubuntu.*')" % self.email + elif self.email == "ubuntu-devel-discuss@lists.ubuntu.com": + t = 'info' + s = "OK (email '%s' long, but special case" % self.email else: t = 'error' s = "(EMAIL NEEDS HUMAN REVIEW) email domain too " \ @@ -515,26 +598,35 @@ def check_framework(self): '''Check framework()''' - t = 'info' n = 'framework' - s = 'OK' - if self.manifest['framework'] not in self.valid_frameworks: - t = 'error' - s = "'%s' is not a supported framework" % \ + l = "http://askubuntu.com/questions/460512/what-framework-should-i-use-in-my-manifest-file" + local_copy = os.path.join(os.path.dirname(__file__), + '../data/frameworks.json') + frameworks = Frameworks(local_copy) + if self.manifest['framework'] in frameworks.AVAILABLE_FRAMEWORKS: + t = 'info' + s = 'OK' + self._add_result(t, n, s) + # If it's an available framework, we're done checking + return + elif self.manifest['framework'] in frameworks.DEPRECATED_FRAMEWORKS: + t = 'warn' + s = "'%s' is deprecated. Please use a newer framework" % \ self.manifest['framework'] - self._add_result(t, n, s) - - obsolete_frameworks = ['ubuntu-sdk-13.10'] - t = 'info' - n = 'current framework' - s = 'OK' - l = None - if self.manifest['framework'] in obsolete_frameworks: + self._add_result(t, n, s, l) + return + elif self.manifest['framework'] in frameworks.OBSOLETE_FRAMEWORKS: t = 'error' s = "'%s' is obsolete. Please use a newer framework" % \ self.manifest['framework'] - l = "http://askubuntu.com/questions/460512/what-framework-should-i-use-in-my-manifest-file" - self._add_result(t, n, s, l) + self._add_result(t, n, s, l) + return + else: + # None of the above checks triggered, this is an unknown framework + t = 'error' + s = "'%s' is not a supported framework" % \ + self.manifest['framework'] + self._add_result(t, n, s, l) def check_click_local_extensions(self): '''Report any click local extensions''' diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_online_accounts.py click-reviewers-tools-0.8/bin/clickreviews/cr_online_accounts.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_online_accounts.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_online_accounts.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,231 @@ +'''cr_online_accounts.py: click online accounts''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import os +# http://lxml.de/tutorial.html +import lxml.etree as etree + + +class ClickReviewAccounts(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "online_accounts") + + self.accounts_files = dict() + self.accounts = dict() + + self.account_hooks = ['account-application', + 'account-provider', + 'account-qml-plugin', + 'account-service'] + for app in self.manifest['hooks']: + for h in self.account_hooks: + if h not in self.manifest['hooks'][app]: + # msg("Skipped missing %s hook for '%s'" % (h, app)) + continue + if not isinstance(self.manifest['hooks'][app][h], str): + error("manifest malformed: hooks/%s/%s is not a str" % ( + app, h)) + + (full_fn, xml) = self._extract_account(app, h) + + if app not in self.accounts_files: + self.accounts_files[app] = dict() + self.accounts_files[app][h] = full_fn + + if app not in self.accounts: + self.accounts[app] = dict() + self.accounts[app][h] = xml + + def _extract_account(self, app, account_type): + '''Extract accounts''' + a = self.manifest['hooks'][app][account_type] + fn = os.path.join(self.unpack_dir, a) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + # qml-plugin points to a QML file, so just set that we have the + # the hook present for now + if account_type == "account-qml-plugin": + return (fn, True) + else: + try: + tree = etree.parse(fn) + xml = tree.getroot() + except Exception as e: + error("accounts xml unparseable: %s (%s):\n%s" % (bn, str(e), + contents)) + return (fn, xml) + + def check_application(self): + '''Check application''' + for app in sorted(self.accounts.keys()): + account_type = "account-application" + + t = 'info' + n = '%s_%s_root' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + + root_tag = self.accounts[app][account_type].tag.lower() + if root_tag != "application": + t = 'error' + s = "'%s' is not 'application'" % root_tag + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_id' % (app, account_type) + s = "OK" + expected_id = "%s_%s" % (self.manifest["name"], app) + if "id" not in self.accounts[app][account_type].keys(): + t = 'error' + s = "Could not find 'id' in application tag" + elif self.accounts[app][account_type].get("id") != expected_id: + t = 'error' + s = "id '%s' != '%s'" % ( + self.accounts[app][account_type].get("id"), + expected_id) + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_services' % (app, account_type) + s = "OK" + if self.accounts[app][account_type].find("services") is None: + t = 'error' + s = "Could not find '' tag" + self._add_result(t, n, s) + + if t == 'error': + continue + + t = 'info' + n = '%s_%s_service' % (app, account_type) + s = "OK" + if self.accounts[app][account_type].find("./services/service") \ + is None: + t = 'error' + s = "Could not find '' tag under " + self._add_result(t, n, s) + + def check_service(self): + '''Check service''' + for app in sorted(self.accounts.keys()): + account_type = "account-service" + + t = 'info' + n = '%s_%s_root' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + + root_tag = self.accounts[app][account_type].tag.lower() + if root_tag != "service": + t = 'error' + s = "'%s' is not 'service'" % root_tag + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_id' % (app, account_type) + s = "OK" + expected_id = "%s_%s" % (self.manifest["name"], app) + if "id" not in self.accounts[app][account_type].keys(): + t = 'error' + s = "Could not find 'id' in service tag" + elif self.accounts[app][account_type].get("id") != expected_id: + t = 'error' + s = "id '%s' != '%s'" % ( + self.accounts[app][account_type].get("id"), + expected_id) + self._add_result(t, n, s) + + if t == 'error': + continue + + for tag in ['type', 'name', 'provider']: + t = 'info' + n = '%s_%s_%s' % (app, account_type, tag) + s = "OK" + if self.accounts[app][account_type].find(tag) is None: + t = 'error' + s = "Could not find '<%s>' tag" % tag + self._add_result(t, n, s) + + def check_provider(self): + '''Check provider''' + for app in sorted(self.accounts.keys()): + account_type = "account-provider" + + t = 'info' + n = '%s_%s' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + else: + t = 'error' + s = "(MANUAL REVIEW) '%s' not allowed" % account_type + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_root' % (app, account_type) + s = "OK" + root_tag = self.accounts[app][account_type].tag.lower() + if root_tag != "provider": + t = 'error' + s = "'%s' is not 'provider'" % root_tag + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_id' % (app, account_type) + s = "OK" + expected_id = "%s_%s" % (self.manifest["name"], app) + if "id" not in self.accounts[app][account_type].keys(): + t = 'error' + s = "Could not find 'id' in provider tag" + elif self.accounts[app][account_type].get("id") != expected_id: + t = 'error' + s = "id '%s' != '%s'" % ( + self.accounts[app][account_type].get("id"), + expected_id) + self._add_result(t, n, s) + + def check_qml_plugin(self): + '''Check qml-plugin''' + for app in sorted(self.accounts.keys()): + account_type = "account-qml-plugin" + + t = 'info' + n = '%s_%s' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + else: + t = 'error' + s = "(MANUAL REVIEW) '%s' not allowed" % account_type + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_push_helper.py click-reviewers-tools-0.8/bin/clickreviews/cr_push_helper.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_push_helper.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_push_helper.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,132 @@ +'''cr_push_helper.py: click push-helper checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import json +import os + + +class ClickReviewPushHelper(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "push_helper") + + self.required_keys = ['exec'] + self.optional_keys = ['app_id'] + + self.push_helper_files = dict() # click-show-files and tests + self.push_helper = dict() + for app in self.manifest['hooks']: + if 'push-helper' not in self.manifest['hooks'][app]: + # msg("Skipped missing push-helper hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['push-helper'], str): + error("manifest malformed: hooks/%s/urls is not str" % app) + (full_fn, jd) = self._extract_push_helper(app) + self.push_helper_files[app] = full_fn + self.push_helper[app] = jd + + def _extract_push_helper(self, app): + '''Get push-helper hook content''' + c = self.manifest['hooks'][app]['push-helper'] + fn = os.path.join(self.unpack_dir, c) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + fh = open_file_read(fn) + contents = "" + for line in fh.readlines(): + contents += line + fh.close() + + try: + jd = json.loads(contents) + except Exception as e: + error("push-helper json unparseable: %s (%s):\n%s" % (bn, + str(e), contents)) + + if not isinstance(jd, dict): + error("push-helper json is malformed: %s:\n%s" % (bn, contents)) + + return (fn, jd) + + def check_valid(self): + '''Check validity of push-helper entries''' + for app in sorted(self.push_helper): + for k in self.push_helper[app].keys(): + t = "info" + n = "valid_%s_%s" % (app, k) + s = "OK" + + if not isinstance(self.push_helper[app][k], str): + t = "error" + s = "'%s' is not a string" % k + elif self.push_helper[app][k] == "": + t = "error" + s = "'%s' is empty" % k + self._add_result(t, n, s) + + for k in self.required_keys: + t = "info" + n = "valid_%s_required_%s" % (app, k) + s = "OK" + if k not in self.push_helper[app]: + t = "error" + s = "'%s' is missing" % k + self._add_result(t, n, s) + + def check_unknown_keys(self): + '''Check unknown''' + for app in sorted(self.push_helper): + unknown = [] + t = "info" + n = "unknown_%s" % app + s = "OK" + for key in self.push_helper[app].keys(): + if key not in self.required_keys and \ + key not in self.optional_keys: + unknown.append(key) + if len(unknown) == 1: + t = "warn" + s = "Unknown field '%s'" % unknown[0] + elif len(unknown) > 1: + t = "warn" + s = "Unknown fields '%s'" % ", ".join(unknown) + self._add_result(t, n, s) + + def check_hooks(self): + '''Verify combinations of click hooks with the push-helper hook''' + for app in sorted(self.manifest['hooks']): + if app not in self.push_helper: + continue + + t = "info" + n = "other_hooks_%s" % app + s = "OK" + + bad = [] + for hook in self.manifest['hooks'][app]: + # Only the apparmor hook can be used with the push-helper + if hook not in ['push-helper', 'apparmor']: + bad.append(hook) + if len(bad) > 0: + t = 'error' + s = "Found unusual hooks with push-helper: %s" % ", ".join(bad) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_scope.py click-reviewers-tools-0.8/bin/clickreviews/cr_scope.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_scope.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_scope.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,135 @@ +'''cr_scope.py: click scope''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, msg +import configparser +import os + + +class ClickReviewScope(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "scope") + + self.scopes = dict() + for app in self.manifest['hooks']: + if 'scope' not in self.manifest['hooks'][app]: + # msg("Skipped missing scope hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['scope'], str): + error("manifest malformed: hooks/%s/scope is not str" % app) + self.scopes[app] = self._extract_scopes(app) + + def _extract_scopes(self, app): + '''Get scopes''' + d = dict() + + s = self.manifest['hooks'][app]['scope'] + fn = os.path.join(self.unpack_dir, s) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + elif not os.path.isdir(fn): + error("'%s' is not a directory" % bn) + + ini_fn = os.path.join(fn, "%s.ini" % self.manifest['name']) + ini_fn_bn = os.path.relpath(ini_fn, self.unpack_dir) + if not os.path.exists(ini_fn): + error("Could not find scope INI file '%s'" % ini_fn_bn) + try: + d["scope_config"] = configparser.ConfigParser() + d["scope_config"].read(ini_fn) + except Exception: + error("scope config unparseable: %s (%s)" % (ini_fn_bn, str(e))) + + d["dir"] = fn + d["dir_rel"] = bn + d["ini_file"] = ini_fn + d["ini_file_rel"] = ini_fn_bn + + return d + + def check_scope_ini(self): + '''Check scope .ini file''' + for app in sorted(self.scopes.keys()): + t = 'info' + n = 'ini_%s_scope_section' % app + s = "OK" + + if len(self.scopes[app]["scope_config"].sections()) > 1: + t = 'error' + s = "'%s' has too many sections: %s" % ( + self.scopes[app]["ini_file_rel"], + ", ".join(self.scopes[app]["scope_config"].sections())) + elif "ScopeConfig" not in \ + self.scopes[app]["scope_config"].sections(): + t = 'error' + s = "Could not find 'ScopeConfig' in '%s'" % ( + self.scopes[app]["ini_file_rel"]) + self._add_result(t, n, s) + continue + self._add_result(t, n, s) + + # Make these all lower case for easier comparisons + required = ['scoperunner', + 'displayname', + 'icon', + 'searchhint'] + optional = ['description', + 'author', + 'art'] + + missing = [] + t = 'info' + n = 'ini_%s_scope_required_fields' % (app) + s = "OK" + for r in required: + if r not in self.scopes[app]["scope_config"]['ScopeConfig']: + missing.append(r) + if len(missing) == 1: + t = 'error' + s = "Missing required field in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + missing[0]) + elif len(missing) > 1: + t = 'error' + s = "Missing required fields in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + ", ".join(missing)) + self._add_result(t, n, s) + + t = 'info' + n = 'ini_%s_scope_unknown_fields' % (app) + s = 'OK' + unknown = [] + for f in self.scopes[app]["scope_config"]['ScopeConfig'].keys(): + if f.lower() not in required and f.lower() not in optional: + unknown.append(f.lower()) + + if len(unknown) == 1: + t = 'warn' + s = "Unknown field in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + unknown[0]) + elif len(unknown) > 1: + t = 'warn' + s = "Unknown fields in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + ", ".join(unknown)) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_security.py click-reviewers-tools-0.8/bin/clickreviews/cr_security.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_security.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_security.py 2014-07-25 13:47:22.000000000 +0000 @@ -18,25 +18,21 @@ from clickreviews.cr_common import ClickReview, error, warn import clickreviews.cr_common as cr_common +import clickreviews.apparmor_policy as apparmor_policy import glob import json import os -easyprof_dir = "/usr/share/apparmor/easyprof" -if not os.path.isdir(easyprof_dir): - error("Error importing easyprof. Please install apparmor-easyprof") -if not os.path.isdir(os.path.join(easyprof_dir, "templates/ubuntu")): - error("Error importing easyprof. Please install apparmor-easyprof-ubuntu") - -import apparmor.easyprof - class ClickReviewSecurity(ClickReview): '''This class represents click lint reviews''' def __init__(self, fn): ClickReview.__init__(self, fn, "security") - self.supported_policy_versions = self._get_supported_policy_versions() + local_copy = os.path.join(os.path.dirname(__file__), + '../data/apparmor-easyprof-ubuntu.json') + p = apparmor_policy.ApparmorPolicy(local_copy) + self.aa_policy = p.policy self.all_fields = ['abstractions', 'author', @@ -70,6 +66,8 @@ 'video', 'webview'] + self.allowed_push_helper_policy_groups = ['push-notification-client'] + self.redflag_templates = ['unconfined'] self.extraneous_templates = ['ubuntu-sdk', 'default'] @@ -79,6 +77,7 @@ # like 'ubuntu-sdk-14.04-html5', 'ubuntu-sdk-14.04-platform', etc self.major_framework_policy = {'ubuntu-sdk-13.10': 1.0, 'ubuntu-sdk-14.04': 1.1, + 'ubuntu-sdk-14.10': 1.2, } self.security_manifests = dict() @@ -132,31 +131,6 @@ k, mp)) return m - def _get_policy_group_meta(self, group, meta, vendor, version): - '''Get meta-information from the policy group''' - cmd_args = ['--show-policy-group', '--policy-groups=%s' % group, - '--policy-version=%s' % version, - '--policy-vendor=%s' % vendor] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(None, options) - tmp = easyp.get_policygroup(group) - except apparmor.easyprof.AppArmorException: - warn("'%s' failed" % " ".join(cmd_args)) - return "" - - text = "" - for line in tmp.splitlines(): - if line.startswith("# %s: " % meta): - text = line.split(':', 1)[1].strip() - elif text != "": - if line.startswith("# "): - text += line[2:] - else: - break - - return text - def _get_security_manifest(self, app): '''Get the security manifest for app''' if app not in self.manifest['hooks']: @@ -168,21 +142,72 @@ m = self.security_manifests[f] return (f, m) - def _get_supported_policy_versions(self): + def _get_policy_versions(self, vendor): '''Get the supported AppArmor policy versions''' - version_dirs = sorted(glob.glob("%s/templates/ubuntu/*" % - easyprof_dir)) + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return None + supported_policy_versions = [] - for d in version_dirs: - if not os.path.isdir(d): - continue - try: - supported_policy_versions.append(float(os.path.basename(d))) - except TypeError: - continue - supported_policy_versions = sorted(supported_policy_versions) + for i in self.aa_policy[vendor].keys(): + supported_policy_versions.append("%.1f" % float(i)) + + return sorted(supported_policy_versions) - return supported_policy_versions + def _get_templates(self, vendor, version, aa_type="all"): + '''Get templates by type''' + templates = [] + if aa_type == "all": + for k in self.aa_policy[vendor][version]['templates'].keys(): + templates += self.aa_policy[vendor][version]['templates'][k] + else: + templates = self.aa_policy[vendor][version]['templates'][aa_type] + + return sorted(templates) + + def _has_policy_version(self, vendor, version): + '''Determine if has specified policy version''' + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return False + + if str(version) not in self.aa_policy[vendor]: + return False + return True + + def _get_highest_policy_version(self, vendor): + '''Determine highest policy version for the vendor''' + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return None + + return float(sorted(self.aa_policy[vendor].keys())[-1]) + + def _get_policy_groups(self, vendor, version, aa_type="all"): + '''Get policy groups by type''' + groups = [] + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return groups + + if not self._has_policy_version(vendor, version): + error("Could not find version '%s'" % version, do_exit=False) + return groups + + v = str(version) + if aa_type == "all": + for k in self.aa_policy[vendor][v]['policy_groups'].keys(): + groups += self.aa_policy[vendor][v]['policy_groups'][k] + else: + groups = self.aa_policy[vendor][v]['policy_groups'][aa_type] + + return sorted(groups) + + def _get_policy_group_type(self, vendor, version, policy_group): + '''Return policy group type''' + for t in self.aa_policy[vendor][version]['policy_groups']: + if policy_group in self.aa_policy[vendor][version]['policy_groups'][t]: + return t def check_policy_vendor(self): '''Check policy_vendor''' @@ -213,17 +238,13 @@ if 'policy_vendor' in m: vendor = m['policy_vendor'] version = str(m['policy_version']) - cmd_args = ['--list-templates', '--policy-vendor=%s' % vendor, - '--policy-version=%s' % version] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - try: - apparmor.easyprof.AppArmorEasyProfile(None, options) - except Exception: + if vendor not in self.aa_policy or \ + not self._has_policy_version(vendor, version): t = 'error' s = 'could not find policy for %s/%s' % (vendor, version) self._add_result(t, n, s) - highest = sorted(self.supported_policy_versions)[-1] + highest = self._get_highest_policy_version(vendor) t = 'info' n = 'policy_version_is_highest (%s, %s)' % (str(highest), f) s = "OK" @@ -287,31 +308,16 @@ if 'policy_vendor' in m: vendor = m['policy_vendor'] version = str(m['policy_version']) - cmd_args = ['--list-templates', '--policy-vendor=%s' % vendor, - '--policy-version=%s' % version] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - templates = [] - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(None, options) - templates = easyp.get_templates() - except Exception: - t = 'error' - s = 'could not find policy for %s/%s' % (vendor, version) - self._add_result(t, n, s) - continue + + templates = self._get_templates(vendor, version) if len(templates) < 1: t = 'error' s = 'could not find templates' self._add_result(t, n, s) continue + self._add_result(t, n, s) - # If we got here, we can see if a valid template was specified - found = False - for i in templates: - if os.path.basename(i) == m['template']: - found = True - break - if not found: + if m['template'] not in self._get_templates(vendor, version): t = 'error' s = "specified unsupported template '%s'" % m['template'] @@ -353,13 +359,85 @@ self._add_result(t, n, s) + def check_template_push_helpers(self): + '''Check template for push-helpers''' + for app in sorted(self.manifest['hooks']): + (f, m) = self._get_security_manifest(app) + t = 'info' + n = 'template_push_helper(%s)' % f + s = "OK" + if 'push-helper' not in self.manifest['hooks'][app]: + continue + if 'template' in m and m['template'] != "ubuntu-sdk": + t = 'error' + s = "template is not 'ubuntu-sdk'" + self._add_result(t, n, s) + + def check_policy_groups_push_helpers(self): + '''Check policy_groups for push-helpers''' + for app in sorted(self.manifest['hooks']): + (f, m) = self._get_security_manifest(app) + t = 'info' + n = 'policy_groups_push_helper(%s)' % f + s = "OK" + if 'push-helper' not in self.manifest['hooks'][app]: + continue + if 'policy_groups' not in m or \ + 'push-notification-client' not in m['policy_groups']: + self._add_result('error', n, + "required group 'push-notification-client' " + "not found") + continue + bad = [] + for p in m['policy_groups']: + if p not in self.allowed_push_helper_policy_groups: + bad.append(p) + if len(bad) > 0: + t = 'error' + s = "found unusual policy groups: %s" % ", ".join(bad) + self._add_result(t, n, s) + + def check_policy_groups_scopes(self): + '''Check policy_groups for scopes''' + for app in sorted(self.manifest['hooks']): + (f, m) = self._get_security_manifest(app) + t = 'info' + n = 'policy_groups_scopes (%s)' % f + s = "OK" +# jdstrand, 2014-06-05: ubuntu-scope-local-content is no longer available +# scope_templates = ['ubuntu-scope-network', +# 'ubuntu-scope-local-content'] + scope_templates = ['ubuntu-scope-network'] + if 'template' not in m or m['template'] not in scope_templates: + continue + + if 'policy_groups' not in m: + continue + + bad = [] + for p in m['policy_groups']: + if m['template'] == 'ubuntu-scope-network': + # networking scopes shouldn't have access to anything + # (for now, this may change with trust store (eg, location) + if p != 'networking': + bad.append(p) +# jdstrand, 2014-06-05: ubuntu-scope-local-content is no longer available +# elif m['template'] == 'ubuntu-scope-local-content': +# if p == 'networking': +# bad.append(p) + + if len(bad) > 0: + t = 'error' + s = "found inappropriate policy groups: %s" % ", ".join(bad) + self._add_result(t, n, s) + def check_policy_groups(self): '''Check policy_groups''' for app in sorted(self.manifest['hooks']): (f, m) = self._get_security_manifest(app) t = 'info' - n = 'policy_groups_exists (%s)' % f + n = 'policy_groups_exists_%s (%s)' % (app, f) if 'policy_groups' not in m: # If template not specified, we just use the default self._add_result('warn', n, 'no policy groups specified') @@ -374,18 +452,8 @@ if 'policy_vendor' in m: vendor = m['policy_vendor'] version = str(m['policy_version']) - cmd_args = ['--list-policy-groups', '--policy-vendor=%s' % vendor, - '--policy-version=%s' % version] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - policy_groups = [] - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(None, options) - policy_groups = easyp.get_policy_groups() - except Exception: - t = 'error' - s = 'could not find policy for %s/%s' % (vendor, version) - self._add_result(t, n, s) - continue + + policy_groups = self._get_policy_groups(version=version, vendor=vendor) if len(policy_groups) < 1: t = 'error' s = 'could not find policy groups' @@ -395,7 +463,7 @@ # Check for duplicates t = 'info' - n = 'policy_groups_duplicates (%s)' % f + n = 'policy_groups_duplicates_%s (%s)' % (app, f) s = 'OK' tmp = [] for p in m['policy_groups']: @@ -410,7 +478,7 @@ # If we got here, we can see if valid policy groups were specified for i in m['policy_groups']: t = 'info' - n = 'policy_groups_valid (%s)' % i + n = 'policy_groups_valid_%s (%s)' % (app, i) s = 'OK' # SDK will leave and empty policy group, report but don't @@ -433,16 +501,22 @@ if found: t = 'info' - n = 'policy_groups_safe (%s)' % i + n = 'policy_groups_safe_%s (%s)' % (app, i) s = 'OK' - usage = self._get_policy_group_meta(i, "Usage", - vendor, version) - if usage != "common": - desc = self._get_policy_group_meta(i, "Description", - vendor, version) + + aa_type = self._get_policy_group_type(vendor, version, i) + if i == "debug": + t = 'error' + s = "(REJECT) %s policy group " % aa_type + \ + "'%s': not for production use" % (i) + elif aa_type == "reserved": + t = 'error' + s = "(MANUAL REVIEW) %s policy group " % aa_type + \ + "'%s': vetted applications only" % (i) + elif aa_type != "common": t = 'error' - s = "(MANUAL REVIEW) %s policy group " % usage + \ - "'%s': %s" % (i, desc) + s = "policy group '%s' has" % i + \ + "unknown type '%s'" % (aa_type) self._add_result(t, n, s) def check_ignored(self): diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_skeleton.py click-reviewers-tools-0.8/bin/clickreviews/cr_skeleton.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_skeleton.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_skeleton.py 2014-07-25 13:47:22.000000000 +0000 @@ -1,6 +1,6 @@ '''cr_skeleton.py: click skeleton''' # -# Copyright (C) 2013 Canonical Ltd. +# Copyright (C) 2014 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_tests.py click-reviewers-tools-0.8/bin/clickreviews/cr_tests.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_tests.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_tests.py 2014-07-25 13:47:22.000000000 +0000 @@ -32,6 +32,14 @@ TEST_SECURITY = dict() TEST_DESKTOP = dict() TEST_WEBAPP_MANIFESTS = dict() +TEST_URLS = dict() +TEST_SCOPES = dict() +TEST_CONTENT_HUB = dict() +TEST_ACCOUNTS_APPLICATION = dict() +TEST_ACCOUNTS_PROVIDER = dict() +TEST_ACCOUNTS_QML_PLUGIN = dict() +TEST_ACCOUNTS_SERVICE = dict() +TEST_PUSH_HELPER = dict() # @@ -59,6 +67,18 @@ "ubuntu-sdk-14.04-html-dev1", "ubuntu-sdk-14.04-papi-dev1", "ubuntu-sdk-14.04-qml-dev1", + "ubuntu-sdk-14.04", + "ubuntu-sdk-14.04-html", + "ubuntu-sdk-14.04-papi", + "ubuntu-sdk-14.04-qml", + "ubuntu-sdk-14.10-dev1", + "ubuntu-sdk-14.10-html-dev1", + "ubuntu-sdk-14.10-papi-dev1", + "ubuntu-sdk-14.10-qml-dev1", + "ubuntu-sdk-14.10-dev2", + "ubuntu-sdk-14.10-html-dev2", + "ubuntu-sdk-14.10-papi-dev2", + "ubuntu-sdk-14.10-qml-dev2", ] @@ -74,7 +94,7 @@ def _get_security_supported_policy_versions(self): '''Pretend we read the contens of /usr/share/apparmor/easyprof''' - return [1.0, 1.1] + return [1.0, 1.1, 1.2] def _extract_desktop_entry(self, app): @@ -92,6 +112,48 @@ return TEST_WEBAPP_MANIFESTS +def _extract_url_dispatcher(self, app): + '''Pretend we read the url dispatcher file''' + return ("%s.url-dispatcher" % app, TEST_URLS[app]) + + +def _extract_scopes(self, app): + '''Pretend we found and read the files in the scope directories''' + return TEST_SCOPES[app] + + +def _extract_content_hub(self, app): + '''Pretend we read the content-hub file''' + return ("%s.content.json" % app, TEST_CONTENT_HUB[app]) + + +def _extract_account(self, app, account_type): + '''Pretend we read the accounts file''' + f = app + val = None + if account_type == "account-application": + f += ".application" + val = TEST_ACCOUNTS_APPLICATION[app] + elif account_type == "account-provider": + f += ".provider" + val = TEST_ACCOUNTS_PROVIDER[app] + elif account_type == "account-qml-plugin": + f += ".qml-plugin" + val = TEST_ACCOUNTS_QML_PLUGIN[app] + elif account_type == "account-service": + f += ".service" + val = TEST_ACCOUNTS_SERVICE[app] + else: # should never get here + raise ValueError("Unknown account_type '%s'" % account_type) + + return (f, val) + + +def _extract_push_helper(self, app): + '''Pretend we read the push-helper file''' + return ("%s.push-helper.json" % app, TEST_PUSH_HELPER[app]) + + # http://docs.python.org/3.4/library/unittest.mock-examples.html # Mock patching. Don't use decorators but instead patch in setUp() of the # child. Set up a list of patches, but don't start them. Create the helper @@ -140,9 +202,6 @@ patches.append(patch( 'clickreviews.cr_security.ClickReviewSecurity._get_security_manifest', _get_security_manifest)) -patches.append(patch( - 'clickreviews.cr_security.ClickReviewSecurity._get_supported_policy_versions', - _get_security_supported_policy_versions)) # desktop overrides patches.append(patch( @@ -155,6 +214,31 @@ 'clickreviews.cr_desktop.ClickReviewDesktop._extract_webapp_manifests', _extract_webapp_manifests)) +# url-dispatcher overrides +patches.append(patch( + 'clickreviews.cr_url_dispatcher.ClickReviewUrlDispatcher._extract_url_dispatcher', + _extract_url_dispatcher)) + +# scope overrides +patches.append(patch( + 'clickreviews.cr_scope.ClickReviewScope._extract_scopes', + _extract_scopes)) + +# content-hub overrides +patches.append(patch( + 'clickreviews.cr_content_hub.ClickReviewContentHub._extract_content_hub', + _extract_content_hub)) + +# online accounts overrides +patches.append(patch( + 'clickreviews.cr_online_accounts.ClickReviewAccounts._extract_account', + _extract_account)) + +# push-helper overrides +patches.append(patch( + 'clickreviews.cr_push_helper.ClickReviewPushHelper._extract_push_helper', + _extract_push_helper)) + def mock_patch(): '''Call in setup of child''' @@ -208,11 +292,21 @@ "%s.json" % self.default_appname self.test_manifest["hooks"][self.default_appname]["desktop"] = \ "%s.desktop" % self.default_appname + self.test_manifest["hooks"][self.default_appname]["urls"] = \ + "%s.url-dispatcher" % self.default_appname self._update_test_manifest() # hooks self.test_security_manifests = dict() self.test_desktop_files = dict() + self.test_url_dispatcher = dict() + self.test_scopes = dict() + self.test_content_hub = dict() + self.test_accounts_application = dict() + self.test_accounts_provider = dict() + self.test_accounts_qml_plugin = dict() + self.test_accounts_service = dict() + self.test_push_helper = dict() for app in self.test_manifest["hooks"].keys(): # setup security manifest for each app self.set_test_security_manifest(app, 'policy_groups', @@ -233,8 +327,34 @@ self.set_test_desktop(app, 'X-Ubuntu-Touch', 'true', no_update=True) + self.set_test_url_dispatcher(app, None, None) + + # Ensure we have no scope entries since they conflict with desktop. + # Scope tests will have to add them as part of their tests. + self.set_test_scope(app, None) + + # Reset to no content-hub entries in manifest + self.set_test_content_hub(app, None, None) + + # Reset to no content-hub entries in manifest + self.set_test_account(app, "account-application", None) + self.set_test_account(app, "account-provider", None) + self.set_test_account(app, "account-qml-plugin", None) + self.set_test_account(app, "account-service", None) + + # Reset to no content-hub entries in manifest + self.set_test_push_helper(app, None, None) + self._update_test_security_manifests() self._update_test_desktop_files() + self._update_test_url_dispatcher() + self._update_test_scopes() + self._update_test_content_hub() + self._update_test_accounts_application() + self._update_test_accounts_provider() + self._update_test_accounts_qml_plugin() + self._update_test_accounts_service() + self._update_test_push_helper() # webapps manifests (leave empty for now) self.test_webapp_manifests = dict() @@ -280,6 +400,75 @@ for i in self.test_webapp_manifests.keys(): TEST_WEBAPP_MANIFESTS[i] = self.test_webapp_manifests[i] + def _update_test_url_dispatcher(self): + global TEST_URLS + TEST_URLS = dict() + for app in self.test_url_dispatcher.keys(): + TEST_URLS[app] = self.test_url_dispatcher[app] + + def _update_test_scopes(self): + global TEST_SCOPES + TEST_SCOPES = dict() + for app in self.test_scopes.keys(): + TEST_SCOPES[app] = self.test_scopes[app] + self.test_manifest["hooks"][app]["scope"] = \ + TEST_SCOPES[app]["dir_rel"] + self._update_test_manifest() + + def _update_test_content_hub(self): + global TEST_CONTENT_HUB + TEST_CONTENT_HUB = dict() + for app in self.test_content_hub.keys(): + TEST_CONTENT_HUB[app] = self.test_content_hub[app] + self.test_manifest["hooks"][app]["content-hub"] = \ + "%s.content.json" % app + self._update_test_manifest() + + def _update_test_accounts_application(self): + global TEST_ACCOUNTS_APPLICATION + TEST_ACCOUNTS_APPLICATION = dict() + for app in self.test_accounts_application.keys(): + TEST_ACCOUNTS_APPLICATION[app] = self.test_accounts_application[app] + self.test_manifest["hooks"][app]["account-application"] = \ + "%s.application" % app + self._update_test_manifest() + + def _update_test_accounts_provider(self): + global TEST_ACCOUNTS_PROVIDER + TEST_ACCOUNTS_PROVIDER = dict() + for app in self.test_accounts_provider.keys(): + TEST_ACCOUNTS_PROVIDER[app] = self.test_accounts_provider[app] + self.test_manifest["hooks"][app]["account-provider"] = \ + "%s.provider" % app + self._update_test_manifest() + + def _update_test_accounts_qml_plugin(self): + global TEST_ACCOUNTS_QML_PLUGIN + TEST_ACCOUNTS_QML_PLUGIN = dict() + for app in self.test_accounts_qml_plugin.keys(): + TEST_ACCOUNTS_QML_PLUGIN[app] = self.test_accounts_qml_plugin[app] + self.test_manifest["hooks"][app]["account-qml-plugin"] = \ + "%s.qml_plugin" % app + self._update_test_manifest() + + def _update_test_accounts_service(self): + global TEST_ACCOUNTS_SERVICE + TEST_ACCOUNTS_SERVICE = dict() + for app in self.test_accounts_service.keys(): + TEST_ACCOUNTS_SERVICE[app] = self.test_accounts_service[app] + self.test_manifest["hooks"][app]["account-service"] = \ + "%s.service" % app + self._update_test_manifest() + + def _update_test_push_helper(self): + global TEST_PUSH_HELPER + TEST_PUSH_HELPER = dict() + for app in self.test_push_helper.keys(): + TEST_PUSH_HELPER[app] = self.test_push_helper[app] + self.test_manifest["hooks"][app]["push-helper"] = \ + "%s.push-helper.json" % app + self._update_test_manifest() + def _update_test_name(self): self.test_name = "%s_%s_%s.click" % (self.test_control['Package'], self.test_control['Version'], @@ -370,7 +559,6 @@ def set_test_webapp_manifest(self, fn, key, value): '''Set key in webapp manifest to value. If value is None, remove key''' - if key is None and value is None: self.test_webapp_manifests[fn] = None self._update_test_webapp_manifests() @@ -386,6 +574,90 @@ self.test_webapp_manifests[fn][key] = value self._update_test_webapp_manifests() + def set_test_url_dispatcher(self, app, key, value, append=False): + '''Set url-dispatcher entries. If value is None, remove''' + if app not in self.test_url_dispatcher: + self.test_url_dispatcher[app] = [] + + if value is None: + self.test_url_dispatcher[app] = [] + else: + if not append: + self.test_url_dispatcher[app] = [] + self.test_url_dispatcher[app].append({key: value}) + self._update_test_url_dispatcher() + + def set_test_scope(self, app, scope): + '''Set scope for app. If it is None, remove''' + if scope is None: + if app in self.test_scopes: + self.test_scopes.pop(app) + if 'scope' in self.test_manifest['hooks'][app]: + self.test_manifest['hooks'][app].pop('scope', None) + else: + self.test_scopes[app] = scope + self._update_test_scopes() + + def set_test_content_hub(self, app, key, value): + '''Set content-hub entries. If value is None, remove key, if key is + None, remove content-hub from manifest''' + if key is None: + if app in self.test_content_hub: + self.test_content_hub.pop(app) + elif value is None: + if key in self.test_content_hub[app]: + self.test_content_hub[app].pop(key) + else: + if app not in self.test_content_hub: + self.test_content_hub[app] = dict() + if key not in self.test_content_hub[app]: + self.test_content_hub[app][key] = [] + self.test_content_hub[app][key].append(value) + self._update_test_content_hub() + + def set_test_account(self, app, account_type, value): + '''Set accounts XML. If value is None, remove from manifest''' + if account_type == "account-application": + d = self.test_accounts_application + elif account_type == "account-provider": + d = self.test_accounts_provider + elif account_type == "account-qml-plugin": + d = self.test_accounts_qml_plugin + elif account_type == "account-service": + d = self.test_accounts_service + else: # should never get here + raise ValueError("Unknown account_type '%s'" % account_type) + + if value is None: + if app in d: + d[app] = None + else: + d[app] = value + + if account_type == "account-application": + self._update_test_accounts_application() + elif account_type == "account-provider": + self._update_test_accounts_provider() + elif account_type == "account-qml-plugin": + self._update_test_accounts_qml_plugin() + elif account_type == "account-service": + self._update_test_accounts_service() + + def set_test_push_helper(self, app, key, value): + '''Set push-helper entries. If value is None, remove key, if key is + None, remove content-hub from manifest''' + if key is None: + if app in self.test_push_helper: + self.test_push_helper.pop(app) + elif value is None: + if key in self.test_push_helper[app]: + self.test_push_helper[app].pop(key) + else: + if app not in self.test_push_helper: + self.test_push_helper[app] = dict() + self.test_push_helper[app][key] = value + self._update_test_push_helper() + def setUp(self): '''Make sure our patches are applied everywhere''' global patches @@ -402,6 +674,22 @@ TEST_SECURITY = dict() global TEST_DESKTOP TEST_DESKTOP = dict() + global TEST_URLS + TEST_URLS = dict() + global TEST_SCOPES + TEST_SCOPES = dict() + global TEST_CONTENT_HUB + TEST_CONTENT_HUB = dict() + global TEST_ACCOUNTS_APPLICATION + TEST_ACCOUNTS_APPLICATION = dict() + global TEST_ACCOUNTS_PROVIDER + TEST_ACCOUNTS_PROVIDER = dict() + global TEST_ACCOUNTS_QML_PLUGIN + TEST_ACCOUNTS_QML_PLUGIN = dict() + global TEST_ACCOUNTS_SERVICE + TEST_ACCOUNTS_APPLICATION = dict() + global TEST_PUSH_HELPER + TEST_PUSH_HELPER = dict() self._reset_test_data() cr_common.recursive_rm(self.desktop_tmpdir) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/cr_url_dispatcher.py click-reviewers-tools-0.8/bin/clickreviews/cr_url_dispatcher.py --- click-reviewers-tools-0.7/bin/clickreviews/cr_url_dispatcher.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/cr_url_dispatcher.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,152 @@ +'''cr_url dispatcher.py: click url_dispatcher''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import json +import os + +# https://wiki.ubuntu.com/URLDispatcher + + +class ClickReviewUrlDispatcher(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "url_dispatcher") + + self.required_keys = ['protocol'] + self.optional_keys = ['domain-suffix'] + + self.url_dispatcher_files = dict() # click-show-files and tests + self.url_dispatcher = dict() + for app in self.manifest['hooks']: + if 'urls' not in self.manifest['hooks'][app]: + # msg("Skipped missing urls hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['urls'], str): + error("manifest malformed: hooks/%s/urls is not str" % app) + (full_fn, jd) = self._extract_url_dispatcher(app) + self.url_dispatcher_files[app] = full_fn + self.url_dispatcher[app] = jd + + def _extract_url_dispatcher(self, app): + '''Get url dispatcher json''' + u = self.manifest['hooks'][app]['urls'] + fn = os.path.join(self.unpack_dir, u) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + fh = open_file_read(fn) + contents = "" + for line in fh.readlines(): + contents += line + fh.close() + + try: + jd = json.loads(contents) + except Exception as e: + error("url-dispatcher json unparseable: %s (%s):\n%s" % (bn, + str(e), contents)) + + if not isinstance(jd, list): + error("url-dispatcher json is malformed: %s:\n%s" % (bn, contents)) + + return (fn, jd) + + def check_required(self): + '''Check url-dispatcher required fields''' + for app in sorted(self.url_dispatcher): + for r in self.required_keys: + found = False + t = 'info' + n = 'required_entry_%s_%s' % (app, r) + s = "OK" + for entry in self.url_dispatcher[app]: + if not isinstance(entry, dict): + t = 'error' + s = "'%s' is not a dict" % str(entry) + self._add_result(t, n, s) + continue + if r in entry: + if not isinstance(entry[r], str): + t = 'error' + s = "'%s' is not a string" % r + elif entry[r] == "": + t = 'error' + s = "'%s' is empty" % r + else: + found = True + if not found and t != 'error': + t = 'error' + s = "Missing required field '%s'" % r + self._add_result(t, n, s) + + def check_optional(self): + '''Check url-dispatcher optional fields''' + for app in sorted(self.url_dispatcher): + for o in self.optional_keys: + found = False + t = 'info' + n = 'optional_entry_%s_%s' % (app, o) + s = "OK" + for entry in self.url_dispatcher[app]: + if not isinstance(entry, dict): + t = 'error' + s = "'%s' is not a dict" % str(entry) + self._add_result(t, n, s) + continue + if o in entry: + if not isinstance(entry[o], str): + t = 'error' + s = "'%s' is not a string" % o + elif entry[o] == "": + t = 'error' + s = "'%s' is empty" % o + else: + found = True + if not found and t != 'error': + s = "OK (skip missing)" + self._add_result(t, n, s) + + def check_unknown(self): + '''Check url-dispatcher unknown fields''' + for app in sorted(self.url_dispatcher): + unknown = [] + for entry in self.url_dispatcher[app]: + t = 'info' + n = 'unknown_entry_%s' % app + s = "OK" + if not isinstance(entry, dict): + t = 'error' + s = "'%s' is not a dict" % str(entry) + self._add_result(t, n, s) + continue + + for f in entry.keys(): + if f not in self.required_keys and \ + f not in self.optional_keys: + unknown.append(f) + + if len(unknown) == 1: + t = 'warn' + s = "Unknown field '%s'" % unknown[0] + elif len(unknown) > 1: + t = 'warn' + s = "Unknown fields '%s'" % ", ".join(unknown) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/frameworks.py click-reviewers-tools-0.8/bin/clickreviews/frameworks.py --- click-reviewers-tools-0.7/bin/clickreviews/frameworks.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/frameworks.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,48 @@ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import clickreviews.remote + +USER_DATA_FILE = os.path.join(clickreviews.remote.DATA_DIR, 'frameworks.json') + +# XXX: This is a hack and will be gone, as soon as myapps has an API for this. +FRAMEWORKS_DATA_URL = \ + "http://bazaar.launchpad.net/~ubuntu-core-dev/+junk/frameworks/view/head:/frameworks.json" + + +def get_frameworks_file(fn): + if fn is None: + fn = USER_DATA_FILE + clickreviews.remote.get_remote_file(fn, FRAMEWORKS_DATA_URL) + + +class Frameworks(object): + DEPRECATED_FRAMEWORKS = [] + OBSOLETE_FRAMEWORKS = [] + AVAILABLE_FRAMEWORKS = [] + + def __init__(self, local_copy_fn=None): + self.FRAMEWORKS = clickreviews.remote.read_cr_file(USER_DATA_FILE, + FRAMEWORKS_DATA_URL, + local_copy_fn) + + for k, v in self.FRAMEWORKS.items(): + if v == 'deprecated': + self.DEPRECATED_FRAMEWORKS.append(k) + elif v == 'obsolete': + self.OBSOLETE_FRAMEWORKS.append(k) + elif v == 'available': + self.AVAILABLE_FRAMEWORKS.append(k) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/remote.py click-reviewers-tools-0.8/bin/clickreviews/remote.py --- click-reviewers-tools-0.7/bin/clickreviews/remote.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/remote.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,96 @@ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json +import os +import re +from socket import timeout +import sys +import time +from urllib import request, parse +from urllib.error import HTTPError, URLError + +DATA_DIR = os.path.join(os.path.expanduser('~/.cache/click-reviewers-tools/')) +UPDATE_INTERVAL = 60 * 60 * 24 * 7 + + +def _update_is_necessary(fn): + return (not os.path.exists(fn)) or \ + (time.time() - os.path.getctime(fn) >= UPDATE_INTERVAL) + + +def _update_is_possible(url): + update = True + try: + request.urlopen(url) + except (HTTPError, URLError): + update = False + except timeout: + update = False + return update + + +def abort(msg=None): + if msg: + print(msg, file=sys.stderr) + print('Aborted.', file=sys.stderr) + sys.exit(1) + + +# +# Public +# +def get_remote_file(fn, url, data_dir=DATA_DIR): + try: + f = request.urlopen(url) + except (HTTPError, URLError) as error: + abort('Data not retrieved because %s.' % error) + except timeout: + abort('Socket timed out.') + html = f.read() + # XXX: This is a hack and will be gone, as soon as myapps has an API for this. + link = re.findall(b'download file', html) + if not link: + abort() + download_link = '{}://{}/{}'.format( + parse.urlparse(url).scheme, + parse.urlparse(url).netloc, + link[0].decode("utf-8")) + f = request.urlopen(download_link) + if not f: + abort() + if os.path.exists(fn): + os.remove(fn) + if not os.path.exists(os.path.dirname(fn)): + os.makedirs(os.path.dirname(fn)) + with open(fn, 'bw') as local_file: + local_file.write(f.read()) + + +def read_cr_file(fn, url, local_copy_fn=None): + '''read click reviews file from remote or local copy: + - fn: where to store the cached file + - url: url to fetch + - local_copy_fn: force use of local copy + ''' + j = {} + if local_copy_fn and os.path.exists(local_copy_fn): + j = json.loads(open(local_copy_fn, 'r').read()) + else: + if _update_is_necessary(fn) and _update_is_possible(url): + get_remote_file(fn, url) + if os.path.exists(fn): + j = json.loads(open(fn, 'r').read()) + return j diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_aaa_example_cr_skeleton.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_aaa_example_cr_skeleton.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_aaa_example_cr_skeleton.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_aaa_example_cr_skeleton.py 2014-07-25 13:47:22.000000000 +0000 @@ -1,6 +1,6 @@ '''test_cr_skeleton.py: tests for the cr_skeleton module''' # -# Copyright (C) 2013 Canonical Ltd. +# Copyright (C) 2014 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_content_hub.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_content_hub.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_content_hub.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_content_hub.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,111 @@ +'''test_cr_content_hub.py: tests for the cr_content-hub module''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_content_hub import ClickReviewContentHub +import clickreviews.cr_tests as cr_tests + + +class TestClickReviewContentHub(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def test_check_unknown_keys_none(self): + '''Test check_unknown() - no unknown''' + self.set_test_content_hub(self.default_appname, "source", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys1(self): + '''Test check_unknown() - one unknown''' + self.set_test_content_hub(self.default_appname, "nonexistent", "foo") + c = ClickReviewContentHub(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys2(self): + '''Test check_unknown() - good with one unknown''' + self.set_test_content_hub(self.default_appname, "source", "pictures") + self.set_test_content_hub(self.default_appname, "nonexistent", "foo") + c = ClickReviewContentHub(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_source(self): + '''Test check_valid() - source''' + self.set_test_content_hub(self.default_appname, "source", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_share(self): + '''Test check_valid() - share''' + self.set_test_content_hub(self.default_appname, "share", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_destination(self): + '''Test check_valid() - destination''' + self.set_test_content_hub(self.default_appname, "destination", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_all(self): + '''Test check_valid() - all''' + self.set_test_content_hub(self.default_appname, "destination", "pictures") + self.set_test_content_hub(self.default_appname, "share", "pictures") + self.set_test_content_hub(self.default_appname, "source", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 6, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_bad_value(self): + '''Test check_valid() - bad value''' + self.set_test_content_hub(self.default_appname, "destination", []) + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_empty_value(self): + '''Test check_valid() - empty value''' + self.set_test_content_hub(self.default_appname, "source", "") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_desktop.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_desktop.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_desktop.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_desktop.py 2014-07-25 13:47:22.000000000 +0000 @@ -36,7 +36,6 @@ expected['warn'] = dict() expected['error'] = dict() expected['info']['desktop_files_usable'] = {"text": "OK"} - expected['info']['desktop_files_available'] = {"text": "OK"} self.check_results(r, expected=expected) def test_check_desktop_file_valid(self): diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_lint.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_lint.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_lint.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_lint.py 2014-07-25 13:47:22.000000000 +0000 @@ -506,6 +506,27 @@ expected_counts = {'info': None, 'warn': 1, 'error': 0} self.check_results(r, expected_counts) + def test_check_maintainer_email_special(self): + '''Test check_maintainer() - ubuntu-devel-discuss@lists.ubuntu.com''' + self.set_test_control("Package", "com.canonical.app") + self.set_test_manifest("maintainer", + "Ubuntu Core Developers " + "") + c = ClickReviewLint(self.test_name) + c.check_maintainer() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + expected = dict() + expected['info'] = dict() + expected['warn'] = dict() + expected['error'] = dict() + expected['info']['lint_maintainer_domain'] = \ + {"text": "OK (email 'ubuntu-devel-discuss@lists.ubuntu.com' long, " + "but special case"} + self.check_results(r, expected=expected) + def test_check_icon(self): '''Test check_icon()''' self.set_test_manifest("icon", "someicon") @@ -580,11 +601,11 @@ def test_check_framework(self): '''Test check_framework()''' - self.set_test_manifest("framework", "ubuntu-sdk-14.04-qml-dev1") + self.set_test_manifest("framework", "ubuntu-sdk-14.10-qml-dev2") c = ClickReviewLint(self.test_name) c.check_framework() r = c.click_report - expected_counts = {'info': 2, 'warn': 0, 'error': 0} + expected_counts = {'info': 1, 'warn': 0, 'error': 0} self.check_results(r, expected_counts) def test_check_framework_bad(self): @@ -596,11 +617,123 @@ expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(r, expected_counts) + def test_check_framework_deprecated(self): + '''Test check_framework() - deprecated''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.check_framework() + r = c.click_report + expected_counts = {'info': None, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + def test_check_framework_obsolete(self): '''Test check_framework() - obsolete''' - self.set_test_manifest("framework", "ubuntu-sdk-13.10") + self.set_test_manifest("framework", "ubuntu-sdk-14.10-qml-dev1") c = ClickReviewLint(self.test_name) c.check_framework() r = c.click_report expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(r, expected_counts) + + def test_check_hooks(self): + '''Test check_hooks()''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_multiple_desktop_apps(self): + '''Test check_hooks() - multiple desktop apps''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + tmp = c.manifest['hooks'][self.default_appname] + c.manifest['hooks']["another-app"] = tmp + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_multiple_apps(self): + '''Test check_hooks() - multiple non-desktop apps''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + tmp = dict() + for k in c.manifest['hooks'][self.default_appname].keys(): + tmp[k] = c.manifest['hooks'][self.default_appname][k] + tmp.pop('desktop') + tmp['scope'] = "some-scope-exec" + c.manifest['hooks']["some-scope"] = tmp + tmp = dict() + for k in c.manifest['hooks'][self.default_appname].keys(): + tmp[k] = c.manifest['hooks'][self.default_appname][k] + tmp.pop('desktop') + tmp['push-helper'] = "push.json" + c.manifest['hooks']["some-push-helper"] = tmp + + c.check_hooks() + r = c.click_report + expected_counts = {'info': 7, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_bad_appname(self): + '''Test check_hooks() - bad appname''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + tmp = c.manifest['hooks'][self.default_appname] + del c.manifest['hooks'][self.default_appname] + c.manifest['hooks']["b@d@ppn@m#"] = tmp + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_missing_apparmor(self): + '''Test check_hooks() - missing apparmor''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + del c.manifest['hooks'][self.default_appname]['apparmor'] + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_has_desktop_and_scope(self): + '''Test check_hooks() - desktop with scope''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.manifest['hooks'][self.default_appname]["scope"] = "some-binary" + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_unknown_nonexistent(self): + '''Test check_hooks_unknown() - nonexistent''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.manifest['hooks'][self.default_appname]["nonexistant"] = "foo" + c.check_hooks_unknown() + r = c.click_report + expected_counts = {'info': None, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_unknown_good(self): + '''Test check_hooks_unknown()''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.check_hooks_unknown() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_redflagged(self): + '''Test check_hooks_redflagged()''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.manifest['hooks'][self.default_appname]["pay-ui"] = "foo" + c.check_hooks_redflagged() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_online_accounts.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_online_accounts.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_online_accounts.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_online_accounts.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,317 @@ +'''test_cr_online_accounts.py: tests for the cr_online accounts module''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_online_accounts import ClickReviewAccounts +import clickreviews.cr_tests as cr_tests +import lxml.etree as etree + + +class TestClickReviewAccounts(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def _stub_application(self, root=None, id=None, do_subtree=True): + '''Stub application xml''' + if id is None: + id = "%s_%s" % (self.test_manifest["name"], self.default_appname) + if root is None: + root = "application" + if id == "": + xml = etree.Element(root) + else: + xml = etree.Element(root, id="%s" % id) + if do_subtree: + services = etree.SubElement(xml, "services") + elem1 = etree.SubElement(services, "service", id="element1") + desc1 = etree.SubElement(elem1, "description") + desc1.text = "elem1 description" + elem2 = etree.SubElement(services, "service", id="element2") + desc2 = etree.SubElement(elem2, "description") + desc2.text = "elem2 description" + return xml + + def _stub_service(self, root=None, id=None, do_subtree=True): + '''Stub service xml''' + if id is None: + id = "%s_%s" % (self.test_manifest["name"], self.default_appname) + if root is None: + root = "service" + if id == "": + xml = etree.Element(root) + else: + xml = etree.Element(root, id="%s" % id) + if do_subtree: + service_type = etree.SubElement(xml, "type") + service_type.text = "webapps" + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + service_provider = etree.SubElement(xml, "provider") + service_provider.text = "some-provider" + return xml + + def _stub_provider(self, root=None, id=None, do_subtree=True): + '''Stub provider xml''' + if id is None: + id = "%s_%s" % (self.test_manifest["name"], self.default_appname) + if root is None: + root = "provider" + if id == "": + xml = etree.Element(root) + else: + xml = etree.Element(root, id="%s" % id) + if do_subtree: + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + service_plugin = etree.SubElement(xml, "plugin") + service_plugin.text = "generic-oauth" + service_domains = etree.SubElement(xml, "domains") + service_domains.text = ".*\.example\.com" + # More can go here, see /usr/share/accounts/providers/* + return xml + + def test_check_application(self): + '''Test check_application()''' + xml = self._stub_application() + # print(etree.tostring(xml)) + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': 4, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_application_not_specified(self): + '''Test check_application() - not specified''' + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_application_wrong_id(self): + '''Test check_application() - wrong id''' + xml = self._stub_application(id="nomatch") + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_missing_id(self): + '''Test check_application() - missing id''' + xml = self._stub_application(id="") + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_wrong_root(self): + '''Test check_application() - wrong root''' + xml = self._stub_application(root="wrongroot") + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_missing_services(self): + '''Test check_application() - missing services''' + xml = self._stub_application(do_subtree=False) + + sometag = etree.SubElement(xml, "sometag") + elem1 = etree.SubElement(sometag, "something", id="element1") + desc1 = etree.SubElement(elem1, "description") + desc1.text = "elem1 description" + + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_missing_service(self): + '''Test check_application() - missing service''' + xml = self._stub_application(do_subtree=False) + + services = etree.SubElement(xml, "services") + elem1 = etree.SubElement(services, "somesubtag", id="element1") + desc1 = etree.SubElement(elem1, "description") + desc1.text = "elem1 description" + + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service(self): + '''Test check_service()''' + xml = self._stub_service() + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': 5, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_service_not_specified(self): + '''Test check_service() - no specified''' + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_service_wrong_id(self): + '''Test check_service() - wrong id''' + xml = self._stub_service(id="nomatch") + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_id(self): + '''Test check_service() - missing id''' + xml = self._stub_service(id="") + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_wrong_root(self): + '''Test check_service() - wrong root''' + xml = self._stub_service(root="wrongroot") + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_type(self): + '''Test check_service() - missing type''' + xml = self._stub_service(do_subtree=False) + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + service_provider = etree.SubElement(xml, "provider") + service_provider.text = "some-provider" + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_name(self): + '''Test check_service() - missing name''' + xml = self._stub_service(do_subtree=False) + service_type = etree.SubElement(xml, "type") + service_type.text = "webapps" + service_provider = etree.SubElement(xml, "provider") + service_provider.text = "some-provider" + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_provider(self): + '''Test check_service() - missing provider''' + xml = self._stub_service(do_subtree=False) + service_type = etree.SubElement(xml, "type") + service_type.text = "webapps" + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_provider(self): + '''Test check_provider()''' + xml = self._stub_provider() + self.set_test_account(self.default_appname, "account-provider", xml) + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + # provider prompts manual review, so for now, need to have error as 1 + expected_counts = {'info': 2, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_provider_not_specified(self): + '''Test check_provider() - no specified''' + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_provider_missing_id(self): + '''Test check_provider() - missing id''' + xml = self._stub_provider(id="") + self.set_test_account(self.default_appname, "account-provider", xml) + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + # provider prompts manual review, so for now, need to have error as +1 + expected_counts = {'info': None, 'warn': 0, 'error': 2} + self.check_results(r, expected_counts) + + def test_check_provider_wrong_id(self): + '''Test check_provider() - wrong id''' + xml = self._stub_provider(id="wrongid") + self.set_test_account(self.default_appname, "account-provider", xml) + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + # provider prompts manual review, so for now, need to have error as +1 + expected_counts = {'info': None, 'warn': 0, 'error': 2} + self.check_results(r, expected_counts) + + def test_check_qml_plugin(self): + '''Test check_qml_plugin()''' + self.set_test_account(self.default_appname, "account-qml-plugin", True) + c = ClickReviewAccounts(self.test_name) + c.check_qml_plugin() + r = c.click_report + # provider prompts manual review, so for now, need to have error as 1 + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_qml_plugin_not_specified(self): + '''Test check_qml_plugin() - no specified''' + c = ClickReviewAccounts(self.test_name) + c.check_qml_plugin() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_push_helper.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_push_helper.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_push_helper.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_push_helper.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,138 @@ +'''test_cr_push_helper.py: tests for the cr_push_helper module''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_push_helper import ClickReviewPushHelper +import clickreviews.cr_tests as cr_tests + + +class TestClickReviewPushHelper(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def test_check_unknown_keys_none(self): + '''Test check_unknown() - no unknown''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys1(self): + '''Test check_unknown() - one unknown''' + self.set_test_push_helper(self.default_appname, "nonexistent", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys2(self): + '''Test check_unknown() - good with one unknown''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_push_helper(self.default_appname, "nonexistent", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_exec(self): + '''Test check_valid() - exec''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_missing_exec(self): + '''Test check_valid() - missing exec''' + self.set_test_push_helper(self.default_appname, "app_id", "foo_foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_app_id(self): + '''Test check_valid() - app_id''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_push_helper(self.default_appname, "app_id", "foo_foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 3, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_bad_value(self): + '''Test check_valid() - bad value''' + self.set_test_push_helper(self.default_appname, "exec", []) + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_empty_value(self): + '''Test check_valid() - empty value''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_push_helper(self.default_appname, "app_id", "") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_empty_value2(self): + '''Test check_valid() - empty value''' + self.set_test_push_helper(self.default_appname, "exec", "") + self.set_test_push_helper(self.default_appname, "app_id", "foo_foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks(self): + '''Test check_hooks()''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + + # remove hooks that are added by the framework + c.manifest['hooks'][self.default_appname].pop('desktop') + c.manifest['hooks'][self.default_appname].pop('urls') + + c.check_hooks() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_bad(self): + '''Test check_hooks() - bad''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + + # The desktop and urls hooks are specified by default in the framework, + # so just running this without other setup should generate an error + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_scope.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_scope.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_scope.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_scope.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,145 @@ +'''test_cr_scope.py: tests for the cr_scope module''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_scope import ClickReviewScope +import clickreviews.cr_tests as cr_tests +import configparser + + +class TestClickReviewScope(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def _create_scope(self, config_dict=None): + '''Create a scope to pass to tests''' + scope = dict() + scope["dir_rel"] = "scope-directory" + scope["ini_file_rel"] = "%s/%s.ini" % (scope["dir_rel"], + self.default_appname) + scope["scope_config"] = configparser.ConfigParser() + scope["scope_config"]['ScopeConfig'] = config_dict + + return scope + + def _stub_config(self): + '''Stub configparser file''' + config_dict = { + 'ScopeRunner': "%s" % self.default_appname, + 'DisplayName': 'Foo', + 'Description': 'Some description', + 'Author': 'Foo Ltd.', + 'Art': '', + 'Icon': 'foo.svg', + 'SearchHint': 'Search Foo', + } + + return config_dict + + def test_check_scope_ini(self): + '''Test check_scope_ini()''' + scope = self._create_scope(self._stub_config()) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': 3, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required1(self): + '''Test check_scope_ini() - missing ScopeRunner''' + config = self._stub_config() + del config['ScopeRunner'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required2(self): + '''Test check_scope_ini() - missing DisplayName''' + config = self._stub_config() + del config['DisplayName'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required3(self): + '''Test check_scope_ini() - missing Icon''' + config = self._stub_config() + del config['Icon'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required4(self): + '''Test check_scope_ini() - missing SearchHint''' + config = self._stub_config() + del config['SearchHint'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required5(self): + '''Test check_scope_ini() - missing multiple''' + config = self._stub_config() + del config['ScopeRunner'] + del config['DisplayName'] + del config['Icon'] + del config['SearchHint'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_nonexistent_field(self): + '''Test check_scope_ini() - non-existent field''' + config = self._stub_config() + config['nonexistent'] = "foo" + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_security.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_security.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_security.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_security.py 2014-07-25 13:47:22.000000000 +0000 @@ -46,7 +46,7 @@ def test_check_policy_version_highest(self): '''Test check_policy_version() - highest''' c = ClickReviewSecurity(self.test_name) - highest_version = sorted(c.supported_policy_versions)[-1] + highest_version = c._get_highest_policy_version("ubuntu") version = highest_version self.set_test_security_manifest(self.default_appname, "policy_version", version) @@ -68,7 +68,7 @@ self.set_test_security_manifest(self.default_appname, "policy_version", bad_version) - highest = sorted(c.supported_policy_versions)[-1] + highest = c._get_highest_policy_version("ubuntu") c.check_policy_version() report = c.click_report @@ -88,7 +88,7 @@ def test_check_policy_version_low(self): '''Test check_policy_version() - low version''' c = ClickReviewSecurity(self.test_name) - highest = sorted(c.supported_policy_versions)[-1] + highest = c._get_highest_policy_version("ubuntu") version = 1.0 if version == highest: print("SKIPPED-- test version '%s' is already highest" % version, @@ -390,6 +390,94 @@ expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(report, expected_counts) + def test_check_policy_groups_scopes_network(self): + '''Test check_policy_groups_scopes() - network''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", []) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_scopes_network2(self): + '''Test check_policy_groups_scopes() - network with networking''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", ["networking"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_scopes_network_missing(self): + '''Test check_policy_groups_scopes() missing - network''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", None) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_scopes_network_bad(self): + '''Test check_policy_groups_scopes() bad - network''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", ["accounts"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + +# jdstrand, 2014-06-05: ubuntu-scope-local-content is no longer available +# def test_check_policy_groups_scopes_localcontent(self): +# '''Test check_policy_groups_scopes() - localcontent''' +# self.set_test_security_manifest(self.default_appname, +# "template", +# "ubuntu-scope-local-content") +# self.set_test_security_manifest(self.default_appname, +# "policy_groups", []) +# c = ClickReviewSecurity(self.test_name) +# c.check_policy_groups_scopes() +# report = c.click_report +# expected_counts = {'info': None, 'warn': 0, 'error': 0} +# self.check_results(report, expected_counts) + +# def test_check_policy_groups_scopes_localcontent_missing(self): +# '''Test check_policy_groups_scopes() missing - localcontent''' +# self.set_test_security_manifest(self.default_appname, +# "template", +# "ubuntu-scope-local-content") +# self.set_test_security_manifest(self.default_appname, +# "policy_groups", None) +# c = ClickReviewSecurity(self.test_name) +# c.check_policy_groups_scopes() +# report = c.click_report +# expected_counts = {'info': 0, 'warn': 0, 'error': 0} +# self.check_results(report, expected_counts) + +# def test_check_policy_groups_scopes_localcontent_bad(self): +# '''Test check_policy_groups_scopes() bad - localcontent''' +# self.set_test_security_manifest(self.default_appname, +# "template", +# "ubuntu-scope-local-content") +# self.set_test_security_manifest(self.default_appname, +# "policy_groups", ["networking"]) +# c = ClickReviewSecurity(self.test_name) +# c.check_policy_groups_scopes() +# report = c.click_report +# expected_counts = {'info': None, 'warn': 0, 'error': 1} +# self.check_results(report, expected_counts) + def test_check_policy_groups(self): '''Test check_policy_groups()''' c = ClickReviewSecurity(self.test_name) @@ -469,7 +557,7 @@ self.check_results(report, expected_counts) def test_check_policy_groups_nonexistent(self): - '''Test check_policy_groups_webapps() - nonexistent''' + '''Test check_policy_groups() - nonexistent''' self.set_test_security_manifest(self.default_appname, "policy_groups", ["networking", "nonexistent"]) @@ -480,7 +568,7 @@ self.check_results(report, expected_counts) def test_check_policy_groups_reserved(self): - '''Test check_policy_groups_webapps() - reserved''' + '''Test check_policy_groups() - reserved''' self.set_test_security_manifest(self.default_appname, "policy_groups", ["video_files", "networking"]) @@ -490,8 +578,20 @@ expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(report, expected_counts) + def test_check_policy_groups_debug(self): + '''Test check_policy_groups() - debug''' + self.set_test_security_manifest(self.default_appname, + "policy_groups", ["debug"]) + self.set_test_security_manifest(self.default_appname, "policy_version", + 1.2) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + def test_check_policy_groups_empty(self): - '''Test check_policy_groups_webapps() - empty''' + '''Test check_policy_groups() - empty''' self.set_test_security_manifest(self.default_appname, "policy_groups", ["", "networking"]) @@ -500,3 +600,87 @@ report = c.click_report expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper_no_hook(self): + '''Test check_policy_groups_pushhelper() - no hook''' + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper(self): + '''Test check_policy_groups_pushhelper()''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper_missing(self): + '''Test check_policy_groups_pushhelper - missing''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + None) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper_bad(self): + '''Test check_policy_groups_pushhelper - bad''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["video_files", + "networking", + "push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + + def test_check_template_pushhelper(self): + '''Test check_template_pushhelper''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-sdk") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_template_push_helpers() + report = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_template_pushhelper_no_hook(self): + '''Test check_template_pushhelper''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-sdk") + c = ClickReviewSecurity(self.test_name) + c.check_template_push_helpers() + report = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_template_pushhelper_wrong_template(self): + '''Test check_template_pushhelper - wrong template()''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-webapp") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_template_push_helpers() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) diff -Nru click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_url_dispatcher.py click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_url_dispatcher.py --- click-reviewers-tools-0.7/bin/clickreviews/tests/test_cr_url_dispatcher.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/clickreviews/tests/test_cr_url_dispatcher.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,211 @@ +'''test_cr_url dispatcher.py: tests for the cr_url_dispatcher module''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_url_dispatcher import ClickReviewUrlDispatcher +import clickreviews.cr_tests as cr_tests + + +class TestClickReviewUrlDispatcher(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def test_check_required(self): + '''Test check_required() - has protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_required_empty_value(self): + '''Test check_required() - empty protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_required_bad_value(self): + '''Test check_required() - bad protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value=[]) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_required_multiple(self): + '''Test check_required() - multiple''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_required_multiple(self): + '''Test check_required() - multiple with nonexistent''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_none(self): + '''Test check_optional() - protocol only''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_empty(self): + '''Test check_optional() - with empty domain-suffix''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_bad(self): + '''Test check_optional() - with bad domain-suffix''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value=[], + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_nonexistent(self): + '''Test check_optional() - with domain-suffix plus nonexistent''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_without_protocol(self): + '''Test check_optional() - with domain-suffix, no protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_without_protocol2(self): + '''Test check_optional() - with domain-suffix, nonexistent, no + protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown(self): + '''Test check_unknown()''' + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_unknown() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_multiple(self): + '''Test check_unknown() - multiple with nonexistent''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_unknown() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/bin/click-run-checks click-reviewers-tools-0.8/bin/click-run-checks --- click-reviewers-tools-0.7/bin/click-run-checks 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/click-run-checks 2014-07-25 13:47:22.000000000 +0000 @@ -40,16 +40,36 @@ prefer_local click-check-lint "$c" echo "" +echo "= click-check-content-hub =" +prefer_local click-check-content-hub "$c" + +echo "" echo "= click-check-desktop =" prefer_local click-check-desktop "$c" echo "" +echo "= click-check-functional =" +prefer_local click-check-functional "$c" + +echo "" +echo "= click-check-online-accounts =" +prefer_local click-check-online-accounts "$c" + +echo "" +echo "= click-check-push-helper =" +prefer_local click-check-push-helper "$c" + +echo "" +echo "= click-check-scope =" +prefer_local click-check-scope "$c" + +echo "" echo "= click-check-security =" prefer_local click-check-security "$c" echo "" -echo "= click-check-functional =" -prefer_local click-check-functional "$c" +echo "= click-check-url-dispatcher =" +prefer_local click-check-url-dispatcher "$c" echo "" echo "" diff -Nru click-reviewers-tools-0.7/bin/click-show-files click-reviewers-tools-0.8/bin/click-show-files --- click-reviewers-tools-0.7/bin/click-show-files 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/click-show-files 2014-07-25 13:47:22.000000000 +0000 @@ -1,7 +1,7 @@ #!/usr/bin/python3 -'''check-skeleton: perform click skeleton checks''' +'''check-show-files: show files''' # -# Copyright (C) 2013 Canonical Ltd. +# Copyright (C) 2014 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,6 +23,11 @@ from clickreviews import cr_desktop from clickreviews import cr_lint from clickreviews import cr_security +from clickreviews import cr_url_dispatcher +from clickreviews import cr_scope +from clickreviews import cr_content_hub +from clickreviews import cr_online_accounts +from clickreviews import cr_push_helper # This script just dumps important files to stdout @@ -42,11 +47,13 @@ cr_common.cleanup_unpack() print("= hooks =") - review_apparmor = cr_security.ClickReviewSecurity(sys.argv[1]) - for f in sorted(review_apparmor.security_manifests): - fh = cr_common.open_file_read(os.path.join(review_apparmor.unpack_dir, - f)) - print("== security: %s ==" % os.path.basename(f)) + + review_content_hub = cr_content_hub.ClickReviewContentHub(sys.argv[1]) + for app in sorted(review_content_hub.content_hub_files): + f = review_content_hub.content_hub_files[app] + fh = cr_common.open_file_read(os.path.join( + review_content_hub.unpack_dir, f)) + print("== content_hub: %s ==" % os.path.basename(f)) for line in fh.readlines(): print(line, end="") fh.close() @@ -62,5 +69,66 @@ for line in fh.readlines(): print(line, end="") fh.close() + print("") + cr_common.cleanup_unpack() + + review_accounts = cr_online_accounts.ClickReviewAccounts(sys.argv[1]) + for app in sorted(review_accounts.accounts_files): + for account_type in review_accounts.account_hooks: + if account_type not in review_accounts.accounts_files[app]: + continue + f = review_accounts.accounts_files[app][account_type] + fh = cr_common.open_file_read(os.path.join( + review_accounts.unpack_dir, f)) + print("== online %s: %s ==" % (account_type, os.path.basename(f))) + for line in fh.readlines(): + print(line, end="") + fh.close() + print("") + cr_common.cleanup_unpack() + + review_push_helper = cr_push_helper.ClickReviewPushHelper(sys.argv[1]) + for app in sorted(review_push_helper.push_helper_files): + f = review_push_helper.push_helper_files[app] + fh = cr_common.open_file_read(os.path.join( + review_push_helper.unpack_dir, f)) + print("== push_helper: %s ==" % os.path.basename(f)) + for line in fh.readlines(): + print(line, end="") + fh.close() + print("") + cr_common.cleanup_unpack() + + review_scope = cr_scope.ClickReviewScope(sys.argv[1]) + for app in sorted(review_scope.scopes): + f = review_scope.scopes[app]["ini_file"] + fh = cr_common.open_file_read(os.path.join(review_scope.unpack_dir, f)) + print("== scope .INI: %s ==" % os.path.basename(f)) + for line in fh.readlines(): + print(line, end="") + fh.close() + print("") + cr_common.cleanup_unpack() + + review_apparmor = cr_security.ClickReviewSecurity(sys.argv[1]) + for f in sorted(review_apparmor.security_manifests): + fh = cr_common.open_file_read(os.path.join(review_apparmor.unpack_dir, + f)) + print("== security: %s ==" % os.path.basename(f)) + for line in fh.readlines(): + print(line, end="") + fh.close() + print("") + cr_common.cleanup_unpack() + + review_url_dispatcher = cr_url_dispatcher.ClickReviewUrlDispatcher(sys.argv[1]) + for app in sorted(review_url_dispatcher.url_dispatcher_files): + f = review_url_dispatcher.url_dispatcher_files[app] + fh = cr_common.open_file_read(os.path.join(review_url_dispatcher.unpack_dir, + f)) + print("== url_dispatcher: %s ==" % os.path.basename(f)) + for line in fh.readlines(): + print(line, end="") + fh.close() print("") cr_common.cleanup_unpack() diff -Nru click-reviewers-tools-0.7/bin/repack-click click-reviewers-tools-0.8/bin/repack-click --- click-reviewers-tools-0.7/bin/repack-click 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/bin/repack-click 2014-07-25 13:47:22.000000000 +0000 @@ -39,7 +39,7 @@ tmpdir = tempfile.mkdtemp(prefix='clickreview-') curdir = os.getcwd() os.chdir(tmpdir) - (rc, out) = cr_common.cmd(['dpkg-deb', '-b', '--nocheck', + (rc, out) = cr_common.cmd(['dpkg-deb', '-b', '--nocheck', '-Zgzip', os.path.abspath(unpack_dir), os.path.join(tmpdir, click_fn)]) os.chdir(curdir) diff -Nru click-reviewers-tools-0.7/bin/update-apparmor-policy click-reviewers-tools-0.8/bin/update-apparmor-policy --- click-reviewers-tools-0.7/bin/update-apparmor-policy 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/update-apparmor-policy 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,34 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews import apparmor_policy +import sys + +def main(filename=None): + apparmor_policy.get_policy_file(filename) + +if __name__ == '__main__': + try: + filename = None + if len(sys.argv) > 2: + print("Usage: %s [file]" % sys.argv[0]) + sys.exit(1) + elif len(sys.argv) == 2: + filename = sys.argv[1] + main(filename) + except KeyboardInterrupt: + print('Aborted.', file=sys.stderr) + sys.exit(1) diff -Nru click-reviewers-tools-0.7/bin/update-frameworks click-reviewers-tools-0.8/bin/update-frameworks --- click-reviewers-tools-0.7/bin/update-frameworks 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/bin/update-frameworks 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,34 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews import frameworks +import sys + +def main(filename=None): + frameworks.get_frameworks_file(filename) + +if __name__ == '__main__': + try: + filename = None + if len(sys.argv) > 2: + print("Usage: %s [file]" % sys.argv[0]) + sys.exit(1) + elif len(sys.argv) == 2: + filename = sys.argv[1] + main(filename) + except KeyboardInterrupt: + print('Aborted.', file=sys.stderr) + sys.exit(1) diff -Nru click-reviewers-tools-0.7/clickreviews/apparmor_policy.py click-reviewers-tools-0.8/clickreviews/apparmor_policy.py --- click-reviewers-tools-0.7/clickreviews/apparmor_policy.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/apparmor_policy.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,37 @@ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import clickreviews.remote + +USER_DATA_FILE = os.path.join(clickreviews.remote.DATA_DIR, + 'apparmor-easyprof-ubuntu.json') + +# XXX: This is a hack and will be gone, as soon as myapps has an API for this. +AA_POLICY_DATA_URL = \ + "http://bazaar.launchpad.net/~click-reviewers/click-reviewers-tools/trunk/view/head:/data/apparmor-easyprof-ubuntu.json" + + +def get_policy_file(fn): + if fn is None: + fn = USER_DATA_FILE + clickreviews.remote.get_remote_file(fn, AA_POLICY_DATA_URL) + + +class ApparmorPolicy(object): + def __init__(self, local_copy_fn=None): + self.policy = clickreviews.remote.read_cr_file(USER_DATA_FILE, + AA_POLICY_DATA_URL, + local_copy_fn) diff -Nru click-reviewers-tools-0.7/clickreviews/cr_common.py click-reviewers-tools-0.8/clickreviews/cr_common.py --- click-reviewers-tools-0.7/clickreviews/cr_common.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_common.py 2014-07-25 13:47:22.000000000 +0000 @@ -60,6 +60,10 @@ self.click_package = fn self._check_path_exists() if not self.click_package.endswith(".click"): + if self.click_package.endswith(".deb"): + error("filename does not end with '.click', but '.deb' " + "instead. See http://askubuntu.com/a/485544/94326 for " + "how click packages are different.") error("filename does not end with '.click'") self.review_type = review_type diff -Nru click-reviewers-tools-0.7/clickreviews/cr_content_hub.py click-reviewers-tools-0.8/clickreviews/cr_content_hub.py --- click-reviewers-tools-0.7/clickreviews/cr_content_hub.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_content_hub.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,115 @@ +'''cr_content_hub.py: click content-hub checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import json +import os + + +class ClickReviewContentHub(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "content_hub") + + self.valid_keys = ['destination', 'share', 'source'] + + self.content_hub_files = dict() # click-show-files and tests + self.content_hub = dict() + for app in self.manifest['hooks']: + if 'content-hub' not in self.manifest['hooks'][app]: + # msg("Skipped missing content-hub hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['content-hub'], str): + error("manifest malformed: hooks/%s/urls is not str" % app) + (full_fn, jd) = self._extract_content_hub(app) + self.content_hub_files[app] = full_fn + self.content_hub[app] = jd + + def _extract_content_hub(self, app): + '''Get content-hub hook content''' + c = self.manifest['hooks'][app]['content-hub'] + fn = os.path.join(self.unpack_dir, c) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + fh = open_file_read(fn) + contents = "" + for line in fh.readlines(): + contents += line + fh.close() + + try: + jd = json.loads(contents) + except Exception as e: + error("content-hub json unparseable: %s (%s):\n%s" % (bn, + str(e), contents)) + + if not isinstance(jd, dict): + error("content-hub json is malformed: %s:\n%s" % (bn, contents)) + + return (fn, jd) + + def check_valid(self): + '''Check validity of content-hub entries''' + for app in sorted(self.content_hub): + for k in self.content_hub[app].keys(): + t = "info" + n = "valid_%s_%s" % (app, k) + s = "OK" + + if not isinstance(self.content_hub[app][k], list): + t = "error" + s = "'%s' is not a list" % k + elif len(self.content_hub[app][k]) < 1: + t = "error" + s = "'%s' is empty" % k + self._add_result(t, n, s) + if t == "error": + continue + + for v in self.content_hub[app][k]: + t = "info" + n = "valid_%s_%s_value" % (app, k) + s = "OK" + if not isinstance(v, str): + t = "error" + s = "'%s' is not a string" % k + elif v == "": + t = "error" + s = "'%s' is empty" % k + self._add_result(t, n, s) + + def check_unknown_keys(self): + '''Check unknown''' + for app in sorted(self.content_hub): + unknown = [] + t = "info" + n = "unknown_%s" % app + s = "OK" + for key in self.content_hub[app].keys(): + if key not in self.valid_keys: + unknown.append(key) + if len(unknown) == 1: + t = "warn" + s = "Unknown field '%s'" % unknown[0] + elif len(unknown) > 1: + t = "warn" + s = "Unknown fields '%s'" % ", ".join(unknown) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/clickreviews/cr_desktop.py click-reviewers-tools-0.8/clickreviews/cr_desktop.py --- click-reviewers-tools-0.7/clickreviews/cr_desktop.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_desktop.py 2014-07-25 13:47:22.000000000 +0000 @@ -16,7 +16,7 @@ from __future__ import print_function -from clickreviews.cr_common import ClickReview, error, open_file_read +from clickreviews.cr_common import ClickReview, error, open_file_read, msg import glob import json import os @@ -36,7 +36,15 @@ self.desktop_hook_entries = 0 for app in self.manifest['hooks']: if 'desktop' not in self.manifest['hooks'][app]: - error("could not find desktop hook for '%s'" % app) + if 'scope' in self.manifest['hooks'][app]: + # msg("Skipped missing desktop hook for scope '%s'" % app) + continue + if 'push-helper' in self.manifest['hooks'][app]: + # msg("Skipped missing desktop hook for push-helper '%s'" % + # app) + continue + else: + error("could not find desktop hook for '%s'" % app) if not isinstance(self.manifest['hooks'][app]['desktop'], str): error("manifest malformed: hooks/%s/desktop is not str" % app) self.desktop_hook_entries += 1 @@ -102,19 +110,13 @@ def check_desktop_file(self): '''Check desktop file''' t = 'info' - n = 'files_available' - s = 'OK' - if len(self._get_desktop_files().keys()) < 1: - t = 'error' - s = 'No .desktop files available.' - self._add_result(t, n, s) - - t = 'info' n = 'files_usable' s = 'OK' if len(self._get_desktop_files().keys()) != self.desktop_hook_entries: t = 'error' s = 'Could not use all specified .desktop files' + elif self.desktop_hook_entries == 0: + s = 'Skipped: could not find any desktop files' self._add_result(t, n, s) def check_desktop_file_valid(self): diff -Nru click-reviewers-tools-0.7/clickreviews/cr_functional.py click-reviewers-tools-0.8/clickreviews/cr_functional.py --- click-reviewers-tools-0.7/clickreviews/cr_functional.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_functional.py 2014-07-25 13:47:22.000000000 +0000 @@ -167,3 +167,24 @@ l = "http://askubuntu.com/questions/417342/what-does-functional-qml-application-uses-qtwebkit-mean/417343" self._add_result(t, n, s, l) + + def check_friends(self): + '''Check that QML applications don't use deprecated Friends API''' + t = 'info' + n = 'qml_application_uses_friends' + s = "OK" + l = None + + qmls = [] + pat_mv = re.compile(r'\n\s*import\s+Friends') + for i in self.qml_files: + qml = open_file_read(i).read() + if pat_mv.search(qml): + qmls.append(os.path.relpath(i, self.unpack_dir)) + + if len(qmls) > 0: + t = 'error' + s = "Found files that use deprecated Friends API: %s" % " ,".join(qmls) + l = "http://askubuntu.com/questions/497551/what-does-functional-qml-application-uses-friends-mean" + + self._add_result(t, n, s, l) diff -Nru click-reviewers-tools-0.7/clickreviews/cr_lint.py click-reviewers-tools-0.8/clickreviews/cr_lint.py --- click-reviewers-tools-0.7/clickreviews/cr_lint.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_lint.py 2014-07-25 13:47:22.000000000 +0000 @@ -21,6 +21,7 @@ import os import re +from clickreviews.frameworks import Frameworks from clickreviews.cr_common import ClickReview, open_file_read, cmd CONTROL_FILE_NAMES = ["control", "manifest", "md5sums", "preinst"] @@ -60,6 +61,20 @@ self._list_all_compiled_binaries() + self.known_hooks = ['account-application', + 'account-provider', + 'account-qml-plugin', + 'account-service', + 'apparmor', + 'content-hub', + 'desktop', + 'pay-ui', + 'push-helper', + 'scope', + 'urls'] + + self.redflagged_hooks = ['pay-ui'] + def _list_control_files(self): '''List all control files with their full path.''' for i in CONTROL_FILE_NAMES: @@ -267,11 +282,20 @@ # Some checks are already handled in # cr_common.py:_verify_manifest_structure() - # We don't support multiple apps in 13.10 - if len(self.manifest['hooks'].keys()) != 1: - self._add_result('error', 'hooks', - "more than one app key specified in hooks") - return + # While we support multiple apps in the hooks db, we don't support + # multiple apps specifying desktop hooks. Eg, it is ok to specify a + # scope, an app and a push-helper, but it isn't ok to specify two apps + t = 'info' + n = 'hooks_multiple_apps' + s = 'OK' + count = 0 + for app in self.manifest['hooks']: + if "desktop" in self.manifest['hooks'][app]: + count += 1 + if count > 1: + t = 'error' + s = 'more than one desktop app specified in hooks' + self._add_result(t, n, s) # Verify keys are well-formatted for app in self.manifest['hooks']: @@ -284,22 +308,78 @@ self._add_result(t, n, s) # Verify we have the required hooks - required = ['apparmor', 'desktop'] + required = ['apparmor'] for f in required: for app in self.manifest['hooks']: t = 'info' n = 'hooks_%s_%s' % (app, f) s = "OK" - if f == "apparmor": + if f in list(filter(lambda a: a.startswith('account-'), + self.known_hooks)): + s = "OK (run check-online-accounts for more checks)" + elif f == "apparmor": s = "OK (run check-security for more checks)" + elif f == "content-hub": + s = "OK (run check-content-hub for more checks)" elif f == "desktop": s = "OK (run check-desktop for more checks)" + elif f == "scope": + s = "OK (run check-scope for more checks)" + elif f == "urls": + s = "OK (run check-url-dispatcher for more checks)" if f not in self.manifest['hooks'][app]: t = 'error' s = "'%s' hook not found for '%s'" % (f, app) self._add_result(t, n, s) + mutually_exclusive = ['scope', 'desktop'] + for app in self.manifest['hooks']: + found = [] + for i in mutually_exclusive: + if i in self.manifest['hooks'][app]: + found.append(i) + if len(found) > 1: + t = 'error' + s = "'%s' hooks should not be used together" % ", ".join(found) + self._add_result(t, n, s) + + def check_hooks_unknown(self): + '''Check if have any unknown hooks''' + t = 'info' + n = 'unknown hooks' + s = 'OK' + + # Verify keys are well-formatted + for app in self.manifest['hooks']: + for hook in self.manifest['hooks'][app]: + t = 'info' + n = 'hooks_%s_%s_known' % (app, hook) + s = "OK" + if hook not in self.known_hooks: + t = 'warn' + s = "unknown hook '%s' in %s" % (hook, app) + self._add_result(t, n, s) + + def check_hooks_redflagged(self): + '''Check if have any redflagged hooks''' + t = 'info' + n = 'redflagged hooks' + s = 'OK' + + for app in self.manifest['hooks']: + found = [] + t = 'info' + n = 'hooks_redflag_%s' % (app) + s = "OK" + for hook in self.manifest['hooks'][app]: + if hook in self.redflagged_hooks: + found.append(hook) + if len(found) > 0: + t = 'error' + s = "(MANUAL REVIEW) '%s' not allowed" % ", ".join(found) + self._add_result(t, n, s) + def check_external_symlinks(self): '''Check if symlinks in the click package go out to the system.''' t = 'info' @@ -447,6 +527,9 @@ t = 'info' s = "OK (email '%s' long, but special case of core apps " \ "'com.ubuntu.*')" % self.email + elif self.email == "ubuntu-devel-discuss@lists.ubuntu.com": + t = 'info' + s = "OK (email '%s' long, but special case" % self.email else: t = 'error' s = "(EMAIL NEEDS HUMAN REVIEW) email domain too " \ @@ -515,26 +598,35 @@ def check_framework(self): '''Check framework()''' - t = 'info' n = 'framework' - s = 'OK' - if self.manifest['framework'] not in self.valid_frameworks: - t = 'error' - s = "'%s' is not a supported framework" % \ + l = "http://askubuntu.com/questions/460512/what-framework-should-i-use-in-my-manifest-file" + local_copy = os.path.join(os.path.dirname(__file__), + '../data/frameworks.json') + frameworks = Frameworks(local_copy) + if self.manifest['framework'] in frameworks.AVAILABLE_FRAMEWORKS: + t = 'info' + s = 'OK' + self._add_result(t, n, s) + # If it's an available framework, we're done checking + return + elif self.manifest['framework'] in frameworks.DEPRECATED_FRAMEWORKS: + t = 'warn' + s = "'%s' is deprecated. Please use a newer framework" % \ self.manifest['framework'] - self._add_result(t, n, s) - - obsolete_frameworks = ['ubuntu-sdk-13.10'] - t = 'info' - n = 'current framework' - s = 'OK' - l = None - if self.manifest['framework'] in obsolete_frameworks: + self._add_result(t, n, s, l) + return + elif self.manifest['framework'] in frameworks.OBSOLETE_FRAMEWORKS: t = 'error' s = "'%s' is obsolete. Please use a newer framework" % \ self.manifest['framework'] - l = "http://askubuntu.com/questions/460512/what-framework-should-i-use-in-my-manifest-file" - self._add_result(t, n, s, l) + self._add_result(t, n, s, l) + return + else: + # None of the above checks triggered, this is an unknown framework + t = 'error' + s = "'%s' is not a supported framework" % \ + self.manifest['framework'] + self._add_result(t, n, s, l) def check_click_local_extensions(self): '''Report any click local extensions''' diff -Nru click-reviewers-tools-0.7/clickreviews/cr_online_accounts.py click-reviewers-tools-0.8/clickreviews/cr_online_accounts.py --- click-reviewers-tools-0.7/clickreviews/cr_online_accounts.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_online_accounts.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,231 @@ +'''cr_online_accounts.py: click online accounts''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import os +# http://lxml.de/tutorial.html +import lxml.etree as etree + + +class ClickReviewAccounts(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "online_accounts") + + self.accounts_files = dict() + self.accounts = dict() + + self.account_hooks = ['account-application', + 'account-provider', + 'account-qml-plugin', + 'account-service'] + for app in self.manifest['hooks']: + for h in self.account_hooks: + if h not in self.manifest['hooks'][app]: + # msg("Skipped missing %s hook for '%s'" % (h, app)) + continue + if not isinstance(self.manifest['hooks'][app][h], str): + error("manifest malformed: hooks/%s/%s is not a str" % ( + app, h)) + + (full_fn, xml) = self._extract_account(app, h) + + if app not in self.accounts_files: + self.accounts_files[app] = dict() + self.accounts_files[app][h] = full_fn + + if app not in self.accounts: + self.accounts[app] = dict() + self.accounts[app][h] = xml + + def _extract_account(self, app, account_type): + '''Extract accounts''' + a = self.manifest['hooks'][app][account_type] + fn = os.path.join(self.unpack_dir, a) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + # qml-plugin points to a QML file, so just set that we have the + # the hook present for now + if account_type == "account-qml-plugin": + return (fn, True) + else: + try: + tree = etree.parse(fn) + xml = tree.getroot() + except Exception as e: + error("accounts xml unparseable: %s (%s):\n%s" % (bn, str(e), + contents)) + return (fn, xml) + + def check_application(self): + '''Check application''' + for app in sorted(self.accounts.keys()): + account_type = "account-application" + + t = 'info' + n = '%s_%s_root' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + + root_tag = self.accounts[app][account_type].tag.lower() + if root_tag != "application": + t = 'error' + s = "'%s' is not 'application'" % root_tag + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_id' % (app, account_type) + s = "OK" + expected_id = "%s_%s" % (self.manifest["name"], app) + if "id" not in self.accounts[app][account_type].keys(): + t = 'error' + s = "Could not find 'id' in application tag" + elif self.accounts[app][account_type].get("id") != expected_id: + t = 'error' + s = "id '%s' != '%s'" % ( + self.accounts[app][account_type].get("id"), + expected_id) + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_services' % (app, account_type) + s = "OK" + if self.accounts[app][account_type].find("services") is None: + t = 'error' + s = "Could not find '' tag" + self._add_result(t, n, s) + + if t == 'error': + continue + + t = 'info' + n = '%s_%s_service' % (app, account_type) + s = "OK" + if self.accounts[app][account_type].find("./services/service") \ + is None: + t = 'error' + s = "Could not find '' tag under " + self._add_result(t, n, s) + + def check_service(self): + '''Check service''' + for app in sorted(self.accounts.keys()): + account_type = "account-service" + + t = 'info' + n = '%s_%s_root' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + + root_tag = self.accounts[app][account_type].tag.lower() + if root_tag != "service": + t = 'error' + s = "'%s' is not 'service'" % root_tag + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_id' % (app, account_type) + s = "OK" + expected_id = "%s_%s" % (self.manifest["name"], app) + if "id" not in self.accounts[app][account_type].keys(): + t = 'error' + s = "Could not find 'id' in service tag" + elif self.accounts[app][account_type].get("id") != expected_id: + t = 'error' + s = "id '%s' != '%s'" % ( + self.accounts[app][account_type].get("id"), + expected_id) + self._add_result(t, n, s) + + if t == 'error': + continue + + for tag in ['type', 'name', 'provider']: + t = 'info' + n = '%s_%s_%s' % (app, account_type, tag) + s = "OK" + if self.accounts[app][account_type].find(tag) is None: + t = 'error' + s = "Could not find '<%s>' tag" % tag + self._add_result(t, n, s) + + def check_provider(self): + '''Check provider''' + for app in sorted(self.accounts.keys()): + account_type = "account-provider" + + t = 'info' + n = '%s_%s' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + else: + t = 'error' + s = "(MANUAL REVIEW) '%s' not allowed" % account_type + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_root' % (app, account_type) + s = "OK" + root_tag = self.accounts[app][account_type].tag.lower() + if root_tag != "provider": + t = 'error' + s = "'%s' is not 'provider'" % root_tag + self._add_result(t, n, s) + + t = 'info' + n = '%s_%s_id' % (app, account_type) + s = "OK" + expected_id = "%s_%s" % (self.manifest["name"], app) + if "id" not in self.accounts[app][account_type].keys(): + t = 'error' + s = "Could not find 'id' in provider tag" + elif self.accounts[app][account_type].get("id") != expected_id: + t = 'error' + s = "id '%s' != '%s'" % ( + self.accounts[app][account_type].get("id"), + expected_id) + self._add_result(t, n, s) + + def check_qml_plugin(self): + '''Check qml-plugin''' + for app in sorted(self.accounts.keys()): + account_type = "account-qml-plugin" + + t = 'info' + n = '%s_%s' % (app, account_type) + s = "OK" + if not account_type in self.accounts[app]: + s = "OK (missing)" + self._add_result(t, n, s) + continue + else: + t = 'error' + s = "(MANUAL REVIEW) '%s' not allowed" % account_type + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/clickreviews/cr_push_helper.py click-reviewers-tools-0.8/clickreviews/cr_push_helper.py --- click-reviewers-tools-0.7/clickreviews/cr_push_helper.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_push_helper.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,132 @@ +'''cr_push_helper.py: click push-helper checks''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import json +import os + + +class ClickReviewPushHelper(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "push_helper") + + self.required_keys = ['exec'] + self.optional_keys = ['app_id'] + + self.push_helper_files = dict() # click-show-files and tests + self.push_helper = dict() + for app in self.manifest['hooks']: + if 'push-helper' not in self.manifest['hooks'][app]: + # msg("Skipped missing push-helper hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['push-helper'], str): + error("manifest malformed: hooks/%s/urls is not str" % app) + (full_fn, jd) = self._extract_push_helper(app) + self.push_helper_files[app] = full_fn + self.push_helper[app] = jd + + def _extract_push_helper(self, app): + '''Get push-helper hook content''' + c = self.manifest['hooks'][app]['push-helper'] + fn = os.path.join(self.unpack_dir, c) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + fh = open_file_read(fn) + contents = "" + for line in fh.readlines(): + contents += line + fh.close() + + try: + jd = json.loads(contents) + except Exception as e: + error("push-helper json unparseable: %s (%s):\n%s" % (bn, + str(e), contents)) + + if not isinstance(jd, dict): + error("push-helper json is malformed: %s:\n%s" % (bn, contents)) + + return (fn, jd) + + def check_valid(self): + '''Check validity of push-helper entries''' + for app in sorted(self.push_helper): + for k in self.push_helper[app].keys(): + t = "info" + n = "valid_%s_%s" % (app, k) + s = "OK" + + if not isinstance(self.push_helper[app][k], str): + t = "error" + s = "'%s' is not a string" % k + elif self.push_helper[app][k] == "": + t = "error" + s = "'%s' is empty" % k + self._add_result(t, n, s) + + for k in self.required_keys: + t = "info" + n = "valid_%s_required_%s" % (app, k) + s = "OK" + if k not in self.push_helper[app]: + t = "error" + s = "'%s' is missing" % k + self._add_result(t, n, s) + + def check_unknown_keys(self): + '''Check unknown''' + for app in sorted(self.push_helper): + unknown = [] + t = "info" + n = "unknown_%s" % app + s = "OK" + for key in self.push_helper[app].keys(): + if key not in self.required_keys and \ + key not in self.optional_keys: + unknown.append(key) + if len(unknown) == 1: + t = "warn" + s = "Unknown field '%s'" % unknown[0] + elif len(unknown) > 1: + t = "warn" + s = "Unknown fields '%s'" % ", ".join(unknown) + self._add_result(t, n, s) + + def check_hooks(self): + '''Verify combinations of click hooks with the push-helper hook''' + for app in sorted(self.manifest['hooks']): + if app not in self.push_helper: + continue + + t = "info" + n = "other_hooks_%s" % app + s = "OK" + + bad = [] + for hook in self.manifest['hooks'][app]: + # Only the apparmor hook can be used with the push-helper + if hook not in ['push-helper', 'apparmor']: + bad.append(hook) + if len(bad) > 0: + t = 'error' + s = "Found unusual hooks with push-helper: %s" % ", ".join(bad) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/clickreviews/cr_scope.py click-reviewers-tools-0.8/clickreviews/cr_scope.py --- click-reviewers-tools-0.7/clickreviews/cr_scope.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_scope.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,135 @@ +'''cr_scope.py: click scope''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, msg +import configparser +import os + + +class ClickReviewScope(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "scope") + + self.scopes = dict() + for app in self.manifest['hooks']: + if 'scope' not in self.manifest['hooks'][app]: + # msg("Skipped missing scope hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['scope'], str): + error("manifest malformed: hooks/%s/scope is not str" % app) + self.scopes[app] = self._extract_scopes(app) + + def _extract_scopes(self, app): + '''Get scopes''' + d = dict() + + s = self.manifest['hooks'][app]['scope'] + fn = os.path.join(self.unpack_dir, s) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + elif not os.path.isdir(fn): + error("'%s' is not a directory" % bn) + + ini_fn = os.path.join(fn, "%s.ini" % self.manifest['name']) + ini_fn_bn = os.path.relpath(ini_fn, self.unpack_dir) + if not os.path.exists(ini_fn): + error("Could not find scope INI file '%s'" % ini_fn_bn) + try: + d["scope_config"] = configparser.ConfigParser() + d["scope_config"].read(ini_fn) + except Exception: + error("scope config unparseable: %s (%s)" % (ini_fn_bn, str(e))) + + d["dir"] = fn + d["dir_rel"] = bn + d["ini_file"] = ini_fn + d["ini_file_rel"] = ini_fn_bn + + return d + + def check_scope_ini(self): + '''Check scope .ini file''' + for app in sorted(self.scopes.keys()): + t = 'info' + n = 'ini_%s_scope_section' % app + s = "OK" + + if len(self.scopes[app]["scope_config"].sections()) > 1: + t = 'error' + s = "'%s' has too many sections: %s" % ( + self.scopes[app]["ini_file_rel"], + ", ".join(self.scopes[app]["scope_config"].sections())) + elif "ScopeConfig" not in \ + self.scopes[app]["scope_config"].sections(): + t = 'error' + s = "Could not find 'ScopeConfig' in '%s'" % ( + self.scopes[app]["ini_file_rel"]) + self._add_result(t, n, s) + continue + self._add_result(t, n, s) + + # Make these all lower case for easier comparisons + required = ['scoperunner', + 'displayname', + 'icon', + 'searchhint'] + optional = ['description', + 'author', + 'art'] + + missing = [] + t = 'info' + n = 'ini_%s_scope_required_fields' % (app) + s = "OK" + for r in required: + if r not in self.scopes[app]["scope_config"]['ScopeConfig']: + missing.append(r) + if len(missing) == 1: + t = 'error' + s = "Missing required field in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + missing[0]) + elif len(missing) > 1: + t = 'error' + s = "Missing required fields in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + ", ".join(missing)) + self._add_result(t, n, s) + + t = 'info' + n = 'ini_%s_scope_unknown_fields' % (app) + s = 'OK' + unknown = [] + for f in self.scopes[app]["scope_config"]['ScopeConfig'].keys(): + if f.lower() not in required and f.lower() not in optional: + unknown.append(f.lower()) + + if len(unknown) == 1: + t = 'warn' + s = "Unknown field in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + unknown[0]) + elif len(unknown) > 1: + t = 'warn' + s = "Unknown fields in '%s': %s" % ( + self.scopes[app]["ini_file_rel"], + ", ".join(unknown)) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/clickreviews/cr_security.py click-reviewers-tools-0.8/clickreviews/cr_security.py --- click-reviewers-tools-0.7/clickreviews/cr_security.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_security.py 2014-07-25 13:47:22.000000000 +0000 @@ -18,25 +18,21 @@ from clickreviews.cr_common import ClickReview, error, warn import clickreviews.cr_common as cr_common +import clickreviews.apparmor_policy as apparmor_policy import glob import json import os -easyprof_dir = "/usr/share/apparmor/easyprof" -if not os.path.isdir(easyprof_dir): - error("Error importing easyprof. Please install apparmor-easyprof") -if not os.path.isdir(os.path.join(easyprof_dir, "templates/ubuntu")): - error("Error importing easyprof. Please install apparmor-easyprof-ubuntu") - -import apparmor.easyprof - class ClickReviewSecurity(ClickReview): '''This class represents click lint reviews''' def __init__(self, fn): ClickReview.__init__(self, fn, "security") - self.supported_policy_versions = self._get_supported_policy_versions() + local_copy = os.path.join(os.path.dirname(__file__), + '../data/apparmor-easyprof-ubuntu.json') + p = apparmor_policy.ApparmorPolicy(local_copy) + self.aa_policy = p.policy self.all_fields = ['abstractions', 'author', @@ -70,6 +66,8 @@ 'video', 'webview'] + self.allowed_push_helper_policy_groups = ['push-notification-client'] + self.redflag_templates = ['unconfined'] self.extraneous_templates = ['ubuntu-sdk', 'default'] @@ -79,6 +77,7 @@ # like 'ubuntu-sdk-14.04-html5', 'ubuntu-sdk-14.04-platform', etc self.major_framework_policy = {'ubuntu-sdk-13.10': 1.0, 'ubuntu-sdk-14.04': 1.1, + 'ubuntu-sdk-14.10': 1.2, } self.security_manifests = dict() @@ -132,31 +131,6 @@ k, mp)) return m - def _get_policy_group_meta(self, group, meta, vendor, version): - '''Get meta-information from the policy group''' - cmd_args = ['--show-policy-group', '--policy-groups=%s' % group, - '--policy-version=%s' % version, - '--policy-vendor=%s' % vendor] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(None, options) - tmp = easyp.get_policygroup(group) - except apparmor.easyprof.AppArmorException: - warn("'%s' failed" % " ".join(cmd_args)) - return "" - - text = "" - for line in tmp.splitlines(): - if line.startswith("# %s: " % meta): - text = line.split(':', 1)[1].strip() - elif text != "": - if line.startswith("# "): - text += line[2:] - else: - break - - return text - def _get_security_manifest(self, app): '''Get the security manifest for app''' if app not in self.manifest['hooks']: @@ -168,21 +142,72 @@ m = self.security_manifests[f] return (f, m) - def _get_supported_policy_versions(self): + def _get_policy_versions(self, vendor): '''Get the supported AppArmor policy versions''' - version_dirs = sorted(glob.glob("%s/templates/ubuntu/*" % - easyprof_dir)) + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return None + supported_policy_versions = [] - for d in version_dirs: - if not os.path.isdir(d): - continue - try: - supported_policy_versions.append(float(os.path.basename(d))) - except TypeError: - continue - supported_policy_versions = sorted(supported_policy_versions) + for i in self.aa_policy[vendor].keys(): + supported_policy_versions.append("%.1f" % float(i)) + + return sorted(supported_policy_versions) - return supported_policy_versions + def _get_templates(self, vendor, version, aa_type="all"): + '''Get templates by type''' + templates = [] + if aa_type == "all": + for k in self.aa_policy[vendor][version]['templates'].keys(): + templates += self.aa_policy[vendor][version]['templates'][k] + else: + templates = self.aa_policy[vendor][version]['templates'][aa_type] + + return sorted(templates) + + def _has_policy_version(self, vendor, version): + '''Determine if has specified policy version''' + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return False + + if str(version) not in self.aa_policy[vendor]: + return False + return True + + def _get_highest_policy_version(self, vendor): + '''Determine highest policy version for the vendor''' + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return None + + return float(sorted(self.aa_policy[vendor].keys())[-1]) + + def _get_policy_groups(self, vendor, version, aa_type="all"): + '''Get policy groups by type''' + groups = [] + if vendor not in self.aa_policy: + error("Could not find vendor '%s'" % vendor, do_exit=False) + return groups + + if not self._has_policy_version(vendor, version): + error("Could not find version '%s'" % version, do_exit=False) + return groups + + v = str(version) + if aa_type == "all": + for k in self.aa_policy[vendor][v]['policy_groups'].keys(): + groups += self.aa_policy[vendor][v]['policy_groups'][k] + else: + groups = self.aa_policy[vendor][v]['policy_groups'][aa_type] + + return sorted(groups) + + def _get_policy_group_type(self, vendor, version, policy_group): + '''Return policy group type''' + for t in self.aa_policy[vendor][version]['policy_groups']: + if policy_group in self.aa_policy[vendor][version]['policy_groups'][t]: + return t def check_policy_vendor(self): '''Check policy_vendor''' @@ -213,17 +238,13 @@ if 'policy_vendor' in m: vendor = m['policy_vendor'] version = str(m['policy_version']) - cmd_args = ['--list-templates', '--policy-vendor=%s' % vendor, - '--policy-version=%s' % version] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - try: - apparmor.easyprof.AppArmorEasyProfile(None, options) - except Exception: + if vendor not in self.aa_policy or \ + not self._has_policy_version(vendor, version): t = 'error' s = 'could not find policy for %s/%s' % (vendor, version) self._add_result(t, n, s) - highest = sorted(self.supported_policy_versions)[-1] + highest = self._get_highest_policy_version(vendor) t = 'info' n = 'policy_version_is_highest (%s, %s)' % (str(highest), f) s = "OK" @@ -287,31 +308,16 @@ if 'policy_vendor' in m: vendor = m['policy_vendor'] version = str(m['policy_version']) - cmd_args = ['--list-templates', '--policy-vendor=%s' % vendor, - '--policy-version=%s' % version] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - templates = [] - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(None, options) - templates = easyp.get_templates() - except Exception: - t = 'error' - s = 'could not find policy for %s/%s' % (vendor, version) - self._add_result(t, n, s) - continue + + templates = self._get_templates(vendor, version) if len(templates) < 1: t = 'error' s = 'could not find templates' self._add_result(t, n, s) continue + self._add_result(t, n, s) - # If we got here, we can see if a valid template was specified - found = False - for i in templates: - if os.path.basename(i) == m['template']: - found = True - break - if not found: + if m['template'] not in self._get_templates(vendor, version): t = 'error' s = "specified unsupported template '%s'" % m['template'] @@ -353,13 +359,85 @@ self._add_result(t, n, s) + def check_template_push_helpers(self): + '''Check template for push-helpers''' + for app in sorted(self.manifest['hooks']): + (f, m) = self._get_security_manifest(app) + t = 'info' + n = 'template_push_helper(%s)' % f + s = "OK" + if 'push-helper' not in self.manifest['hooks'][app]: + continue + if 'template' in m and m['template'] != "ubuntu-sdk": + t = 'error' + s = "template is not 'ubuntu-sdk'" + self._add_result(t, n, s) + + def check_policy_groups_push_helpers(self): + '''Check policy_groups for push-helpers''' + for app in sorted(self.manifest['hooks']): + (f, m) = self._get_security_manifest(app) + t = 'info' + n = 'policy_groups_push_helper(%s)' % f + s = "OK" + if 'push-helper' not in self.manifest['hooks'][app]: + continue + if 'policy_groups' not in m or \ + 'push-notification-client' not in m['policy_groups']: + self._add_result('error', n, + "required group 'push-notification-client' " + "not found") + continue + bad = [] + for p in m['policy_groups']: + if p not in self.allowed_push_helper_policy_groups: + bad.append(p) + if len(bad) > 0: + t = 'error' + s = "found unusual policy groups: %s" % ", ".join(bad) + self._add_result(t, n, s) + + def check_policy_groups_scopes(self): + '''Check policy_groups for scopes''' + for app in sorted(self.manifest['hooks']): + (f, m) = self._get_security_manifest(app) + t = 'info' + n = 'policy_groups_scopes (%s)' % f + s = "OK" +# jdstrand, 2014-06-05: ubuntu-scope-local-content is no longer available +# scope_templates = ['ubuntu-scope-network', +# 'ubuntu-scope-local-content'] + scope_templates = ['ubuntu-scope-network'] + if 'template' not in m or m['template'] not in scope_templates: + continue + + if 'policy_groups' not in m: + continue + + bad = [] + for p in m['policy_groups']: + if m['template'] == 'ubuntu-scope-network': + # networking scopes shouldn't have access to anything + # (for now, this may change with trust store (eg, location) + if p != 'networking': + bad.append(p) +# jdstrand, 2014-06-05: ubuntu-scope-local-content is no longer available +# elif m['template'] == 'ubuntu-scope-local-content': +# if p == 'networking': +# bad.append(p) + + if len(bad) > 0: + t = 'error' + s = "found inappropriate policy groups: %s" % ", ".join(bad) + self._add_result(t, n, s) + def check_policy_groups(self): '''Check policy_groups''' for app in sorted(self.manifest['hooks']): (f, m) = self._get_security_manifest(app) t = 'info' - n = 'policy_groups_exists (%s)' % f + n = 'policy_groups_exists_%s (%s)' % (app, f) if 'policy_groups' not in m: # If template not specified, we just use the default self._add_result('warn', n, 'no policy groups specified') @@ -374,18 +452,8 @@ if 'policy_vendor' in m: vendor = m['policy_vendor'] version = str(m['policy_version']) - cmd_args = ['--list-policy-groups', '--policy-vendor=%s' % vendor, - '--policy-version=%s' % version] - (options, args) = apparmor.easyprof.parse_args(cmd_args) - policy_groups = [] - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(None, options) - policy_groups = easyp.get_policy_groups() - except Exception: - t = 'error' - s = 'could not find policy for %s/%s' % (vendor, version) - self._add_result(t, n, s) - continue + + policy_groups = self._get_policy_groups(version=version, vendor=vendor) if len(policy_groups) < 1: t = 'error' s = 'could not find policy groups' @@ -395,7 +463,7 @@ # Check for duplicates t = 'info' - n = 'policy_groups_duplicates (%s)' % f + n = 'policy_groups_duplicates_%s (%s)' % (app, f) s = 'OK' tmp = [] for p in m['policy_groups']: @@ -410,7 +478,7 @@ # If we got here, we can see if valid policy groups were specified for i in m['policy_groups']: t = 'info' - n = 'policy_groups_valid (%s)' % i + n = 'policy_groups_valid_%s (%s)' % (app, i) s = 'OK' # SDK will leave and empty policy group, report but don't @@ -433,16 +501,22 @@ if found: t = 'info' - n = 'policy_groups_safe (%s)' % i + n = 'policy_groups_safe_%s (%s)' % (app, i) s = 'OK' - usage = self._get_policy_group_meta(i, "Usage", - vendor, version) - if usage != "common": - desc = self._get_policy_group_meta(i, "Description", - vendor, version) + + aa_type = self._get_policy_group_type(vendor, version, i) + if i == "debug": + t = 'error' + s = "(REJECT) %s policy group " % aa_type + \ + "'%s': not for production use" % (i) + elif aa_type == "reserved": + t = 'error' + s = "(MANUAL REVIEW) %s policy group " % aa_type + \ + "'%s': vetted applications only" % (i) + elif aa_type != "common": t = 'error' - s = "(MANUAL REVIEW) %s policy group " % usage + \ - "'%s': %s" % (i, desc) + s = "policy group '%s' has" % i + \ + "unknown type '%s'" % (aa_type) self._add_result(t, n, s) def check_ignored(self): diff -Nru click-reviewers-tools-0.7/clickreviews/cr_skeleton.py click-reviewers-tools-0.8/clickreviews/cr_skeleton.py --- click-reviewers-tools-0.7/clickreviews/cr_skeleton.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_skeleton.py 2014-07-25 13:47:22.000000000 +0000 @@ -1,6 +1,6 @@ '''cr_skeleton.py: click skeleton''' # -# Copyright (C) 2013 Canonical Ltd. +# Copyright (C) 2014 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff -Nru click-reviewers-tools-0.7/clickreviews/cr_tests.py click-reviewers-tools-0.8/clickreviews/cr_tests.py --- click-reviewers-tools-0.7/clickreviews/cr_tests.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_tests.py 2014-07-25 13:47:22.000000000 +0000 @@ -32,6 +32,14 @@ TEST_SECURITY = dict() TEST_DESKTOP = dict() TEST_WEBAPP_MANIFESTS = dict() +TEST_URLS = dict() +TEST_SCOPES = dict() +TEST_CONTENT_HUB = dict() +TEST_ACCOUNTS_APPLICATION = dict() +TEST_ACCOUNTS_PROVIDER = dict() +TEST_ACCOUNTS_QML_PLUGIN = dict() +TEST_ACCOUNTS_SERVICE = dict() +TEST_PUSH_HELPER = dict() # @@ -59,6 +67,18 @@ "ubuntu-sdk-14.04-html-dev1", "ubuntu-sdk-14.04-papi-dev1", "ubuntu-sdk-14.04-qml-dev1", + "ubuntu-sdk-14.04", + "ubuntu-sdk-14.04-html", + "ubuntu-sdk-14.04-papi", + "ubuntu-sdk-14.04-qml", + "ubuntu-sdk-14.10-dev1", + "ubuntu-sdk-14.10-html-dev1", + "ubuntu-sdk-14.10-papi-dev1", + "ubuntu-sdk-14.10-qml-dev1", + "ubuntu-sdk-14.10-dev2", + "ubuntu-sdk-14.10-html-dev2", + "ubuntu-sdk-14.10-papi-dev2", + "ubuntu-sdk-14.10-qml-dev2", ] @@ -74,7 +94,7 @@ def _get_security_supported_policy_versions(self): '''Pretend we read the contens of /usr/share/apparmor/easyprof''' - return [1.0, 1.1] + return [1.0, 1.1, 1.2] def _extract_desktop_entry(self, app): @@ -92,6 +112,48 @@ return TEST_WEBAPP_MANIFESTS +def _extract_url_dispatcher(self, app): + '''Pretend we read the url dispatcher file''' + return ("%s.url-dispatcher" % app, TEST_URLS[app]) + + +def _extract_scopes(self, app): + '''Pretend we found and read the files in the scope directories''' + return TEST_SCOPES[app] + + +def _extract_content_hub(self, app): + '''Pretend we read the content-hub file''' + return ("%s.content.json" % app, TEST_CONTENT_HUB[app]) + + +def _extract_account(self, app, account_type): + '''Pretend we read the accounts file''' + f = app + val = None + if account_type == "account-application": + f += ".application" + val = TEST_ACCOUNTS_APPLICATION[app] + elif account_type == "account-provider": + f += ".provider" + val = TEST_ACCOUNTS_PROVIDER[app] + elif account_type == "account-qml-plugin": + f += ".qml-plugin" + val = TEST_ACCOUNTS_QML_PLUGIN[app] + elif account_type == "account-service": + f += ".service" + val = TEST_ACCOUNTS_SERVICE[app] + else: # should never get here + raise ValueError("Unknown account_type '%s'" % account_type) + + return (f, val) + + +def _extract_push_helper(self, app): + '''Pretend we read the push-helper file''' + return ("%s.push-helper.json" % app, TEST_PUSH_HELPER[app]) + + # http://docs.python.org/3.4/library/unittest.mock-examples.html # Mock patching. Don't use decorators but instead patch in setUp() of the # child. Set up a list of patches, but don't start them. Create the helper @@ -140,9 +202,6 @@ patches.append(patch( 'clickreviews.cr_security.ClickReviewSecurity._get_security_manifest', _get_security_manifest)) -patches.append(patch( - 'clickreviews.cr_security.ClickReviewSecurity._get_supported_policy_versions', - _get_security_supported_policy_versions)) # desktop overrides patches.append(patch( @@ -155,6 +214,31 @@ 'clickreviews.cr_desktop.ClickReviewDesktop._extract_webapp_manifests', _extract_webapp_manifests)) +# url-dispatcher overrides +patches.append(patch( + 'clickreviews.cr_url_dispatcher.ClickReviewUrlDispatcher._extract_url_dispatcher', + _extract_url_dispatcher)) + +# scope overrides +patches.append(patch( + 'clickreviews.cr_scope.ClickReviewScope._extract_scopes', + _extract_scopes)) + +# content-hub overrides +patches.append(patch( + 'clickreviews.cr_content_hub.ClickReviewContentHub._extract_content_hub', + _extract_content_hub)) + +# online accounts overrides +patches.append(patch( + 'clickreviews.cr_online_accounts.ClickReviewAccounts._extract_account', + _extract_account)) + +# push-helper overrides +patches.append(patch( + 'clickreviews.cr_push_helper.ClickReviewPushHelper._extract_push_helper', + _extract_push_helper)) + def mock_patch(): '''Call in setup of child''' @@ -208,11 +292,21 @@ "%s.json" % self.default_appname self.test_manifest["hooks"][self.default_appname]["desktop"] = \ "%s.desktop" % self.default_appname + self.test_manifest["hooks"][self.default_appname]["urls"] = \ + "%s.url-dispatcher" % self.default_appname self._update_test_manifest() # hooks self.test_security_manifests = dict() self.test_desktop_files = dict() + self.test_url_dispatcher = dict() + self.test_scopes = dict() + self.test_content_hub = dict() + self.test_accounts_application = dict() + self.test_accounts_provider = dict() + self.test_accounts_qml_plugin = dict() + self.test_accounts_service = dict() + self.test_push_helper = dict() for app in self.test_manifest["hooks"].keys(): # setup security manifest for each app self.set_test_security_manifest(app, 'policy_groups', @@ -233,8 +327,34 @@ self.set_test_desktop(app, 'X-Ubuntu-Touch', 'true', no_update=True) + self.set_test_url_dispatcher(app, None, None) + + # Ensure we have no scope entries since they conflict with desktop. + # Scope tests will have to add them as part of their tests. + self.set_test_scope(app, None) + + # Reset to no content-hub entries in manifest + self.set_test_content_hub(app, None, None) + + # Reset to no content-hub entries in manifest + self.set_test_account(app, "account-application", None) + self.set_test_account(app, "account-provider", None) + self.set_test_account(app, "account-qml-plugin", None) + self.set_test_account(app, "account-service", None) + + # Reset to no content-hub entries in manifest + self.set_test_push_helper(app, None, None) + self._update_test_security_manifests() self._update_test_desktop_files() + self._update_test_url_dispatcher() + self._update_test_scopes() + self._update_test_content_hub() + self._update_test_accounts_application() + self._update_test_accounts_provider() + self._update_test_accounts_qml_plugin() + self._update_test_accounts_service() + self._update_test_push_helper() # webapps manifests (leave empty for now) self.test_webapp_manifests = dict() @@ -280,6 +400,75 @@ for i in self.test_webapp_manifests.keys(): TEST_WEBAPP_MANIFESTS[i] = self.test_webapp_manifests[i] + def _update_test_url_dispatcher(self): + global TEST_URLS + TEST_URLS = dict() + for app in self.test_url_dispatcher.keys(): + TEST_URLS[app] = self.test_url_dispatcher[app] + + def _update_test_scopes(self): + global TEST_SCOPES + TEST_SCOPES = dict() + for app in self.test_scopes.keys(): + TEST_SCOPES[app] = self.test_scopes[app] + self.test_manifest["hooks"][app]["scope"] = \ + TEST_SCOPES[app]["dir_rel"] + self._update_test_manifest() + + def _update_test_content_hub(self): + global TEST_CONTENT_HUB + TEST_CONTENT_HUB = dict() + for app in self.test_content_hub.keys(): + TEST_CONTENT_HUB[app] = self.test_content_hub[app] + self.test_manifest["hooks"][app]["content-hub"] = \ + "%s.content.json" % app + self._update_test_manifest() + + def _update_test_accounts_application(self): + global TEST_ACCOUNTS_APPLICATION + TEST_ACCOUNTS_APPLICATION = dict() + for app in self.test_accounts_application.keys(): + TEST_ACCOUNTS_APPLICATION[app] = self.test_accounts_application[app] + self.test_manifest["hooks"][app]["account-application"] = \ + "%s.application" % app + self._update_test_manifest() + + def _update_test_accounts_provider(self): + global TEST_ACCOUNTS_PROVIDER + TEST_ACCOUNTS_PROVIDER = dict() + for app in self.test_accounts_provider.keys(): + TEST_ACCOUNTS_PROVIDER[app] = self.test_accounts_provider[app] + self.test_manifest["hooks"][app]["account-provider"] = \ + "%s.provider" % app + self._update_test_manifest() + + def _update_test_accounts_qml_plugin(self): + global TEST_ACCOUNTS_QML_PLUGIN + TEST_ACCOUNTS_QML_PLUGIN = dict() + for app in self.test_accounts_qml_plugin.keys(): + TEST_ACCOUNTS_QML_PLUGIN[app] = self.test_accounts_qml_plugin[app] + self.test_manifest["hooks"][app]["account-qml-plugin"] = \ + "%s.qml_plugin" % app + self._update_test_manifest() + + def _update_test_accounts_service(self): + global TEST_ACCOUNTS_SERVICE + TEST_ACCOUNTS_SERVICE = dict() + for app in self.test_accounts_service.keys(): + TEST_ACCOUNTS_SERVICE[app] = self.test_accounts_service[app] + self.test_manifest["hooks"][app]["account-service"] = \ + "%s.service" % app + self._update_test_manifest() + + def _update_test_push_helper(self): + global TEST_PUSH_HELPER + TEST_PUSH_HELPER = dict() + for app in self.test_push_helper.keys(): + TEST_PUSH_HELPER[app] = self.test_push_helper[app] + self.test_manifest["hooks"][app]["push-helper"] = \ + "%s.push-helper.json" % app + self._update_test_manifest() + def _update_test_name(self): self.test_name = "%s_%s_%s.click" % (self.test_control['Package'], self.test_control['Version'], @@ -370,7 +559,6 @@ def set_test_webapp_manifest(self, fn, key, value): '''Set key in webapp manifest to value. If value is None, remove key''' - if key is None and value is None: self.test_webapp_manifests[fn] = None self._update_test_webapp_manifests() @@ -386,6 +574,90 @@ self.test_webapp_manifests[fn][key] = value self._update_test_webapp_manifests() + def set_test_url_dispatcher(self, app, key, value, append=False): + '''Set url-dispatcher entries. If value is None, remove''' + if app not in self.test_url_dispatcher: + self.test_url_dispatcher[app] = [] + + if value is None: + self.test_url_dispatcher[app] = [] + else: + if not append: + self.test_url_dispatcher[app] = [] + self.test_url_dispatcher[app].append({key: value}) + self._update_test_url_dispatcher() + + def set_test_scope(self, app, scope): + '''Set scope for app. If it is None, remove''' + if scope is None: + if app in self.test_scopes: + self.test_scopes.pop(app) + if 'scope' in self.test_manifest['hooks'][app]: + self.test_manifest['hooks'][app].pop('scope', None) + else: + self.test_scopes[app] = scope + self._update_test_scopes() + + def set_test_content_hub(self, app, key, value): + '''Set content-hub entries. If value is None, remove key, if key is + None, remove content-hub from manifest''' + if key is None: + if app in self.test_content_hub: + self.test_content_hub.pop(app) + elif value is None: + if key in self.test_content_hub[app]: + self.test_content_hub[app].pop(key) + else: + if app not in self.test_content_hub: + self.test_content_hub[app] = dict() + if key not in self.test_content_hub[app]: + self.test_content_hub[app][key] = [] + self.test_content_hub[app][key].append(value) + self._update_test_content_hub() + + def set_test_account(self, app, account_type, value): + '''Set accounts XML. If value is None, remove from manifest''' + if account_type == "account-application": + d = self.test_accounts_application + elif account_type == "account-provider": + d = self.test_accounts_provider + elif account_type == "account-qml-plugin": + d = self.test_accounts_qml_plugin + elif account_type == "account-service": + d = self.test_accounts_service + else: # should never get here + raise ValueError("Unknown account_type '%s'" % account_type) + + if value is None: + if app in d: + d[app] = None + else: + d[app] = value + + if account_type == "account-application": + self._update_test_accounts_application() + elif account_type == "account-provider": + self._update_test_accounts_provider() + elif account_type == "account-qml-plugin": + self._update_test_accounts_qml_plugin() + elif account_type == "account-service": + self._update_test_accounts_service() + + def set_test_push_helper(self, app, key, value): + '''Set push-helper entries. If value is None, remove key, if key is + None, remove content-hub from manifest''' + if key is None: + if app in self.test_push_helper: + self.test_push_helper.pop(app) + elif value is None: + if key in self.test_push_helper[app]: + self.test_push_helper[app].pop(key) + else: + if app not in self.test_push_helper: + self.test_push_helper[app] = dict() + self.test_push_helper[app][key] = value + self._update_test_push_helper() + def setUp(self): '''Make sure our patches are applied everywhere''' global patches @@ -402,6 +674,22 @@ TEST_SECURITY = dict() global TEST_DESKTOP TEST_DESKTOP = dict() + global TEST_URLS + TEST_URLS = dict() + global TEST_SCOPES + TEST_SCOPES = dict() + global TEST_CONTENT_HUB + TEST_CONTENT_HUB = dict() + global TEST_ACCOUNTS_APPLICATION + TEST_ACCOUNTS_APPLICATION = dict() + global TEST_ACCOUNTS_PROVIDER + TEST_ACCOUNTS_PROVIDER = dict() + global TEST_ACCOUNTS_QML_PLUGIN + TEST_ACCOUNTS_QML_PLUGIN = dict() + global TEST_ACCOUNTS_SERVICE + TEST_ACCOUNTS_APPLICATION = dict() + global TEST_PUSH_HELPER + TEST_PUSH_HELPER = dict() self._reset_test_data() cr_common.recursive_rm(self.desktop_tmpdir) diff -Nru click-reviewers-tools-0.7/clickreviews/cr_url_dispatcher.py click-reviewers-tools-0.8/clickreviews/cr_url_dispatcher.py --- click-reviewers-tools-0.7/clickreviews/cr_url_dispatcher.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/cr_url_dispatcher.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,152 @@ +'''cr_url dispatcher.py: click url_dispatcher''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function + +from clickreviews.cr_common import ClickReview, error, open_file_read, msg +import json +import os + +# https://wiki.ubuntu.com/URLDispatcher + + +class ClickReviewUrlDispatcher(ClickReview): + '''This class represents click lint reviews''' + def __init__(self, fn): + ClickReview.__init__(self, fn, "url_dispatcher") + + self.required_keys = ['protocol'] + self.optional_keys = ['domain-suffix'] + + self.url_dispatcher_files = dict() # click-show-files and tests + self.url_dispatcher = dict() + for app in self.manifest['hooks']: + if 'urls' not in self.manifest['hooks'][app]: + # msg("Skipped missing urls hook for '%s'" % app) + continue + if not isinstance(self.manifest['hooks'][app]['urls'], str): + error("manifest malformed: hooks/%s/urls is not str" % app) + (full_fn, jd) = self._extract_url_dispatcher(app) + self.url_dispatcher_files[app] = full_fn + self.url_dispatcher[app] = jd + + def _extract_url_dispatcher(self, app): + '''Get url dispatcher json''' + u = self.manifest['hooks'][app]['urls'] + fn = os.path.join(self.unpack_dir, u) + + bn = os.path.basename(fn) + if not os.path.exists(fn): + error("Could not find '%s'" % bn) + + fh = open_file_read(fn) + contents = "" + for line in fh.readlines(): + contents += line + fh.close() + + try: + jd = json.loads(contents) + except Exception as e: + error("url-dispatcher json unparseable: %s (%s):\n%s" % (bn, + str(e), contents)) + + if not isinstance(jd, list): + error("url-dispatcher json is malformed: %s:\n%s" % (bn, contents)) + + return (fn, jd) + + def check_required(self): + '''Check url-dispatcher required fields''' + for app in sorted(self.url_dispatcher): + for r in self.required_keys: + found = False + t = 'info' + n = 'required_entry_%s_%s' % (app, r) + s = "OK" + for entry in self.url_dispatcher[app]: + if not isinstance(entry, dict): + t = 'error' + s = "'%s' is not a dict" % str(entry) + self._add_result(t, n, s) + continue + if r in entry: + if not isinstance(entry[r], str): + t = 'error' + s = "'%s' is not a string" % r + elif entry[r] == "": + t = 'error' + s = "'%s' is empty" % r + else: + found = True + if not found and t != 'error': + t = 'error' + s = "Missing required field '%s'" % r + self._add_result(t, n, s) + + def check_optional(self): + '''Check url-dispatcher optional fields''' + for app in sorted(self.url_dispatcher): + for o in self.optional_keys: + found = False + t = 'info' + n = 'optional_entry_%s_%s' % (app, o) + s = "OK" + for entry in self.url_dispatcher[app]: + if not isinstance(entry, dict): + t = 'error' + s = "'%s' is not a dict" % str(entry) + self._add_result(t, n, s) + continue + if o in entry: + if not isinstance(entry[o], str): + t = 'error' + s = "'%s' is not a string" % o + elif entry[o] == "": + t = 'error' + s = "'%s' is empty" % o + else: + found = True + if not found and t != 'error': + s = "OK (skip missing)" + self._add_result(t, n, s) + + def check_unknown(self): + '''Check url-dispatcher unknown fields''' + for app in sorted(self.url_dispatcher): + unknown = [] + for entry in self.url_dispatcher[app]: + t = 'info' + n = 'unknown_entry_%s' % app + s = "OK" + if not isinstance(entry, dict): + t = 'error' + s = "'%s' is not a dict" % str(entry) + self._add_result(t, n, s) + continue + + for f in entry.keys(): + if f not in self.required_keys and \ + f not in self.optional_keys: + unknown.append(f) + + if len(unknown) == 1: + t = 'warn' + s = "Unknown field '%s'" % unknown[0] + elif len(unknown) > 1: + t = 'warn' + s = "Unknown fields '%s'" % ", ".join(unknown) + self._add_result(t, n, s) diff -Nru click-reviewers-tools-0.7/clickreviews/frameworks.py click-reviewers-tools-0.8/clickreviews/frameworks.py --- click-reviewers-tools-0.7/clickreviews/frameworks.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/frameworks.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,48 @@ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import clickreviews.remote + +USER_DATA_FILE = os.path.join(clickreviews.remote.DATA_DIR, 'frameworks.json') + +# XXX: This is a hack and will be gone, as soon as myapps has an API for this. +FRAMEWORKS_DATA_URL = \ + "http://bazaar.launchpad.net/~ubuntu-core-dev/+junk/frameworks/view/head:/frameworks.json" + + +def get_frameworks_file(fn): + if fn is None: + fn = USER_DATA_FILE + clickreviews.remote.get_remote_file(fn, FRAMEWORKS_DATA_URL) + + +class Frameworks(object): + DEPRECATED_FRAMEWORKS = [] + OBSOLETE_FRAMEWORKS = [] + AVAILABLE_FRAMEWORKS = [] + + def __init__(self, local_copy_fn=None): + self.FRAMEWORKS = clickreviews.remote.read_cr_file(USER_DATA_FILE, + FRAMEWORKS_DATA_URL, + local_copy_fn) + + for k, v in self.FRAMEWORKS.items(): + if v == 'deprecated': + self.DEPRECATED_FRAMEWORKS.append(k) + elif v == 'obsolete': + self.OBSOLETE_FRAMEWORKS.append(k) + elif v == 'available': + self.AVAILABLE_FRAMEWORKS.append(k) diff -Nru click-reviewers-tools-0.7/clickreviews/remote.py click-reviewers-tools-0.8/clickreviews/remote.py --- click-reviewers-tools-0.7/clickreviews/remote.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/remote.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,96 @@ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json +import os +import re +from socket import timeout +import sys +import time +from urllib import request, parse +from urllib.error import HTTPError, URLError + +DATA_DIR = os.path.join(os.path.expanduser('~/.cache/click-reviewers-tools/')) +UPDATE_INTERVAL = 60 * 60 * 24 * 7 + + +def _update_is_necessary(fn): + return (not os.path.exists(fn)) or \ + (time.time() - os.path.getctime(fn) >= UPDATE_INTERVAL) + + +def _update_is_possible(url): + update = True + try: + request.urlopen(url) + except (HTTPError, URLError): + update = False + except timeout: + update = False + return update + + +def abort(msg=None): + if msg: + print(msg, file=sys.stderr) + print('Aborted.', file=sys.stderr) + sys.exit(1) + + +# +# Public +# +def get_remote_file(fn, url, data_dir=DATA_DIR): + try: + f = request.urlopen(url) + except (HTTPError, URLError) as error: + abort('Data not retrieved because %s.' % error) + except timeout: + abort('Socket timed out.') + html = f.read() + # XXX: This is a hack and will be gone, as soon as myapps has an API for this. + link = re.findall(b'download file', html) + if not link: + abort() + download_link = '{}://{}/{}'.format( + parse.urlparse(url).scheme, + parse.urlparse(url).netloc, + link[0].decode("utf-8")) + f = request.urlopen(download_link) + if not f: + abort() + if os.path.exists(fn): + os.remove(fn) + if not os.path.exists(os.path.dirname(fn)): + os.makedirs(os.path.dirname(fn)) + with open(fn, 'bw') as local_file: + local_file.write(f.read()) + + +def read_cr_file(fn, url, local_copy_fn=None): + '''read click reviews file from remote or local copy: + - fn: where to store the cached file + - url: url to fetch + - local_copy_fn: force use of local copy + ''' + j = {} + if local_copy_fn and os.path.exists(local_copy_fn): + j = json.loads(open(local_copy_fn, 'r').read()) + else: + if _update_is_necessary(fn) and _update_is_possible(url): + get_remote_file(fn, url) + if os.path.exists(fn): + j = json.loads(open(fn, 'r').read()) + return j diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_aaa_example_cr_skeleton.py click-reviewers-tools-0.8/clickreviews/tests/test_aaa_example_cr_skeleton.py --- click-reviewers-tools-0.7/clickreviews/tests/test_aaa_example_cr_skeleton.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_aaa_example_cr_skeleton.py 2014-07-25 13:47:22.000000000 +0000 @@ -1,6 +1,6 @@ '''test_cr_skeleton.py: tests for the cr_skeleton module''' # -# Copyright (C) 2013 Canonical Ltd. +# Copyright (C) 2014 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_content_hub.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_content_hub.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_content_hub.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_content_hub.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,111 @@ +'''test_cr_content_hub.py: tests for the cr_content-hub module''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_content_hub import ClickReviewContentHub +import clickreviews.cr_tests as cr_tests + + +class TestClickReviewContentHub(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def test_check_unknown_keys_none(self): + '''Test check_unknown() - no unknown''' + self.set_test_content_hub(self.default_appname, "source", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys1(self): + '''Test check_unknown() - one unknown''' + self.set_test_content_hub(self.default_appname, "nonexistent", "foo") + c = ClickReviewContentHub(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys2(self): + '''Test check_unknown() - good with one unknown''' + self.set_test_content_hub(self.default_appname, "source", "pictures") + self.set_test_content_hub(self.default_appname, "nonexistent", "foo") + c = ClickReviewContentHub(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_source(self): + '''Test check_valid() - source''' + self.set_test_content_hub(self.default_appname, "source", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_share(self): + '''Test check_valid() - share''' + self.set_test_content_hub(self.default_appname, "share", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_destination(self): + '''Test check_valid() - destination''' + self.set_test_content_hub(self.default_appname, "destination", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_all(self): + '''Test check_valid() - all''' + self.set_test_content_hub(self.default_appname, "destination", "pictures") + self.set_test_content_hub(self.default_appname, "share", "pictures") + self.set_test_content_hub(self.default_appname, "source", "pictures") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 6, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_bad_value(self): + '''Test check_valid() - bad value''' + self.set_test_content_hub(self.default_appname, "destination", []) + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_empty_value(self): + '''Test check_valid() - empty value''' + self.set_test_content_hub(self.default_appname, "source", "") + c = ClickReviewContentHub(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_desktop.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_desktop.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_desktop.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_desktop.py 2014-07-25 13:47:22.000000000 +0000 @@ -36,7 +36,6 @@ expected['warn'] = dict() expected['error'] = dict() expected['info']['desktop_files_usable'] = {"text": "OK"} - expected['info']['desktop_files_available'] = {"text": "OK"} self.check_results(r, expected=expected) def test_check_desktop_file_valid(self): diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_lint.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_lint.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_lint.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_lint.py 2014-07-25 13:47:22.000000000 +0000 @@ -506,6 +506,27 @@ expected_counts = {'info': None, 'warn': 1, 'error': 0} self.check_results(r, expected_counts) + def test_check_maintainer_email_special(self): + '''Test check_maintainer() - ubuntu-devel-discuss@lists.ubuntu.com''' + self.set_test_control("Package", "com.canonical.app") + self.set_test_manifest("maintainer", + "Ubuntu Core Developers " + "") + c = ClickReviewLint(self.test_name) + c.check_maintainer() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + expected = dict() + expected['info'] = dict() + expected['warn'] = dict() + expected['error'] = dict() + expected['info']['lint_maintainer_domain'] = \ + {"text": "OK (email 'ubuntu-devel-discuss@lists.ubuntu.com' long, " + "but special case"} + self.check_results(r, expected=expected) + def test_check_icon(self): '''Test check_icon()''' self.set_test_manifest("icon", "someicon") @@ -580,11 +601,11 @@ def test_check_framework(self): '''Test check_framework()''' - self.set_test_manifest("framework", "ubuntu-sdk-14.04-qml-dev1") + self.set_test_manifest("framework", "ubuntu-sdk-14.10-qml-dev2") c = ClickReviewLint(self.test_name) c.check_framework() r = c.click_report - expected_counts = {'info': 2, 'warn': 0, 'error': 0} + expected_counts = {'info': 1, 'warn': 0, 'error': 0} self.check_results(r, expected_counts) def test_check_framework_bad(self): @@ -596,11 +617,123 @@ expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(r, expected_counts) + def test_check_framework_deprecated(self): + '''Test check_framework() - deprecated''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.check_framework() + r = c.click_report + expected_counts = {'info': None, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + def test_check_framework_obsolete(self): '''Test check_framework() - obsolete''' - self.set_test_manifest("framework", "ubuntu-sdk-13.10") + self.set_test_manifest("framework", "ubuntu-sdk-14.10-qml-dev1") c = ClickReviewLint(self.test_name) c.check_framework() r = c.click_report expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(r, expected_counts) + + def test_check_hooks(self): + '''Test check_hooks()''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_multiple_desktop_apps(self): + '''Test check_hooks() - multiple desktop apps''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + tmp = c.manifest['hooks'][self.default_appname] + c.manifest['hooks']["another-app"] = tmp + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_multiple_apps(self): + '''Test check_hooks() - multiple non-desktop apps''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + tmp = dict() + for k in c.manifest['hooks'][self.default_appname].keys(): + tmp[k] = c.manifest['hooks'][self.default_appname][k] + tmp.pop('desktop') + tmp['scope'] = "some-scope-exec" + c.manifest['hooks']["some-scope"] = tmp + tmp = dict() + for k in c.manifest['hooks'][self.default_appname].keys(): + tmp[k] = c.manifest['hooks'][self.default_appname][k] + tmp.pop('desktop') + tmp['push-helper'] = "push.json" + c.manifest['hooks']["some-push-helper"] = tmp + + c.check_hooks() + r = c.click_report + expected_counts = {'info': 7, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_bad_appname(self): + '''Test check_hooks() - bad appname''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + tmp = c.manifest['hooks'][self.default_appname] + del c.manifest['hooks'][self.default_appname] + c.manifest['hooks']["b@d@ppn@m#"] = tmp + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_missing_apparmor(self): + '''Test check_hooks() - missing apparmor''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + del c.manifest['hooks'][self.default_appname]['apparmor'] + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_has_desktop_and_scope(self): + '''Test check_hooks() - desktop with scope''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.manifest['hooks'][self.default_appname]["scope"] = "some-binary" + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks_unknown_nonexistent(self): + '''Test check_hooks_unknown() - nonexistent''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.manifest['hooks'][self.default_appname]["nonexistant"] = "foo" + c.check_hooks_unknown() + r = c.click_report + expected_counts = {'info': None, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_unknown_good(self): + '''Test check_hooks_unknown()''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.check_hooks_unknown() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_redflagged(self): + '''Test check_hooks_redflagged()''' + self.set_test_manifest("framework", "ubuntu-sdk-13.10") + c = ClickReviewLint(self.test_name) + c.manifest['hooks'][self.default_appname]["pay-ui"] = "foo" + c.check_hooks_redflagged() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_online_accounts.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_online_accounts.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_online_accounts.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_online_accounts.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,317 @@ +'''test_cr_online_accounts.py: tests for the cr_online accounts module''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_online_accounts import ClickReviewAccounts +import clickreviews.cr_tests as cr_tests +import lxml.etree as etree + + +class TestClickReviewAccounts(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def _stub_application(self, root=None, id=None, do_subtree=True): + '''Stub application xml''' + if id is None: + id = "%s_%s" % (self.test_manifest["name"], self.default_appname) + if root is None: + root = "application" + if id == "": + xml = etree.Element(root) + else: + xml = etree.Element(root, id="%s" % id) + if do_subtree: + services = etree.SubElement(xml, "services") + elem1 = etree.SubElement(services, "service", id="element1") + desc1 = etree.SubElement(elem1, "description") + desc1.text = "elem1 description" + elem2 = etree.SubElement(services, "service", id="element2") + desc2 = etree.SubElement(elem2, "description") + desc2.text = "elem2 description" + return xml + + def _stub_service(self, root=None, id=None, do_subtree=True): + '''Stub service xml''' + if id is None: + id = "%s_%s" % (self.test_manifest["name"], self.default_appname) + if root is None: + root = "service" + if id == "": + xml = etree.Element(root) + else: + xml = etree.Element(root, id="%s" % id) + if do_subtree: + service_type = etree.SubElement(xml, "type") + service_type.text = "webapps" + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + service_provider = etree.SubElement(xml, "provider") + service_provider.text = "some-provider" + return xml + + def _stub_provider(self, root=None, id=None, do_subtree=True): + '''Stub provider xml''' + if id is None: + id = "%s_%s" % (self.test_manifest["name"], self.default_appname) + if root is None: + root = "provider" + if id == "": + xml = etree.Element(root) + else: + xml = etree.Element(root, id="%s" % id) + if do_subtree: + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + service_plugin = etree.SubElement(xml, "plugin") + service_plugin.text = "generic-oauth" + service_domains = etree.SubElement(xml, "domains") + service_domains.text = ".*\.example\.com" + # More can go here, see /usr/share/accounts/providers/* + return xml + + def test_check_application(self): + '''Test check_application()''' + xml = self._stub_application() + # print(etree.tostring(xml)) + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': 4, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_application_not_specified(self): + '''Test check_application() - not specified''' + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_application_wrong_id(self): + '''Test check_application() - wrong id''' + xml = self._stub_application(id="nomatch") + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_missing_id(self): + '''Test check_application() - missing id''' + xml = self._stub_application(id="") + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_wrong_root(self): + '''Test check_application() - wrong root''' + xml = self._stub_application(root="wrongroot") + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_missing_services(self): + '''Test check_application() - missing services''' + xml = self._stub_application(do_subtree=False) + + sometag = etree.SubElement(xml, "sometag") + elem1 = etree.SubElement(sometag, "something", id="element1") + desc1 = etree.SubElement(elem1, "description") + desc1.text = "elem1 description" + + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_application_missing_service(self): + '''Test check_application() - missing service''' + xml = self._stub_application(do_subtree=False) + + services = etree.SubElement(xml, "services") + elem1 = etree.SubElement(services, "somesubtag", id="element1") + desc1 = etree.SubElement(elem1, "description") + desc1.text = "elem1 description" + + self.set_test_account(self.default_appname, "account-application", xml) + c = ClickReviewAccounts(self.test_name) + c.check_application() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service(self): + '''Test check_service()''' + xml = self._stub_service() + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': 5, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_service_not_specified(self): + '''Test check_service() - no specified''' + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_service_wrong_id(self): + '''Test check_service() - wrong id''' + xml = self._stub_service(id="nomatch") + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_id(self): + '''Test check_service() - missing id''' + xml = self._stub_service(id="") + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_wrong_root(self): + '''Test check_service() - wrong root''' + xml = self._stub_service(root="wrongroot") + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_type(self): + '''Test check_service() - missing type''' + xml = self._stub_service(do_subtree=False) + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + service_provider = etree.SubElement(xml, "provider") + service_provider.text = "some-provider" + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_name(self): + '''Test check_service() - missing name''' + xml = self._stub_service(do_subtree=False) + service_type = etree.SubElement(xml, "type") + service_type.text = "webapps" + service_provider = etree.SubElement(xml, "provider") + service_provider.text = "some-provider" + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_service_missing_provider(self): + '''Test check_service() - missing provider''' + xml = self._stub_service(do_subtree=False) + service_type = etree.SubElement(xml, "type") + service_type.text = "webapps" + service_name = etree.SubElement(xml, "name") + service_name.text = "Foo" + self.set_test_account(self.default_appname, "account-service", xml) + c = ClickReviewAccounts(self.test_name) + c.check_service() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_provider(self): + '''Test check_provider()''' + xml = self._stub_provider() + self.set_test_account(self.default_appname, "account-provider", xml) + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + # provider prompts manual review, so for now, need to have error as 1 + expected_counts = {'info': 2, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_provider_not_specified(self): + '''Test check_provider() - no specified''' + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_provider_missing_id(self): + '''Test check_provider() - missing id''' + xml = self._stub_provider(id="") + self.set_test_account(self.default_appname, "account-provider", xml) + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + # provider prompts manual review, so for now, need to have error as +1 + expected_counts = {'info': None, 'warn': 0, 'error': 2} + self.check_results(r, expected_counts) + + def test_check_provider_wrong_id(self): + '''Test check_provider() - wrong id''' + xml = self._stub_provider(id="wrongid") + self.set_test_account(self.default_appname, "account-provider", xml) + c = ClickReviewAccounts(self.test_name) + c.check_provider() + r = c.click_report + # provider prompts manual review, so for now, need to have error as +1 + expected_counts = {'info': None, 'warn': 0, 'error': 2} + self.check_results(r, expected_counts) + + def test_check_qml_plugin(self): + '''Test check_qml_plugin()''' + self.set_test_account(self.default_appname, "account-qml-plugin", True) + c = ClickReviewAccounts(self.test_name) + c.check_qml_plugin() + r = c.click_report + # provider prompts manual review, so for now, need to have error as 1 + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_qml_plugin_not_specified(self): + '''Test check_qml_plugin() - no specified''' + c = ClickReviewAccounts(self.test_name) + c.check_qml_plugin() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_push_helper.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_push_helper.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_push_helper.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_push_helper.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,138 @@ +'''test_cr_push_helper.py: tests for the cr_push_helper module''' +# +# Copyright (C) 2013 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_push_helper import ClickReviewPushHelper +import clickreviews.cr_tests as cr_tests + + +class TestClickReviewPushHelper(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def test_check_unknown_keys_none(self): + '''Test check_unknown() - no unknown''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys1(self): + '''Test check_unknown() - one unknown''' + self.set_test_push_helper(self.default_appname, "nonexistent", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_keys2(self): + '''Test check_unknown() - good with one unknown''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_push_helper(self.default_appname, "nonexistent", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_unknown_keys() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_exec(self): + '''Test check_valid() - exec''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 2, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_missing_exec(self): + '''Test check_valid() - missing exec''' + self.set_test_push_helper(self.default_appname, "app_id", "foo_foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_app_id(self): + '''Test check_valid() - app_id''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_push_helper(self.default_appname, "app_id", "foo_foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': 3, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_valid_bad_value(self): + '''Test check_valid() - bad value''' + self.set_test_push_helper(self.default_appname, "exec", []) + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_empty_value(self): + '''Test check_valid() - empty value''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_push_helper(self.default_appname, "app_id", "") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_valid_empty_value2(self): + '''Test check_valid() - empty value''' + self.set_test_push_helper(self.default_appname, "exec", "") + self.set_test_push_helper(self.default_appname, "app_id", "foo_foo") + c = ClickReviewPushHelper(self.test_name) + c.check_valid() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_hooks(self): + '''Test check_hooks()''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + + # remove hooks that are added by the framework + c.manifest['hooks'][self.default_appname].pop('desktop') + c.manifest['hooks'][self.default_appname].pop('urls') + + c.check_hooks() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_hooks_bad(self): + '''Test check_hooks() - bad''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + c = ClickReviewPushHelper(self.test_name) + + # The desktop and urls hooks are specified by default in the framework, + # so just running this without other setup should generate an error + c.check_hooks() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_scope.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_scope.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_scope.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_scope.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,145 @@ +'''test_cr_scope.py: tests for the cr_scope module''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_scope import ClickReviewScope +import clickreviews.cr_tests as cr_tests +import configparser + + +class TestClickReviewScope(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def _create_scope(self, config_dict=None): + '''Create a scope to pass to tests''' + scope = dict() + scope["dir_rel"] = "scope-directory" + scope["ini_file_rel"] = "%s/%s.ini" % (scope["dir_rel"], + self.default_appname) + scope["scope_config"] = configparser.ConfigParser() + scope["scope_config"]['ScopeConfig'] = config_dict + + return scope + + def _stub_config(self): + '''Stub configparser file''' + config_dict = { + 'ScopeRunner': "%s" % self.default_appname, + 'DisplayName': 'Foo', + 'Description': 'Some description', + 'Author': 'Foo Ltd.', + 'Art': '', + 'Icon': 'foo.svg', + 'SearchHint': 'Search Foo', + } + + return config_dict + + def test_check_scope_ini(self): + '''Test check_scope_ini()''' + scope = self._create_scope(self._stub_config()) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': 3, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required1(self): + '''Test check_scope_ini() - missing ScopeRunner''' + config = self._stub_config() + del config['ScopeRunner'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required2(self): + '''Test check_scope_ini() - missing DisplayName''' + config = self._stub_config() + del config['DisplayName'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required3(self): + '''Test check_scope_ini() - missing Icon''' + config = self._stub_config() + del config['Icon'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required4(self): + '''Test check_scope_ini() - missing SearchHint''' + config = self._stub_config() + del config['SearchHint'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_missing_required5(self): + '''Test check_scope_ini() - missing multiple''' + config = self._stub_config() + del config['ScopeRunner'] + del config['DisplayName'] + del config['Icon'] + del config['SearchHint'] + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_scope_ini_nonexistent_field(self): + '''Test check_scope_ini() - non-existent field''' + config = self._stub_config() + config['nonexistent'] = "foo" + scope = self._create_scope(config) + + self.set_test_scope(self.default_appname, scope) + c = ClickReviewScope(self.test_name) + c.check_scope_ini() + r = c.click_report + expected_counts = {'info': None, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_security.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_security.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_security.py 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_security.py 2014-07-25 13:47:22.000000000 +0000 @@ -46,7 +46,7 @@ def test_check_policy_version_highest(self): '''Test check_policy_version() - highest''' c = ClickReviewSecurity(self.test_name) - highest_version = sorted(c.supported_policy_versions)[-1] + highest_version = c._get_highest_policy_version("ubuntu") version = highest_version self.set_test_security_manifest(self.default_appname, "policy_version", version) @@ -68,7 +68,7 @@ self.set_test_security_manifest(self.default_appname, "policy_version", bad_version) - highest = sorted(c.supported_policy_versions)[-1] + highest = c._get_highest_policy_version("ubuntu") c.check_policy_version() report = c.click_report @@ -88,7 +88,7 @@ def test_check_policy_version_low(self): '''Test check_policy_version() - low version''' c = ClickReviewSecurity(self.test_name) - highest = sorted(c.supported_policy_versions)[-1] + highest = c._get_highest_policy_version("ubuntu") version = 1.0 if version == highest: print("SKIPPED-- test version '%s' is already highest" % version, @@ -390,6 +390,94 @@ expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(report, expected_counts) + def test_check_policy_groups_scopes_network(self): + '''Test check_policy_groups_scopes() - network''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", []) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_scopes_network2(self): + '''Test check_policy_groups_scopes() - network with networking''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", ["networking"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_scopes_network_missing(self): + '''Test check_policy_groups_scopes() missing - network''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", None) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_scopes_network_bad(self): + '''Test check_policy_groups_scopes() bad - network''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-scope-network") + self.set_test_security_manifest(self.default_appname, + "policy_groups", ["accounts"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_scopes() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + +# jdstrand, 2014-06-05: ubuntu-scope-local-content is no longer available +# def test_check_policy_groups_scopes_localcontent(self): +# '''Test check_policy_groups_scopes() - localcontent''' +# self.set_test_security_manifest(self.default_appname, +# "template", +# "ubuntu-scope-local-content") +# self.set_test_security_manifest(self.default_appname, +# "policy_groups", []) +# c = ClickReviewSecurity(self.test_name) +# c.check_policy_groups_scopes() +# report = c.click_report +# expected_counts = {'info': None, 'warn': 0, 'error': 0} +# self.check_results(report, expected_counts) + +# def test_check_policy_groups_scopes_localcontent_missing(self): +# '''Test check_policy_groups_scopes() missing - localcontent''' +# self.set_test_security_manifest(self.default_appname, +# "template", +# "ubuntu-scope-local-content") +# self.set_test_security_manifest(self.default_appname, +# "policy_groups", None) +# c = ClickReviewSecurity(self.test_name) +# c.check_policy_groups_scopes() +# report = c.click_report +# expected_counts = {'info': 0, 'warn': 0, 'error': 0} +# self.check_results(report, expected_counts) + +# def test_check_policy_groups_scopes_localcontent_bad(self): +# '''Test check_policy_groups_scopes() bad - localcontent''' +# self.set_test_security_manifest(self.default_appname, +# "template", +# "ubuntu-scope-local-content") +# self.set_test_security_manifest(self.default_appname, +# "policy_groups", ["networking"]) +# c = ClickReviewSecurity(self.test_name) +# c.check_policy_groups_scopes() +# report = c.click_report +# expected_counts = {'info': None, 'warn': 0, 'error': 1} +# self.check_results(report, expected_counts) + def test_check_policy_groups(self): '''Test check_policy_groups()''' c = ClickReviewSecurity(self.test_name) @@ -469,7 +557,7 @@ self.check_results(report, expected_counts) def test_check_policy_groups_nonexistent(self): - '''Test check_policy_groups_webapps() - nonexistent''' + '''Test check_policy_groups() - nonexistent''' self.set_test_security_manifest(self.default_appname, "policy_groups", ["networking", "nonexistent"]) @@ -480,7 +568,7 @@ self.check_results(report, expected_counts) def test_check_policy_groups_reserved(self): - '''Test check_policy_groups_webapps() - reserved''' + '''Test check_policy_groups() - reserved''' self.set_test_security_manifest(self.default_appname, "policy_groups", ["video_files", "networking"]) @@ -490,8 +578,20 @@ expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(report, expected_counts) + def test_check_policy_groups_debug(self): + '''Test check_policy_groups() - debug''' + self.set_test_security_manifest(self.default_appname, + "policy_groups", ["debug"]) + self.set_test_security_manifest(self.default_appname, "policy_version", + 1.2) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + def test_check_policy_groups_empty(self): - '''Test check_policy_groups_webapps() - empty''' + '''Test check_policy_groups() - empty''' self.set_test_security_manifest(self.default_appname, "policy_groups", ["", "networking"]) @@ -500,3 +600,87 @@ report = c.click_report expected_counts = {'info': None, 'warn': 0, 'error': 1} self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper_no_hook(self): + '''Test check_policy_groups_pushhelper() - no hook''' + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper(self): + '''Test check_policy_groups_pushhelper()''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper_missing(self): + '''Test check_policy_groups_pushhelper - missing''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + None) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + + def test_check_policy_groups_pushhelper_bad(self): + '''Test check_policy_groups_pushhelper - bad''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["video_files", + "networking", + "push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_policy_groups_push_helpers() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) + + def test_check_template_pushhelper(self): + '''Test check_template_pushhelper''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-sdk") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_template_push_helpers() + report = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_template_pushhelper_no_hook(self): + '''Test check_template_pushhelper''' + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-sdk") + c = ClickReviewSecurity(self.test_name) + c.check_template_push_helpers() + report = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 0} + self.check_results(report, expected_counts) + + def test_check_template_pushhelper_wrong_template(self): + '''Test check_template_pushhelper - wrong template()''' + self.set_test_push_helper(self.default_appname, "exec", "foo") + self.set_test_security_manifest(self.default_appname, + "template", "ubuntu-webapp") + self.set_test_security_manifest(self.default_appname, + "policy_groups", + ["push-notification-client"]) + c = ClickReviewSecurity(self.test_name) + c.check_template_push_helpers() + report = c.click_report + expected_counts = {'info': None, 'warn': 0, 'error': 1} + self.check_results(report, expected_counts) diff -Nru click-reviewers-tools-0.7/clickreviews/tests/test_cr_url_dispatcher.py click-reviewers-tools-0.8/clickreviews/tests/test_cr_url_dispatcher.py --- click-reviewers-tools-0.7/clickreviews/tests/test_cr_url_dispatcher.py 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/clickreviews/tests/test_cr_url_dispatcher.py 2014-07-25 13:47:22.000000000 +0000 @@ -0,0 +1,211 @@ +'''test_cr_url dispatcher.py: tests for the cr_url_dispatcher module''' +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from clickreviews.cr_url_dispatcher import ClickReviewUrlDispatcher +import clickreviews.cr_tests as cr_tests + + +class TestClickReviewUrlDispatcher(cr_tests.TestClickReview): + """Tests for the lint review tool.""" + def setUp(self): + # Monkey patch various file access classes. stop() is handled with + # addCleanup in super() + cr_tests.mock_patch() + super() + + def test_check_required(self): + '''Test check_required() - has protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_required_empty_value(self): + '''Test check_required() - empty protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_required_bad_value(self): + '''Test check_required() - bad protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value=[]) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_required_multiple(self): + '''Test check_required() - multiple''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_required_multiple(self): + '''Test check_required() - multiple with nonexistent''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_required() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_none(self): + '''Test check_optional() - protocol only''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_empty(self): + '''Test check_optional() - with empty domain-suffix''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_bad(self): + '''Test check_optional() - with bad domain-suffix''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value=[], + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 0, 'warn': 0, 'error': 1} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_nonexistent(self): + '''Test check_optional() - with domain-suffix plus nonexistent''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_without_protocol(self): + '''Test check_optional() - with domain-suffix, no protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_optional_domain_suffix_without_protocol2(self): + '''Test check_optional() - with domain-suffix, nonexistent, no + protocol''' + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_optional() + r = c.click_report + expected_counts = {'info': 1, 'warn': 0, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown(self): + '''Test check_unknown()''' + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo") + c = ClickReviewUrlDispatcher(self.test_name) + c.check_unknown() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) + + def test_check_unknown_multiple(self): + '''Test check_unknown() - multiple with nonexistent''' + self.set_test_url_dispatcher(self.default_appname, + key="protocol", + value="some-protocol") + self.set_test_url_dispatcher(self.default_appname, + key="domain-suffix", + value="example.com", + append=True) + self.set_test_url_dispatcher(self.default_appname, + key="nonexistent", + value="foo", + append=True) + c = ClickReviewUrlDispatcher(self.test_name) + c.check_unknown() + r = c.click_report + expected_counts = {'info': 0, 'warn': 1, 'error': 0} + self.check_results(r, expected_counts) diff -Nru click-reviewers-tools-0.7/data/apparmor-easyprof-ubuntu.json click-reviewers-tools-0.8/data/apparmor-easyprof-ubuntu.json --- click-reviewers-tools-0.7/data/apparmor-easyprof-ubuntu.json 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/data/apparmor-easyprof-ubuntu.json 2014-07-25 13:47:58.000000000 +0000 @@ -0,0 +1,129 @@ +{ + "ubuntu": { + "1.0": { + "templates": { + "common": [ + "default", + "ubuntu-sdk", + "ubuntu-webapp" + ], + "reserved": [ + "unconfined" + ] + }, + "policy_groups": { + "common": [ + "audio", + "camera", + "connectivity", + "content_exchange", + "content_exchange_source", + "location", + "microphone", + "networking", + "sensors", + "usermetrics", + "video" + ], + "reserved": [ + "accounts", + "calendar", + "contacts", + "friends", + "history", + "music_files", + "music_files_read", + "picture_files", + "picture_files_read", + "video_files", + "video_files_read" + ] + } + }, + "1.1": { + "templates": { + "common": [ + "default", + "ubuntu-sdk", + "ubuntu-webapp" + ], + "reserved": [ + "unconfined" + ] + }, + "policy_groups": { + "common": [ + "audio", + "camera", + "connectivity", + "content_exchange", + "content_exchange_source", + "location", + "microphone", + "networking", + "sensors", + "usermetrics", + "video", + "webview" + ], + "reserved": [ + "accounts", + "calendar", + "contacts", + "debug", + "friends", + "history", + "music_files", + "music_files_read", + "picture_files", + "picture_files_read", + "video_files", + "video_files_read" + ] + } + }, + "1.2": { + "templates": { + "common": [ + "default", + "ubuntu-scope-network", + "ubuntu-sdk", + "ubuntu-webapp" + ], + "reserved": [ + "unconfined" + ] + }, + "policy_groups": { + "common": [ + "audio", + "camera", + "connectivity", + "content_exchange", + "content_exchange_source", + "location", + "microphone", + "networking", + "push-notification-client", + "sensors", + "usermetrics", + "video", + "webview" + ], + "reserved": [ + "accounts", + "calendar", + "contacts", + "debug", + "history", + "music_files", + "music_files_read", + "picture_files", + "picture_files_read", + "video_files", + "video_files_read" + ] + } + } + } +} diff -Nru click-reviewers-tools-0.7/data/frameworks.json click-reviewers-tools-0.8/data/frameworks.json --- click-reviewers-tools-0.7/data/frameworks.json 1970-01-01 00:00:00.000000000 +0000 +++ click-reviewers-tools-0.8/data/frameworks.json 2014-07-25 13:47:57.000000000 +0000 @@ -0,0 +1,19 @@ +{ + "ubuntu-sdk-13.10": "deprecated", + "ubuntu-sdk-14.04-dev1": "deprecated", + "ubuntu-sdk-14.04-html-dev1": "deprecated", + "ubuntu-sdk-14.04-papi-dev1": "deprecated", + "ubuntu-sdk-14.04-qml-dev1": "deprecated", + "ubuntu-sdk-14.04": "available", + "ubuntu-sdk-14.04-html": "available", + "ubuntu-sdk-14.04-papi": "available", + "ubuntu-sdk-14.04-qml": "available", + "ubuntu-sdk-14.10-dev1": "obsolete", + "ubuntu-sdk-14.10-html-dev1": "obsolete", + "ubuntu-sdk-14.10-papi-dev1": "obsolete", + "ubuntu-sdk-14.10-qml-dev1": "obsolete", + "ubuntu-sdk-14.10-dev2": "available", + "ubuntu-sdk-14.10-html-dev2": "available", + "ubuntu-sdk-14.10-papi-dev2": "available", + "ubuntu-sdk-14.10-qml-dev2": "available" +} diff -Nru click-reviewers-tools-0.7/debian/bzr-builder.manifest click-reviewers-tools-0.8/debian/bzr-builder.manifest --- click-reviewers-tools-0.7/debian/bzr-builder.manifest 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/debian/bzr-builder.manifest 2014-07-25 13:47:23.000000000 +0000 @@ -1,2 +1,2 @@ -# bzr-builder format 0.3 deb-version {debupstream}-0~194 -lp:click-reviewers-tools revid:daniel.holbach@canonical.com-20140505081411-j0unfdezjcp18mwt +# bzr-builder format 0.3 deb-version {debupstream}-0~214 +lp:click-reviewers-tools revid:jamie@ubuntu.com-20140725133725-z9bl03us55byg6lw diff -Nru click-reviewers-tools-0.7/debian/changelog click-reviewers-tools-0.8/debian/changelog --- click-reviewers-tools-0.7/debian/changelog 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/debian/changelog 2014-07-25 13:47:23.000000000 +0000 @@ -1,15 +1,69 @@ -click-reviewers-tools (0.7-0~194~ubuntu14.04.1) trusty; urgency=low +click-reviewers-tools (0.8-0~214~ubuntu14.04.1) trusty; urgency=low * Auto build. - -- Daniel Holbach Mon, 05 May 2014 08:33:55 +0000 + -- Daniel Holbach Fri, 25 Jul 2014 13:47:23 +0000 -click-reviewers-tools (0.7) UNRELEASED; urgency=medium +click-reviewers-tools (0.8) UNRELEASED; urgency=medium - * clickreviews/cr_lint.py: add link to more info about "Please use newer + [ Zoltan Balogh ] + * Give an error if the app is using deprecated Friends API (LP: #1340869) + + [ Martin Albisetti, Daniel Holbach ] + * refactor the way we handle frameworks into a central static list which + should be easy to update. + + [ Jamie Strandboge ] + * updated clickreviews/cr_tests.py for 14.10*dev2 + * bin/repack-click: use -Zgzip when repacking to remain compatible with + debfile (ie, click install) + * warn on new hooks + * implement url-dispatcher hook checks + * implement scope hook checks + * implement content-hub hook checks + * debian/control: Build-Depends and Depends on python3-lxml + * implement account-* hook checks + * redflag the upcoming pay-ui hook + * update security tests to not require apparmor-easyprof-ubuntu or + apparmor-easyprof by using a static list to ease updating + * debian/control: drop Build-Depends and Depends on apparmor-easyprof and + apparmor-easyprof-ubuntu + * update data/apparmor-easyprof-ubuntu.json to not include friends policy + group in 1.2 (LP: #1340869) + * refactor the way we handle apparmor policy into a central static list + which should be easy to update. + * implement push-helper tests (LP: #1346481) + + [ Daniel Holbach ] + * refer to documentation about click in case we encounter .deb packages. + + -- Jamie Strandboge Thu, 24 Jul 2014 14:04:23 -0500 + +click-reviewers-tools (0.7.1) utopic; urgency=medium + + * Merge r198: + [ Jamie Strandboge ] + - ubuntu-scope-local-content template is no longer available. + + -- Daniel Holbach Thu, 05 Jun 2014 16:21:33 +0200 + +click-reviewers-tools (0.7) utopic; urgency=medium + + [ Daniel Holbach ] + * clickreviews/cr_lint.py: add link to more info about "Please use newer framework". Thanks Alan Pope. - -- Daniel Holbach Tue, 29 Apr 2014 16:37:24 +0200 + [ Jamie Strandboge ] + * add 14.10 frameworks. Thanks Martin Albisetti for initial patch + * 13.10 frameworks should be deprecated instead of obsolete and warn when + using deprecated framework + * add click scopes checks + * special case ubuntu-devel-discuss@lists.ubuntu.com + * implement check_hooks() lint tests + * debian/control: Depends on apparmor-easyprof-ubuntu >= 1.2.2 + (LP: #1324121) + + -- Jamie Strandboge Wed, 28 May 2014 23:48:04 +0200 click-reviewers-tools (0.6) utopic; urgency=medium diff -Nru click-reviewers-tools-0.7/debian/control click-reviewers-tools-0.8/debian/control --- click-reviewers-tools-0.7/debian/control 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/debian/control 2014-07-25 13:47:22.000000000 +0000 @@ -2,12 +2,11 @@ Section: devel Priority: optional Maintainer: Ubuntu Appstore Developers -Build-Depends: apparmor-easyprof, - apparmor-easyprof-ubuntu (>= 1.0.44), - debhelper (>= 9~), +Build-Depends: debhelper (>= 9~), python3-all (>= 3.2~), python3-apt, python3-debian, + python3-lxml, python3-magic, python3-setuptools, python3-simplejson, @@ -20,10 +19,9 @@ Package: click-reviewers-tools Architecture: all -Depends: apparmor-easyprof, - apparmor-easyprof-ubuntu (>= 1.0.44), - python3-apt, +Depends: python3-apt, python3-debian, + python3-lxml, python3-magic, python3-simplejson, python3-xdg, diff -Nru click-reviewers-tools-0.7/debian/rules click-reviewers-tools-0.8/debian/rules --- click-reviewers-tools-0.7/debian/rules 2014-05-05 08:33:55.000000000 +0000 +++ click-reviewers-tools-0.8/debian/rules 2014-07-25 13:47:22.000000000 +0000 @@ -18,7 +18,8 @@ rm -rf build *.egg-info .pybuild find -name \*.pyc -print0 | xargs -0r rm -f find -name __pycache__ -print0 | xargs -0r rm -rf - + -$(shell python3 ./bin/update-frameworks ./data/frameworks.json) + -$(shell python3 ./bin/update-apparmor-policy ./data/apparmor-easyprof-ubuntu.json) override_dh_auto_build: dh_auto_build