← Back to team overview

lp-scanner-team team mailing list archive

lp:~mike-powerthroughwords/lp-scanner/bug-1174157-security-permission-exempt-teams into lp:lp-scanner

 

Mike Heald has proposed merging lp:~mike-powerthroughwords/lp-scanner/bug-1174157-security-permission-exempt-teams into lp:lp-scanner.

Requested reviews:
  The Launchpad Security Scanner Dev Team (lp-scanner-team)
Related bugs:
  Bug #1174157 in lp-scanner: "Add config option to exempt Private Security permission check"
  https://bugs.launchpad.net/lp-scanner/+bug/1174157

For more details, see:
https://code.launchpad.net/~mike-powerthroughwords/lp-scanner/bug-1174157-security-permission-exempt-teams/+merge/164407

Rewrote the tests. They pass now, and, there are some.
Added some changes for python3.
Added new config section that allows you to exempt certain teams from giving their project team access to the security bugs.
-- 
https://code.launchpad.net/~mike-powerthroughwords/lp-scanner/bug-1174157-security-permission-exempt-teams/+merge/164407
Your team The Launchpad Security Scanner Dev Team is requested to review the proposed merge of lp:~mike-powerthroughwords/lp-scanner/bug-1174157-security-permission-exempt-teams into lp:lp-scanner.
=== modified file 'security-scanner.py'
--- security-scanner.py	2013-04-25 05:02:23 +0000
+++ security-scanner.py	2013-05-17 14:01:29 +0000
@@ -3,7 +3,8 @@
 # Copyright 2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 #
-# Original Ugly Proof of Concept Code by Joey Stanford @ UDS Maverick 2010-05-14
+# Original Ugly Proof of Concept Code by Joey Stanford @ UDS Maverick
+# 2010-05-14
 # Dusting and Cleaning by Brad Crittenden 2010-05-17
 # Heavy Overhaul by Karl Fogel 2010-05-21
 # Tests added by Diogo Matsubara 2010-10-18
@@ -14,7 +15,7 @@
 from __future__ import print_function
 
 
-USAGE="""\
+USAGE = """
 Scan projects and report on potential security exposures.
 Run '%prog --help' for detailed usage.
 
@@ -22,7 +23,15 @@
 
 """
 
-import cStringIO
+# python2/3 compatibility
+try:
+    import cStringIO
+    from urllib import urlopen
+except ImportError:
+    from io import StringIO as cStringIO
+    from urllib.request import urlopen
+
+import collections
 import functools
 import optparse
 import os
@@ -30,13 +39,14 @@
 import re
 import sys
 import unittest
-from urllib import urlopen
 
 from launchpadlib.launchpad import Launchpad
-from launchpadlib.errors import *
-from lazr.restfulclient.errors import *
-
-
+from launchpadlib.errors import HTTPError
+
+
+# this implements ConfigParser in a way that isn't compatible with
+# ConfigParser. Replacing this would mean changing our config files
+# so, leaving for the moment.
 class ConfigParser():
     """Temporarily re-implement a subset of Python's ConfigParser.
 Python 2.7 and 3.0 will have the fix for http://bugs.python.org/issue7005
@@ -109,6 +119,8 @@
             # Real ConfigParser handles multiline options in which
             # leading whitespace is significant.  We don't bother with
             # that here -- we just strip both sides and then parse it.
+            # ALSO: comments in the staging-config.cfg make the real
+            # ConfigParser choke.
             line = line.strip()
             if len(line) == 0 or line[0] == ';' or line[0] == '#':
                 continue
@@ -126,11 +138,10 @@
     def read(self, file_or_filenames):
         lst = []
         successful_files = []
-        if type(file_or_filenames) == type(""):
+        if isinstance(file_or_filenames, str):
             lst.append(file_or_filenames)
-        elif (type(file_or_filenames) == type([]) or
-              type(file_or_filenames) == type(())):
-          lst = file_or_filenames
+        elif isinstance(file_or_filenames, collections.Iterable):
+            lst = file_or_filenames
         for fname in lst:
             try:
                 self.readfp(open(fname))  # GC closes filehandle, right?
@@ -143,14 +154,14 @@
         return self._sections
 
     def has_section(self, section_name):
