package: debsecan
severity: wishlist
tags: patch

I'm trying to migrate my systems to python3 only, so I've worked on a
python3 backend patch for debsecan.

The 2to3 script got most of the way automatically, although there was
some trickiness with the removal of __cmp__ and compressed data
strings, but otherwise it was rather straightforward.

I also made sure to maintain backwards compatibility with python2 via
try/except in a few places.

I've done some modest testing, getting the same output with either
python2 or python3 as backend.

Please see attached.

Best wishes,
Mike
diff -Nru debsecan-0.4.18/debian/changelog debsecan-0.4.18+nmu1/debian/changelog
--- debsecan-0.4.18/debian/changelog	2015-02-22 19:13:07.000000000 +0000
+++ debsecan-0.4.18+nmu1/debian/changelog	2015-09-27 00:16:24.000000000 +0000
@@ -1,3 +1,10 @@
+debsecan (0.4.18+nmu1) UNRELEASED; urgency=medium
+
+  * Non-maintainer upload.
+  * Add support for python3.
+
+ -- Michael Gilbert <mgilb...@debian.org>  Sat, 26 Sep 2015 21:50:21 +0000
+
 debsecan (0.4.18) unstable; urgency=low
 
   * Increase compatibility with Python 2.6 in squeeze.  The
diff -Nru debsecan-0.4.18/debian/control debsecan-0.4.18+nmu1/debian/control
--- debsecan-0.4.18/debian/control	2015-02-22 19:06:12.000000000 +0000
+++ debsecan-0.4.18+nmu1/debian/control	2015-09-27 00:01:52.000000000 +0000
@@ -9,7 +9,7 @@
 
 Package: debsecan
 Architecture: all
-Depends: debconf | debconf-2.0, python (>= 2.3), python-apt, ${misc:Depends},
+Depends: debconf | debconf-2.0, python-apt | python3-apt, ${misc:Depends},
  ca-certificates
 Recommends: cron, exim4 | mail-transport-agent
 Description: Debian Security Analyzer
diff -Nru debsecan-0.4.18/debian/rules debsecan-0.4.18+nmu1/debian/rules
--- debsecan-0.4.18/debian/rules	2010-03-07 16:57:27.000000000 +0000
+++ debsecan-0.4.18+nmu1/debian/rules	2015-09-27 00:02:36.000000000 +0000
@@ -30,8 +30,10 @@
 
 	# Add here commands to install the package into debian/<packagename>.
 	install -d debian/`dh_listpackages`/var/lib/debsecan
-	install -D -m 0755 src/debsecan \
+	install -D -m 0755 debsecan \
 		debian/`dh_listpackages`/usr/bin/debsecan
+	install -D -m 0755 src/debsecan \
+		debian/`dh_listpackages`/usr/share/debsecan/debsecan
 	install -D -m 0755 src/debsecan-create-cron \
 		debian/`dh_listpackages`/usr/sbin/debsecan-create-cron
 	install -D -m 0755 doc/debsecan.1 \
diff -Nru debsecan-0.4.18/debsecan debsecan-0.4.18+nmu1/debsecan
--- debsecan-0.4.18/debsecan	1970-01-01 00:00:00.000000000 +0000
+++ debsecan-0.4.18+nmu1/debsecan	2015-09-27 00:23:39.000000000 +0000
@@ -0,0 +1,10 @@
+#!/bin/sh -e
+
+if [ -e /usr/share/doc/python3-apt/copyright ]; then
+    /usr/bin/python3 /usr/share/debsecan/debsecan $@
+elif [ -e /usr/share/doc/python-apt/copyright ]; then
+    /usr/bin/python /usr/share/debsecan/debsecan $@
+else
+    echo "error: python-apt package not found"
+    exit 1
+fi
diff -Nru debsecan-0.4.18/src/debsecan debsecan-0.4.18+nmu1/src/debsecan
--- debsecan-0.4.18/src/debsecan	2014-03-29 21:09:58.000000000 +0000
+++ debsecan-0.4.18+nmu1/src/debsecan	2015-09-27 00:08:36.000000000 +0000
@@ -1,6 +1,7 @@
 #!/usr/bin/python
 # debsecan - Debian Security Analyzer
 # Copyright (C) 2005, 2006, 2007 Florian Weimer
