Package: security-tracker Severity: wishlist The attached patch implements a first pass at a per-maintainer page of security issues. It involves some database schema changes to it will require a full reimport of all the data. My SQL knowledge isn't great, so there are some deficiencies: I'm not sure if the adding another table is the right way to go, nor if I used the right table name. I'm not sure if the getBugsForMaintainer is correct, especially wrt version numbers/releases/etc. I am not sure how to implement a getDSAsForMaintainer function to add DSAs related to the maintainer at the bottom of the page. -- bye, pabs http://wiki.debian.org/PaulWise
Index: lib/python/security_db.py =================================================================== --- lib/python/security_db.py (revision 18462) +++ lib/python/security_db.py (working copy) @@ -38,6 +38,7 @@ import sys import types import zlib +import email.utils import debian_support import dist_config @@ -123,6 +124,9 @@ # Enable WAL. This means that updates will not block readers. c.execute("PRAGMA journal_mode = WAL") + # Enable foreign keys + c.execute("PRAGMA foreign_keys=ON") + self.schema_version = 22 self._initFunctions() @@ -198,15 +202,23 @@ cursor.execute( """CREATE TABLE source_packages - (name TEXT NOT NULL, + (id INTEGER, + name TEXT NOT NULL, release TEXT NOT NULL, subrelease TEXT NOT NULL, archive TEXT NOT NULL, version TEXT NOT NULL, version_id INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (name, release, subrelease, archive))""") + UNIQUE (name, release, subrelease, archive), + PRIMARY KEY(id ASC))""") cursor.execute( + """CREATE TABLE source_package_maintainers + (source_package_id INTEGER NOT NULL, + maintainer TEXT NOT NULL, + FOREIGN KEY(source_package_id) REFERENCES source_packages(id) ON DELETE CASCADE)""") + + cursor.execute( """CREATE TABLE binary_packages (name TEXT NOT NULL, release TEXT NOT NULL, @@ -348,14 +360,14 @@ AND sidp.release = 'sid' AND sidp.subrelease = '' AND sidp.archive = sp.archive AND sidst.bug_name = st.bug_name - AND sidst.package = sidp.rowid) AS unstable_vulnerable, + AND sidst.package = sidp.id) AS unstable_vulnerable, COALESCE((SELECT NOT vulnerable FROM source_packages AS tsecp, source_package_status AS tsecst WHERE tsecp.name = sp.name AND tsecp.release = 'wheezy' AND tsecp.subrelease = 'security' AND tsecp.archive = sp.archive AND tsecst.bug_name = st.bug_name - AND tsecst.package = tsecp.rowid), 0) AS testing_security_fixed, + AND tsecst.package = tsecp.id), 0) AS testing_security_fixed, (SELECT range_remote FROM nvd_data WHERE cve_name = st.bug_name) AS remote, (EXISTS (SELECT * FROM package_notes_nodsa AS pnd @@ -363,7 +375,7 @@ AND pnd.package = sp.name AND pnd.release = 'wheezy')) AS no_dsa FROM source_package_status AS st, source_packages AS sp - WHERE st.vulnerable > 0 AND sp.rowid = st.package + WHERE st.vulnerable > 0 AND sp.id = st.package AND sp.release = 'wheezy' AND sp.subrelease = '' ORDER BY sp.name, st.urgency, st.bug_name""") @@ -380,7 +392,7 @@ AND pnd.package = sp.name AND pnd.release = '%s')) AS no_dsa FROM source_package_status AS st, source_packages AS sp - WHERE st.vulnerable > 0 AND sp.rowid = st.package + WHERE st.vulnerable > 0 AND sp.id = st.package AND sp.release = '%s' AND sp.subrelease = '' AND NOT COALESCE((SELECT NOT vulnerable FROM source_packages AS secp, source_package_status AS secst @@ -388,7 +400,7 @@ AND secp.release = '%s' AND secp.subrelease = 'security' AND secp.archive = sp.archive AND secst.bug_name = st.bug_name - AND secst.package = secp.rowid), 0) + AND secst.package = secp.id), 0) ORDER BY sp.name, urgency_to_number(urgency), st.bug_name""" % (name, nickname, nickname, nickname)) @@ -465,6 +477,7 @@ for pkg in packages: pkg_name = None pkg_version = None + pkg_maintainers = [] pkg_arch = None pkg_source = None pkg_source_version = None @@ -473,6 +486,8 @@ pkg_name = contents elif name == "Version": pkg_version = contents + elif name in ("Maintainer", "Uploaders"): + pkg_maintainers += [mail for (name, mail) in email.utils.getaddresses([contents])] elif name == "Source": match = re_source.match(contents) if match is None: @@ -482,6 +497,7 @@ (pkg_source, pkg_source_version) = match.groups() elif name == "Architecture": pkg_arch = contents + pkg_maintainers = list(set(pkg_maintainers)) if pkg_name is None: raise SyntaxError\ ("package record does not contain package name") @@ -497,10 +513,10 @@ oversion = debian_support.Version(data[pkg_name][0]) if oversion < debian_support.Version(pkg_version): data[pkg_name] = (pkg_version, pkg_arch, - pkg_source, pkg_source_version) + pkg_source, pkg_source_version, pkg_maintainers) else: data[pkg_name] = (pkg_version, pkg_arch, - pkg_source, pkg_source_version) + pkg_source, pkg_source_version, pkg_maintainers) return data @@ -546,6 +562,7 @@ print " reading source packages" for filename in glob.glob(directory + '/*_Sources'): + print filename match = re_sources.match(filename) if match is None: raise ValueError, "invalid file name: " + `filename` @@ -561,18 +578,26 @@ (release, subrelease, archive)) self._clearVersions(cursor) - def gen(): - for name in parsed.keys(): - (version, archs, source, source_version) = parsed[name] - assert source is None - assert source_version is None - yield name, release, subrelease, archive, version - cursor.executemany( - """INSERT INTO source_packages - (name, release, subrelease, archive, version) - VALUES (?, ?, ?, ?, ?)""", - gen()) - + for name in parsed.keys(): + (version, archs, source, source_version, maintainers) = parsed[name] + assert source is None + assert source_version is None + cursor.execute( + """INSERT INTO source_packages + (name, release, subrelease, archive, version) + VALUES (?, ?, ?, ?, ?)""", + (name, release, subrelease, archive, version)) + last_id = self.db.last_insert_rowid() + def gen(): + for maintainer in maintainers: + yield last_id, maintainer + cursor.executemany( + """INSERT INTO source_package_maintainers + (source_package_id, maintainer) + VALUES (?, ?)""", + gen()) + print 'done importing' + def _readBinaryPackages(self, cursor, directory): """Reads from a directory with binary package files.""" @@ -618,7 +643,7 @@ (unch, parsed) = self._parseFile(cursor, filename) unchanged = unchanged and unch for name in parsed.keys(): - (version, arch, source, source_version) = parsed[name] + (version, arch, source, source_version, maintainer) = parsed[name] if source is None: source = name if source_version is None: @@ -1001,7 +1026,7 @@ cursor.execute( """INSERT INTO source_package_status - SELECT n.bug_name, p.rowid, + SELECT n.bug_name, p.id, CASE WHEN n.fixed_version == 'undetermined' THEN 2 ELSE CASE WHEN n.fixed_version IS NULL THEN 1 ELSE CASE WHEN p.version_id < n.fixed_version_id THEN 1 @@ -1017,7 +1042,7 @@ print " source packages (qualified)" cursor.execute( """INSERT OR REPLACE INTO source_package_status - SELECT n.bug_name, p.rowid, + SELECT n.bug_name, p.id, CASE WHEN n.fixed_version == 'undetermined' THEN 2 ELSE CASE WHEN n.fixed_version IS NULL THEN 1 ELSE CASE WHEN p.version_id < n.fixed_version_id THEN 1 @@ -1081,7 +1106,7 @@ """SELECT DISTINCT sp.name, st.vulnerable, n.urgency FROM source_package_status AS st, source_packages AS sp, package_notes AS n - WHERE st.bug_name = ? AND sp.rowid = st.package + WHERE st.bug_name = ? AND sp.id = st.package AND sp.release = 'sid' AND n.bug_name = st.bug_name AND n.package = sp.name ORDER BY sp.name""", @@ -1145,7 +1170,7 @@ st.vulnerable, n.urgency FROM source_package_status AS st, source_packages AS sp, package_notes AS n - WHERE st.bug_name = ? AND sp.rowid = st.package + WHERE st.bug_name = ? AND sp.id = st.package AND sp.release = ? AND sp.subrelease IN ('', 'security') AND n.bug_name = st.bug_name AND n.package = sp.name ORDER BY sp.name""", @@ -1279,7 +1304,7 @@ AND p.release = ? AND p.subrelease IN ('', 'security') AND st.bug_name = ? - AND st.package = p.rowid + AND st.package = p.id ORDER BY p.version COLLATE version DESC""" else: fix_available_sql = '' @@ -1615,7 +1640,7 @@ release_name(p.release, p.subrelease, p.archive) AS release, p.version AS version, s.vulnerable AS vulnerable FROM source_package_status AS s, source_packages AS p - WHERE s.bug_name = ? AND p.rowid = s.package + WHERE s.bug_name = ? AND p.id = s.package AND release in ('lenny', 'squeeze', 'wheezy', 'sid', 'experimental') ORDER BY release_to_number(p.release), p.subrelease) GROUP BY package, version, vulnerable @@ -1661,14 +1686,14 @@ source_package_status AS st2 WHERE sp2.name = sp.name AND sp2.release = sp.release AND sp2.subrelease = 'security' AND sp2.archive = sp.archive - AND st2.package = sp2.rowid AND st2.bug_name = st.bug_name + AND st2.package = sp2.id AND st2.bug_name = st.bug_name ORDER BY st2.vulnerable DESC), 1)) AS vulnerable, st.urgency = 'unimportant' OR NOT vulnerable AS unimportant FROM source_packages AS sp, source_package_status AS st, bugs WHERE sp.name = ? AND sp.release IN ('lenny', 'squeeze', 'wheezy', 'sid') AND sp.subrelease <> 'security' - AND st.package = sp.rowid + AND st.package = sp.id AND bugs.name = st.bug_name AND bugs.name NOT LIKE 'DSA-%' GROUP BY bugs.name, bugs.description, sp.name) @@ -1683,6 +1708,33 @@ AND bugs.name LIKE 'DSA-%' AND p.package = ?""", (package,)) + def getBugsForMaintainer(self, cursor, maintainer, vulnerable, unimportant): + """Returns a generator for a list of (BUG, DESCRIPTION) pairs + which have the requested status. Only bugs affecting supported + releases are returned.""" + return cursor.execute( + """SELECT DISTINCT pkg, name, description + FROM (SELECT sp.name AS pkg, bugs.name AS name, bugs.description AS description, + MAX(st.vulnerable + AND COALESCE((SELECT st2.vulnerable FROM source_packages AS sp2, + source_package_status AS st2 + WHERE sp2.name = sp.name AND sp2.release = sp.release + AND sp2.subrelease = 'security' AND sp2.archive = sp.archive + AND st2.package = sp2.id AND st2.bug_name = st.bug_name + ORDER BY st2.vulnerable DESC), 1)) AS vulnerable, + st.urgency = 'unimportant' OR NOT vulnerable AS unimportant + FROM source_packages AS sp, source_package_maintainers AS m, source_package_status AS st, bugs + WHERE m.maintainer = ? + AND m.source_package_id = sp.id + AND sp.release IN ('lenny', 'squeeze', 'wheezy', 'sid') + AND sp.subrelease <> 'security' + AND st.package = sp.id + AND bugs.name = st.bug_name + AND bugs.name NOT LIKE 'DSA-%' + GROUP BY bugs.name, bugs.description, sp.name) + WHERE vulnerable = ? AND unimportant = ? + ORDER BY name""", (maintainer, vulnerable, unimportant)) + def getTODOs(self, cursor=None, hide_check=False): """Returns a list of pairs (BUG-NAME, DESCRIPTION).""" if cursor is None: Index: bin/tracker_service.py =================================================================== --- bin/tracker_service.py (revision 18462) +++ bin/tracker_service.py (working copy) @@ -132,6 +132,7 @@ self.register('*', self.page_object) self.register('redirect/*', self.page_redirect) self.register('source-package/*', self.page_source_package) + self.register('maintainer/*', self.page_maintainer) self.register('status/release/oldstable', self.page_status_release_oldstable) self.register('status/release/stable', self.page_status_release_stable) @@ -278,6 +279,9 @@ if self.db.isSourcePackage(c, obj): return RedirectResult(self.url_source_package(url, obj, full=True)) + if '@' in obj: + return RedirectResult(self.url_maintainer(url, obj, full=True)) + return self.page_not_found(url, obj) def page_bug(self, url, name, redirect): @@ -576,6 +580,49 @@ replacement='No known security announcements.') ]) + def page_maintainer(self, path, params, url): + maintainer = path[0] + + def gen_versions(): + for (releases, version) in self.db.getSourcePackageVersions( + self.db.cursor(), maintainer): + yield ', '.join(releases), version + def gen_bug_list(lst): + for (pkg, bug, description) in lst: + yield self.make_xref(url, pkg), self.make_xref(url, bug), description + + caption=('Package','Bug', 'Description') + return self.create_page( + url, 'Information on maintainer ' + maintainer, + [make_menu(lambda x: x, + (self.url_ddpo(url, maintainer), + maintainer + ' in the Debian Developer\'s Packages Overview'), + (self.url_debian_bug_maintainer(url, maintainer), + maintainer + ' in the Bug Tracking System'), + (self.url_buildd(url, maintainer), + maintainer + ' in the Buildd status page'), + (self.url_portfolio(url, maintainer), + maintainer + ' in the Debian Member Portfolio Service')), + + H2('Open issues'), + make_table(gen_bug_list(self.db.getBugsForMaintainer + (self.db.cursor(), maintainer, True, False)), + caption=caption, + replacement='No known open issues.'), + + H2('Open unimportant issues'), + make_table(gen_bug_list(self.db.getBugsForMaintainer + (self.db.cursor(), maintainer, True, True)), + caption=caption, + replacement='No known unimportant issues.'), + + H2('Resolved issues'), + make_table(gen_bug_list(self.db.getBugsForMaintainer + (self.db.cursor(), maintainer, False, True)), + caption=caption, + replacement='No known resolved issues.') + ]) + def page_status_release_stable_oldstable(self, release, params, url): assert release in ('stable', 'oldstable') @@ -1181,7 +1228,7 @@ status=status) def make_search_button(self, url): - return FORM("Search for package or bug name: ", + return FORM("Search for maintainer email or package or bug name: ", INPUT(type='text', name='query', onkeyup="onSearch(this.value)", onmousemove="onSearch(this.value)"), @@ -1223,17 +1270,34 @@ def url_debian_bug_pkg(self, url, debian): return url.absolute("http://bugs.debian.org/cgi-bin/pkgreport.cgi", pkg=debian) + def url_debian_bug_maintainer(self, url, maintainer): + return url.absolute("http://bugs.debian.org/cgi-bin/pkgreport.cgi", + maint=maintainer) def url_pts(self, url, package): return url.absolute("http://packages.qa.debian.org/common/index.html", src=package) def url_testing_status(self, url, package): return url.absolute("http://release.debian.org/migration/testing.pl", package=package) + def url_portfolio(self, url, maintainer): + return url.absolute("http://portfolio.debian.net/result", + email=maintainer) + def url_ddpo(self, url, maintainer): + return url.absolute("http://qa.debian.org/developer.php", + email=maintainer) + def url_buildd(self, url, maintainer): + return url.absolute("https://buildd.debian.org/status/package.php", + p=maintainer, compact='compact', comaint='yes') def url_source_package(self, url, package, full=False): if full: return url.scriptRelativeFull("source-package/" + package) else: return url.scriptRelative("source-package/" + package) + def url_maintainer(self, url, maintainer, full=False): + if full: + return url.scriptRelativeFull("maintainer/" + maintainer) + else: + return url.scriptRelative("maintainer/" + maintainer) def make_xref(self, url, name): return A(url.scriptRelative(name), name)
Attachment:
signature.asc
Description: This is a digitally signed message part