-        return self._sections.has_key(section_name)
+        return section_name in self._sections
 
     def options(self, section):
         return self._sections[section].keys()
 
     def has_option(self, section_name, option_name):
         if (self.has_section(section_name)
-            and self._sections[section_name].has_key(option_name)):
+                and option_name in self._sections[section_name]):
             return True
         else:
             return False
@@ -179,7 +190,7 @@
             service_root=service_name.lower(),
             version="devel",
             allow_access_levels=access_levels)
-    except HTTPError, e:
+    except HTTPError as e:
         print_HTTPError(e)
         raise e
     return lp
@@ -214,83 +225,110 @@
     """Print information about possible security leaks."""
     printf = functools.partial(print, file=outfile)
 
-    project_groups     = config.options('project-groups')
+    project_groups = config.options('project-groups')
     suspicious_drivers = config.options('suspicious-drivers')
-    allowed_owners     = config.options('allowed-owners')
-    branch_exceptions  = config.options('branch-exceptions')
-    bug_exceptions     = config.options('bug-exceptions')
-    ppa_exceptions     = config.options('ppa-exceptions')
+    allowed_owners = config.options('allowed-owners')
+    branch_exceptions = config.options('branch-exceptions')
+    bug_exceptions = config.options('bug-exceptions')
+    ppa_exceptions = config.options('ppa-exceptions')
     allowed_mailing_lists = config.options('mailinglist-exceptions')
     nonstandard_project_teams = config.options('nonstandard-project-teams')
+    private_security_exceptions = config.options(
+        'security-bug-sharing-exceptions')
 
     # Traverse the project groups.
     for project_group in sorted(project_groups):
         # Collect results, so we can output them grouped and sorted (that
         # way results from different runs can be meaningfully compared).
-        unset_drivers   = {}  # Map project objects to None.
-        wrong_drivers   = {}  # Map project objects to None.
-        wrong_drivers_owners = {} # Map project objects to None.
-        wrong_owners    = {}  # Map project objects to None.
+        unset_drivers = {}  # Map project objects to None.
+        wrong_drivers = {}  # Map project objects to None.
+        wrong_drivers_owners = {}  # Map project objects to None.
+        wrong_owners = {}  # Map project objects to None.
         public_branches = {}  # Map branch objects to project objects.
-        public_bugs     = {}  # Map bug objects to project objects.
-        public_ppas     = {}  # Map PPA objects to project objects.
-        project_team_private_security = {} # Map project objects to sharing policy
+        public_bugs = {}  # Map bug objects to project objects.
+        public_ppas = {}  # Map PPA objects to project objects.
+        project_team_private_security = {}  # project objects to sharing policy
         exposed_mailing_lists = {}  # Map team driver to None
 
         # Get list of projects associated with the project group.
         for project in lp.project_groups[project_group].projects:
+
             if project.driver is None:
+                # Missing project driver
                 unset_drivers[project] = None
             elif project.driver.name in suspicious_drivers:
+                # Driver that is a bit suspicious
                 wrong_drivers[project] = None
             elif (project.driver.team_owner and
                   project.driver.team_owner.name not in allowed_owners):
+                # Team owner that's not allowed to own this
                 wrong_drivers_owners[project] = None
+
             if project.owner.name not in allowed_owners:
+                # Project owner that's not allowed to own the project
                 wrong_owners[project] = None
+
             for branch in project.getBranches():
                 if (not branch.private and
-                    branch.name not in branch_exceptions):
+                        branch.name not in branch_exceptions):
+                    # Public branch that we haven't made an exception for
                     public_branches[branch] = project
+
             if project.name not in bug_exceptions:
-                bug_tasks = project.searchTasks(order_by='id') #split for performance
+                bug_tasks = project.searchTasks(order_by='id')
+                # public bugs where the project isn't allowed to
+                # have public bugs and the bug itself isn't an exception also
                 for bug_task in bug_tasks:
-                    if not bug_task.bug.private: #split for performance
-                       if str(bug_task.bug.id) not in bug_exceptions:
-                        public_bugs[bug_task.bug] = project
+                    if not bug_task.bug.private:
+                        if str(bug_task.bug.id) not in bug_exceptions:
+                            public_bugs[bug_task.bug] = project
+
             for ppa in project.owner.ppas:
+                # any ppas that are public and not listed as an exception
                 if (not ppa.private and
-                    ppa.name not in ppa_exceptions):
+                        ppa.name not in ppa_exceptions):
                     public_ppas[ppa] = project