+# Copyright (C) 2015 Michael Gilbert <mgilb...@debian.org>
 #
 # 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
@@ -19,7 +20,6 @@
 VERSION = "0.4"
 
 import copy
-from cStringIO import StringIO
 from optparse import OptionParser
 import os
 import os.path
@@ -28,10 +28,23 @@
 import sys
 import time
 import types
-import urllib2
 import zlib
 import apt_pkg
 
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from io import StringIO
+
+try:
+    from urllib2 import urlopen
+    from urllib2 import Request
+    from urllib2 import HTTPError
+except ImportError:
+    from urllib.error import HTTPError
+    from urllib.request import urlopen
+    from urllib.request import Request
+
 apt_pkg.init()
 try:
     version_compare = apt_pkg.version_compare
@@ -56,7 +69,7 @@
     """
 
     def __init__(self, filename, lineno, msg):
-        assert type(lineno) == types.IntType
+        assert type(lineno) == int
         self.filename = filename
         self.lineno = lineno
         self.msg = msg
@@ -65,9 +78,9 @@
         return self.msg
 
     def __repr__(self):
-        return "ParseError(%s, %d, %s)" % (`self.filename`,
+        return "ParseError(%s, %d, %s)" % (repr(self.filename),
                                            self.lineno,
-                                           `self.msg`)
+                                           repr(self.msg))
 
     def printOut(self, file):
         """Writes a machine-parsable error message to file."""
@@ -78,17 +91,35 @@
     """Version class which uses the original APT comparison algorithm."""
     def __init__(self, version):
         """Creates a new Version object."""
-        assert type(version) == types.StringType, `version`
-        assert version <> ""
+        assert type(version) == str, repr(version)
+        assert version != ""
         self.__asString = version
 
     def __str__(self):
         return self.__asString
 
     def __repr__(self):
-        return 'Version(%s)' % `self.__asString`
+        return 'Version(%s)' % repr(self.__asString)
+
+    def __lt__(self, other):
+        return version_compare(self.__asString, other.__asString) < 0
+
+    def __eq__(self, other):
+        return version_compare(self.__asString, other.__asString) == 0
+
+    def __gt__(self, other):
+        raise NotImplementedError()
+
+    def __le__(self, other):
+        raise NotImplementedError()
 
-    def __cmp__(self, other):
+    def __ge__(self, other):
+        raise NotImplementedError()
+
+    def __ne__(self, other):
+        raise NotImplementedError()
+
+    def compare(self, other):
         return version_compare(self.__asString, other.__asString)
 
 class PackageFile:
@@ -128,7 +159,7 @@
 
             match = self.re_field.match(line)
             if not match:
-                self.raiseSyntaxError("expected package field, got " + `line`)
+                self.raiseSyntaxError("expected package field, got " + repr(line))
             (name, contents) = match.groups()
             contents = contents or ''
 
@@ -161,9 +192,9 @@
 
 def safe_open(name, mode="r"):
     try:
-        return file(name, mode)
-    except IOError, e:
-        sys.stdout.write("error: could not open %s: %s\n" % (`name`, e.strerror))
+        return open(name, mode)
+    except IOError as e:
+        sys.stdout.write("error: could not open %s: %s\n" % (repr(name), e.strerror))
         sys.exit(2)
 
 # Configuration file parser
@@ -234,8 +265,8 @@
         def onComment(self, line, lineno):
             new_file.append(line)
         def onKey(self, line, lineno, key, value, trailer):
-            if new_config.has_key(key):
-                if new_config[key] <> value:
+            if key in new_config:
+                if new_config[key] != value:
                     new_file.append("%s=%s%s"
                                     % (key, new_config[key], trailer))
                 else:
@@ -245,15 +276,15 @@
                 new_file.append(line)
     Parser(name).parse()
 
-    remaining = new_config.keys()
+    remaining = list(new_config.keys())
     remaining.sort()
     if remaining:
-        if remaining[-1] <> "\n":
+        if remaining[-1] != "\n":
             new_file.append("\n")
         for k in remaining:
             new_file.append("%s=%s\n" % (k, new_config[k]))
 
-    conf = file(name, "w+")
+    conf = open(name, "w+")
     try:
         for line in new_file:
             conf.write(line)
@@ -266,7 +297,11 @@
     import ssl
     from inspect import getargspec
     from inspect import stack
-    from httplib import HTTPConnection
+
+    try:
+        from httplib import HTTPConnection
+    except ImportError:
+        from http.client import HTTPConnection
 
     wrap_socket_orig = ssl.wrap_socket
     set_ciphers = "ciphers" in getargspec(wrap_socket_orig)[0]
@@ -381,7 +416,7 @@
                 "error: at most one whitelist option may be specified\n")
             sys.exit(1)
 
-        for (k, v) in options.__dict__.items():
+        for (k, v) in list(options.__dict__.items()):
             if type(v) == types.MethodType or v is None:
                 continue
             if k not in ("whitelist", "whitelist_add", "whitelist_remove",
@@ -413,13 +448,13 @@
     if options.no_obsolete and not options.suite:
         sys.stderr.write("error: --no-obsolete requires --suite\n")
         sys.exit(1)
-    if options.update_history and options.format <> 'report':
+    if options.update_history and options.format != 'report':
         sys.stderr.write("error: --update-history requires report format\n")
         sys.exit(1)
-    if options.cron and options.format <> 'report':
+    if options.cron and options.format != 'report':
         sys.stderr.write("error: --cron requires report format\n")
         sys.exit(1)
-    if options.mailto and options.format <> 'report':
+    if options.mailto and options.format != 'report':
         sys.stderr.write("error: --mailto requires report format\n")
         sys.exit(1)
     options.need_history = options.format == 'report'
@@ -473,10 +508,12 @@
                        ' ' : False}[flags[2]]
         self.fix_available = flags[3] == 'F'
 
-    def is_vulnerable(self, (bin_pkg, bin_ver), (src_pkg, src_ver)):
+    def is_vulnerable(self, bin_pair, src_pair):
         """Returns true if the specified binary package is subject to
         this vulnerability."""
         self._parse()
+        (bin_pkg, bin_ver) = bin_pair
+        (src_pkg, src_ver) = src_pair
         if self.binary_package and bin_pkg == self.package:
             if self.unstable_version:
                 return bin_ver < self.unstable_version
@@ -506,37 +543,37 @@
 
     def _parse(self):
         """Further parses the object."""
-        if type(self.unstable_version) == types.StringType:
+        if type(self.unstable_version) == str:
             if self.unstable_version:
                 self.unstable_version = Version(self.unstable_version)
             else:
                 self.unstable_version = None
-            self.other_versions = map(Version, self.other_versions)
+            self.other_versions = list(map(Version, self.other_versions))
 
 def fetch_data(options, config):
     """Returns a dictionary PACKAGE -> LIST-OF-VULNERABILITIES."""
     url = options.source or config.get("SOURCE", None) \
         or "https://security-tracker.debian.org/tracker/"; \
            "debsecan/release/1/"
-    if url[-1] <> "/":
+    if url[-1] != "/":
         url += "/"
     if options.suite:
         url += options.suite
     else:
         url += 'GENERIC'
-    r = urllib2.Request(url)
+    r = Request(url)
     r.add_header('User-Agent', 'debsecan/' + VERSION)
     try:
-        u = urllib2.urlopen(r)
+        u = urlopen(r)
         # In cron mode, we suppress almost all errors because we
         # assume that they are due to lack of Internet connectivity.
-    except urllib2.HTTPError, e:
+    except HTTPError as e:
         if (not options.cron) or e.code == 404:
             sys.stderr.write("error: while downloading %s:\n%s\n" % (url, e))
             sys.exit(1)
         else:
             sys.exit(0)
-    except urllib2.URLError, e:
+    except URLError as e:
         if not options.cron:            # no e.code check here
             # Be conservative about the attributes offered by
             # URLError.  They are undocumented, and strerror is not
@@ -559,8 +596,14 @@
             data.append(d)
         else:
             break
-    data = StringIO(zlib.decompress(''.join(data)))
-    if data.readline() <> "VERSION 1\n":
+
+    raw = zlib.decompress(b''.join(data))
+    try:
+        data = StringIO(raw)
+    except TypeError:
+        data = StringIO(raw.decode('utf-8'))
+
+    if data.readline() != "VERSION 1\n":
         sys.stderr.write("error: server sends data in unknown format\n")
         sys.exit(1)
 
@@ -597,7 +640,7 @@
         else:
             source_to_binary[sp] = []
 
-    for vs in packages.values():
+    for vs in list(packages.values()):
         for v in vs:
             if not v.binary_package:
                 v.binary_packages = source_to_binary.get(v.package, None)
@@ -633,7 +676,7 @@
 
     def known(self, v):
         """Returns true if the vulnerability is known."""
-        return self.history.has_key(v)
+        return v in self.history
 
     def fixed(self, v):
         """Returns true if the vulnerability is known and has been
