/[secure-testing]/lib/python/security_db.py
ViewVC logotype

Contents of /lib/python/security_db.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 14604 - (show annotations) (download) (as text)
Tue May 4 16:42:46 2010 UTC (3 years, 1 month ago) by fw
File MIME type: text/x-python
File size: 77602 byte(s)
security_db.DB.getBugsForSourcePackage(): restrict to supported releases
1 # security_db.py -- simple, CVE-driven Debian security bugs database
2 # Copyright (C) 2005 Florian Weimer <fw@deneb.enyo.de>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 """This module implements a small database for tracking security bugs.
19
20 Note that the database is always secondary to the text files. The
21 database is only an implementation tool, and not used for maintaining
22 the data.
23
24 The data is kept in a SQLite 3 database.
25
26 FIXME: Document the database schema once it is finished.
27 """
28
29 import apsw
30 import base64
31 import bugs
32 import cPickle
33 import cStringIO
34 import glob
35 import os
36 import os.path
37 import re
38 import sys
39 import types
40 import zlib
41
42 import debian_support
43 import dist_config
44
45 class InsertError(Exception):
46 """Class for capturing insert errors.
47
48 The 'errors' member collects all error messages.
49 """
50
51 def __init__(self, errors):
52 assert len(errors) > 0, errors
53 assert type(errors) == types.ListType, errors
54 assert type(errors[0])== types.StringType, errors
55 self.errors = errors
56
57 def __str__(self):
58 return self.errors[0] + ' [more...]'
59
60 def mergeLists(a, b):
61 """Merges two lists."""
62 if type(a) == types.StringType:
63 if a == "":
64 a = []
65 else:
66 a = a.split(',')
67 if type(b) == types.StringType:
68 if b == "":
69 b = []
70 else:
71 b = b.split(',')
72 result = {}
73 for x in a:
74 result[x] = 1
75 for x in b:
76 result[x] = 1
77 result = result.keys()
78 result.sort()
79 return result
80
81 class NVDEntry:
82 """A class for an entry in the nvd_data table.
83 Objects have the same fileds as the table."""
84 def __init__(self, row, description):
85 for x in range(len(row)):
86 setattr(self, description[x][0], row[x])
87 def rangeString(self):
88 result = []
89 if self.range_local:
90 result.append("local")
91 if self.range_remote:
92 result.append("remote")
93 if self.range_user_init:
94 result.append("user-initiated")
95 return ", ".join(result)
96
97 class SchemaMismatch(Exception):
98 """Raised to indicate a schema mismatch.
99
100 The caller is expected to remove and regenerate the database."""
101
102 class DB:
103 """Access to the security database.
104
105 This is a wrapper around an SQLite database object (which is
106 accessible as the "db" member.
107
108 Most operations need a special cursor object, which can be created
109 with a cursor object. The name "cursor" is somewhat of a
110 misnomer because these objects are quite versatile.
111 """
112
113 def __init__(self, name, verbose=False):
114 self.name = name
115 self.db = apsw.Connection(name)
116 self.verbose = verbose
117
118 self.schema_version = 21
119 self._initFunctions()
120
121 c = self.cursor()
122 for (v,) in c.execute("PRAGMA user_version"):
123 if v == 0:
124 self.initSchema()
125 elif v == 20:
126 self._initSchema20()
127 elif v <> self.schema_version:
128 if self.verbose:
129 print "DB: schema version mismatch: expected %d, got %d" \
130 % (self.schema_version, v)
131 raise SchemaMismatch, `v`
132 # Database has been created at this point. Small race
133 # condition here (the already opened database might refer
134 # to an older file).
135 self.__stat = os.stat(self.name)
136 return
137 assert False
138
139 def __del__(self):
140 self.db.close()
141
142 def refresh(self):
143 """Checks if the database file is still the same and reopens
144 it if necessary."""
145 current = os.stat(self.name)
146 if os.path.samestat(self.__stat, current):
147 return
148 self.__stat = current
149 self.db = apsw.Connection(self.name)
150 self._initFunctions()
151
152 def cursor(self):
153 """Creates a new database cursor.
154
155 Also see the writeTxn method."""
156 return self.db.cursor()
157
158 def writeTxn(self):
159 """Creates a cursor for an exclusive transaction.
160
161 No other process may modify the database at the same time.
162 After finishing the work, you should invoke the commit or
163 rollback methods below.
164 """
165 c = self.cursor()
166 c.execute("BEGIN TRANSACTION EXCLUSIVE")
167 return c
168
169 def commit(self, cursor):
170 """Makes the changes in the transaction permanent."""
171 cursor.execute("COMMIT")
172
173 def rollback(self, cursor):
174 """Undos the changes in the transaction."""
175 cursor.execute("ROLLBACK")
176
177 def initSchema(self):
178 """Creates the database schema."""
179 cursor = self.cursor()
180
181 # This gives us better performance (it's usually the file
182 # system block size). This must come first to be ffective.
183
184 cursor.execute("PRAGMA page_size = 4096")
185
186 # Set the schema version to an invalid value which is
187 # different from zero. We can use this to detect a partially
188 # created schema.
189
190 cursor.execute("PRAGMA user_version = 1")
191
192 cursor.execute("""CREATE TABLE inodeprints
193 (file TEXT NOT NULL PRIMARY KEY,
194 inodeprint TEXT NOT NULL,
195 parsed BLOB)""")
196
197 cursor.execute("""CREATE TABLE version_linear_order
198 (id INTEGER NOT NULL PRIMARY KEY,
199 version TEXT NOT NULL UNIQUE)""")
200
201 cursor.execute(
202 """CREATE TABLE source_packages
203 (name TEXT NOT NULL,
204 release TEXT NOT NULL,
205 subrelease TEXT NOT NULL,
206 archive TEXT NOT NULL,
207 version TEXT NOT NULL,
208 version_id INTEGER NOT NULL DEFAULT 0,
209 PRIMARY KEY (name, release, subrelease, archive))""")
210
211 cursor.execute(
212 """CREATE TABLE binary_packages
213 (name TEXT NOT NULL,
214 release TEXT NOT NULL,
215 subrelease TEXT NOT NULL,
216 archive TEXT NOT NULL,
217 version TEXT NOT NULL,
218 source TEXT NOT NULL,
219 source_version TEXT NOT NULL,
220 archs TEXT NOT NULL,
221 PRIMARY KEY (name, release, subrelease, archive, version, source,
222 source_version))""")
223 cursor.execute(
224 """CREATE INDEX binary_packages_source
225 ON binary_packages(source)""")
226
227 cursor.execute("""CREATE TABLE package_notes
228 (id INTEGER NOT NULL PRIMARY KEY,
229 bug_name TEXT NOT NULL,
230 package TEXT NOT NULL,
231 fixed_version TEXT
232 CHECK (fixed_version IS NULL OR fixed_version <> ''),
233 fixed_version_id INTEGER NOT NULL DEFAULT 0,
234 release TEXT NOT NULL,
235 package_kind TEXT NOT NULL DEFAULT 'unknown',
236 urgency TEXT NOT NULL,
237 bug_origin TEXT NOT NULL DEFAULT '')""")
238 cursor.execute(
239 """CREATE UNIQUE INDEX package_notes_bug
240 ON package_notes(bug_name, package, release)""")
241 cursor.execute(
242 """CREATE INDEX package_notes_package
243 ON package_notes(package)""")
244
245 cursor.execute("""CREATE TABLE debian_bugs
246 (bug INTEGER NOT NULL,
247 note INTEGER NOT NULL,
248 PRIMARY KEY (bug, note))""")
249
250 cursor.execute("""CREATE TABLE bugs
251 (name TEXT NOT NULL PRIMARY KEY,
252 cve_status TEXT NOT NULL
253 CHECK (cve_status IN
254 ('', 'CANDIDATE', 'ASSIGNED', 'RESERVED', 'REJECTED')),
255 not_for_us INTEGER NOT NULL CHECK (not_for_us IN (0, 1)),
256 description TEXT NOT NULL,
257 release_date TEXT NOT NULL,
258 source_file TEXT NOT NULL,
259 source_line INTEGER NOT NULL)""")
260
261 cursor.execute("""CREATE TABLE bugs_notes
262 (bug_name TEXT NOT NULL CHECK (typ <> ''),
263 typ TEXT NOT NULL CHECK (typ IN ('TODO', 'NOTE')),
264 release TEXT NOT NULL DEFAULT '',
265 comment TEXT NOT NULL CHECK (comment <> ''))""")
266
267 cursor.execute("""CREATE TABLE bugs_xref
268 (source TEXT NOT NULL,
269 target TEXT NOT NULL,
270 PRIMARY KEY (source, target))""")
271 cursor.execute("CREATE INDEX bugs_xref_target ON bugs_xref(target)")
272
273 cursor.execute("""CREATE TABLE bug_status
274 (bug_name TEXT NOT NULL,
275 release TEXT NOT NULL,
276 status TEXT NOT NULL
277 CHECK (status IN ('vulnerable', 'fixed', 'unknown', 'undetermined',
278 'partially-fixed', 'todo')),
279 reason TEXT NOT NULL,
280 PRIMARY KEY (bug_name, release))""")
281
282 cursor.execute("""CREATE TABLE source_package_status
283 (bug_name TEXT NOT NULL,
284 package INTEGER NOT NULL,
285 vulnerable INTEGER NOT NULL,
286 urgency TEXT NOT NULL,
287 PRIMARY KEY (bug_name, package))""")
288 cursor.execute(
289 """CREATE INDEX source_package_status_package
290 ON source_package_status(package)""")
291
292 cursor.execute(
293 "CREATE TABLE removed_packages (name TEXT NOT NULL PRIMARY KEY)")
294
295 cursor.execute(
296 """CREATE TABLE nvd_data
297 (cve_name TEXT NOT NULL PRIMARY KEY,
298 cve_desc TEXT NOT NULL,
299 discovered TEXT NOT NULL,
300 published TEXT NOT NULL,
301 severity TEXT NOT NULL,
302 range_local INTEGER,
303 range_remote INTEGER,
304 range_user_init INTEGER,
305 loss_avail INTEGER NOT NULL,
306 loss_conf INTEGER NOT NULL,
307 loss_int INTEGER NOT NULL,
308 loss_sec_prot_user INTEGER NOT NULL,
309 loss_sec_prot_admin INTEGER NOT NULL,
310 loss_sec_prot_other INTEGER NOT NULL)""")
311
312 cursor.execute(
313 """CREATE TABLE debsecan_data
314 (name TEXT NOT NULL PRIMARY KEY,
315 data TEXT NOT NULL)""")
316
317 self._initNoDSA(cursor)
318 self._initViews(cursor)
319
320 cursor.execute("PRAGMA user_version = %d" % self.schema_version)
321
322 def _initSchema20(self):
323 cursor = self.db.cursor()
324
325 cursor.execute("PRAGMA user_version = 1")
326 self._initNoDSA(cursor)
327 self._initViews(cursor)
328 cursor.execute("DELETE FROM inodeprints WHERE file ='data/CVE/list'")
329 cursor.execute("PRAGMA user_version = %d" % self.schema_version)
330
331 def _initNoDSA(self, cursor):
332 cursor.execute(
333 """CREATE TABLE package_notes_nodsa
334 (bug_name TEXT NOT NULL,
335 package TEXT NOT NULL,
336 release TEXT NOT NULL,
337 reason TEXT NOT NULL,
338 comment TEXT NOT NULL,
339 PRIMARY KEY (bug_name, package, release))
340 """)
341
342 def _initViews(self, cursor):
343 for view in ('testing_status', 'stable_status', 'oldstable_status'):
344 try:
345 cursor.execute('DROP VIEW ' + view)
346 except apsw.SQLError:
347 pass
348
349 cursor.execute(
350 """CREATE VIEW testing_status AS
351 SELECT DISTINCT sp.name AS package, st.bug_name AS bug,
352 sp.archive AS section, st.urgency AS urgency,
353 st.vulnerable AS vulnerable,
354 (SELECT vulnerable
355 FROM source_packages AS sidp, source_package_status AS sidst
356 WHERE sidp.name = sp.name
357 AND sidp.release = 'sid' AND sidp.subrelease = ''
358 AND sidp.archive = sp.archive
359 AND sidst.bug_name = st.bug_name
360 AND sidst.package = sidp.rowid) AS unstable_vulnerable,
361 COALESCE((SELECT NOT vulnerable
362 FROM source_packages AS tsecp, source_package_status AS tsecst
363 WHERE tsecp.name = sp.name
364 AND tsecp.release = 'squeeze' AND tsecp.subrelease = 'security'
365 AND tsecp.archive = sp.archive
366 AND tsecst.bug_name = st.bug_name
367 AND tsecst.package = tsecp.rowid), 0) AS testing_security_fixed,
368 (SELECT range_remote FROM nvd_data
369 WHERE cve_name = st.bug_name) AS remote,
370 (EXISTS (SELECT * FROM package_notes_nodsa AS pnd
371 WHERE pnd.bug_name = st.bug_name
372 AND pnd.package = sp.name
373 AND pnd.release = 'squeeze')) AS no_dsa
374 FROM source_package_status AS st, source_packages AS sp
375 WHERE st.vulnerable > 0 AND sp.rowid = st.package
376 AND sp.release = 'squeeze' AND sp.subrelease = ''
377 ORDER BY sp.name, st.urgency, st.bug_name""")
378
379 for (name, nickname) in (('stable', 'lenny'), ('oldstable', 'etch'),):
380 cursor.execute(
381 """CREATE VIEW %s_status AS
382 SELECT DISTINCT sp.name AS package, st.bug_name AS bug,
383 sp.archive AS section, st.urgency AS urgency,
384 st.vulnerable AS vulnerable,
385 (SELECT range_remote FROM nvd_data
386 WHERE cve_name = st.bug_name) AS remote,
387 (EXISTS (SELECT * FROM package_notes_nodsa AS pnd
388 WHERE pnd.bug_name = st.bug_name
389 AND pnd.package = sp.name
390 AND pnd.release = '%s')) AS no_dsa
391 FROM source_package_status AS st, source_packages AS sp
392 WHERE st.vulnerable > 0 AND sp.rowid = st.package
393 AND sp.release = '%s' AND sp.subrelease = ''
394 AND NOT COALESCE((SELECT NOT vulnerable
395 FROM source_packages AS secp, source_package_status AS secst
396 WHERE secp.name = sp.name
397 AND secp.release = '%s' AND secp.subrelease = 'security'
398 AND secp.archive = sp.archive
399 AND secst.bug_name = st.bug_name
400 AND secst.package = secp.rowid), 0)
401 ORDER BY sp.name, urgency_to_number(urgency), st.bug_name"""
402 % (name, nickname, nickname, nickname))
403
404 def _initFunctions(self):
405 """Registers user-defined SQLite functions."""
406
407 def string_list_add(lst, *args):
408 for arg in args:
409 lst.append(arg)
410 def string_list_to_string(lst):
411 return ', '.join(lst)
412 def string_list_factory():
413 return ([], string_list_add, string_list_to_string)
414 self.db.createaggregatefunction("string_list", string_list_factory)
415
416 def string_set_add(lst, *args):
417 for arg in args:
418 for arch in arg.split(','):
419 lst[arch] = True
420 def string_set_to_archs(lst):
421 l = lst.keys()
422 l.sort()
423 return ','.join(l)
424 def string_set_factory():
425 return ({}, string_set_add, string_set_to_archs)
426 self.db.createaggregatefunction("string_set", string_set_factory)
427
428 urgencies = ['high', 'medium', 'low', 'unimportant']
429 def urgency_to_number(u):
430 try:
431 return urgencies.index(u)
432 except ValueError:
433 return 999
434 self.db.createscalarfunction("urgency_to_number", urgency_to_number, 1)
435
436 releases = ['potato', 'woody', 'sarge', 'etch', 'lenny', 'squeeze', 'sid']
437 def release_to_number(u):
438 try:
439 return releases.index(u)
440 except ValueError:
441 return -1
442 self.db.createscalarfunction("release_to_number", release_to_number, 1)
443
444 def release_name(release, subrelease, archive):
445 if archive <> 'main':
446 release = release + '/' + archive
447 if subrelease:
448 return "%s (%s)" % (release, subrelease)
449 else:
450 return release
451 self.db.createscalarfunction("release_name", release_name, 3)
452
453 self.db.createcollation("version", debian_support.version_compare)
454
455 def filePrint(self, filename):
456 """Returns a fingerprint string for filename."""
457
458 st = os.stat(filename)
459 # The "1" is a version number which can be used to trigger a
460 # re-read if the code has changed in an incompatible way.
461 return `(st.st_size, st.st_ino, st.st_mtime, 1)`
462
463 def _parseFile(self, cursor, filename):
464 current_print = self.filePrint(filename)
465
466 def do_parse(packages):
467 if self.verbose:
468 print " reading " + `filename`
469
470 re_source = re.compile\
471 (r'^([a-zA-Z0-9.+-]+)(?:\s+\(([a-zA-Z0-9.+:~-]+)\))?$')
472
473 data = {}
474 for pkg in packages:
475 pkg_name = None
476 pkg_version = None
477 pkg_arch = None
478 pkg_source = None
479 pkg_source_version = None
480 for (name, contents) in pkg:
481 if name == "Package":
482 pkg_name = contents
483 elif name == "Version":
484 pkg_version = contents
485 elif name == "Source":
486 match = re_source.match(contents)
487 if match is None:
488 raise SyntaxError(('package %s references '
489 + 'invalid source package %s') %
490 (pkg_name, `contents`))
491 (pkg_source, pkg_source_version) = match.groups()
492 elif name == "Architecture":
493 pkg_arch = contents
494 if pkg_name is None:
495 raise SyntaxError\
496 ("package record does not contain package name")
497 if pkg_version is None:
498 raise SyntaxError\
499 ("package record for %s does not contain version"
500 % pkg_name)
501 if pkg_arch is None:
502 raise SyntaxError\
503 ("package record for %s lacks Architecture: field"
504 % pkg_name)
505 if pkg_name in data:
506 oversion = debian_support.Version(data[pkg_name][0])
507 if oversion < debian_support.Version(pkg_version):
508 data[pkg_name] = (pkg_version, pkg_arch,
509 pkg_source, pkg_source_version)
510 else:
511 data[pkg_name] = (pkg_version, pkg_arch,
512 pkg_source, pkg_source_version)
513
514 return data
515
516 def toString(data):
517 result = cStringIO.StringIO()
518 cPickle.dump(data, result)
519 return buffer(result.getvalue())
520
521 for (old_print, contents) in cursor.execute(
522 "SELECT inodeprint, parsed FROM inodeprints WHERE file = ?",
523 (filename,)):
524 if old_print == current_print:
525 return (True, cPickle.load(cStringIO.StringIO(contents)))
526 result = do_parse(debian_support.PackageFile(filename))
527 cursor.execute("""UPDATE inodeprints SET inodeprint = ?, parsed = ?
528 WHERE file = ?""", (current_print, toString(result), filename))
529 return (False, result)
530
531 # No inodeprints entry, load file and add one.
532 result = do_parse(debian_support.PackageFile(filename))
533 cursor.execute("""INSERT INTO inodeprints (file, inodeprint, parsed)
534 VALUES (?, ?, ?)""", (filename, current_print, toString(result)))
535 return (False, result)
536
537 def readPackages(self, cursor, directory):
538 """Reads a directory of package files."""
539
540 if self.verbose:
541 print "readPackages:"
542
543 self._readSourcePackages(cursor, directory)
544 self._readBinaryPackages(cursor, directory)
545
546 if self.verbose:
547 print " finished"
548
549 def _readSourcePackages(self, cursor, directory):
550 """Reads from directory with source package files."""
551
552 re_sources = re.compile(r'.*/([a-z-]+)_([a-z-]*)_([a-z-]+)_Sources$')
553
554 if self.verbose:
555 print " reading source packages"
556
557 for filename in glob.glob(directory + '/*_Sources'):
558 match = re_sources.match(filename)
559 if match is None:
560 raise ValueError, "invalid file name: " + `filename`
561
562 (release, subrelease, archive) = match.groups()
563 (unchanged, parsed) = self._parseFile(cursor, filename)
564 if unchanged:
565 continue
566
567 cursor.execute(
568 """DELETE FROM source_packages
569 WHERE release = ? AND subrelease = ? AND archive = ?""",
570 (release, subrelease, archive))
571 self._clearVersions(cursor)
572
573 def gen():
574 for name in parsed.keys():
575 (version, archs, source, source_version) = parsed[name]
576 assert source is None
577 assert source_version is None
578 yield name, release, subrelease, archive, version
579 cursor.executemany(
580 """INSERT INTO source_packages
581 (name, release, subrelease, archive, version)
582 VALUES (?, ?, ?, ?, ?)""",
583 gen())
584
585 def _readBinaryPackages(self, cursor, directory):
586 """Reads from a directory with binary package files."""
587
588 re_packages \
589 = re.compile(
590 r'.*/([a-z-]+)_([a-z-]*)_([a-z-]+)_([a-z0-9-]+)_Packages$')
591
592 if self.verbose:
593 print " reading binary packages"
594
595 # First check for any changes.
596
597 filenames = glob.glob(directory + '/*_Packages')
598 filenames.sort()
599 changed = False
600 for filename in filenames:
601 changed = True
602 for (old_print,) in cursor.execute(
603 "SELECT inodeprint FROM inodeprints WHERE file = ?",
604 (filename,)):
605 if self.filePrint(filename) == old_print:
606 changed = False
607 if changed:
608 break
609 if not changed:
610 if self.verbose:
611 print " finished (no changes)"
612 return
613
614 # Real import. We have to re-read all Packages files even if
615 # only some of them have changed because the database only
616 # stores aggregated data, and there is no efficient way to
617 # handle updates of the records related to a single file.
618
619 packages = {}
620 unchanged = True
621 for filename in filenames:
622 match = re_packages.match(filename)
623 if match is None:
624 raise ValueError, "invalid file name: " + `filename`
625
626 (release, subrelease, archive, architecture) = match.groups()
627 (unch, parsed) = self._parseFile(cursor, filename)
628 unchanged = unchanged and unch
629 for name in parsed.keys():
630 (version, arch, source, source_version) = parsed[name]
631 if source is None:
632 source = name
633 if source_version is None:
634 source_version = version
635 if arch <> 'all' and arch <> architecture:
636 raise ValueError, ("invalid architecture %s for package %s"
637 % (arch, name))
638 key = (name, release, subrelease, archive, version,
639 source, source_version)
640 if packages.has_key(key):
641 packages[key][arch] = 1
642 else:
643 packages[key] = {arch : 1}
644
645 if unchanged:
646 if self.verbose:
647 print " finished (no changes)"
648 return
649
650 if self.verbose:
651 print " deleting old data"
652 cursor.execute("DELETE FROM binary_packages")
653 self._clearVersions(cursor)
654
655 l = packages.keys()
656
657 if len(l) == 0:
658 raise ValueError, "no binary packages found"
659
660 l.sort()
661 def gen():
662 for key in l:
663 archs = packages[key].keys()
664 archs.sort()
665 archs = ','.join(archs)
666 yield key + (archs,)
667
668 if self.verbose:
669 print " storing binary package data"
670
671 cursor.executemany(
672 """INSERT INTO binary_packages
673 (name, release, subrelease, archive, version,
674 source, source_version, archs)
675 VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
676 gen())
677
678 def readBugs(self, cursor, path):
679 if self.verbose:
680 print "readBugs:"
681
682 def clear_db(cleared=[False]):
683 # Avoid clearing the database multiple times.
684 if cleared[0]:
685 return
686 else:
687 cleared[0] = True
688
689 cursor.execute("DELETE FROM debian_bugs")
690 cursor.execute("DELETE FROM bugs")
691 cursor.execute("DELETE FROM package_notes")
692 cursor.execute("DELETE FROM bugs_notes")
693 cursor.execute("DELETE FROM bugs_xref")
694 cursor.execute("DELETE FROM package_notes_nodsa")
695 cursor.execute("DELETE FROM removed_packages")
696
697 # The *_status tables are regenerated anyway, no need to
698 # delete them here.
699
700 self._clearVersions(cursor)
701
702 def do_parse(source, cleared=[False]):
703 errors = []
704
705 clear_db()
706
707 if self.verbose:
708 print " reading " + `source.name`
709
710 for bug in source:
711 try:
712 bug.writeDB(cursor)
713 except ValueError, e:
714 errors.append("%s: %d: error: %s"
715 % (bug.source_file, bug.source_line, e))
716 if errors:
717 raise InsertError(errors)
718
719 cursor.executemany(
720 "INSERT OR IGNORE INTO removed_packages (name) VALUES (?)",
721 map(lambda x: (x,), source.removed_packages.keys()))
722
723 def has_changed(filename):
724 current_print = self.filePrint(filename)
725 for (old_print,) in cursor.execute(
726 "SELECT inodeprint FROM inodeprints WHERE file = ?",
727 (filename,)):
728 if old_print == current_print:
729 return False
730 else:
731 return True
732 return True
733
734 source_removed_packages = '/packages/removed-packages'
735 sources = ((bugs.CVEFile, '/CVE/list'),
736 (bugs.DSAFile, '/DSA/list'),
737 (bugs.DTSAFile, '/DTSA/list'),
738 (None, source_removed_packages))
739
740 unchanged = True
741 for (_, name) in sources:
742 if has_changed(path + name):
743 unchanged = False
744 break
745 if unchanged:
746 if self.verbose:
747 print " finished (no changes)"
748 return
749
750 clear_db()
751
752 def read_one(source):
753 filename = source.name
754 current_print = self.filePrint(filename)
755
756 do_parse(source)
757 cursor.execute(
758 """INSERT OR REPLACE INTO inodeprints (inodeprint, file)
759 VALUES (?, ?)""", (current_print, filename))
760
761 for (cls, name) in sources:
762 if cls is None:
763 continue
764 read_one(cls(path + name))
765
766 if self.verbose:
767 print " update removed packages"
768 self.readRemovedPackages(cursor, path + source_removed_packages)
769
770 errors = []
771
772 if self.verbose:
773 print " check cross-references"
774
775 for (bug,) in cursor.execute(
776 """SELECT DISTINCT target FROM bugs_xref
777 EXCEPT SELECT name FROM bugs"""):
778 if bug[0:3] == "VU#":
779 continue
780 errors.append("reference to unknown bug " + bug)
781
782 if self.verbose:
783 print " copy notes"
784
785 # Copy notes from DSA/DTSA to CVE.
786
787 old_source = ''
788 for source, target in list(cursor.execute(
789 """SELECT source, target FROM bugs_xref
790 WHERE (source LIKE 'DTSA-%' OR source LIKE 'DSA-%')
791 AND target LIKE 'CVE-%'""")):
792 if source <> old_source:
793 source_bug = bugs.BugFromDB(cursor, source)
794 old_source = source
795 for n in source_bug.notes:
796 # We do not copy recursively.
797 assert not n.bug_origin
798
799 if n.release:
800 rel = str(n.release)
801 else:
802 rel = ''
803 present = False
804
805 for (version, note_id) in list(cursor.execute(
806 """SELECT fixed_version, id
807 FROM package_notes
808 WHERE bug_name = ? AND package = ? AND release = ?""",
809 (target, n.package, rel))):
810 if version is None:
811 # The target is marked as unfixed. Our
812 # version cannot win.
813 present = True
814 continue
815
816 if (n.fixed_version is None
817 or n.fixed_version > debian_support.Version(version)):
818 # If our version is larger, it is the definitive one.
819 # Remove the existing entry in this case.
820 cursor.execute(
821 "DELETE FROM debian_bugs WHERE note = ?",
822 (note_id,))
823 cursor.execute(
824 """DELETE FROM package_notes
825 WHERE bug_name = ? AND package = ?
826 AND release = ?""",
827 (target, n.package, rel))
828 else:
829 present = True
830 if not present:
831 n.writeDB(cursor, target, bug_origin=source)
832
833 if errors:
834 raise InsertError(errors)
835
836 if self.verbose:
837 print " finished"
838
839 def availableReleases(self, cursor=None):
840 """Returns a list of tuples (RELEASE, ARCHIVE,
841 SOURCES-PRESENT, ARCHITECTURE-LIST)."""
842 if cursor is None:
843 cursor = self.cursor()
844
845 releases = {}
846 for r in cursor.execute(
847 """SELECT DISTINCT release, subrelease, archive
848 FROM source_packages"""):
849 releases[r] = (True, [])
850
851 for (rel, subrel, archive, archs) in cursor.execute(
852 """SELECT DISTINCT release, subrelease, archive, archs
853 FROM binary_packages"""):
854 key = (rel, subrel, archive)
855 if not releases.has_key(key):
856 releases[key] = (False, [])
857 releases[key][1][:] = mergeLists(releases[key][1], archs)
858
859 result = []
860 for ((rel, subrel, archive), (sources, archs)) in releases.items():
861 result.append((rel, subrel, archive, sources, archs))
862 result.sort()
863
864 return result
865
866 def getFunnyPackageVersions(self):
867 """Returns a list of (PACKAGE, RELEASE, ARCHIVE, VERSION,
868 SOURCE-VERSION) tuples such that PACKAGE is both a source and
869 binary package, but the associated version numbers are
870 different."""
871
872 return list(self.db.cursor().execute(
873 """SELECT DISTINCT name, release, archive, version, source_version
874 FROM binary_packages
875 WHERE name = source AND version <> source_version
876 ORDER BY name, release, archive"""))
877
878 def _clearVersions(self, cursor):
879 cursor.execute("DELETE FROM version_linear_order")
880
881 def _updateVersions(self, cursor):
882 """Updates the linear version table."""
883
884 if self.verbose:
885 print "updateVersions:"
886
887 for x in cursor.execute("SELECT * FROM version_linear_order LIMIT 1"):
888 if self.verbose:
889 print " finished (no changes)"
890 return
891
892 if self.verbose:
893 print " reading"
894
895 versions = []
896 for (v,) in cursor.execute(
897 """SELECT DISTINCT *
898 FROM (SELECT fixed_version FROM package_notes
899 WHERE fixed_version IS NOT NULL
900 UNION ALL SELECT version FROM source_packages)"""):
901 versions.append(debian_support.Version(v))
902
903 if self.verbose:
904 print " calculating linear order"
905 versions.sort()
906
907 if self.verbose:
908 print " storing linear order"
909 for v in versions:
910 cursor.execute(
911 "INSERT INTO version_linear_order (version) VALUES (?)",
912 (str(v),))
913
914 if self.verbose:
915 print " updating package notes"
916 cursor.execute(
917 """UPDATE package_notes
918 SET fixed_version_id = (SELECT id FROM version_linear_order
919 WHERE version = package_notes.fixed_version)
920 WHERE fixed_version IS NOT NULL""")
921
922 if self.verbose:
923 print " updating source packages"
924 cursor.execute(
925 """UPDATE source_packages
926 SET version_id = (SELECT id FROM version_linear_order
927 WHERE version = source_packages.version)""")
928
929 if self.verbose:
930 print " finished"
931
932 def calculateVulnerabilities(self, cursor):
933 """Calculate vulnerable packages.
934
935 To each package note, a release-specific vulnerability status
936 is attached. Currently, only squeeze/testing is processed.
937
938 Returns a list strings describing inconsistencies.
939 """
940
941 result = []
942
943 self._updateVersions(cursor)
944
945 if self.verbose:
946 print "calculateVulnerabilities:"
947 print " checking version consistency in package notes"
948
949 # The following does not work because stable->security ->
950 # testing -> unstable propagation is no longer available.
951 if False:
952 # Ignore squeeze/testing because stable issues may be
953 # fast-tracked into testing, bypassing unstable.
954 for (bug_name, pkg_name, rel, unstable_ver, rel_ver) \
955 in list(cursor.execute(
956 """SELECT a.bug_name, a.package, b.release,
957 a.fixed_version, b.fixed_version
958 FROM package_notes a, package_notes b
959 WHERE a.bug_name = b.bug_name AND a.package = b.package
960 AND a.release = '' AND b.release NOT IN ('', 'squeeze')
961 AND a.fixed_version IS NOT NULL
962 AND a.fixed_version_id < b.fixed_version_id""")):
963 b = bugs.BugFromDB(cursor, bug_name)
964 result.append("%s:%d: inconsistent versions for package %s"
965 % (b.source_file, b.source_line, pkg_name))
966 result.append("%s:%d: unstable: %s"
967 % (b.source_file, b.source_line, unstable_ver))
968 result.append("%s:%d: release %s: %s"
969 % (b.source_file, b.source_line, `rel`, rel_ver))
970
971 if self.verbose:
972 print " checking source packages"
973 cursor.execute(
974 """UPDATE package_notes SET package_kind = 'unknown'
975 WHERE package_kind IN ('source', 'binary')""")
976 cursor.execute(
977 """UPDATE package_notes SET package_kind = 'source'
978 WHERE package_kind = 'unknown'
979 AND EXISTS (SELECT * FROM source_packages AS p
980 WHERE p.name = package_notes.package)""")
981 cursor.execute(
982 """UPDATE package_notes SET package_kind = 'source'
983 WHERE package_kind = 'unknown'
984 AND EXISTS (SELECT * FROM removed_packages AS p
985 WHERE p.name = package_notes.package)""")
986
987 for (bug_name, package) in list(cursor.execute(
988 """SELECT n.bug_name, n.package
989 FROM package_notes AS n
990 WHERE n.package_kind = 'itp'
991 AND ((EXISTS (SELECT * FROM source_packages
992 WHERE name = n.package))
993 OR (EXISTS (SELECT * FROM binary_packages
994 WHERE name = n.package)))""")):
995 b = bugs.BugFromDB(cursor, bug_name)
996 result.append("%s:%d: ITPed package %s is in the archive"
997 % (b.source_file, b.source_line, package))
998
999 if result:
1000 return result
1001
1002 if self.verbose:
1003 print " remove old status"
1004 cursor.execute("DELETE FROM source_package_status")
1005 cursor.execute("DELETE FROM bug_status")
1006
1007 if self.verbose:
1008 print " calculate package status"
1009 print " source packages (unqualified)"
1010
1011 cursor.execute(
1012 """INSERT INTO source_package_status
1013 SELECT n.bug_name, p.rowid,
1014 CASE WHEN n.fixed_version == 'undetermined' THEN 2
1015 ELSE CASE WHEN n.fixed_version IS NULL THEN 1
1016 ELSE CASE WHEN p.version_id < n.fixed_version_id THEN 1
1017 ELSE 0 END END END,
1018 n.urgency
1019 FROM package_notes AS n, source_packages AS p
1020 WHERE n.release = '' AND p.name = n.package""")
1021
1022 # Release annotations always override previous results,
1023 # therefore we use INSERT OR REPLACE.
1024
1025 if self.verbose:
1026 print " source packages (qualified)"
1027 cursor.execute(
1028 """INSERT OR REPLACE INTO source_package_status
1029 SELECT n.bug_name, p.rowid,
1030 CASE WHEN n.fixed_version == 'undetermined' THEN 2
1031 ELSE CASE WHEN n.fixed_version IS NULL THEN 1
1032 ELSE CASE WHEN p.version_id < n.fixed_version_id THEN 1
1033 ELSE 0 END END END,
1034 n.urgency
1035 FROM package_notes AS n, source_packages AS p
1036 WHERE p.name = n.package
1037 AND p.release = n.release""")
1038
1039 # assign nvd urgencies to those that have not yet been assigned
1040 if self.verbose:
1041 print " insert nvd urgencies"
1042 cursor.execute(
1043 """REPLACE INTO source_package_status
1044 SELECT s.bug_name, s.package, s.vulnerable,
1045 CASE WHEN n.severity == 'Medium' THEN 'medium**'
1046 ELSE CASE WHEN n.severity == 'High' THEN 'high**'
1047 ELSE CASE WHEN n.severity == 'Low' THEN 'low**'
1048 ELSE 'not yet assigned' END END END
1049 FROM nvd_data AS n, source_package_status AS s
1050 WHERE s.bug_name == n.cve_name
1051 AND s.urgency == 'not yet assigned'""")
1052 cursor.execute(
1053 """REPLACE INTO package_notes
1054 SELECT p.id, p.bug_name, p.package, p.fixed_version,
1055 p.fixed_version_id, p.release, p.package_kind,
1056 CASE WHEN n.severity == 'Medium' THEN 'medium'
1057 ELSE CASE WHEN n.severity == 'High' THEN 'high'
1058 ELSE CASE WHEN n.severity == 'Low' THEN 'low'
1059 ELSE 'not yet assigned' END END END,
1060 p.bug_origin
1061 FROM nvd_data AS n, package_notes AS p
1062 WHERE p.bug_name == n.cve_name
1063 AND p.urgency == 'not yet assigned'""")
1064
1065 # Calculate the release-specific bug status.
1066
1067 if self.verbose:
1068 print " calculate release status"
1069
1070 c = self.cursor()
1071
1072 for (bug_name,) in cursor.execute(
1073 "SELECT name FROM bugs WHERE NOT not_for_us"):
1074
1075 self._calcUnstable(c, bug_name)
1076 self._calcTesting(c, bug_name, 'testing', 'squeeze')
1077 self._calcTesting(c, bug_name, 'stable', 'lenny')
1078 self._calcTesting(c, bug_name, 'oldstable', 'etch')
1079
1080 return result
1081
1082 def _calcUnstable(self, cursor, bug_name):
1083 """Update bug_status with bug_name for unstable."""
1084
1085 vulnerable_packages = []
1086 undetermined_packages = []
1087 unimportant_packages = []
1088 have_something = False
1089 for (package, vulnerable, urgency) in cursor.execute(
1090 """SELECT DISTINCT sp.name, st.vulnerable, n.urgency
1091 FROM source_package_status AS st,
1092 source_packages AS sp, package_notes AS n
1093 WHERE st.bug_name = ? AND sp.rowid = st.package
1094 AND sp.release = 'sid'
1095 AND n.bug_name = st.bug_name AND n.package = sp.name
1096 ORDER BY sp.name""",
1097 (bug_name,)):
1098 have_something = True
1099 if vulnerable == 1:
1100 if urgency == 'unimportant':
1101 unimportant_packages.append( package )
1102 else:
1103 vulnerable_packages.append(package)
1104 elif vulnerable == 2:
1105 undetermined_packages.append(package)
1106
1107 if vulnerable_packages or undetermined_packages:
1108 pkgs = ""
1109 status = 'undetermined'
1110 if vulnerable_packages:
1111 status = 'vulnerable'
1112 if len(vulnerable_packages) == 1:
1113 pkgs += "package %s is vulnerable. " % vulnerable_packages[0]
1114 else:
1115 pkgs += ("packages %s are vulnerable. "
1116 % ', '.join(vulnerable_packages))
1117 if undetermined_packages:
1118 if len(undetermined_packages) == 1:
1119 pkgs += "package %s may be vulnerable but needs to be checked." % undetermined_packages[0]
1120 else:
1121 pkgs += ("packages %s may be vulnerable but need to be checked."
1122 % ', '.join(undetermined_packages))
1123 cursor.execute("""INSERT INTO bug_status
1124 (bug_name, release, status, reason)
1125 VALUES (?, 'unstable', ?, ?)""", (bug_name, status, pkgs))
1126 elif unimportant_packages:
1127 if len(unimportant_packages) == 1:
1128 pkgs = "package %s is vulnerable; however, the security impact is unimportant." % unimportant_packages[0]
1129 else:
1130 pkgs = "packages %s are vulnerable; however, the security impact is unimportant." % (', '.join(unimportant_packages))
1131 cursor.execute("""INSERT INTO bug_status
1132 (bug_name, release, status, reason)
1133 VALUES (?, 'unstable', 'fixed', ?)""", (bug_name, pkgs))
1134 else:
1135 if have_something:
1136 status = "not vulnerable."
1137 else:
1138 status = "not known to be vulnerable."
1139 cursor.execute("""INSERT INTO bug_status
1140 (bug_name, release, status, reason)
1141 VALUES (?, 'unstable', 'fixed', ?)""",
1142 (bug_name, status))
1143
1144 def _calcTesting(self, cursor, bug_name, suite, nickname):
1145 """Update bug_status with bug_name for testing/stable."""
1146
1147 # Note that there is at most one source package per
1148 # note/release/subrelease triple, but we should check that
1149 # here.
1150
1151 status = {'' : {}, 'security' : {}}
1152 for (package, note, subrelease, vulnerable, urgency) in cursor.execute(
1153 """SELECT DISTINCT sp.name, n.id, sp.subrelease,
1154 st.vulnerable, n.urgency
1155 FROM source_package_status AS st,
1156 source_packages AS sp, package_notes AS n
1157 WHERE st.bug_name = ? AND sp.rowid = st.package
1158 AND sp.release = ? AND sp.subrelease IN ('', 'security')
1159 AND n.bug_name = st.bug_name AND n.package = sp.name
1160 ORDER BY sp.name""",
1161 (bug_name, nickname)):
1162 status[subrelease][(package, note)] = (vulnerable,urgency)
1163
1164 # Check if any packages in plain testing are vulnerable, and
1165 # if all of those have been fixed in the security archive.
1166 fixed_in_security = True
1167 unfixed_pkgs = {}
1168 undet_pkgs = {}
1169 unimp_pkgs = {}
1170 for ((package, note), (vulnerable, urgency)) in status[''].items():
1171 if vulnerable == 1:
1172 if urgency == 'unimportant':
1173 unimp_pkgs[package] = True
1174 else:
1175 unfixed_pkgs[package] = True
1176 if status['security'].get((package, note), True):
1177 fixed_in_security = False
1178 elif vulnerable == 2:
1179 undet_pkgs[package] = True
1180
1181 unfixed_pkgs = unfixed_pkgs.keys()
1182 unfixed_pkgs.sort()
1183 undet_pkgs = undet_pkgs.keys()
1184 undet_pkgs.sort()
1185 unimp_pkgs = unimp_pkgs.keys()
1186 unimp_pkgs.sort()
1187
1188 pkgs = ""
1189 result = "undetermined"
1190 if len(unfixed_pkgs) == 0 and len(undet_pkgs) == 0:
1191 if len(status[''].keys()) == 0:
1192 pkgs += "not known to be vulnerable."
1193 else:
1194 pkgs += "not vulnerable."
1195 result = "fixed"
1196 if len(unfixed_pkgs) > 0:
1197 if len(unfixed_pkgs) == 1:
1198 pkgs += "package " + unfixed_pkgs[0] + " is "
1199 else:
1200 pkgs += "packages " + ", ".join(unfixed_pkgs) + " are "
1201 if fixed_in_security:
1202 pkgs = "%sfixed in %s-security. " % (pkgs, suite)
1203 if suite == "stable":
1204 result = "fixed"
1205 else:
1206 result = "partially-fixed"
1207 else:
1208 pkgs += "vulnerable. "
1209 result = "vulnerable"
1210 if len(undet_pkgs) > 0:
1211 if len(undet_pkgs) == 1:
1212 pkgs += "package " + undet_pkgs[0] + " may be vulnerable but needs to be checked."
1213 else:
1214 pkgs += "packages " + ", ".join(undet_pkgs) + " may be vulnerable but need to be checked."
1215 if len(unimp_pkgs) > 0 and len(undet_pkgs) == 0 and len(unfixed_pkgs) == 0:
1216 result = "fixed"
1217 if len(unimp_pkgs) == 1:
1218 pkgs = "package %s is vulnerable; however, the security impact is unimportant." % unimp_pkgs[0]
1219 else:
1220 pkgs = "packages %s are vulnerable; however, the security impact is unimportant." % (', '.join(unimp_pkgs))
1221
1222 cursor.execute("""INSERT INTO bug_status
1223 (bug_name, release, status, reason)
1224 VALUES (?, ?, ?, ?)""",
1225 (bug_name, suite, result, pkgs))
1226
1227 def calculateDebsecan0(self, release):
1228 """Create data for the debsecan tool (VERSION 0 format)."""
1229
1230 c = self.cursor()
1231
1232 c.execute("""CREATE TEMPORARY TABLE vulnlist (
1233 name TEXT NOT NULL,
1234 package TEXT NOT NULL,
1235 note INTEGER NOT NULL,
1236 PRIMARY KEY (name, package)
1237 )""")
1238
1239 # Populate the table with the unstable vulnerabilities;
1240 # override them with the release-specific status.
1241
1242 c.execute("""INSERT INTO vulnlist
1243 SELECT bug_name, package, id FROM package_notes WHERE release = ''""")
1244
1245 if release:
1246 c.execute("""INSERT OR REPLACE INTO vulnlist
1247 SELECT bug_name, package, id FROM package_notes
1248 WHERE release = ?""", (release,))
1249 else:
1250 release = 'sid'
1251
1252 c.execute("""DELETE FROM vulnlist WHERE name LIKE 'TEMP-0000000-%'""")
1253
1254 urgency_to_flag = {'low' : 'L', 'medium' : 'M', 'high' : 'H',
1255 'not yet assigned' : ' '}
1256
1257 result = ["VERSION 0\n"]
1258 for (name, package, fixed_version, kind, urgency, remote, description,
1259 note_id) in list(c.execute("""SELECT
1260 vulnlist.name, vulnlist.package,
1261 COALESCE(n.fixed_version, ''),
1262 n.package_kind, n.urgency,
1263 (SELECT range_remote FROM nvd_data
1264 WHERE cve_name = vulnlist.name) AS remote,
1265 bugs.description,
1266 n.id
1267 FROM vulnlist, bugs, package_notes AS n
1268 WHERE bugs.name = vulnlist.name
1269 AND n.id = vulnlist.note
1270 ORDER BY vulnlist.package""")):
1271 if fixed_version == '0' or urgency == 'unimportant' \
1272 or kind not in ('source', 'binary', 'unknown'):
1273 continue
1274
1275 # Normalize FAKE-* names a bit. The line number (which
1276 # makes the name unique) is completely useless for the
1277 # client.
1278
1279 if name[0:5] == 'TEMP-':
1280 name = '-'.join(name.split('-')[0:2])
1281
1282 # Determine if a fix is available for the specific
1283 # release.
1284
1285 fix_available = ' '
1286 if kind == 'source':
1287 fix_available_sql = """SELECT st.vulnerable
1288 FROM source_packages AS p, source_package_status AS st
1289 WHERE p.name = ?
1290 AND p.release = ?
1291 AND p.subrelease IN ('', 'security')
1292 AND st.bug_name = ?
1293 AND st.package = p.rowid
1294 ORDER BY p.version COLLATE version DESC"""
1295 else:
1296 fix_available_sql = ''
1297
1298 if fix_available_sql:
1299 for (v,) in c.execute(fix_available_sql,
1300 (package, release, name)):
1301 assert v is not None
1302 if not v:
1303 fix_available = 'F'
1304 break
1305
1306 if kind == 'source':
1307 kind = 'S'
1308 elif kind == 'binary':
1309 kind = 'B'
1310 else:
1311 kind = ' '
1312
1313 if remote is None:
1314 remote = '?'
1315 elif remote:
1316 remote = 'R'
1317 else:
1318 remote = ' '
1319
1320 result.append("%s,%c%c%c%c,%s,%s,%s\n"
1321 % (name,
1322 kind, urgency_to_flag[urgency], remote,
1323 fix_available,
1324 package, fixed_version, description))
1325 result = base64.encodestring(zlib.compress(''.join(result), 9))
1326
1327 c.execute(
1328 "INSERT OR REPLACE INTO debsecan_data (name, data) VALUES (?, ?)",
1329 ('release/' + release, result))
1330
1331 c.execute("DROP TABLE vulnlist")
1332
1333 def calculateDebsecan1(self):
1334 """Calculates debsecan data (release-independent, VERSION 1)."""
1335
1336 c = self.cursor()
1337
1338 result_start = ['VERSION 1']
1339 bug_to_index = {}
1340 bug_to_remote_flag = {}
1341
1342 def fill_bug_to_index():
1343 index = 0
1344 for (bug, desc, remote) in c.execute(
1345 """SELECT DISTINCT p.bug_name, b.description,
1346 (SELECT range_remote FROM nvd_data
1347 WHERE cve_name = p.bug_name)
1348 FROM package_notes AS p, bugs AS b
1349 WHERE (p.bug_name LIKE 'CVE-%' OR p.bug_name LIKE 'TEMP-%')
1350 AND p.bug_name NOT LIKE 'TEMP-0000000-%'
1351 AND p.urgency <> 'unimportant'
1352 AND COALESCE(p.fixed_version, '') <> '0'
1353 AND p.package_kind IN ('source', 'binary', 'unknown')
1354 AND b.name = p.bug_name
1355 ORDER BY p.bug_name"""):
1356 if remote is None:
1357 remote = '?'
1358 elif remote:
1359 remote = 'R'
1360 else:
1361 remote = ' '
1362
1363 # Normalize FAKE-* names a bit. The line number (which
1364 # makes the name unique) is completely useless for the
1365 # client.
1366
1367 if bug[0:5] == 'TEMP-':
1368 name = '-'.join(bug.split('-')[0:2])
1369 else:
1370 name = bug
1371
1372 result_start.append("%s,,%s" % (name, desc))
1373 bug_to_index[bug] = index
1374 bug_to_remote_flag[bug] = remote
1375 index += 1
1376 result_start.append('')
1377 fill_bug_to_index()
1378
1379 urgency_to_flag = {'low' : 'L', 'medium' : 'M', 'high' : 'H',
1380 'not yet assigned' : ' '}
1381
1382 vuln_list = []
1383 source_packages = {}
1384 def fill_vuln_list(source_packages=source_packages):
1385 for (bug, package) in list(c.execute(
1386 """SELECT DISTINCT bug_name, package
1387 FROM package_notes
1388 WHERE (bug_name LIKE 'CVE-%' OR bug_name LIKE 'TEMP-%')
1389 AND bug_name NOT LIKE 'TEMP-0000000-%'
1390 AND package_kind IN ('source', 'binary', 'unknown')
1391 GROUP BY package, bug_name
1392 ORDER BY package, bug_name""")):
1393
1394 # By default, unstable is unfixed even if there are
1395 # only release-specific annotations available. This
1396 # is slightly at odds with the web front end (see
1397 # data/latently-vulnerable) which does not normally
1398 # report unstable versions as vulnerable in this case.
1399 # However, in our tracking model, the main branch
1400 # (sid) cannot be non-vulnerable, while the
1401 # release-specific branches are.
1402 unstable_fixed = ''
1403
1404 total_urgency = ''
1405 other_versions = {}
1406 is_binary = False
1407 is_unknown = False
1408 fixed_releases = {}
1409 for (release, kind, urgency, version) in list(c.execute(
1410 """SELECT release, package_kind, urgency, fixed_version
1411 FROM package_notes WHERE bug_name = ? AND package = ?""",
1412 (bug, package))):
1413 if not total_urgency:
1414 total_urgency = urgency
1415 elif total_urgency == 'unknown':
1416 if urgency <> 'unimportant':
1417 total_urgency = urgency
1418 elif urgency == 'unknown':
1419 if total_urgency == 'unimportant':
1420 total_urgency = 'unknown'
1421 elif bugs.internUrgency(urgency) \
1422 > bugs.internUrgency(total_urgency):
1423 total_urgency = urgency
1424
1425 if kind == 'binary':
1426 is_binary = True
1427 elif kind == 'source':
1428 source_packages[package] = True
1429 else:
1430 is_unknown = True
1431
1432 if release == '':
1433 unstable_fixed = version
1434 if version:
1435 v_ref = debian_support.Version(version)
1436 for (v,) in c.execute("""SELECT version
1437 FROM source_packages WHERE name = ?
1438 AND release = 'sid' AND subrelease = ''""",
1439 (package,)):
1440 if debian_support.Version(v) >= v_ref:
1441 fixed_releases['sid'] = True
1442 break
1443 elif version is not None:
1444 fixed_releases[release] = True
1445
1446 # Collect newer versions in the same release
1447 # (which are supposed to fix the same bug).
1448
1449 v_ref = debian_support.Version(version)
1450 for (v,) in c.execute("""SELECT fixed_version
1451 FROM package_notes
1452 WHERE package = ? AND release = ?""",
1453 (package, release)):
1454 if v is None:
1455 continue
1456 if debian_support.Version(v) >= v_ref:
1457 other_versions[v] = True
1458
1459 # The second part of this SELECT statement
1460 # covers binary-only NMUs.
1461 for (v,) in c.execute("""SELECT version
1462 FROM source_packages WHERE name = ?1
1463 AND release = ?2 AND subrelease IN ('', 'security')
1464 UNION ALL SELECT source_version
1465 FROM binary_packages WHERE source = ?1
1466 AND release = ?2 AND subrelease IN ('', 'security')""",
1467 (package, release)):
1468 if debian_support.Version(v) >= v_ref:
1469 other_versions[v] = True
1470
1471 if not total_urgency:
1472 total_urgency = 'unknown'
1473
1474 # Check if the issue does not actually mark any
1475 # packages as vulnerable. (If unstable_fixed == '0',
1476 # release-specific annotations cannot create
1477 # vulnerabilities, either.)
1478 if total_urgency == 'unimportant' or unstable_fixed == '0':
1479 continue
1480
1481 if unstable_fixed is None:
1482 unstable_fixed = ''
1483 bs_flag = 'S'
1484 if is_binary:
1485 assert not is_unknown
1486 bs_flag = 'B'
1487 elif is_unknown:
1488 bs_flag = ' '
1489
1490 other_versions = other_versions.keys()
1491 other_versions.sort()
1492 other_versions = ' '.join(other_versions)
1493
1494 vuln_list.append(("%s,%d,%c%c%c"
1495 % (package, bug_to_index[bug],
1496 bs_flag, urgency_to_flag[total_urgency],
1497 bug_to_remote_flag[bug]),
1498 fixed_releases.keys(),
1499 ",%s,%s"
1500 % (unstable_fixed, other_versions)))
1501 fill_vuln_list()
1502 source_packages = source_packages.keys()
1503 source_packages.sort()
1504
1505 def store_value(name, value):
1506 value = base64.encodestring(zlib.compress(value, 9))
1507 c.execute("""INSERT OR REPLACE INTO debsecan_data
1508 VALUES (?, ?)""", (name, value))
1509
1510 def gen_release(release):
1511 result = result_start[:]
1512
1513 for (prefix, releases, suffix) in vuln_list:
1514 if release in releases:
1515 fixed = 'F'
1516 else:
1517 fixed = ' '
1518 result.append(prefix + fixed + suffix)
1519 result.append('')
1520
1521 for sp in source_packages:
1522 bp_list = []
1523 for (bp,) in c.execute("""SELECT name FROM binary_packages
1524 WHERE source = ? AND release = ? AND subrelease = ''
1525 ORDER BY name""",
1526 (sp, release)):
1527 bp_list.append(bp)
1528 if bp_list <> [sp]:
1529 # We intentionally store the empty list, it means
1530 # that the source package is obsolete as a whole.
1531 result.append("%s,%s" % (sp, ' '.join(bp_list)))
1532 result.append('')
1533
1534 store_value('release/1/' + release, '\n'.join(result))
1535
1536 for release in ('sid', 'etch', 'lenny', 'squeeze'):
1537 gen_release(release)
1538
1539 result = result_start
1540 for (prefix, release, suffix) in vuln_list:
1541 result.append(prefix + ' ' + suffix)
1542 result.append('')
1543 result.append('')
1544 result.append('')
1545 store_value ('release/1/GENERIC', '\n'.join(result))
1546
1547 def calculateDebsecan(self):
1548 """Calculate all debsecan data."""
1549 for release in ('', 'etch', 'lenny', 'squeeze'):
1550 self.calculateDebsecan0(release)
1551 self.calculateDebsecan1()
1552
1553 def getDebsecan(self, name):
1554 """Returns the debsecan data item NAME."""
1555 for (data,) in self.cursor().execute(
1556 "SELECT data FROM debsecan_data WHERE name = ?", (name,)):
1557 return base64.decodestring(data)
1558 else:
1559 return None
1560
1561 def updateNVD(self, cursor, data, incremental):
1562 """Adds (and overwrites) NVD data stored in the database. This
1563 can be used for incremental updates if incremental is True."""
1564 if not incremental:
1565 cursor.execute("DELETE FROM nvd_data");
1566 cursor.executemany("INSERT OR REPLACE INTO nvd_data VALUES (?"
1567 + (", ?" * (len(data[0]) - 1))
1568 + ")", data)
1569
1570 def getNVD(self, cursor, cve_name):
1571 """Returns a dictionary with NVD data corresponding to the CVE name,
1572 or None."""
1573 for row in cursor.execute("SELECT * FROM nvd_data WHERE cve_name = ?",
1574 (cve_name,)):
1575 return NVDEntry(row, cursor.getdescription())
1576 return None
1577
1578 def getSourcePackageVersions(self, cursor, pkg):
1579 """A generator which returns tuples (RELEASE-LIST, VERSION),
1580 the available versions of the source package pkg."""
1581
1582 for (releases, version) in cursor.execute(
1583 """SELECT string_list(release) AS releases, version
1584 FROM (SELECT release, version FROM source_packages
1585 WHERE name = ?
1586 ORDER BY release_to_number(release))
1587 GROUP BY version""", (pkg,)):
1588 yield releases.split(', '), version
1589
1590 def getBinaryPackageVersions(self, cursor, pkg):
1591 """A generator which returns tuples (RELEASE-LIST,
1592 SOURCE-PACKAGE, VERSION, ARCH-LIST), the available versions of
1593 the binary package pkg."""
1594
1595 for (releases, source, version, archs) in cursor.execute(
1596 """SELECT string_list(release) AS releases, source, version, archs
1597 FROM (SELECT release, source, version, string_set(archs) AS archs
1598 FROM binary_packages
1599 WHERE name = ?
1600 GROUP BY release, source, version
1601 ORDER BY release_to_number(release))
1602 GROUP BY source, version, archs""", (pkg,)):
1603 yield releases.split(', '), source, version, archs.split(',')
1604
1605 def getBinaryPackagesForSource(self, cursor, pkg):
1606 """A generator which returns tuples (PACKAGES, RELEASE-LIST,
1607 VERSION), the available binary packages built from the source
1608 package pkg."""
1609
1610 for (packages, releases, version, archs) in cursor.execute(
1611 """SELECT string_list(package) AS packages, releases, version,
1612 archs
1613 FROM (SELECT package, string_list(rel) AS releases, version, archs
1614 FROM (SELECT name AS package,
1615 release_name(release, subrelease, archive) AS rel,
1616 version, string_set(archs) AS archs
1617 FROM binary_packages
1618 WHERE source = ?
1619 GROUP BY name, release, subrelease, archive, version
1620 ORDER BY release_to_number(release), subrelease)
1621 GROUP BY package, version, archs
1622 ORDER BY package)
1623 GROUP BY releases, version, archs
1624 ORDER BY version COLLATE version""", (pkg,)):
1625 yield (packages.split(', '), releases.split(', '),
1626 archs.split(','), version)
1627
1628 def getSourcePackages(self, cursor, bug):
1629 """A generator which returns tuples (SOURCE-PACKAGE,
1630 RELEASE-LIST, VERSION, VULNERABLE-FLAG) of source packages
1631 which are related to the given bug."""
1632
1633 for (package, releases, version, vulnerable) in cursor.execute(
1634 """SELECT package, string_list(release), version, vulnerable
1635 FROM (SELECT p.name AS package,
1636 release_name(p.release, p.subrelease, p.archive) AS release,
1637 p.version AS version, s.vulnerable AS vulnerable
1638 FROM source_package_status AS s, source_packages AS p
1639 WHERE s.bug_name = ? AND p.rowid = s.package
1640 ORDER BY release_to_number(p.release), p.subrelease)
1641 GROUP BY package, version, vulnerable
1642 ORDER BY package, version COLLATE version""",
1643 (bug,)):
1644 yield package, releases.split(', '), version, vulnerable
1645
1646 def getBugsFromDebianBug(self, cursor, number):
1647 """A generator which returns a list of tuples
1648 (BUG-NAME, URGENCY, DESCRIPTION)."""
1649
1650 return cursor.execute(
1651 """SELECT DISTINCT bugs.name, package_notes.urgency,
1652 bugs.description
1653 FROM debian_bugs, package_notes, bugs
1654 WHERE debian_bugs.bug = ? AND package_notes.id = debian_bugs.note
1655 AND bugs.name = package_notes.bug_name
1656 ORDER BY bug_name""", (number,))
1657
1658 def isSourcePackage(self, cursor, pkg):
1659 """Returns a true value if pkg is a source package."""
1660 ((flag,),) = cursor.execute(
1661 "SELECT EXISTS (SELECT * FROM source_packages WHERE name = ?)",
1662 (pkg,))
1663 return flag
1664
1665 def isBinaryPackage(self, cursor, pkg):
1666 """Returns a true value if pkg is a binary package."""
1667 ((flag,),) = cursor.execute(
1668 "SELECT EXISTS (SELECT * FROM binary_packages WHERE name = ?)",
1669 (pkg,))
1670 return flag
1671
1672 def getBugsForSourcePackage(self, cursor, pkg, vulnerable):
1673 """Returns a generator for a list of (BUG, DESCRIPTION) pairs
1674 which have the requested status. Only bugs affecting supported
1675 releases are returned."""
1676 return cursor.execute(
1677 """SELECT DISTINCT name, description
1678 FROM (SELECT bugs.name AS name, bugs.description AS description,
1679 MAX(st.vulnerable
1680 AND COALESCE((SELECT st2.vulnerable FROM source_packages AS sp2,
1681 source_package_status AS st2
1682 WHERE sp2.name = sp.name AND sp2.release = sp.release
1683 AND sp2.subrelease = 'security' AND sp2.archive = sp.archive
1684 AND st2.package = sp2.rowid AND st2.bug_name = st.bug_name
1685 ORDER BY st2.vulnerable DESC), 1)) AS vulnerable
1686 FROM source_packages AS sp, source_package_status AS st, bugs
1687 WHERE sp.name = ?
1688 AND sp.release IN ('lenny', 'squeeze', 'sid')
1689 AND sp.subrelease <> 'security'
1690 AND st.package = sp.rowid
1691 AND bugs.name = st.bug_name
1692 AND st.urgency <> 'unimportant'
1693 GROUP BY bugs.name, bugs.description, sp.name)
1694 WHERE vulnerable = ?
1695 ORDER BY name""", (pkg, vulnerable))
1696
1697 def getTODOs(self, cursor=None, hide_check=False):
1698 """Returns a list of pairs (BUG-NAME, DESCRIPTION)."""
1699 if cursor is None:
1700 cursor = self.cursor()
1701 if hide_check:
1702 return cursor.execute(
1703 """SELECT DISTINCT bugs.name, bugs.description
1704 FROM bugs_notes, bugs
1705 WHERE bugs_notes.typ = 'TODO'
1706 AND bugs_notes.comment <> 'check'
1707 AND bugs.name = bugs_notes.bug_name
1708 ORDER BY name """)
1709 else:
1710 return cursor.execute(
1711 """SELECT DISTINCT bugs.name, bugs.description
1712 FROM bugs_notes, bugs
1713 WHERE bugs_notes.typ = 'TODO'
1714 AND bugs.name = bugs_notes.bug_name
1715 ORDER BY name """)
1716
1717 def getBugXrefs(self, cursor, bug):
1718 """Returns a generator for a list of bug names. The listed
1719 bugs refer to the given bug, or the bug refers to them."""
1720
1721 for (bug_name,) in cursor.execute(
1722 """SELECT DISTINCT bug
1723 FROM (SELECT target AS bug
1724 FROM bugs_xref WHERE source = ?
1725 UNION ALL SELECT source AS bug
1726 FROM bugs_xref WHERE target = ?
1727 UNION ALL SELECT bug_origin AS bug FROM package_notes
1728 WHERE bug_name = ? AND bug_origin <> '')
1729 WHERE bug <> ?
1730 ORDER BY bug""", (bug, bug, bug, bug)):
1731 yield bug_name
1732
1733 def readRemovedPackages(self, cursor, filename):
1734 """Reads a file of removed packages and stores it in the database.
1735 The original contents of the removed_packages table is preserved."""
1736
1737 f = file(filename)
1738
1739 re_package = re.compile(r'^\s*([a-z0-9]\S+)\s*$')
1740
1741 # Not very good error reporting, but changes to that file are
1742 # rare.
1743
1744 def gen():
1745 for line in f:
1746 if line == '':
1747 break
1748 if line[0] == '#' or line == '\n':
1749 continue
1750 match = re_package.match(line)
1751 if match:
1752 yield match.groups()
1753 else:
1754 raise ValueError, "not a package: " + `line`
1755
1756 cursor.executemany(
1757 "INSERT OR IGNORE INTO removed_packages (name) VALUES (?)", gen())
1758
1759 def getUnknownPackages(self, cursor):
1760 """Returns a generator for a list of unknown packages.
1761 Each entry has the form (PACKAGE, BUG-LIST)."""
1762
1763 old_package = ''
1764 bugs = []
1765 for (package, bug_name) in cursor.execute(
1766 """SELECT DISTINCT package, bug_name
1767 FROM package_notes WHERE package_kind = 'unknown'
1768 AND NOT EXISTS (SELECT * FROM removed_packages
1769 WHERE name = package)
1770 ORDER BY package, bug_name"""):
1771 if package <> old_package:
1772 if old_package:
1773 yield (old_package, bugs)
1774 old_package = package
1775 bugs = []
1776 bugs.append(bug_name)
1777 if old_package:
1778 yield (old_package, bugs)
1779
1780 def getFakeBugs(self, cursor=None):
1781 """Returns a list of pairs (BUG-NAME, DESCRIPTION)."""
1782
1783 if cursor is None:
1784 cursor = self.cursor()
1785
1786 return list(cursor.execute(
1787 """SELECT name, description FROM bugs
1788 WHERE name > 'TEMP-' AND name LIKE 'TEMP-%'
1789 ORDER BY name"""))
1790
1791 def getITPs(self, cursor):
1792 """Returns a generator for a list of unknown packages.
1793 Each entry has the form (PACKAGE, BUG-LIST, DEBIAN-BUG-LIST)."""
1794
1795 # The "|| ''" is required to convert the string_set argument
1796 # to a string.
1797 for (package, bugs, debian_bugs) in cursor.execute(
1798 """SELECT DISTINCT n.package, string_set(n.bug_name),
1799 string_set(db.bug || '')
1800 FROM package_notes AS n, debian_bugs AS db
1801 WHERE package_kind = 'itp'
1802 AND db.note = n.id
1803 GROUP BY n.package
1804 ORDER BY n.package"""):
1805 yield (package, bugs.split(','), map(int, debian_bugs.split(',')))
1806
1807 def getEffectiveVersion(self, release, pkg, purpose, cache=None, cursor=None):
1808 """Retrieve the effective version of a source package in a release.
1809
1810 The effective version is the version that matches the recommended
1811 sources.list file for the intended purpose. For suitable values
1812 of purpose, see dist_config.
1813 """
1814 # The cache is structured as a (RELEASE, PACKAGE) => VAL
1815 # dict, where VAL is either a dict PURPOSE => VERSION,
1816 # a VERSION, or None.
1817 if cache is not None:
1818 sp = (release, pkg)
1819 if cache.has_key(sp):
1820 d = cache[sp]
1821 if d.__class__ == dict:
1822 return d.get(purpose, None)
1823 else:
1824 return d
1825
1826 if cursor is None:
1827 cursor = self.cursor()
1828
1829 rel = dist_config.releases[release]
1830 purposes = rel['purpose']
1831 results = {}
1832
1833 Version = debian_support.Version
1834 for (part, ver) in cursor.execute(
1835 """SELECT DISTINCT subrelease, version FROM source_packages
1836 WHERE release = ? AND name = ?""", (str(release), pkg)):
1837 ver = Version(ver)
1838 for (purpose, permitted) in purposes.items():
1839 if part not in permitted:
1840 continue
1841 if results.has_key(purpose):
1842 oldver = results[purpose]
1843 if ver <= oldver:
1844 continue
1845 results[purpose] = ver
1846
1847 if cache is not None:
1848 vers = set(map(str, results.values()))
1849 l = len(vers)
1850 if l == 1:
1851 for r in vers:
1852 cache[sp] = Version(r)
1853 elif l == 0:
1854 cache[sp] = None
1855 else:
1856 cache[sp] = results
1857
1858 return results.get(purpose, None)
1859
1860 def check(self, cursor=None):
1861 """Runs a simple consistency check and prints the results."""
1862
1863 if cursor is None:
1864 cursor = self.cursor()
1865
1866 for (package, release, archive, architecture, source) in\
1867 cursor.execute(
1868 """SELECT package, release, archive, architecture, source
1869 FROM binary_packages
1870 WHERE NOT EXISTS
1871 (SELECT *
1872 FROM source_packages AS sp
1873 WHERE sp.package = binary_packages.source
1874 AND sp.release = binary_packages.release
1875 AND sp.archive = binary_packages.archive)
1876 """):
1877 print "error: binary package without source package"
1878 print " binary package:", package
1879 print " release:", release
1880 if archive:
1881 print " archive:", archive
1882 print " architecture:", architecture
1883 print " missing source package:", source
1884
1885 for (package, release, archive, architecture, version,
1886 source, source_version) \
1887 in cursor.execute("""SELECT binary_packages.package,
1888 binary_packages.release, binary_packages.archive,
1889 binary_packages.architecture,binary_packages.version,
1890 sp.package, sp.version
1891 FROM binary_packages, source_packages AS sp
1892 WHERE sp.package = binary_packages.source
1893 AND sp.release = binary_packages.release
1894 AND sp.archive = binary_packages.archive
1895 AND sp.version <> binary_packages.source_version"""):
1896 relation = cmp(debian_support.Version(version),
1897 debian_support.Version(source_version))
1898 assert relation <> 0
1899 if relation <= 0:
1900 print "error: binary package is older than source package"
1901 else:
1902 print "warning: binary package is newer than source package"
1903 print " binary package: %s (%s)" % (package, version)
1904 print " source package: %s (%s)" % (source, source_version)
1905 print " release:", release
1906 if archive:
1907 print " archive:", archive
1908 print " architecture:", architecture
1909
1910 def test():
1911 assert mergeLists('', '') == [], mergeLists('', '')
1912 assert mergeLists('', []) == []
1913 assert mergeLists('a', 'a') == ['a']
1914 assert mergeLists('a', 'b') == ['a', 'b']
1915 assert mergeLists('a,c', 'b') == ['a', 'b', 'c']
1916 assert mergeLists('a,c', ['b', 'de']) == ['a', 'b', 'c', 'de']
1917
1918 import os
1919 db_file = 'test_security.db'
1920 try:
1921 db = DB(db_file)
1922 except SchemaMismatch:
1923 os.unlink(db_file)
1924 db = DB(db_file)
1925
1926 cursor = db.writeTxn()
1927 db.readBugs(cursor, '../../data')
1928 db.commit(cursor)
1929
1930 b = bugs.BugFromDB(cursor, 'CVE-2005-2491')
1931 assert b.name == 'CVE-2005-2491', b.name
1932 assert b.description == 'Integer overflow in pcre_compile.c in Perl Compatible Regular ...', b.description
1933 assert len(b.xref) == 2, b.xref
1934 assert not b.not_for_us
1935 assert 'DSA-800-1' in b.xref, b.xref
1936 assert 'DTSA-10-1' in b.xref, b.xref
1937 assert tuple(b.comments) == (('NOTE', 'gnumeric/goffice includes one as well; according to upstream not exploitable in gnumeric,'),
1938 ('NOTE', 'new copy will be included any way')),\
1939 b.comments
1940
1941 assert len(b.notes) == 4, len(b.notes)
1942
1943 for n in b.notes:
1944 assert n.release is None
1945 if n.package == 'pcre3':
1946 assert n.fixed_version == debian_support.Version('6.3-0.1etch1')
1947 assert tuple(n.bugs) == (324531,), n.bugs
1948 assert n.urgency == bugs.internUrgency('medium')
1949 elif n.package == 'python2.1':
1950 assert n.fixed_version == debian_support.Version('2.1.3dfsg-3')
1951 assert len(n.bugs) == 0, n.bugs
1952 assert n.urgency == bugs.internUrgency('medium')
1953 elif n.package == 'python2.2':
1954 assert n.fixed_version == debian_support.Version('2.2.3dfsg-4')
1955 assert len(n.bugs) == 0, n.bugs
1956 assert n.urgency == bugs.internUrgency('medium')
1957 elif n.package == 'python2.3':
1958 assert n.fixed_version == debian_support.Version('2.3.5-8')
1959 assert len(n.bugs) == 0, n.bugs
1960 assert n.urgency == bugs.internUrgency('medium')
1961 else:
1962 assert False
1963
1964 assert bugs.BugFromDB(cursor, 'DSA-311').isKernelOnly()
1965
1966 if __name__ == "__main__":
1967 test()

  ViewVC Help
Powered by ViewVC 1.1.5