+
             project_team_grantee = False
+
             for grantee in sharing.getPillarGranteeData(pillar=project):
+                # if the grantee is the project team and has not been
+                # granted permission to private security bugs
                 name = grantee["name"]
                 if (name == project.name + "-team" or
-                    name in nonstandard_project_teams):
+                        name in nonstandard_project_teams):
                     project_team_grantee = True
                     permissions = grantee["permissions"]
-                    if ("PRIVATESECURITY" not in permissions
-                        or permissions["PRIVATESECURITY"] != "ALL"):
+                    if (name not in private_security_exceptions
+                            and (
+                                "PRIVATESECURITY" not in permissions
+                                or permissions["PRIVATESECURITY"] != "ALL")):
                         project_team_private_security[project] = None
                     break
+
             if not project_team_grantee:
                 project_team_private_security[project] = None
+
             # check for rogue mailing lists on the project driver
             if (project.driver and
-                project.driver.name not in allowed_mailing_lists):
-                targeturl = "https://lists.launchpad.net/"+project.driver.name
+                    project.driver.name not in allowed_mailing_lists):
+                targeturl = "https://lists.launchpad.net/%s"; % (
+                    project.driver.name)
                 if urlopen(targeturl).getcode() != 404:
-                   exposed_mailing_lists[project.driver] = None
+                    exposed_mailing_lists[project.driver] = None
 
         # Only print output for this project group if there were problems.
         printed_project_section_start = [False]
+
         def header(message):
             if not printed_project_section_start[0]:
                 printf("* In project group %s:\n" % project_group)
                 printed_project_section_start[0] = True
             printf("  === %s ===\n" % message)
+
         def footer(message):
             printf("\n  (%s)" % message)
+
         def maybe_custom_message(section):
             "If there is a custom message for a section, print it."
             if config.has_option("custom-messages", section):
@@ -304,6 +342,7 @@
             footer("The driver should be set to the project team.")
             maybe_custom_message("unset-drivers")
             printf("")
+
         if wrong_drivers:
             header("Probably Wrong Driver")
             for key in sorted(wrong_drivers.keys()):
@@ -312,6 +351,7 @@
             footer("The driver should probably be set to the project team.")
             maybe_custom_message("wrong-drivers")
             printf("")
+
         if wrong_owners:
             header("Wrong Maintainer (Owner)")
             for key in sorted(wrong_owners.keys()):
@@ -320,6 +360,7 @@
             footer("Are those the right owners?")
             maybe_custom_message("wrong-owners")
             printf("")
+
         if wrong_drivers_owners:
             header("Wrong Team Owner")
             for key in sorted(wrong_drivers_owners.keys()):
@@ -328,6 +369,7 @@
             footer("Are those the right owners?")
             maybe_custom_message("wrong-owners")
             printf("")
+
         if public_branches:
             header("Exposed Branches")
             for key in sorted(public_branches.keys()):
@@ -336,6 +378,7 @@
             footer("Those branches are exposed -- should any be private?")
             maybe_custom_message("public-branches")
             printf("")
+
         if public_bugs:
             header("Exposed Bugs")
             for key in sorted(public_bugs.keys()):
@@ -344,6 +387,7 @@
             footer("Those bugs are exposed -- should any be private?")
             maybe_custom_message("public-bugs")
             printf("")
+
         if public_ppas:
             header("Exposed PPAs")
             for key in sorted(public_ppas.keys()):
@@ -352,6 +396,7 @@
             footer("Those PPAs are exposed -- should any be private?")
             maybe_custom_message("public-ppas")
             printf("")
+
         if project_team_private_security:
             header("Project does not share Private Security bugs with "
                    "project team")
@@ -362,6 +407,7 @@
                    "the project team")
             maybe_custom_message("project-team-private-security")
             printf("")
+
         if exposed_mailing_lists:
             header("Public Mailing Lists")
             for key in sorted(exposed_mailing_lists.keys()):
@@ -371,106 +417,6 @@
             printf("")
 
 