@@ -647,7 +690,7 @@
         self.history = {}
 
         try:
-            f = file(name)
+            f = open(name)
         except IOError:
             return
 
@@ -681,8 +724,8 @@
         if name and os.path.exists(name):
             src = safe_open(name)
             line = src.readline()
-            if line <> 'VERSION 0\n':
-                raise SyntaxError, "invalid whitelist file, got: " + `line`
+            if line != 'VERSION 0\n':
+                raise SyntaxError("invalid whitelist file, got: " + repr(line))
             for line in src:
                 if line[-1] == '\n':
                     line = line[:-1]
@@ -717,7 +760,7 @@
                 removed = True
             except KeyError:
                 pass
-            for bug_pkg in self.bug_package_dict.keys():
+            for bug_pkg in list(self.bug_package_dict.keys()):
                 if bug_pkg[0] == bug:
                     del self.bug_package_dict[bug_pkg]
                     removed = True
@@ -736,8 +779,8 @@
 
     def check(self, bug, package):
         """Returns true if the bug/package pair is whitelisted."""
-        return self.bug_dict.has_key(bug) \
-               or self.bug_package_dict.has_key((bug, package))
+        return bug in self.bug_dict \
+               or (bug, package) in self.bug_package_dict
 
     def update(self):
         """Write the whitelist file back to disk, if the data has changed."""
