summaryrefslogtreecommitdiffstats
path: root/lib/python/sectracker/analyzers.py
blob: 856196e5dbbb4aebdbfe3f8d069cb1b8571a334e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# sectracker.analyzers -- vulnerability analysis
# Copyright (C) 2010 Florian Weimer <fw@deneb.enyo.de>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import apt_pkg as _apt_pkg
import re as _re

from sectracker.xcollections import namedtuple as _namedtuple

# vercmp is the Debian version comparison algorithm
_apt_pkg.init()
try:
    vercmp = _apt_pkg.version_compare
except AttributeError:
    vercmp = _apt_pkg.VersionCompare

def mergelists(listfiles, diag):
    """Merge the (already parsed) list files in listfiles.
    Returns a dictionary mapping bug names to bug tuples.
    If duplicate bug names are encountered, an error is recorded
    in diag."""
    result = {}
    for listfile in listfiles:
        for bug in listfile.list:
            header = bug.header
            name = header.name
            if name in result:
                diag.error("duplicate bug %r" % name,
                           file=header.file, line=header.header.line)
                diag.error("location of previous bug",
                           file=result[name].file, line=result[name].line)
                continue
            result[name] = bug
    return result

def extractversions(config, bugdb, diag):
    """Extracts version information from list files.

    Uses the repository configuration config to obtain a nested
    dictionary, mapping release names to packages and sets of
    versions.  Then scans the bug database dictionary for additional
    versions for those releases.  If an unknown release is
    encountered, an error message is added to diag."""

    rpv = config.releasepackageversions()
    for bug in bugdb.values():
        for ann in bug.annotations:
            if ann.type == "package" and ann.version is not None \
                    and ann.release is not None:
                if ann.release not in rpv:
                    diag.error(file=bug.file, line=ann.line,
                               message="unknown release: %r" % ann.release)
                else:
                    pv = rpv[ann.release]
                    if ann.package in pv:
                        pv[ann.package].add(ann.version)
                    else:
                        pv[ann.package] = set((ann.version,))
    return rpv

_re_source = _re.compile("^DT?SA-")

def copysources(bugdb, diag):
    """Returns a dictionary, mapping bug names to their copy sources.
    
    As a side effect, this checks cross-references.  Errors found
    there are recorded in diag."""

    result = {}
    for bug in bugdb.values():
        copy_source = bug.header.name
        if not _re_source.match(copy_source):
            copy_source = None
        for ann in bug.annotations:
            if ann.type != "xref":
                continue
            for target in ann.bugs:
                if target not in bugdb:
                    diag.error("reference to unknown bug %r" % target,
                               file=bug.file, line=ann.line)
                    continue
                if copy_source is not None:
                    if target in result:
                        result[target].add(copy_source)
                    else:
                        result[target] = set((copy_source,))
    return result

Vulnerability = _namedtuple("Vulnerability", "bug package fixed fixed_other")

def fixedversions(bugdb, copysrc, versions, diag):
    """Determine vulnerable versions.

    Returns named tuples with fields "bug", "package", "fixed",
    "fixed_other"."""

    assert "sid" in versions # should come from extractversions()

    def buildpackages1(bug, target=None):
        packages = {}
        xref = () # current {} contents
        for ann in bug.annotations:
            # only copy if target is listed in current {} list
            if ann.type == "package" and (target is None or target in xref):
                if ann.package not in packages:
                    packages[ann.package] = {}
                pkg = packages[ann.package]
                pkg[ann.release] = (bug, ann)
            elif ann.type == "xref":
                xref = ann.bugs
        return packages

    def buildpackages(bug):
        packages = buildpackages1(bug)
        if bug.header.name not in copysrc:
            return packages
        copiers = [buildpackages1(bugdb[b], target=bug.header.name)
                   for b in copysrc[bug.header.name]]
        for c in copiers:
            for pname, creleases in c.items():
                if pname not in packages:
                    packages[pname] = creleases
                    continue
                preleases = packages[pname]
                for rel, cbugann in creleases.items():
                    if rel in preleases:
                        pbug, pann = preleases[rel]
                        cbug, cann = cbugann
                        if pbug is bug:
                            # Never override annotations in the CVE file.
                            continue
                        diag.warning("annotation on %s overridden"
                                     % pbug.header.name,
                                     file=pbug.file, line=pann.line)
                        diag.warning("  by annotation on %s via %s" 
                                     % (cbug.header.name, bug.header.name),
                                     file=cbug.file, line=cann.line)
                    preleases[rel] = cbugann
        return packages

    def latentlyvulnerable(packages):
        for pname, preleases in packages.items():
            if None not in preleases:
                diag.warning("package %s is latently vulnerable in unstable"
                             % pname,
                             file=bug.file, line=bug.header.line)
                for (pbug, pann) in preleases.values():
                    diag.warning("%s vulnerability in %s"
                                 % (pname, pann.release),
                                 file=pbug.file, line=pann.line)

    def convertversion(ann):
        # None: unfixed
        # version-string: fixed in that version
        # True: never vulnerable
        if ann.urgency == "unimportant" or ann.kind == "not-affected":
            return True
        ver = ann.version
        if ver is not None:
            return ver
        return None
        
    def extractunstable(preleases):
        if None not in preleases:
            return None
        return convertversion(preleases[None][1])

    def getversions(pname, version_items=versions.items()):
        # FIXME: extractversions() should return flipped nested
        # dictionary, to make the following easier.
        for rel, pkgs in version_items:
            if rel == "sid":
                continue
            if pname in pkgs:
                for ver in pkgs[pname]:
                    yield rel, ver

    result = []
    for bug in bugdb.values():
        if _re_source.match(bug.header.name):
            # Copy sources are dealt with by copying their
            # annotations.
            continue

        packages = buildpackages(bug)
        latentlyvulnerable(packages)

        for pname, preleases in packages.items():
            unstable_fixed = extractunstable(preleases)
            if unstable_fixed is True:
                # unstable was never vulnerable, which overrides
                # all other annoations
                continue

            other_versions = set()
            for rel, ver in getversions(pname):
                if unstable_fixed is not None \
                        and vercmp(ver, unstable_fixed) >= 0:
                    # This version is already covered by the
                    # unstable fix.
                    continue
                if rel in preleases:
                    refver = convertversion(preleases[rel][1])
                    if refver is None:
                        continue
                    if refver is True:
                        # Annotations like <not-affected>.
                        other_versions.add(ver)
                        continue
                    if vercmp(ver, refver) >= 0:
                        other_versions.add(ver)
            result.append(Vulnerability(bug.header.name, pname,
                                        unstable_fixed, other_versions))
    return result

def bestversion(config, codename, pkg, requested_members=None):
    """Returns the source package with the highest version among the
    requested sub-repository members."""
    members = config.distributions[codename]["members"]
    fm = config.filemap()
    bestver = None
    bestpkg = None
    for name, mems in members.items():
        if requested_members is None or name in requested_members:
            for mem in mems:
                for comp in fm[mem].values():
                    if pkg in comp:
                        curpkg = comp[pkg]
                        curver = curpkg.version
                        if bestver is None or vercmp(curver, bestver) > 0:
                            bestver = curver
                            bestpkg = curpkg
    return bestpkg

© 2014-2024 Faster IT GmbH | imprint | privacy policy