-class TestSecurityScanner(unittest.TestCase):
-
-    def setUp(self):
-        # Set up objects used by tests.
-        self._create_objects()
-        # Get the read-only instance.
-        self.lp = get_lp_instance('staging')
-        self.config = ConfigParser()
-        self.config.read('staging-config.cfg')
-
-    def _create_objects(self):
-        """Set up test objects used by tests."""
-        # To set up test objects we need a writable lp instance.
-        lp = get_lp_instance('staging', read_only=False)
-
-        # It's not possible to create a project group using the API, so let's
-        # modify an existing one.
-        test_project = lp.project_groups['test-project']
-        # Remove all projects from this project group.
-        for project in test_project.projects:
-            project.project_group = None
-            project.lp_save()
-
-        # Create a couple of new projects, if they don't exist already.
-        sample_project_names = ["project1234", "project2341"]
-        self.sample_projects = []
-        for name in sample_project_names:
-            try:
-                project = lp.projects.new_project(
-                    name=name, display_name=name, summary=name, title=name)
-                project.lp_save()
-            except HTTPError, e:
-                # Project already exists.
-                project = lp.projects[name]
-                # XXX 2010-10-18 matsubara: Due to bug 662740, we can't set an
-                # attribute for this object without fetching the
-                # representation first.
-                ignored = project.name
-            self.sample_projects.append(project)
-
-        # Add them to the test-project project group.
-        for project in self.sample_projects:
-            project.project_group = test_project
-            project.lp_save()
-
-        # Make all bugs private on those projects.
-        for project in self.sample_projects:
-            for btask in project.searchTasks():
-                bug = btask.bug
-                if not bug.private:
-                    bug.private = True
-                    bug.lp_save()
-
-        # File new bugs on those projects.
-        self.sample_bugs = []
-        self.sample_bugs.append(lp.bugs.createBug(
-            target=self.sample_projects[0], description='foo', title='bar'))
-        self.sample_bugs.append(lp.bugs.createBug(
-            target=self.sample_projects[1], description='foo', title='bar'))
-
-    def _get_sample_report(self):
-        """Fill data in sample report from objects created by tests."""
-        security_report = open('staging-report.txt').read()
-        # XXX 2010-10-18 matsubara: Missing data for PPA and Branch in the
-        # report since I can't create those through the API.
-        data = {
-            'project1': self.sample_projects[0].name,
-            'project2': self.sample_projects[1].name,
-            'bug1': self.sample_bugs[0].id,
-            'bug2': self.sample_bugs[1].id,
-            'owner': self.lp.me.display_name
-            }
-        return security_report % data
-
-    def test_read_config(self):
-        expected_sections = [
-            'allowed-owners', 'branch-exceptions', 'bug-exceptions',
-            'custom-messages', 'ppa-exceptions', 'project-groups',
-            'suspicious-drivers']
-        sections = sorted(self.config.sections().keys())
-        self.assertEqual(sections, expected_sections)
-
-    def test_security_scanner(self):
-        outfile = cStringIO.StringIO()
-        security_scanner(self.lp, self.config, outfile)
-        outfile.seek(0)
-        security_report = outfile.read()
-        sample_security_report = self._get_sample_report()
-        # XXX 2010-10-15 matsubara: Use self.assertMultiLineEqual() here when
-        # python2.7 is available. We'll get better output (i.e. diff-like)
-        # when there's a test failure.
-        self.assertEqual(security_report, sample_security_report)
-
-
-def _selftest():
-    """Run tests for this script using the staging web service."""
-    suite = unittest.TestLoader().loadTestsFromTestCase(TestSecurityScanner)
-    unittest.TextTestRunner(verbosity=2).run(suite)
-
-
 def main():
     # Login to service.
     opts, args = parse_args()
@@ -493,3 +439,303 @@
 
 if __name__ == "__main__":
     main()