@@ -746,11 +789,11 @@
         new_name = self.name + '.new'
         f = safe_open(new_name, "w+")
         f.write("VERSION 0\n")
-        l = self.bug_dict.keys()
+        l = list(self.bug_dict.keys())
         l.sort()
         for bug in l:
             f.write(bug + ",\n")
-        l = self.bug_package_dict.keys()
+        l = list(self.bug_package_dict.keys())
         l.sort()
         for bug_pkg in l:
             f.write("%s,%s\n" % bug_pkg)
@@ -759,9 +802,9 @@
 
     def show(self, file):
         l = []
-        for bug in self.bug_dict.keys():
+        for bug in list(self.bug_dict.keys()):
             file.write("%s (all packages)\n" % bug)
-        for (bug, pkg) in self.bug_package_dict.keys():
+        for (bug, pkg) in list(self.bug_package_dict.keys()):
             l.append("%s %s\n" % (bug, pkg))
         l.sort()
         for line in l:
@@ -772,14 +815,14 @@
     while args:
         bug = args[0]
         if bug == '' or (not ('A' <= bug[0] <= 'Z')) or ',' in bug:
-            sys.stderr.write("error: %s is not a bug name\n" % `bug`)
+            sys.stderr.write("error: %s is not a bug name\n" % repr(bug))
             sys.exit(1)
         del args[0]
         pkg_found = False
         while args:
             pkg = args[0]
             if (not pkg) or ',' in pkg:
