| 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 debian_support
|
| 32 |
import os
|
| 33 |
import re
|
| 34 |
import sys
|
| 35 |
import types
|
| 36 |
|
| 37 |
class InsertError(Exception):
|
| 38 |
"""Class for capturing insert errors.
|
| 39 |
|
| 40 |
The 'errors' member collects all error messages.
|
| 41 |
"""
|
| 42 |
|
| 43 |
def __init__(self, errors):
|
| 44 |
assert len(errors) > 0, errors
|
| 45 |
assert type(errors) == types.ListType, errors
|
| 46 |
assert type(errors[0])== types.StringType, errors
|
| 47 |
self.errors = errors
|
| 48 |
|
| 49 |
def __str__(self):
|
| 50 |
return self.errors[0] + ' [more...]'
|
| 51 |
|
| 52 |
class DB:
|
| 53 |
"""Access to the security database.
|
| 54 |
|
| 55 |
This is a wrapper around an SQLite database object (which is
|
| 56 |
accessible as the "db" member.
|
| 57 |
|
| 58 |
Most operations need a special cursor object, which can be created
|
| 59 |
with a cursor object. The name "cursor" is somewhat of a
|
| 60 |
misnomer because these objects are quite versatile.
|
| 61 |
"""
|
| 62 |
|
| 63 |
def __init__(self, name, verbose=False):
|
| 64 |
self.db = apsw.Connection(name)
|
| 65 |
self.verbose = verbose
|
| 66 |
|
| 67 |
def cursor(self):
|
| 68 |
"""Creates a new database cursor.
|
| 69 |
|
| 70 |
Also see the writeTxn method."""
|
| 71 |
return self.db.cursor()
|
| 72 |
|
| 73 |
def writeTxn(self):
|
| 74 |
"""Creates a cursor for an exclusive transaction.
|
| 75 |
|
| 76 |
No other process may modify the database at the same time.
|
| 77 |
After finishing the work, you should invoke the commit or
|
| 78 |
rollback methods below.
|
| 79 |
"""
|
| 80 |
c = self.cursor()
|
| 81 |
c.execute("BEGIN TRANSACTION EXCLUSIVE")
|
| 82 |
return c
|
| 83 |
|
| 84 |
def commit(self, cursor):
|
| 85 |
"""Makes the changes in the transaction permanent."""
|
| 86 |
cursor.execute("COMMIT")
|
| 87 |
|
| 88 |
def rollback(self, cursor):
|
| 89 |
"""Undos the changes in the transaction."""
|
| 90 |
cursor.execute("ROLLBACK")
|
| 91 |
|
| 92 |
def initSchema(self):
|
| 93 |
"""Creates the database schema."""
|
| 94 |
cursor = self.cursor()
|
| 95 |
|
| 96 |
cursor.execute("""CREATE TABLE inodeprints
|
| 97 |
(file TEXT NOT NULL PRIMARY KEY,
|
| 98 |
inodeprint TEXT NOT NULL)""")
|
| 99 |
|
| 100 |
cursor.execute("""CREATE TABLE version_linear_order
|
| 101 |
(id INTEGER NOT NULL PRIMARY KEY,
|
| 102 |
version TEXT NOT NULL UNIQUE)""")
|
| 103 |
|
| 104 |
cursor.execute("""CREATE TABLE source_packages
|
| 105 |
(package TEXT NOT NULL,
|
| 106 |
release TEXT NOT NULL, archive TEXT NOT NULL,
|
| 107 |
version TEXT NOT NULL,
|
| 108 |
version_id INTEGER NOT NULL DEFAULT 0,
|
| 109 |
PRIMARY KEY (package, release, archive));""")
|
| 110 |
|
| 111 |
cursor.execute("""CREATE TABLE binary_packages
|
| 112 |
(package TEXT NOT NULL,
|
| 113 |
release TEXT NOT NULL, archive TEXT NOT NULL,
|
| 114 |
architecture TEXT NOT NULL,
|
| 115 |
version TEXT NOT NULL,
|
| 116 |
version_id INTEGER NOT NULL DEFAULT 0,
|
| 117 |
source TEXT NOT NULL, source_version TEXT NOT NULL,
|
| 118 |
PRIMARY KEY (package, release, archive, architecture));""")
|
| 119 |
cursor.execute("""CREATE INDEX binary_packages_source
|
| 120 |
ON binary_packages(source)""")
|
| 121 |
|
| 122 |
cursor.execute("""CREATE TABLE package_notes
|
| 123 |
(id INTEGER NOT NULL PRIMARY KEY,
|
| 124 |
bug_name TEXT NOT NULL,
|
| 125 |
package TEXT NOT NULL,
|
| 126 |
fixed_version TEXT
|
| 127 |
CHECK (fixed_version IS NULL OR fixed_version <> ''),
|
| 128 |
fixed_version_id INTEGER NOT NULL DEFAULT 0,
|
| 129 |
release TEXT NOT NULL,
|
| 130 |
urgency TEXT NOT NULL)""")
|
| 131 |
|
| 132 |
cursor.execute("""CREATE TABLE debian_bugs
|
| 133 |
(bug INTEGER NOT NULL,
|
| 134 |
note INTEGER NOT NULL,
|
| 135 |
PRIMARY KEY (bug, note))""")
|
| 136 |
|
| 137 |
cursor.execute("""CREATE TABLE bugs
|
| 138 |
(name TEXT NOT NULL PRIMARY KEY,
|
| 139 |
cve_status TEXT NOT NULL
|
| 140 |
CHECK (cve_status IN
|
| 141 |
('', 'CANDIDATE', 'ASSIGNED', 'RESERVED', 'REJECTED')),
|
| 142 |
not_for_us INTEGER NOT NULL CHECK (not_for_us IN (0, 1)),
|
| 143 |
description TEXT NOT NULL,
|
| 144 |
source_file TEXT NOT NULL,
|
| 145 |
source_line INTEGER NOT NULL)""")
|
| 146 |
|
| 147 |
cursor.execute("""CREATE TABLE bugs_notes
|
| 148 |
(bug_name TEXT NOT NULL CHECK (typ <> ''),
|
| 149 |
typ TEXT NOT NULL CHECK (typ IN ('TODO', 'NOTE')),
|
| 150 |
release TEXT NOT NULL DEFAULT '',
|
| 151 |
comment TEXT NOT NULL CHECK (comment <> ''))""")
|
| 152 |
|
| 153 |
cursor.execute("""CREATE TABLE bugs_xref
|
| 154 |
(source TEXT NOT NULL,
|
| 155 |
target TEXT NOT NULL,
|
| 156 |
normalized_target TEXT NOT NULL DEFAULT '',
|
| 157 |
PRIMARY KEY (source, target))""")
|
| 158 |
|
| 159 |
cursor.execute("""CREATE TABLE bugs_status
|
| 160 |
(bug_name TEXT NOT NULL,
|
| 161 |
release TEXT NOT NULL,
|
| 162 |
note INTEGER NOT NULL,
|
| 163 |
reason TEXT NOT NULL,
|
| 164 |
PRIMARY KEY (bug_name, release, note))""")
|
| 165 |
|
| 166 |
def updateSources(self, cursor, release, archive, packages):
|
| 167 |
"""Reads a Sources file and adds it to the database.
|
| 168 |
|
| 169 |
Old records for the same release/archive pair are removed.
|
| 170 |
|
| 171 |
cursor - cursor used to update the database
|
| 172 |
release - Debian release (e.g. sarge)
|
| 173 |
archive - fork of a release (e.g. security)
|
| 174 |
packages - debian_support.PackageFile object with source packages
|
| 175 |
"""
|
| 176 |
|
| 177 |
cursor.execute('DELETE FROM source_packages '
|
| 178 |
+ 'WHERE release = ? AND archive = ?',
|
| 179 |
(release, archive))
|
| 180 |
|
| 181 |
for pkg in packages:
|
| 182 |
pkg_name = None
|
| 183 |
pkg_version = None
|
| 184 |
for (name, contents) in pkg:
|
| 185 |
if name == "Package":
|
| 186 |
pkg_name = contents
|
| 187 |
elif name == "Version":
|
| 188 |
pkg_version = debian_support.Version(contents)
|
| 189 |
if pkg_name is None:
|
| 190 |
raise SyntaxError\
|
| 191 |
("package record does not contain package name")
|
| 192 |
if pkg_version is None:
|
| 193 |
raise SyntaxError\
|
| 194 |
("package record for %s does not contain version"
|
| 195 |
% pkg_name)
|
| 196 |
cursor.execute('INSERT INTO source_packages '
|
| 197 |
+ '(package, release, archive, version) '
|
| 198 |
+ 'VALUES (?, ?, ?, ?)',
|
| 199 |
(pkg_name, release, archive, str(pkg_version)))
|
| 200 |
|
| 201 |
def updatePackages(self, cursor,
|
| 202 |
release, archive, architecture,
|
| 203 |
packages):
|
| 204 |
"""Reads a Packages file and adds it to the database.
|
| 205 |
|
| 206 |
Old records for the same release/archive/architecture
|
| 207 |
triple are removed.
|
| 208 |
|
| 209 |
cursor - cursor used to update the database
|
| 210 |
release - Debian release (e.g. sarge)
|
| 211 |
archive - fork of a release (e.g. security)
|
| 212 |
architecture - architecture of binary packages (e.g. i386)
|
| 213 |
packages - debian_support.PackageFile object with binary packages
|
| 214 |
"""
|
| 215 |
|
| 216 |
re_source = re.compile\
|
| 217 |
(r'^([a-zA-Z0-9.+-]+)(?:\s+\(([a-zA-Z0-9.+:-]+)\))?$')
|
| 218 |
|
| 219 |
cursor.execute('DELETE FROM binary_packages '
|
| 220 |
+ 'WHERE release = ? AND archive = ? AND architecture = ?',
|
| 221 |
(release, archive, architecture))
|
| 222 |
|
| 223 |
|
| 224 |
for pkg in packages:
|
| 225 |
pkg_name = None
|
| 226 |
pkg_version = None
|
| 227 |
pkg_source = None
|
| 228 |
pkg_source_version = None
|
| 229 |
for (name, contents) in pkg:
|
| 230 |
if name == "Package":
|
| 231 |
pkg_name = contents
|
| 232 |
elif name == "Version":
|
| 233 |
pkg_version = debian_support.Version(contents)
|
| 234 |
elif name == "Source":
|
| 235 |
match = re_source.match(contents)
|
| 236 |
if match is None:
|
| 237 |
raise SyntaxError(('binary package %s references '
|
| 238 |
+ 'invalid source package %s') %
|
| 239 |
(pkg_name, `contents`))
|
| 240 |
(pkg_source, pkg_source_version) = match.groups()
|
| 241 |
|
| 242 |
if pkg_name is None:
|
| 243 |
raise SyntaxError\
|
| 244 |
("binary package record does not contain package name")
|
| 245 |
if pkg_version is None:
|
| 246 |
raise SyntaxError\
|
| 247 |
("binary record for %s does not contain version"
|
| 248 |
% pkg_name)
|
| 249 |
if pkg_source is None:
|
| 250 |
pkg_source = pkg_name
|
| 251 |
if pkg_source_version is None:
|
| 252 |
pkg_source_version = pkg_version
|
| 253 |
|
| 254 |
cursor.execute('INSERT INTO binary_packages '
|
| 255 |
+ '(package, release, archive, architecture,'
|
| 256 |
+ 'version, source, source_version) '
|
| 257 |
+ 'VALUES (?, ?, ?, ?, ?, ?, ?)',
|
| 258 |
(pkg_name, release, archive, architecture,
|
| 259 |
str(pkg_version),
|
| 260 |
pkg_source, str(pkg_source_version)))
|
| 261 |
|
| 262 |
def filePrint(self, filename):
|
| 263 |
"""Returns a fingerprint string for filename."""
|
| 264 |
|
| 265 |
st = os.stat(filename)
|
| 266 |
# The "1" is a version number which can be used to trigger a
|
| 267 |
# re-read if the code has changed in an incompatible way.
|
| 268 |
return `(st.st_size, st.st_ino, st.st_mtime, 1)`
|
| 269 |
|
| 270 |
def _maybeUpdate(self, cursor, args, filename, action):
|
| 271 |
"""Internal routine used for conditional update."""
|
| 272 |
|
| 273 |
current_print = self.filePrint(filename)
|
| 274 |
|
| 275 |
for (old_print,) in cursor.execute\
|
| 276 |
("SELECT inodeprint FROM inodeprints WHERE file = ?", (filename,)):
|
| 277 |
if old_print == current_print:
|
| 278 |
return
|
| 279 |
if self.verbose:
|
| 280 |
print "maybeUpdate: updating", `args`
|
| 281 |
result = apply(action, (cursor,) + args
|
| 282 |
+ (debian_support.PackageFile(filename),))
|
| 283 |
cursor.execute("""UPDATE inodeprints SET inodeprint = ?
|
| 284 |
WHERE file = ?""", (current_print, filename))
|
| 285 |
return result
|
| 286 |
|
| 287 |
# No inodeprints entry, load file and add one.
|
| 288 |
result = apply(action, (cursor,) + args
|
| 289 |
+ (debian_support.PackageFile(filename),))
|
| 290 |
cursor.execute("""INSERT INTO inodeprints (file, inodeprint)
|
| 291 |
VALUES (?, ?)""", (filename, current_print))
|
| 292 |
return result
|
| 293 |
|
| 294 |
|
| 295 |
def maybeUpdateSources(self, cursor, release, archive, filename):
|
| 296 |
"""Reads the Sources file filename if it has been modified."""
|
| 297 |
self._maybeUpdate(cursor, (release, archive), filename,
|
| 298 |
self.updateSources)
|
| 299 |
|
| 300 |
def maybeUpdatePackages(self, cursor, release, archive, arch, filename):
|
| 301 |
"""Reads the Packages file filename if it has been modified."""
|
| 302 |
self._maybeUpdate(cursor, (release, archive, arch), filename,
|
| 303 |
self.updatePackages)
|
| 304 |
|
| 305 |
def deleteBugs(self, cursor):
|
| 306 |
"""Deletes all record bug reports from the database."""
|
| 307 |
cursor.execute("DELETE FROM package_notes")
|
| 308 |
cursor.execute("DELETE FROM debian_bugs")
|
| 309 |
cursor.execute("DELETE FROM bugs")
|
| 310 |
cursor.execute("DELETE FROM bugs_notes")
|
| 311 |
cursor.execute("DELETE FROM bugs_xref")
|
| 312 |
|
| 313 |
def insertBugs(self, cursor, source):
|
| 314 |
"""Reads the CAN/CVE/DSA/DTSA file and writes them to the database."""
|
| 315 |
|
| 316 |
errors = []
|
| 317 |
for bug in source:
|
| 318 |
try:
|
| 319 |
bug.writeDB(cursor)
|
| 320 |
except ValueError, e:
|
| 321 |
errors.append("%s: %d: error: %s"
|
| 322 |
% (bug.source_file, bug.source_line, e))
|
| 323 |
if errors:
|
| 324 |
raise InsertError(errors)
|
| 325 |
|
| 326 |
def finishBugs(self, cursor):
|
| 327 |
"""After inserting new bugs, update cross-references.
|
| 328 |
|
| 329 |
Returns a list of warning messages."""
|
| 330 |
|
| 331 |
warnings = []
|
| 332 |
|
| 333 |
# Check that there are no CAN/CVE collisions.
|
| 334 |
|
| 335 |
for b1, b2 in list(cursor.execute\
|
| 336 |
("""SELECT b1.name, b2.name FROM bugs AS b1, bugs AS b2
|
| 337 |
WHERE b1.name LIKE 'CVE-%'
|
| 338 |
AND b2.name = 'CAN-' || substr(b1.name, 5, 9)""")):
|
| 339 |
b1 = bugs.BugFromDB(cursor, b1)
|
| 340 |
b2 = bugs.BugFromDB(cursor, b2)
|
| 341 |
|
| 342 |
warnings.append("%s:%d: duplicate CVE entries %s and %s"
|
| 343 |
% (b1.source_file, b1.source_line,
|
| 344 |
b1.name, b2.name))
|
| 345 |
warnings.append("%s:%d: location of %s"
|
| 346 |
% (b1.source_file, b1.source_line, b1.name))
|
| 347 |
warnings.append("%s:%d: location of %s"
|
| 348 |
% (b2.source_file, b2.source_line, b2.name))
|
| 349 |
|
| 350 |
# Normalize the CAN/CVE references to the entry which is
|
| 351 |
# actually in the database. After the CAN -> CVE transition,
|
| 352 |
# this can go away (but we should check that the
|
| 353 |
# cross-references are valid).
|
| 354 |
|
| 355 |
for source, target in list(cursor.execute\
|
| 356 |
("""SELECT source, target FROM bugs_xref
|
| 357 |
WHERE normalized_target = ''""")):
|
| 358 |
if bugs.BugBase.re_cve_name.match(target):
|
| 359 |
can_target = 'CAN-' + target[4:]
|
| 360 |
cve_target = 'CVE-' + target[4:]
|
| 361 |
|
| 362 |
found = False
|
| 363 |
for (t,) in list(cursor.execute("""SELECT name FROM bugs
|
| 364 |
WHERE name IN (?, ?)""", (can_target, cve_target))):
|
| 365 |
cursor.execute("""UPDATE bugs_xref
|
| 366 |
SET normalized_target = ?
|
| 367 |
WHERE source = ? AND target = ?""",
|
| 368 |
(t, source, target))
|
| 369 |
found = True
|
| 370 |
break
|
| 371 |
if not found:
|
| 372 |
b = bugs.BugFromDB(cursor, source)
|
| 373 |
warnings.append\
|
| 374 |
("%s: %d: reference to unknwown CVE entry %s"
|
| 375 |
% (b.source_file, b.source_line, target))
|
| 376 |
|
| 377 |
# Check that the DSA/DTSA references are valid.
|
| 378 |
|
| 379 |
for source, target in list(cursor.execute
|
| 380 |
("""SELECT source, target FROM bugs_xref
|
| 381 |
WHERE target LIKE 'DSA%' OR target LIKE 'DTSA%'""")):
|
| 382 |
found = False
|
| 383 |
for (b,) in cursor.execute("SELECT name FROM bugs WHERE name = ?",
|
| 384 |
(target,)):
|
| 385 |
found = True
|
| 386 |
if not found:
|
| 387 |
b = bugs.BugFromDB(cursor, source)
|
| 388 |
warnings.append\
|
| 389 |
("%s: %d: reference to unknwown advisory %s"
|
| 390 |
% (b.source_file, b.source_line, target))
|
| 391 |
|
| 392 |
return warnings
|
| 393 |
|
| 394 |
def availableReleases(self, cursor=None):
|
| 395 |
"""Returns a list of tuples (RELEASE, ARCHIVE, ARCHITECTURE-LIST)."""
|
| 396 |
if cursor is None:
|
| 397 |
cursor = self.cursor()
|
| 398 |
|
| 399 |
releases = {}
|
| 400 |
for r in cursor.execute(
|
| 401 |
"SELECT DISTINCT release, archive FROM source_packages"):
|
| 402 |
releases[r] = ['(sources)']
|
| 403 |
|
| 404 |
for (rel, archive, arch) in cursor.execute(
|
| 405 |
"""SELECT DISTINCT release, archive, architecture
|
| 406 |
FROM binary_packages"""):
|
| 407 |
try:
|
| 408 |
releases[(rel, archive)].append(arch)
|
| 409 |
except KeyError:
|
| 410 |
releases[(rel, archive)] = [arch]
|
| 411 |
|
| 412 |
result = []
|
| 413 |
for ((rel, archive), archs) in releases.items():
|
| 414 |
archs.sort()
|
| 415 |
result.append((rel, archive, archs))
|
| 416 |
result.sort()
|
| 417 |
|
| 418 |
return result
|
| 419 |
|
| 420 |
def getVersion(self, cursor, release, package):
|
| 421 |
"""Returns the version number for package in release.
|
| 422 |
|
| 423 |
Package can be a source or binary package. Binary package
|
| 424 |
versions take precedence.
|
| 425 |
|
| 426 |
Security updates etc. are not considered."""
|
| 427 |
|
| 428 |
versions = list(cursor.execute(
|
| 429 |
"""SELECT version FROM binary_packages
|
| 430 |
WHERE package = ? AND release = ?""", (package, release)))
|
| 431 |
if versions:
|
| 432 |
return min(map(lambda (v,): debian_support.Version(v), versions))
|
| 433 |
|
| 434 |
versions = list(cursor.execute(
|
| 435 |
"""SELECT version FROM source_packages
|
| 436 |
WHERE package = ? AND release = ?""", (package, release)))
|
| 437 |
if versions:
|
| 438 |
assert len(versions) == 1
|
| 439 |
return debian_support.Version(versions[0][0])
|
| 440 |
|
| 441 |
return None
|
| 442 |
|
| 443 |
def releaseContainsPackage(self, cursor, release, package):
|
| 444 |
"""Returns True if the source or binary package exists in release."""
|
| 445 |
for (c,) in cursor.execute(
|
| 446 |
"""SELECT version FROM binary_packages
|
| 447 |
WHERE package = ? AND release = ?""", (package, release)):
|
| 448 |
return True
|
| 449 |
for (c,) in cursor.execute(
|
| 450 |
"""SELECT version FROM source_packages
|
| 451 |
WHERE package = ? AND release = ?""", (package, release)):
|
| 452 |
return True
|
| 453 |
return False
|
| 454 |
|
| 455 |
def _updateVersions(self, cursor):
|
| 456 |
"""Updates the linear version table."""
|
| 457 |
|
| 458 |
cursor.execute("DELETE FROM version_linear_order");
|
| 459 |
|
| 460 |
if self.verbose:
|
| 461 |
print "updateVersions:"
|
| 462 |
print " reading"
|
| 463 |
|
| 464 |
versions = []
|
| 465 |
for (v,) in cursor.execute(
|
| 466 |
"""SELECT DISTINCT *
|
| 467 |
FROM (SELECT fixed_version FROM package_notes
|
| 468 |
WHERE fixed_version IS NOT NULL
|
| 469 |
UNION ALL SELECT version FROM source_packages
|
| 470 |
UNION ALL SELECT version FROM binary_packages)"""):
|
| 471 |
if v is None:
|
| 472 |
continue
|
| 473 |
versions.append(debian_support.Version(v))
|
| 474 |
|
| 475 |
if self.verbose:
|
| 476 |
print " calculating linear oder"
|
| 477 |
versions.sort()
|
| 478 |
|
| 479 |
if self.verbose:
|
| 480 |
print " storing linear order"
|
| 481 |
for v in versions:
|
| 482 |
cursor.execute(
|
| 483 |
"INSERT INTO version_linear_order (version) VALUES (?)",
|
| 484 |
(str(v),))
|
| 485 |
|
| 486 |
if self.verbose:
|
| 487 |
print " updating package notes"
|
| 488 |
cursor.execute(
|
| 489 |
"""UPDATE package_notes
|
| 490 |
SET fixed_version_id = (SELECT id FROM version_linear_order
|
| 491 |
WHERE version = package_notes.fixed_version)
|
| 492 |
WHERE fixed_version IS NOT NULL""")
|
| 493 |
|
| 494 |
if self.verbose:
|
| 495 |
print " updating source packages"
|
| 496 |
cursor.execute(
|
| 497 |
"""UPDATE source_packages
|
| 498 |
SET version_id = (SELECT id FROM version_linear_order
|
| 499 |
WHERE version = source_packages.version)""")
|
| 500 |
|
| 501 |
if self.verbose:
|
| 502 |
print " updating binary packages"
|
| 503 |
cursor.execute(
|
| 504 |
"""UPDATE binary_packages
|
| 505 |
SET version_id = (SELECT id FROM version_linear_order
|
| 506 |
WHERE version = binary_packages.version)""")
|
| 507 |
|
| 508 |
if self.verbose:
|
| 509 |
print " finished"
|
| 510 |
|
| 511 |
def calculateVulnerabilities(self, cursor):
|
| 512 |
"""Calculate vulnerable packages.
|
| 513 |
|
| 514 |
To each package note, a release-specific vulnerability status
|
| 515 |
is attached. Currently, only etch/testing is processed.
|
| 516 |
"""
|
| 517 |
|
| 518 |
self._updateVersions(cursor)
|
| 519 |
|
| 520 |
if self.verbose:
|
| 521 |
print "calculateVulnerabilities:"
|
| 522 |
print " clearing old data"
|
| 523 |
cursor.execute("DELETE FROM bugs_status")
|
| 524 |
|
| 525 |
def markVulnerable(bug, release, note, reason):
|
| 526 |
cursor.execute("""INSERT INTO bugs_status
|
| 527 |
(bug_name, release, note, reason) VALUES (?, ?, ?, ?)""",
|
| 528 |
(bug.name, release, note, reason))
|
| 529 |
|
| 530 |
def calcVuln(bug):
|
| 531 |
vulnerable = False
|
| 532 |
note_found = False
|
| 533 |
|
| 534 |
for n in bug.notes:
|
| 535 |
# ignore all notes conditioned on releases.
|
| 536 |
if n.release is not None: # assumes 'etch'
|
| 537 |
continue
|
| 538 |
note_found = True
|
| 539 |
v = self.getVersion(cursor, 'etch', n.package)
|
| 540 |
if v is None:
|
| 541 |
# Package is not in testing, go on.
|
| 542 |
continue
|
| 543 |
if n.affects(v):
|
| 544 |
vulnerable = True
|
| 545 |
markVulnerable(b, 'etch', n.id,
|
| 546 |
"%s (%s) is vulnerable, %s"
|
| 547 |
% (n.package, v, n.fixedVersion()))
|
| 548 |
|
| 549 |
if bug.hasTODO():
|
| 550 |
vulnerable = True
|
| 551 |
markVulnerable(b, 'etch', 0, 'TODO items present')
|
| 552 |
elif not note_found:
|
| 553 |
# We found no matching note. Maybe all packages have
|
| 554 |
# been removed?
|
| 555 |
if bug.notes:
|
| 556 |
for n in bug.notes:
|
| 557 |
if self.releaseContainsPackage \
|
| 558 |
(cursor, 'etch', n.package):
|
| 559 |
markVulnerable(b, 'etch', 0,
|
| 560 |
'applicable package note for %s missing'
|
| 561 |
% n.package)
|
| 562 |
vulnerable = True
|
| 563 |
else:
|
| 564 |
vulnerable = True
|
| 565 |
markVulnerable(b, 'etch', 0, 'status is unclear')
|
| 566 |
|
| 567 |
return vulnerable
|
| 568 |
|
| 569 |
# First handle the DSAs. Cache results in DSA_status (used
|
| 570 |
# for CAN/CVE below).
|
| 571 |
|
| 572 |
if self.verbose:
|
| 573 |
print " reading DSAs"
|
| 574 |
bug_names = list(cursor.execute(
|
| 575 |
"""SELECT name FROM bugs
|
| 576 |
WHERE name LIKE 'DSA-%' AND NOT not_for_us"""))
|
| 577 |
DSA_status = {}
|
| 578 |
if self.verbose:
|
| 579 |
print " rating DSAs"
|
| 580 |
for (bug_name,) in bug_names:
|
| 581 |
b = bugs.BugFromDB(cursor, bug_name)
|
| 582 |
DSA_status[bug_name] = calcVuln(b)
|
| 583 |
|
| 584 |
# Process the CAN/CVE/FAKE entries. If an entry has no
|
| 585 |
# package annotations, but it references a non-vulnerable DSA,
|
| 586 |
# we assume that the current is not affect either.
|
| 587 |
|
| 588 |
if self.verbose:
|
| 589 |
print " reading other entries"
|
| 590 |
bug_names = list(cursor.execute(
|
| 591 |
"""SELECT name FROM bugs
|
| 592 |
WHERE (NOT not_for_us)
|
| 593 |
AND NOT (name LIKE 'DSA-%' OR name LIKE 'DTSA-%')"""))
|
| 594 |
if self.verbose:
|
| 595 |
print " rating other entries"
|
| 596 |
for (bug_name,) in bug_names:
|
| 597 |
b = bugs.BugFromDB(cursor, bug_name)
|
| 598 |
if b.notes:
|
| 599 |
calcVuln(b)
|
| 600 |
continue
|
| 601 |
|
| 602 |
if b.hasTODO():
|
| 603 |
markVulnerable(b, 'etch', 0, 'TODO items present')
|
| 604 |
continue
|
| 605 |
|
| 606 |
dsa_found = False
|
| 607 |
for x in b.xref:
|
| 608 |
if x[0:4] == 'DSA-':
|
| 609 |
dsa_found = True
|
| 610 |
if DSA_status[x]:
|
| 611 |
markVulnerable(b, 'etch', 0,
|
| 612 |
'vulnerability %s referenced' % x)
|
| 613 |
break
|
| 614 |
if not dsa_found:
|
| 615 |
markVulnerable(b, 'etch', 0, 'status is unclear')
|
| 616 |
|
| 617 |
if self.verbose:
|
| 618 |
print " finished"
|
| 619 |
|
| 620 |
def check(self, cursor=None):
|
| 621 |
"""Runs a simple consistency check and prints the results."""
|
| 622 |
|
| 623 |
if cursor is None:
|
| 624 |
cursor = self.cursor()
|
| 625 |
|
| 626 |
for (package, release, archive, architecture, source) in\
|
| 627 |
cursor.execute(
|
| 628 |
"""SELECT package, release, archive, architecture, source
|
| 629 |
FROM binary_packages
|
| 630 |
WHERE NOT EXISTS
|
| 631 |
(SELECT *
|
| 632 |
FROM source_packages AS sp
|
| 633 |
WHERE sp.package = binary_packages.source
|
| 634 |
AND sp.release = binary_packages.release
|
| 635 |
AND sp.archive = binary_packages.archive)
|
| 636 |
"""):
|
| 637 |
print "error: binary package without source package"
|
| 638 |
print " binary package:", package
|
| 639 |
print " release:", release
|
| 640 |
if archive:
|
| 641 |
print " archive:", archive
|
| 642 |
print " architecture:", architecture
|
| 643 |
print " missing source package:", source
|
| 644 |
|
| 645 |
for (package, release, archive, architecture, version,
|
| 646 |
source, source_version) \
|
| 647 |
in cursor.execute("""SELECT binary_packages.package,
|
| 648 |
binary_packages.release, binary_packages.archive,
|
| 649 |
binary_packages.architecture,binary_packages.version,
|
| 650 |
sp.package, sp.version
|
| 651 |
FROM binary_packages, source_packages AS sp
|
| 652 |
WHERE sp.package = binary_packages.source
|
| 653 |
AND sp.release = binary_packages.release
|
| 654 |
AND sp.archive = binary_packages.archive
|
| 655 |
AND sp.version <> binary_packages.source_version"""):
|
| 656 |
relation = cmp(debian_support.Version(version),
|
| 657 |
debian_support.Version(source_version))
|
| 658 |
assert relation <> 0
|
| 659 |
if relation <= 0:
|
| 660 |
print "error: binary package is older than source package"
|
| 661 |
else:
|
| 662 |
print "warning: binary package is newer than source package"
|
| 663 |
print " binary package: %s (%s)" % (package, version)
|
| 664 |
print " source package: %s (%s)" % (source, source_version)
|
| 665 |
print " release:", release
|
| 666 |
if archive:
|
| 667 |
print " archive:", archive
|
| 668 |
print " architecture:", architecture
|
| 669 |
|
| 670 |
def test():
|
| 671 |
import os
|
| 672 |
|
| 673 |
if os.path.exists('test_security.db'):
|
| 674 |
os.unlink('test_security.db')
|
| 675 |
db = DB('test_security.db')
|
| 676 |
db.initSchema()
|
| 677 |
|
| 678 |
data_prefix = '../../data/packages/'
|
| 679 |
cursor = db.writeTxn()
|
| 680 |
db.updateSources(cursor, 'sarge', 'main',
|
| 681 |
debian_support.PackageFile(data_prefix + 'sarge_main_Sources'))
|
| 682 |
db.updatePackages(cursor, 'sarge', 'main', 'i386',
|
| 683 |
debian_support.PackageFile(data_prefix
|
| 684 |
+ 'sarge_main_i386_Packages'))
|
| 685 |
db.updatePackages(cursor, 'sarge', 'main', 'ia64',
|
| 686 |
debian_support.PackageFile(data_prefix
|
| 687 |
+ 'sarge_main_ia64_Packages'))
|
| 688 |
db.commit(cursor)
|
| 689 |
|
| 690 |
assert str(db.getVersion(cursor, 'sarge', 'ale')) == '0.7.1-1', \
|
| 691 |
db.getVersion(cursor, 'sarge', 'ale')
|
| 692 |
|
| 693 |
# db.check(cursor)
|
| 694 |
|
| 695 |
cursor = db.writeTxn()
|
| 696 |
db.deleteBugs(cursor)
|
| 697 |
db.insertBugs(cursor, bugs.CVEFile('../../data/CAN/list'))
|
| 698 |
db.insertBugs(cursor, bugs.CVEFile('../../data/CVE/list',
|
| 699 |
no_version_needs_note=False))
|
| 700 |
db.insertBugs(cursor, bugs.DSAFile('../../data/DSA/list'))
|
| 701 |
db.insertBugs(cursor, bugs.DTSAFile('../../data/DTSA/list'))
|
| 702 |
db.finishBugs(cursor)
|
| 703 |
db.commit(cursor)
|
| 704 |
|
| 705 |
b = bugs.BugFromDB(cursor, 'CAN-2005-2491')
|
| 706 |
assert b.name == 'CAN-2005-2491', b.name
|
| 707 |
assert b.description == 'Integer overflow in pcre_compile.c in Perl Compatible Regular ...', b.description
|
| 708 |
assert len(b.xref) == 2, b.xref
|
| 709 |
assert not b.not_for_us
|
| 710 |
assert 'DSA-800-1' in b.xref, b.xref
|
| 711 |
assert 'DTSA-10-1' in b.xref, b.xref
|
| 712 |
assert tuple(b.comments) == (('NOTE', 'gnumeric/goffice includes one as well; according to upstream not exploitable in gnumeric,'),
|
| 713 |
('NOTE', 'new copy will be included any way')),\
|
| 714 |
b.comments
|
| 715 |
|
| 716 |
assert len(b.notes) == 4, len(b.notes)
|
| 717 |
|
| 718 |
for n in b.notes:
|
| 719 |
assert n.release is None
|
| 720 |
if n.package == 'pcre3':
|
| 721 |
assert n.fixed_version == debian_support.Version('6.3-0.1etch1')
|
| 722 |
assert tuple(n.bugs) == (324531,), n.bugs
|
| 723 |
assert n.urgency == bugs.internUrgency('medium')
|
| 724 |
elif n.package == 'python2.1':
|
| 725 |
assert n.fixed_version == debian_support.Version('2.1.3dfsg-3')
|
| 726 |
assert len(n.bugs) == 0, n.bugs
|
| 727 |
assert n.urgency == bugs.internUrgency('medium')
|
| 728 |
elif n.package == 'python2.2':
|
| 729 |
assert n.fixed_version == debian_support.Version('2.2.3dfsg-4')
|
| 730 |
assert len(n.bugs) == 0, n.bugs
|
| 731 |
assert n.urgency == bugs.internUrgency('medium')
|
| 732 |
elif n.package == 'python2.3':
|
| 733 |
assert n.fixed_version == debian_support.Version('2.3.5-8')
|
| 734 |
assert len(n.bugs) == 0, n.bugs
|
| 735 |
assert n.urgency == bugs.internUrgency('medium')
|
| 736 |
else:
|
| 737 |
assert False
|
| 738 |
|
| 739 |
assert bugs.BugFromDB(cursor, 'DSA-311').isKernelOnly()
|
| 740 |
|
| 741 |
if __name__ == "__main__":
|
| 742 |
test()
|