+
+
+try:
+    import mocker
+
+    class TestSecurityScanner(mocker.MockerTestCase, unittest.TestCase):
+
+        def setUp(self):
+            super(TestSecurityScanner, self).setUp()
+            self.lp = self.mocker.mock()
+            self.sharing = self.mocker.mock()
+            self.config = ConfigParser()
+            self.config.read('staging-config.cfg')
+
+        def _setup_owner(
+                self, name="Owner Name", display_name="Owner Display Name",
+                ppas=[]):
+            mock_owner = self.mocker.mock()
+            mock_owner.name
+            self.mocker.result(name)
+            self.mocker.count(0, None)
+            mock_owner.display_name
+            self.mocker.result(display_name)
+            self.mocker.count(0, None)
+            mock_owner.ppas
+            self.mocker.result(ppas)
+            self.mocker.count(0, None)
+            return mock_owner
+
+        def _setup_driver(
+                self, name="Driver Name", team_owner=None,
+                mailing_list_status_code=404):
+            mock_driver = self.mocker.mock()
+            mock_driver.name
+            self.mocker.result(name)
+            self.mocker.count(0, None)
+            mock_driver.team_owner
+            self.mocker.result(team_owner)
+            self.mocker.count(0, None)
+            urlopen_mock = self.mocker.replace(urlopen)
+            urlopen_mock(
+                "https://lists.launchpad.net/"; + name).getcode()
+            self.mocker.result(mailing_list_status_code)
+            self.mocker.count(0, None)
+            return mock_driver
+
+        def _setup_project(
+                self, driver=None, owner=None, branches=[],
+                name="project-name", tasks=[]):
+            if owner is None:
+                owner = self._setup_owner()
+            mock_project = self.mocker.mock()
+            mock_project.driver
+            self.mocker.result(driver)
+            self.mocker.count(0, None)
+            mock_project.owner
+            self.mocker.result(owner)
+            self.mocker.count(0, None)
+            mock_project.getBranches()
+            self.mocker.result(branches)
+            mock_project.name
+            self.mocker.result(name)
+            self.mocker.count(0, None)
+            mock_project.searchTasks(order_by='id')
+            self.mocker.result(tasks)
+            return mock_project
+
+        def _setup_ppa(self, name="mock-ppa", private=False):
+            mock_ppa = self.mocker.mock()
+            mock_ppa.private
+            self.mocker.result(private)
+            self.mocker.count(0, None)
+            mock_ppa.name
+            self.mocker.result(name)
+            self.mocker.count(0, None)
+            return mock_ppa
+
+        def _setup_branch(self, name="branch", private=False):
+            mock_branch = self.mocker.mock()
+            mock_branch.private
+            self.mocker.result(private)
+            mock_branch.name
+            self.mocker.result(name)
+            self.mocker.count(0, None)
+            return mock_branch
+
+        def _setup_bug(self, id=42, private=False):
+            mock_bug = self.mocker.mock()
+            mock_bug.id
+            self.mocker.result(id)
+            self.mocker.count(0, None)
+            mock_bug.private
+            self.mocker.result(private)
+            self.mocker.count(0, None)
+            return mock_bug
+
+        def _setup_task(self, bug=None):
+            mock_task = self.mocker.mock()
+            mock_task.bug
+            self.mocker.result(bug)
+            self.mocker.count(0, None)
+            return mock_task
+
+        def _setup_project_group(self, group_name, projects=[]):
+            self.lp.project_groups[group_name].projects
+            self.mocker.result(projects)
+
+        def _setup_project_grantee(
+                self, project, name="Mr Grantee", permissions=[]):
+            mock_grantee = self.mocker.mock()
+            mock_grantee["name"]
+            self.mocker.result(name)
+            self.mocker.count(0, None)
+            mock_grantee["permissions"]
+            self.mocker.result(permissions)
+            self.mocker.count(0, None)
+            self.sharing.getPillarGranteeData(pillar=project)
+            self.mocker.result([mock_grantee, ])
+            self.mocker.count(0, None)
+
+        def test_read_config(self):
+            expected_sections = [
+                'allowed-owners', 'branch-exceptions', 'bug-exceptions',
+                'custom-messages', 'mailinglist-exceptions',
+                'nonstandard-project-teams', 'ppa-exceptions',
+                'project-groups', 'security-bug-sharing-exceptions',
+                'suspicious-drivers']
+            sections = self.config.sections().keys()
+            self.assertEqual(set(sections), set(expected_sections))
+
+        def test_project_with_no_driver(self):
+            mock_project = self._setup_project()
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(mock_project)
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue("Driver Not Set" in security_report)
+            self.assertTrue(
+                "project project-name has no driver" in security_report)
+
+        def test_project_with_wrong_driver(self):
+            mock_project = self._setup_project(
+                driver=self._setup_driver(name="kfogel"))
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(mock_project)
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue("Probably Wrong Driver" in security_report)
+            self.assertTrue(
+                "project project-name has driver kfogel" in security_report)
+
+        def test_project_with_wrong_maintainer(self):
+            mock_project = self._setup_project(
+                driver=self._setup_driver(name="Driver"), name="Test",
+                owner=self._setup_owner(
+                    name="not-joey", display_name="Not Joey"))
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(mock_project)
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue("Wrong Maintainer (Owner)" in security_report)
+            self.assertTrue(
+                "project Test has owner Not Joey" in security_report)
+
+        def test_project_with_public_branches(self):
+            mock_branch = self._setup_branch(private=False, name="pubbranch")
+            mock_project = self._setup_project(
+                branches=[mock_branch, ], name="project")
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(mock_project)
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue("Exposed Branches" in security_report)
+            self.assertTrue(
+                "branch pubbranch (on project project) is public"
+                in security_report)
+
+        def test_project_with_public_bugs(self):
+            mock_bug = self._setup_bug(private=False)
+            mock_task = self._setup_task(bug=mock_bug)
+            mock_project = self._setup_project(
+                name="project", tasks=[mock_task, ])
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(mock_project)
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue("Exposed Bugs" in security_report)
+            self.assertTrue(
+                "bug 42 (on project project) is public"
+                in security_report)
+
+        def test_project_with_public_ppas(self):
+            mock_ppa = self._setup_ppa(private=False)
+            mock_owner = self._setup_owner(ppas=[mock_ppa, ])
+            mock_project = self._setup_project(
+                name="project", owner=mock_owner)
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(mock_project)
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue("Exposed PPAs" in security_report)
+            self.assertTrue(
+                "PPA mock-ppa (on project project) is public"
+                in security_report)
+
+        def test_project_not_sharing_security_bugs(self):
+            mock_project = self._setup_project(name="project")
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(
+                mock_project, name="project-team", permissions=[])
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue(
+                "project project does not share Private Security bugs "
+                "with the project team"
+                in security_report)
+
+        def test_project_with_rogue_mailing_lists_on_driver(self):
+            driver = self._setup_driver(
+                name="not-in-allowed-mailing-lists",
+                mailing_list_status_code=200)
+            mock_project = self._setup_project(name="project", driver=driver)
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(
+                mock_project, name="project-team", permissions=[])
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertTrue(
+                "Team not-in-allowed-mailing-lists "
+                "has a public mailing list" in security_report)
+
+        def test_project_that_doesnt_have_any_issues(self):
+            mock_project = self._setup_project(
+                name="ok-project",
+                driver=self._setup_driver(name="valid-driver"),
+                owner=self._setup_owner(
+                    name="joey", ppas=[self._setup_ppa(private=True), ]),
+                branches=[self._setup_branch(private=True), ],
+                tasks=[self._setup_task(bug=self._setup_bug(private=True)), ]
+            )
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(
+                mock_project, name="ok-project-team",
+                permissions={"PRIVATESECURITY": "ALL"})
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertEqual("", security_report)
+
+        def test_project_not_sharing_security_bugs_exception(self):
+            mock_project = self._setup_project(name="some-project")
+            self._setup_project_group("test-project", [mock_project, ])
+            self._setup_project_grantee(
+                mock_project, name="some-project-team", permissions={})
+            self.mocker.replay()
+            outfile = cStringIO.StringIO()
+            security_scanner(self.lp, self.sharing, self.config, outfile)
+            outfile.seek(0)
+            security_report = outfile.read()
+            self.assertFalse(
+                "project some-project does not share Private Security bugs "
+                "with the project team"
+                in security_report)
+
+
+except ImportError:
+    if __name__ == "security-scanner":
+        raise
+
+
+def _selftest():
+    """Run tests for this script using the staging web service."""
+    suite = unittest.TestLoader().loadTestsFromTestCase(TestSecurityScanner)
+    unittest.TextTestRunner(verbosity=2).run(suite)

=== modified file 'staging-config.cfg'
--- staging-config.cfg	2010-12-01 23:44:52 +0000
+++ staging-config.cfg	2013-05-17 14:01:29 +0000
@@ -53,3 +53,8 @@
 #
 [custom-messages]
 wrong-owners = NOTE: You've got yourself a wrong owner there buddy!  
+
+[security-bug-sharing-exceptions]
+some-project-team
+
+[nonstandard-project-teams]


Follow ups