-                sys.stderr.write("error: %s is not a package name\n" % `bug`)
+                sys.stderr.write("error: %s is not a package name\n" % repr(bug))
                 sys.exit(1)
             if 'A' <= pkg[0] <= 'Z':
                 break
@@ -844,7 +887,7 @@
     def record(self, v, bp, sp):
         self.bugs[v.bug] = 1
     def finish(self):
-        bugs = self.bugs.keys()
+        bugs = list(self.bugs.keys())
         bugs.sort()
         for b in bugs:
             self.target.write(b)
@@ -853,17 +896,17 @@
     def __init__(self, target, options, history):
         Formatter.__init__(self, target, options, history)
         self.packages = {}
-    def record(self, v, (bin_name, bin_version), sp):
-        self.packages[bin_name] = 1
+    def record(self, v, bin_pair, sp):
+        self.packages[bin_pair[0]] = 1
     def finish(self):
-        packages = self.packages.keys()
+        packages = list(self.packages.keys())
         packages.sort()
         for p in packages:
             self.target.write(p)
 
 class SummaryFormatter(Formatter):
-    def record(self, v,
-               (bin_name, bin_version), (src_name, src_version)):
+    def record(self, v, bin_pair, src_pair):
+        (bin_name, bin_version) = bin_pair
         notes = []
         if v.fix_available:
             notes.append("fixed")
@@ -880,13 +923,13 @@
             self.target.write("%s %s" % (v.bug, bin_name))
 
 class SimpleFormatter(Formatter):
-    def record(self, v,
-               (bin_name, bin_version), (src_name, src_version)):
-        self.target.write("%s %s" % (v.bug, bin_name))
+    def record(self, v, bin_pair, src_pair):
+        self.target.write("%s %s" % (v.bug, bin_pair[0]))
 
 class DetailFormatter(Formatter):
-    def record(self, v,
-               (bin_name, bin_version), (src_name, src_version)):
+    def record(self, v, bin_pair, src_pair):
+        (bin_name, bin_version) = bin_pair
+        (src_name, src_version) = src_pair
         notes = []
         if v.fix_available:
             notes.append("fixed")
@@ -943,7 +986,7 @@
         new_name = name + '.new'
         f = safe_open(new_name, "w+")
         f.write("VERSION 1\n%d\n" % int(time.time()))
-        for ((bug, package), fixed) in self.new_history.items():
+        for ((bug, package), fixed) in list(self.new_history.items()):
             if fixed:
                 fixed = 'F'
             else:
@@ -958,13 +1001,14 @@
         # need special treatment, too.
         self.record(v, bp, sp)
 
-    def record(self, v,
-               (bin_name, bin_version), (src_name, src_version)):
+    def record(self, v, bin_pair, src_pair):
 
+        (bin_name, bin_version) = bin_pair
+        (src_name, src_version) = src_pair
         v = v.installed(src_name, bin_name)
         bn = (v.bug, bin_name)
         if not self.whitelist.check(v.bug, bin_name):
-            if self.bugs.has_key(v.bug):
+            if v.bug in self.bugs:
                 self.bugs[v.bug].append(v)
             else:
                 self.bugs[v.bug] = [v]
@@ -987,10 +1031,10 @@
         """Returns true if the system's vulnerability status changed
         since the last run."""
 
