/[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 1994 - (show annotations) (download) (as text)
Thu Sep 15 10:11:44 2005 UTC (7 years, 8 months ago) by fw
File MIME type: text/x-python
File size: 38890 byte(s)
Implement bin/update-db, to update the database with a single command.
Most processing is skipped if no input files have been modified.

lib/python/security_db.py (SchemaMismatch):
  New exception.
(DB):
  Handle schema versioning.
(DB.initSchema):
  Add subrelease column to source_packages and binary_packages.
  Set user_version.
  Remove stray commit.
(DB._parseFile):
  Return information to the caller if the file is unchanged.
(DB.readPackages):
  Move deletion code to callees.
(DB._readSourcePackages, DB._readBinaryPackages):
  Implement incremental updates.  Add subrelease.
  Need to invoke _clearVersions if any changes are made.
(DB.deleteBugs, DB.finishBugs):
  Moved into readBugs.
(DB.insertBugs):
  Rename ...
(DB.readBugs):
  ... to this one.  Implement incremental updates.
  Invoke _clearVersions if necessary.
(DB._clearVersions):
  Add.
(DB._updateVersions):
  Skip processing if _clearVersions has not been invoked.
(DB.getVersion, DB.releaseContainsPackage, DB._synthesizeReleases):
  Obsolete, remove.
(test):
  Update.

lib/python/bugs.py (CANFile, CVEFile):
  Split into two classes, which handle the differences between the two
  files.

bin/check-syntax:
  Update accordingly.

bin/update-db:
  New database update script.  Implements incremental updates.

Makefile:
  Remove references to bin/update-packages.  Simplify drastically.
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 bugs
31 import cPickle
32 import cStringIO
33 import debian_support
34 import glob
35 import os
36 import re
37 import sys
38 import types
39
40 class InsertError(Exception):
41 """Class for capturing insert errors.
42
43 The 'errors' member collects all error messages.
44 """
45
46 def __init__(self, errors):
47 assert len(errors) > 0, errors
48 assert type(errors) == types.ListType, errors
49 assert type(errors[0])== types.StringType, errors
50 self.errors = errors
51
52 def __str__(self):
53 return self.errors[0] + ' [more...]'
54
55 def mergeLists(a, b):
56 """Merges two lists."""
57 if type(a) == types.StringType:
58 if a == "":
59 a = []
60 else:
61 a = a.split(',')
62 if type(b) == types.StringType:
63 if b == "":
64 b = []
65 else:
66 b = b.split(',')
67 result = {}
68 for x in a:
69 result[x] = 1
70 for x in b:
71 result[x] = 1
72 result = result.keys()
73 result.sort()
74 return result
75
76 class SchemaMismatch(Exception):
77 """Raised to indicate a schema mismatch.
78
79 The caller is expected to remove and regenerate the database."""
80
81 class DB:
82 """Access to the security database.
83
84 This is a wrapper around an SQLite database object (which is
85 accessible as the "db" member.
86
87 Most operations need a special cursor object, which can be created
88 with a cursor object. The name "cursor" is somewhat of a
89 misnomer because these objects are quite versatile.
90 """
91
92 def __init__(self, name, verbose=False):
93 self.db = apsw.Connection(name)
94 self.verbose = verbose
95 self.nicknames = {'etch': 'testing',
96 'sarge' : 'stable',
97 'woody': 'oldstable'}
98
99 c = self.cursor()
100 for (v,) in c.execute("PRAGMA user_version"):
101 if v == 0:
102 self.initSchema()
103 if v <> 1:
104 raise SchemaMismatch, `v`
105 return
106 assert False
107
108 def cursor(self):
109 """Creates a new database cursor.
110
111 Also see the writeTxn method."""
112 return self.db.cursor()
113
114 def writeTxn(self):
115 """Creates a cursor for an exclusive transaction.
116
117 No other process may modify the database at the same time.
118 After finishing the work, you should invoke the commit or
119 rollback methods below.
120 """
121 c = self.cursor()
122 c.execute("BEGIN TRANSACTION EXCLUSIVE")
123 return c
124
125 def commit(self, cursor):
126 """Makes the changes in the transaction permanent."""
127 cursor.execute("COMMIT")
128
129 def rollback(self, cursor):
130 """Undos the changes in the transaction."""
131 cursor.execute("ROLLBACK")
132
133 def initSchema(self):
134 """Creates the database schema."""
135 cursor = self.cursor()
136
137 cursor.execute("""CREATE TABLE inodeprints
138 (file TEXT NOT NULL PRIMARY KEY,
139 inodeprint TEXT NOT NULL,
140 parsed BLOB)""")
141
142 cursor.execute(
143 """CREATE TABLE nicknames
144 (realname TEXT NOT NULL,
145 nickname TEXT NOT NULL)""")
146 cursor.executemany("INSERT INTO nicknames VALUES (?, ?)",
147 self.nicknames.items())
148
149 cursor.execute("""CREATE TABLE version_linear_order
150 (id INTEGER NOT NULL PRIMARY KEY,
151 version TEXT NOT NULL UNIQUE)""")
152
153 cursor.execute(
154 """CREATE TABLE source_packages
155 (name TEXT NOT NULL,
156 release TEXT NOT NULL,
157 subrelease TEXT NOT NULL,
158 archive TEXT NOT NULL,
159 version TEXT NOT NULL,
160 version_id INTEGER NOT NULL DEFAULT 0,
161 PRIMARY KEY (name, release, subrelease, archive))""")
162
163 cursor.execute(
164 """CREATE TABLE binary_packages
165 (name TEXT NOT NULL,
166 release TEXT NOT NULL,
167 subrelease TEXT NOT NULL,
168 archive TEXT NOT NULL,
169 version TEXT NOT NULL,
170 source TEXT NOT NULL,
171 source_version TEXT NOT NULL,
172 archs TEXT NOT NULL,
173 version_id INTEGER NOT NULL DEFAULT 0,
174 PRIMARY KEY (name, release, subrelease, archive, version, source,
175 source_version))""")
176 cursor.execute(
177 """CREATE INDEX binary_packages_source
178 ON binary_packages(source)""")
179
180 cursor.execute("""CREATE TABLE package_notes
181 (id INTEGER NOT NULL PRIMARY KEY,
182 bug_name TEXT NOT NULL,
183 package TEXT NOT NULL,
184 fixed_version TEXT
185 CHECK (fixed_version IS NULL OR fixed_version <> ''),
186 fixed_version_id INTEGER NOT NULL DEFAULT 0,
187 release TEXT NOT NULL,
188 urgency TEXT NOT NULL)""")
189 cursor.execute(
190 "CREATE INDEX package_notes_bug ON package_notes(bug_name)")
191
192 cursor.execute("""CREATE TABLE debian_bugs
193 (bug INTEGER NOT NULL,
194 note INTEGER NOT NULL,
195 PRIMARY KEY (bug, note))""")
196
197 cursor.execute("""CREATE TABLE bugs
198 (name TEXT NOT NULL PRIMARY KEY,
199 cve_status TEXT NOT NULL
200 CHECK (cve_status IN
201 ('', 'CANDIDATE', 'ASSIGNED', 'RESERVED', 'REJECTED')),
202 not_for_us INTEGER NOT NULL CHECK (not_for_us IN (0, 1)),
203 description TEXT NOT NULL,
204 source_file TEXT NOT NULL,
205 source_line INTEGER NOT NULL)""")
206
207 cursor.execute("""CREATE TABLE bugs_notes
208 (bug_name TEXT NOT NULL CHECK (typ <> ''),
209 typ TEXT NOT NULL CHECK (typ IN ('TODO', 'NOTE')),
210 release TEXT NOT NULL DEFAULT '',
211 comment TEXT NOT NULL CHECK (comment <> ''))""")
212
213 cursor.execute("""CREATE TABLE bugs_xref
214 (source TEXT NOT NULL,
215 target TEXT NOT NULL,
216 normalized_target TEXT NOT NULL DEFAULT '',
217 PRIMARY KEY (source, target))""")
218
219 cursor.execute("""CREATE TABLE bugs_status
220 (bug_name TEXT NOT NULL,
221 release TEXT NOT NULL,
222 note INTEGER NOT NULL,
223 reason TEXT NOT NULL,
224 PRIMARY KEY (bug_name, release, note))""")
225
226 cursor.execute("""CREATE TABLE source_package_status
227 (note INTEGER NOT NULL,
228 package INTEGER NOT NULL,
229 vulnerable INTEGER NOT NULL,
230 PRIMARY KEY (note, package))""")
231 cursor.execute(
232 """CREATE INDEX source_package_status_package
233 ON source_package_status(package)""")
234
235 cursor.execute("""CREATE TABLE binary_package_status
236 (note INTEGER NOT NULL,
237 package INTEGER NOT NULL,
238 vulnerable INTEGER NOT NULL,
239 PRIMARY KEY (note, package))""")
240 cursor.execute(
241 """CREATE INDEX binary_package_status_package
242 ON binary_package_status(package)""")
243
244 # Put this at the end. Any exception will leave the schema
245 # version at 0, so we automatically recreate the schema once
246 # the application is started after the underlying error has
247 # been fixed.
248
249 cursor.execute("PRAGMA user_version = 1")
250
251 def filePrint(self, filename):
252 """Returns a fingerprint string for filename."""
253
254 st = os.stat(filename)
255 # The "1" is a version number which can be used to trigger a
256 # re-read if the code has changed in an incompatible way.
257 return `(st.st_size, st.st_ino, st.st_mtime, 1)`
258
259 def _parseFile(self, cursor, filename):
260 current_print = self.filePrint(filename)
261
262 def do_parse(packages):
263 if self.verbose:
264 print " parseFile: reading " + `filename`
265
266 re_source = re.compile\
267 (r'^([a-zA-Z0-9.+-]+)(?:\s+\(([a-zA-Z0-9.+:-]+)\))?$')
268
269 data = []
270 for pkg in packages:
271 pkg_name = None
272 pkg_version = None
273 pkg_source = None
274 pkg_source_version = None
275 for (name, contents) in pkg:
276 if name == "Package":
277 pkg_name = contents
278 elif name == "Version":
279 pkg_version = contents
280 elif name == "Source":
281 match = re_source.match(contents)
282 if match is None:
283 raise SyntaxError(('package %s references '
284 + 'invalid source package %s') %
285 (pkg_name, `contents`))
286 (pkg_source, pkg_source_version) = match.groups()
287 if pkg_name is None:
288 raise SyntaxError\
289 ("package record does not contain package name")
290 if pkg_version is None:
291 raise SyntaxError\
292 ("package record for %s does not contain version"
293 % pkg_name)
294 data.append((pkg_name, pkg_version,
295 pkg_source, pkg_source_version))
296
297 return data
298
299 def toString(data):
300 result = cStringIO.StringIO()
301 cPickle.dump(data, result)
302 return result.getvalue()
303
304 for (old_print, contents) in cursor.execute(
305 "SELECT inodeprint, parsed FROM inodeprints WHERE file = ?",
306 (filename,)):
307 if old_print == current_print:
308 return (True, cPickle.load(cStringIO.StringIO(contents)))
309 result = do_parse(debian_support.PackageFile(filename))
310 cursor.execute("""UPDATE inodeprints SET inodeprint = ?, parsed = ?
311 WHERE file = ?""", (current_print, toString(result), filename))
312 return (False, result)
313
314 # No inodeprints entry, load file and add one.
315 result = do_parse(debian_support.PackageFile(filename))
316 cursor.execute("""INSERT INTO inodeprints (file, inodeprint, parsed)
317 VALUES (?, ?, ?)""", (filename, current_print, toString(result)))
318 return (False, result)
319
320 def readPackages(self, cursor, directory):
321 """Reads a directory of package files."""
322
323 if self.verbose:
324 print "readPackages:"
325
326 self._readSourcePackages(cursor, directory)
327 self._readBinaryPackages(cursor, directory)
328
329 if self.verbose:
330 print " finished"
331
332 def _readSourcePackages(self, cursor, directory):
333 """Reads from directory with source package files."""
334
335 re_sources = re.compile(r'.*/([a-z-]+)_([a-z-]*)_([a-z-]+)_Sources$')
336
337
338 if self.verbose:
339 print " reading source packages"
340
341 for filename in glob.glob(directory + '/*_Sources'):
342 match = re_sources.match(filename)
343 if match is None:
344 raise ValueError, "invalid file name: " + `filename`
345
346 (release, subrelease, archive) = match.groups()
347 (unchanged, parsed) = self._parseFile(cursor, filename)
348 if unchanged:
349 continue
350
351 cursor.execute(
352 """DELETE FROM source_packages
353 WHERE release = ? AND subrelease = ? AND archive = ?""",
354 (release, subrelease, archive))
355 self._clearVersions(cursor)
356
357 def gen():
358 for (name, version, source, source_version) in parsed:
359 assert source is None
360 assert source_version is None
361 yield name, release, subrelease, archive, version
362 cursor.executemany(
363 """INSERT INTO source_packages
364 (name, release, subrelease, archive, version)
365 VALUES (?, ?, ?, ?, ?)""",
366 gen())
367
368 def _readBinaryPackages(self, cursor, directory):
369 """Reads from a directory with binary package files."""
370
371 re_packages \
372 = re.compile(
373 r'.*/([a-z-]+)_([a-z-]*)_([a-z-]+)_([a-z0-9]+)_Packages$')
374
375 if self.verbose:
376 print " reading binary packages"
377
378 packages = {}
379 unchanged = True
380 for filename in glob.glob(directory + '/*_Packages'):
381 match = re_packages.match(filename)
382 if match is None:
383 raise ValueError, "invalid file name: " + `filename`
384
385 (release, subrelease, archive, architecture) = match.groups()
386 (unch, parsed) = self._parseFile(cursor, filename)
387 unchanged = unchanged and unch
388 for (name, version, source, source_version) in parsed:
389 if source is None:
390 source = name
391 if source_version is None:
392 source_version = version
393
394 key = (name, release, subrelease, archive, version,
395 source, source_version)
396 if packages.has_key(key):
397 packages[key][architecture] = 1
398 else:
399 packages[key] = {architecture : 1}
400
401 if unchanged:
402 if self.verbose:
403 print " finished (no changes)"
404 return
405
406 if self.verbose:
407 print " deleting old data"
408 cursor.execute("DELETE FROM binary_packages")
409 self._clearVersions(cursor)
410
411 l = packages.keys()
412
413 if len(l) == 0:
414 raise ValueError, "no binary packages found"
415
416 l.sort()
417 def gen():
418 for key in l:
419 archs = packages[key].keys()
420 archs.sort()
421 archs = ','.join(archs)
422 yield key + (archs,)
423
424 if self.verbose:
425 print " storing binary package data"
426
427 cursor.executemany(
428 """INSERT INTO binary_packages
429 (name, release, subrelease, archive, version,
430 source, source_version, archs)
431 VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
432 gen())
433
434 def readBugs(self, cursor, path):
435 if self.verbose:
436 print "readBugs:"
437
438 def clear_db(filename):
439 cursor.execute(
440 """CREATE TEMPORARY TABLE bugs_to_delete
441 (tbd TEXT NOT NULL PRIMARY KEY)""")
442 cursor.execute(
443 """INSERT INTO bugs_to_delete
444 SELECT name FROM bugs WHERE source_file = ?""",
445 (filename,))
446
447 cursor.execute(
448 """DELETE FROM debian_bugs
449 WHERE EXISTS (SELECT 1
450 FROM package_notes AS p, bugs_to_delete AS b
451 WHERE p.id = debian_bugs.note
452 AND p.bug_name = b.tbd)""")
453
454 cursor.execute("""DELETE FROM bugs
455 WHERE EXISTS (SELECT * FROM bugs_to_delete
456 WHERE tbd = name)""")
457 cursor.execute("""DELETE FROM package_notes
458 WHERE EXISTS (SELECT * FROM bugs_to_delete
459 WHERE tbd = bug_name)""")
460 cursor.execute("""DELETE FROM bugs_notes
461 WHERE EXISTS (SELECT * FROM bugs_to_delete
462 WHERE tbd = bug_name)""")
463 cursor.execute("""DELETE FROM bugs_xref
464 WHERE EXISTS (SELECT * FROM bugs_to_delete
465 WHERE tbd = source)""")
466
467 # The *_status tables are regenerated anyway, no need to
468 # delete them here.
469
470 cursor.execute("""DROP TABLE bugs_to_delete""")
471
472 self._clearVersions(cursor)
473
474 def do_parse(source):
475 errors = []
476
477 if self.verbose:
478 print " reading " + `source.name`
479
480 clear_db(source.name)
481
482 for bug in source:
483 try:
484 bug.writeDB(cursor)
485 except ValueError, e:
486 errors.append("%s: %d: error: %s"
487 % (bug.source_file, bug.source_line, e))
488 if errors:
489 raise InsertError(errors)
490
491 def read_one(source):
492 filename = source.name
493 current_print = self.filePrint(filename)
494
495 for (old_print,) in cursor.execute(
496 "SELECT inodeprint FROM inodeprints WHERE file = ?",
497 (filename,)):
498 if old_print == current_print:
499 return False
500 do_parse(source)
501 cursor.execute(
502 "UPDATE inodeprints SET inodeprint = ? WHERE file = ?",
503 (current_print, filename))
504 return True
505
506 # No inodeprints entry, load file and add one.
507 do_parse(source)
508 cursor.execute(
509 "INSERT INTO inodeprints (file, inodeprint) VALUES (?, ?)",
510 (filename, current_print))
511 return True
512
513 unchanged = True
514 if read_one(bugs.CANFile(path + '/CAN/list')):
515 unchanged = False
516 if read_one(bugs.CVEFile(path + '/CVE/list')):
517 unchanged = False
518 if read_one(bugs.DSAFile(path + '/DSA/list')):
519 unchanged = False
520 if read_one(bugs.DTSAFile(path + '/DTSA/list')):
521 unchanged = False
522
523 if unchanged:
524 if self.verbose:
525 print " finished (no changes)"
526 return
527
528 errors = []
529
530 if self.verbose:
531 print " checking CAN/CVE collisions"
532
533 for b1, b2 in list(cursor.execute\
534 ("""SELECT b1.name, b2.name FROM bugs AS b1, bugs AS b2
535 WHERE b1.name LIKE 'CVE-%'
536 AND b2.name = 'CAN-' || substr(b1.name, 5, 9)""")):
537 b1 = bugs.BugFromDB(cursor, b1)
538 b2 = bugs.BugFromDB(cursor, b2)
539
540 errors.append("%s:%d: duplicate CVE entries %s and %s"
541 % (b1.source_file, b1.source_line,
542 b1.name, b2.name))
543 errors.append("%s:%d: location of %s"
544 % (b1.source_file, b1.source_line, b1.name))
545 errors.append("%s:%d: location of %s"
546 % (b2.source_file, b2.source_line, b2.name))
547
548 # Normalize the CAN/CVE references to the entry which is
549 # actually in the database. After the CAN -> CVE transition,
550 # this can go away (but we should check that the
551 # cross-references are valid).
552
553 if self.verbose:
554 print " normalize CAN/CVE references"
555
556 for source, target in list(cursor.execute\
557 ("""SELECT source, target FROM bugs_xref
558 WHERE normalized_target = ''""")):
559 if bugs.BugBase.re_cve_name.match(target):
560 can_target = 'CAN-' + target[4:]
561 cve_target = 'CVE-' + target[4:]
562
563 found = False
564 for (t,) in list(cursor.execute("""SELECT name FROM bugs
565 WHERE name IN (?, ?)""", (can_target, cve_target))):
566 cursor.execute("""UPDATE bugs_xref
567 SET normalized_target = ?
568 WHERE source = ? AND target = ?""",
569 (t, source, target))
570 found = True
571 break
572 if not found:
573 b = bugs.BugFromDB(cursor, source)
574 errors.append\
575 ("%s: %d: reference to unknwown CVE entry %s"
576 % (b.source_file, b.source_line, target))
577
578 if self.verbose:
579 print " check DSA/DTSA references"
580
581 for source, target in list(cursor.execute
582 ("""SELECT source, target FROM bugs_xref
583 WHERE target LIKE 'DSA%' OR target LIKE 'DTSA%'""")):
584 found = False
585 for (b,) in cursor.execute("SELECT name FROM bugs WHERE name = ?",
586 (target,)):
587 found = True
588 if not found:
589 b = bugs.BugFromDB(cursor, source)
590 errors.append\
591 ("%s: %d: reference to unknwown advisory %s"
592 % (b.source_file, b.source_line, target))
593
594 if errors:
595 raise InsertErrors(errors)
596
597 if self.verbose:
598 print " finished"
599
600 def availableReleases(self, cursor=None):
601 """Returns a list of tuples (RELEASE, ARCHIVE,
602 SOURCES-PRESENT, ARCHITECTURE-LIST)."""
603 if cursor is None:
604 cursor = self.cursor()
605
606 releases = {}
607 for r in cursor.execute(
608 """SELECT DISTINCT release, subrelease, archive
609 FROM source_packages"""):
610 releases[r] = (True, [])
611
612 for (rel, subrel, archive, archs) in cursor.execute(
613 """SELECT DISTINCT release, subrelease, archive, archs
614 FROM binary_packages"""):
615 key = (rel, subrel, archive)
616 if not releases.has_key(key):
617 releases[key] = (False, [])
618 releases[key][1][:] = mergeLists(releases[key][1], archs)
619
620 result = []
621 for ((rel, subrel, archive), (sources, archs)) in releases.items():
622 result.append((rel, subrel, archive, sources, archs))
623 result.sort()
624
625 return result
626
627 def getFunnyPackageVersions(self):
628 """Returns a list of (PACKAGE, RELEASE, ARCHIVE, VERSION,
629 SOURCE-VERSION) tuples such that PACKAGE is both a source and
630 binary package, but the associated version numbers are
631 different."""
632
633 return list(self.db.cursor().execute(
634 """SELECT DISTINCT name, release, archive, version, source_version
635 FROM binary_packages
636 WHERE name = source AND version <> source_version
637 ORDER BY name, release, archive"""))
638
639 def _clearVersions(self, cursor):
640 cursor.execute("DELETE FROM version_linear_order")
641
642 def _updateVersions(self, cursor):
643 """Updates the linear version table."""
644
645 if self.verbose:
646 print "updateVersions:"
647
648 for x in cursor.execute("SELECT * FROM version_linear_order LIMIT 1"):
649 if self.verbose:
650 print " finished (no changes)"
651 return
652
653 if self.verbose:
654 print " reading"
655
656 versions = []
657 for (v,) in cursor.execute(
658 """SELECT DISTINCT *
659 FROM (SELECT fixed_version FROM package_notes
660 WHERE fixed_version IS NOT NULL
661 UNION ALL SELECT version FROM source_packages
662 UNION ALL SELECT version FROM binary_packages)"""):
663 versions.append(debian_support.Version(v))
664
665 if self.verbose:
666 print " calculating linear order"
667 versions.sort()
668
669 if self.verbose:
670 print " storing linear order"
671 for v in versions:
672 cursor.execute(
673 "INSERT INTO version_linear_order (version) VALUES (?)",
674 (str(v),))
675
676 if self.verbose:
677 print " updating package notes"
678 cursor.execute(
679 """UPDATE package_notes
680 SET fixed_version_id = (SELECT id FROM version_linear_order
681 WHERE version = package_notes.fixed_version)
682 WHERE fixed_version IS NOT NULL""")
683
684 if self.verbose:
685 print " updating source packages"
686 cursor.execute(
687 """UPDATE source_packages
688 SET version_id = (SELECT id FROM version_linear_order
689 WHERE version = source_packages.version)""")
690
691 if self.verbose:
692 print " updating binary packages"
693 cursor.execute(
694 """UPDATE binary_packages
695 SET version_id = (SELECT id FROM version_linear_order
696 WHERE version = binary_packages.version)""")
697
698 if self.verbose:
699 print " finished"
700
701 def calculateVulnerabilities(self, cursor):
702 """Calculate vulnerable packages.
703
704 To each package note, a release-specific vulnerability status
705 is attached. Currently, only etch/testing is processed.
706
707 Returns a list strings describing inconsistencies.
708 """
709
710 result = []
711
712 self._updateVersions(cursor)
713 # self._synthesizeReleases(cursor)
714
715 if self.verbose:
716 print "calculateVulnerabilities:"
717 print " check for version consistency in package notes"
718 for (bug_name, pkg_name, rel, unstable_ver, rel_ver) \
719 in list(cursor.execute(
720 """SELECT a.bug_name, a.package, a.release,
721 a.fixed_version, b.fixed_version
722 FROM package_notes a, package_notes b
723 WHERE a.bug_name = b.bug_name AND a.package = b.package
724 AND a.release = '' AND b.release <> ''
725 AND a.fixed_version_id < b.fixed_version_id""")):
726 b = bugs.BugFromDB(cursor, bug_name)
727 result.append("%s:%d: inconsistent versions for package %s"
728 % (b.source_file, b.source_line, pkg_name))
729 result.append("%s:%d: unstable: %s"
730 % (b.source_file, b.source_line, rel_ver))
731 result.append("%s:%d: release %s: %s"
732 % (b.source_file, b.source_line, `rel`, rel_ver))
733
734 if self.verbose:
735 print " create temporary tables"
736 cursor.execute(
737 """CREATE TEMPORARY TABLE tmp_bug_releases
738 (bug_name TEXT NOT NULL,
739 release TEXT NOT NULL,
740 PRIMARY KEY (bug_name, release))""")
741 for (bug_name, release) in list(cursor.execute(
742 """SELECT DISTINCT bug_name, release FROM package_notes
743 WHERE release <> ''""")):
744 data = [(bug_name, release),
745 (bug_name, release + '-security')]
746 try:
747 data.append((bug_name, self.nicknames[release]))
748 except KeyError:
749 pass
750 cursor.executemany("INSERT INTO tmp_bug_releases VALUES (?, ?)",
751 data)
752
753 if self.verbose:
754 print " remove old status"
755 cursor.execute("DELETE FROM source_package_status")
756 cursor.execute("DELETE FROM binary_package_status")
757 if self.verbose:
758 print " calculate package status"
759 print " source packages (unqualified)"
760
761 # If there is a single package note qualified with a specific
762 # release, ignore all the unqualified annotations (even for
763 # other packages). This is implemented by looking at the
764 # tmp_bug_releases table.
765
766 cursor.execute(
767 """INSERT INTO source_package_status
768 SELECT n.id, p.rowid,
769 n.fixed_version IS NULL OR p.version_id < n.fixed_version_id
770 FROM package_notes AS n, source_packages AS p
771 WHERE n.release = '' AND p.name = n.package
772 AND NOT EXISTS (SELECT * FROM tmp_bug_releases AS t
773 WHERE t.bug_name = n.bug_name
774 AND t.release = p.release)""")
775 if self.verbose:
776 print " source packages (qualified)"
777 cursor.execute(
778 """INSERT INTO source_package_status
779 SELECT n.id, p.rowid,
780 n.fixed_version IS NULL OR p.version_id < n.fixed_version_id
781 FROM package_notes AS n, source_packages AS p
782 WHERE p.name = n.package
783 AND (p.release = n.release
784 OR p.release = (n.release || '-security')
785 OR p.release = (SELECT nickname FROM nicknames
786 WHERE realname = n.release))""")
787
788 # Same story for binary packages. We prefer source packages,
789 # so we skip all notes which have already source packages
790 # attached.
791
792 if self.verbose:
793 print " binary packages (unqualified)"
794 cursor.execute(
795 """INSERT INTO binary_package_status
796 SELECT n.id, p.rowid,
797 n.fixed_version IS NULL OR p.version_id < n.fixed_version_id
798 FROM package_notes AS n, binary_packages AS p
799 WHERE n.release = '' AND p.name = n.package
800 AND (NOT EXISTS (SELECT * FROM tmp_bug_releases AS t
801 WHERE t.bug_name = n.bug_name
802 AND t.release = p.release))
803 AND (NOT EXISTS (SELECT * FROM source_package_status AS s
804 WHERE s.package = p.rowid))""")
805
806 if self.verbose:
807 print " binary packages (qualified)"
808 cursor.execute(
809 """INSERT INTO binary_package_status
810 SELECT n.id, p.rowid,
811 n.fixed_version IS NULL OR p.version_id < n.fixed_version_id
812 FROM package_notes AS n, binary_packages AS p
813 WHERE p.name = n.package
814 AND (p.release = n.release
815 OR p.release = n.release || '-security'
816 OR p.release = (SELECT nickname FROM nicknames
817 WHERE realname = n.release))
818 AND (NOT EXISTS (SELECT * FROM source_package_status AS s
819 WHERE s.package = p.rowid))""")
820
821 return
822
823
824 if self.verbose:
825 print " clearing old data"
826 cursor.execute("DELETE FROM bugs_status")
827
828 def markVulnerable(bug, release, note, reason):
829 cursor.execute("""INSERT INTO bugs_status
830 (bug_name, release, note, reason) VALUES (?, ?, ?, ?)""",
831 (bug.name, release, note, reason))
832
833 def calcVuln(bug):
834 vulnerable = False
835 note_found = False
836
837 for n in bug.notes:
838 # ignore all notes conditioned on releases.
839 if n.release is not None: # assumes 'etch'
840 continue
841 note_found = True
842 v = self.getVersion(cursor, 'etch', n.package)
843 if v is None:
844 # Package is not in testing, go on.
845 continue
846 if n.affects(v):
847 vulnerable = True
848 markVulnerable(b, 'etch', n.id,
849 "%s (%s) is vulnerable, %s"
850 % (n.package, v, n.fixedVersion()))
851
852 if bug.hasTODO():
853 vulnerable = True
854 markVulnerable(b, 'etch', 0, 'TODO items present')
855 elif not note_found:
856 # We found no matching note. Maybe all packages have
857 # been removed?
858 if bug.notes:
859 for n in bug.notes:
860 if self.releaseContainsPackage \
861 (cursor, 'etch', n.package):
862 markVulnerable(b, 'etch', 0,
863 'applicable package note for %s missing'
864 % n.package)
865 vulnerable = True
866 else:
867 vulnerable = True
868 markVulnerable(b, 'etch', 0, 'status is unclear')
869
870 return vulnerable
871
872 # First handle the DSAs. Cache results in DSA_status (used
873 # for CAN/CVE below).
874
875 if self.verbose:
876 print " reading DSAs"
877 bug_names = list(cursor.execute(
878 """SELECT name FROM bugs
879 WHERE name LIKE 'DSA-%' AND NOT not_for_us"""))
880 DSA_status = {}
881 if self.verbose:
882 print " rating DSAs"
883 for (bug_name,) in bug_names:
884 b = bugs.BugFromDB(cursor, bug_name)
885 DSA_status[bug_name] = calcVuln(b)
886
887 # Process the CAN/CVE/FAKE entries. If an entry has no
888 # package annotations, but it references a non-vulnerable DSA,
889 # we assume that the current is not affect either.
890
891 if self.verbose:
892 print " reading other entries"
893 bug_names = list(cursor.execute(
894 """SELECT name FROM bugs
895 WHERE (NOT not_for_us)
896 AND NOT (name LIKE 'DSA-%' OR name LIKE 'DTSA-%')"""))
897 if self.verbose:
898 print " rating other entries"
899 for (bug_name,) in bug_names:
900 b = bugs.BugFromDB(cursor, bug_name)
901 if b.notes:
902 calcVuln(b)
903 continue
904
905 if b.hasTODO():
906 markVulnerable(b, 'etch', 0, 'TODO items present')
907 continue
908
909 dsa_found = False
910 for x in b.xref:
911 if x[0:4] == 'DSA-':
912 dsa_found = True
913 if DSA_status[x]:
914 markVulnerable(b, 'etch', 0,
915 'vulnerability %s referenced' % x)
916 break
917 if not dsa_found:
918 markVulnerable(b, 'etch', 0, 'status is unclear')
919
920 if self.verbose:
921 print " finished"
922
923 return result
924
925 def check(self, cursor=None):
926 """Runs a simple consistency check and prints the results."""
927
928 if cursor is None:
929 cursor = self.cursor()
930
931 for (package, release, archive, architecture, source) in\
932 cursor.execute(
933 """SELECT package, release, archive, architecture, source
934 FROM binary_packages
935 WHERE NOT EXISTS
936 (SELECT *
937 FROM source_packages AS sp
938 WHERE sp.package = binary_packages.source
939 AND sp.release = binary_packages.release
940 AND sp.archive = binary_packages.archive)
941 """):
942 print "error: binary package without source package"
943 print " binary package:", package
944 print " release:", release
945 if archive:
946 print " archive:", archive
947 print " architecture:", architecture
948 print " missing source package:", source
949
950 for (package, release, archive, architecture, version,
951 source, source_version) \
952 in cursor.execute("""SELECT binary_packages.package,
953 binary_packages.release, binary_packages.archive,
954 binary_packages.architecture,binary_packages.version,
955 sp.package, sp.version
956 FROM binary_packages, source_packages AS sp
957 WHERE sp.package = binary_packages.source
958 AND sp.release = binary_packages.release
959 AND sp.archive = binary_packages.archive
960 AND sp.version <> binary_packages.source_version"""):
961 relation = cmp(debian_support.Version(version),
962 debian_support.Version(source_version))
963 assert relation <> 0
964 if relation <= 0:
965 print "error: binary package is older than source package"
966 else:
967 print "warning: binary package is newer than source package"
968 print " binary package: %s (%s)" % (package, version)
969 print " source package: %s (%s)" % (source, source_version)
970 print " release:", release
971 if archive:
972 print " archive:", archive
973 print " architecture:", architecture
974
975 def test():
976 assert mergeLists('', '') == [], mergeLists('', '')
977 assert mergeLists('', []) == []
978 assert mergeLists('a', 'a') == ['a']
979 assert mergeLists('a', 'b') == ['a', 'b']
980 assert mergeLists('a,c', 'b') == ['a', 'b', 'c']
981 assert mergeLists('a,c', ['b', 'de']) == ['a', 'b', 'c', 'de']
982
983 import os
984 db_file = 'test_security.db'
985 try:
986 db = DB(db_file)
987 except SchemaMismatch:
988 os.unlink(db_file)
989 db = DB(db_file)
990
991 cursor = db.writeTxn()
992 db.readBugs(cursor, '../../data')
993 db.commit(cursor)
994
995 b = bugs.BugFromDB(cursor, 'CAN-2005-2491')
996 assert b.name == 'CAN-2005-2491', b.name
997 assert b.description == 'Integer overflow in pcre_compile.c in Perl Compatible Regular ...', b.description
998 assert len(b.xref) == 2, b.xref
999 assert not b.not_for_us
1000 assert 'DSA-800-1' in b.xref, b.xref
1001 assert 'DTSA-10-1' in b.xref, b.xref
1002 assert tuple(b.comments) == (('NOTE', 'gnumeric/goffice includes one as well; according to upstream not exploitable in gnumeric,'),
1003 ('NOTE', 'new copy will be included any way')),\
1004 b.comments
1005
1006 assert len(b.notes) == 4, len(b.notes)
1007
1008 for n in b.notes:
1009 assert n.release is None
1010 if n.package == 'pcre3':
1011 assert n.fixed_version == debian_support.Version('6.3-0.1etch1')
1012 assert tuple(n.bugs) == (324531,), n.bugs
1013 assert n.urgency == bugs.internUrgency('medium')
1014 elif n.package == 'python2.1':
1015 assert n.fixed_version == debian_support.Version('2.1.3dfsg-3')
1016 assert len(n.bugs) == 0, n.bugs
1017 assert n.urgency == bugs.internUrgency('medium')
1018 elif n.package == 'python2.2':
1019 assert n.fixed_version == debian_support.Version('2.2.3dfsg-4')
1020 assert len(n.bugs) == 0, n.bugs
1021 assert n.urgency == bugs.internUrgency('medium')
1022 elif n.package == 'python2.3':
1023 assert n.fixed_version == debian_support.Version('2.3.5-8')
1024 assert len(n.bugs) == 0, n.bugs
1025 assert n.urgency == bugs.internUrgency('medium')
1026 else:
1027 assert False
1028
1029 assert bugs.BugFromDB(cursor, 'DSA-311').isKernelOnly()
1030
1031 if __name__ == "__main__":
1032 test()

  ViewVC Help
Powered by ViewVC 1.1.5