-        for (k, v) in self.new_history.items():
-            if (not self.history.known(k)) or self.history.fixed(k) <> v:
+        for (k, v) in list(self.new_history.items()):
+            if (not self.history.known(k)) or self.history.fixed(k) != v:
                 return True
-        return len(self.fixed_bugs.keys()) > 0
+        return len(list(self.fixed_bugs.keys())) > 0
 
     def finish(self):
         if self.options.mailto and not self._status_changed():
@@ -1010,10 +1054,10 @@
 the correct suite, run "dpkg-reconfigure debsecan" as root.""")
         w("")
 
-        for vlist in self.bugs.values():
+        for vlist in list(self.bugs.values()):
             vlist.sort(lambda a, b: cmp(a.package, b.package))
 
-        blist = self.bugs.items()
+        blist = list(self.bugs.items())
         blist.sort()
 
         self._bug_found = False
@@ -1086,7 +1130,7 @@
                     else:
                         is_new = (not self.history.known(bug_package)) \
                                  or self.history.fixed(bug_package)
-                    if v.fix_available <> fix_status or is_new <> new_status:
+                    if v.fix_available != fix_status or is_new != new_status:
                         continue
 
                     if first_bug:
@@ -1099,7 +1143,7 @@
                         have_obsolete = True
 
                     notes = vuln_to_notes(v)
-                    if pkg_vulns.has_key(notes):
+                    if notes in pkg_vulns:
                         pkg_vulns[notes].append(v)
                     else:
                         pkg_vulns[notes] = [v]
@@ -1107,7 +1151,7 @@
                 indent = "    "
                 if len(pkg_vulns) > 0:
                     self._bug_found = True
-                    notes = pkg_vulns.keys()
+                    notes = list(pkg_vulns.keys())
                     notes.sort()
                     # any v will do, because we've aggregated by v.bug
                     v = pkg_vulns[notes[0]][0]
@@ -1153,12 +1197,12 @@
 
         def scan_fixed():
             bugs = {}
-            for (bug, package) in self.fixed_bugs.keys():
-                if bugs.has_key(bug):
+            for (bug, package) in list(self.fixed_bugs.keys()):
+                if bug in bugs:
                     bugs[bug].append(package)
                 else:
                     bugs[bug] = [package]
-            bug_names = bugs.keys()
+            bug_names = list(bugs.keys())
             bug_names.sort()
 
             first_bug = True
@@ -1236,11 +1280,11 @@
     try:
         return msg % format_values
     except ValueError:
-        sys.stderr.write("error: invalid format string: %s\n" % `msg`)
+        sys.stderr.write("error: invalid format string: %s\n" % repr(msg))
         sys.exit(2)
-    except KeyError, e:
+    except KeyError as e:
         sys.stderr.write("error: invalid key %s in format string %s\n"
-                         % (`e.args[0]`, `msg`))
+                         % (repr(e.args[0]), repr(msg)))
         sys.exit(2)
 
 # Targets
@@ -1281,8 +1325,7 @@
 
 class TargetPrint(Target):
     def write(self, line):
-        print line
-
+        sys.stdout.write(line + '\n')
 
 def rate_system(target, options, vulns, history):
     """Read /var/lib/dpkg/status and discover vulnerable packages.
@@ -1314,7 +1357,7 @@
                 if match is None:
                     raise SyntaxError(('package %s references '
                                        + 'invalid source package %s') %
-                                      (pkg_name, `contents`))
+                                      (pkg_name, repr(contents)))
                 (pkg_source, pkg_source_version) = match.groups()
         if pkg_name is None:
             raise SyntaxError\
@@ -1364,7 +1407,7 @@
     if (options.update_config):
         update_config(options.config)
         sys.exit(0)
-    if options.cron and config.get("REPORT", "true") <> "true":
+    if options.cron and config.get("REPORT", "true") != "true":
         # Do nothing in cron mode if reporting is disabled.
         sys.exit(0)
     if options.need_history:

Reply via email to