summaryrefslogtreecommitdiffstats
path: root/meta/classes/cve-report.bbclass
blob: 35d58d0821a9969a9f4a0393c9cdb63cad910525 (plain)
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
# Class to inherit when you want to generate a CVE reports.
#
# Generates package list file and package CVE report.
#
# Example:
#   echo 'INHERIT += "cve-report"' >> conf/local.conf
#   bitbake -c report_cve core-image-minimal
#
# Variables to be passed to "cvert-*" scripts:
#
# CVE_REPORT_MODE[foss]
#     Path to the CVE FOSS report to be generated.
#
# CVE_REPORT_MODE[restore]
#     Path to the CVE dump data file.
#
#     E.g. for multiple MACHINEs:
#     (1) generate CVE dump:
#         cvert-update --store /path/to/cvedump $TEMP/nvdfeed
#     (2) for mach in $(get_machine_list); do
#             (source oe-init-build-env "build-$mach";
#             echo 'CVE_REPORT_MODE[restore] = "/path/to/cvedump"' >> conf/local.conf;
#             echo 'CVE_REPORT_MODE[foss] = "/path/to/report-foss-'${mach}'"' >> conf/local.conf;
#             MACHINE=$mach bitbake -c report_cve core-image-minimal)
#         done
#
# CVE_REPORT_MODE[offline]
#     Either "0" or "1". Offline mode ("--offline" parameter for cvert-* scripts).
#
# CVE_REPORT_MODE[feeddir]
#     Path to the NVD feed directory.
#
# CVE_REPORT_MODE[packagelist]
#     Path to the package list file to be generated.
#
# CVE_REPORT_MODE[packageonly]
#     Either "0" or "1". Generate package list file, then stop.
#
# CVE_REPORT_MODE[blacklist]
#     Ignore specific class.
#

CVE_REPORT_MODE[foss] ?= "${LOG_DIR}/cvert/report-foss.txt"
CVE_REPORT_MODE[offline] ?= "0"
CVE_REPORT_MODE[feeddir] ?= "${LOG_DIR}/nvdfeeds"
CVE_REPORT_MODE[packagelist] ?= "${LOG_DIR}/cvert/package.lst"
CVE_REPORT_MODE[packageonly] ?= "0"
CVE_REPORT_MODE[blacklist] ?= "native,nativesdk,cross,crosssdk,cross-canadian,packagegroup,image"

CVE_PRODUCT ??= "${BPN}"
CVE_VERSION ??= "${PV}"

addhandler generate_report_handler
generate_report_handler[eventmask] = "bb.event.BuildCompleted"

def cvert_update(d):
    """Update NVD storage and prepare CVE dump"""

    import tempfile
    import subprocess

    bb.utils.export_proxies(d)

    dump = os.path.join(d.getVar("LOG_DIR"), "cvedump")

    bb.note("Updating CVE database: %s" % dump)

    cmd = [
        "cvert-update",
        "--store", dump,
        "--debug",
        d.getVarFlag("CVE_REPORT_MODE", "feeddir")
    ]

    if d.getVarFlag("CVE_REPORT_MODE", "offline") != "0":
        cmd.append("--offline")

    try:
        bb.debug(2, "Call '%s'" % " ".join(cmd))
        output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode()
        bb.debug(2, "Output: %s" % output)
    except subprocess.CalledProcessError as e:
        bb.error("Failed to run cvert-update: '%s'\n%s: %s" % (" ".join(cmd), e, e.output))

    return dump

# copied from cve-check.bbclass
def get_patches_cves(d):
    """Get patches that solve CVEs using the "CVE: " tag"""

    import re

    pn = d.getVar("PN")
    cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+")

    # Matches last CVE-1234-211432 in the file name, also if written
    # with small letters. Not supporting multiple CVE id's in a single
    # file name.
    cve_file_name_match = re.compile(".*([Cc][Vv][Ee]\-\d{4}\-\d+)")

    patched_cves = set()
    bb.debug(2, "Looking for patches that solves CVEs for %s" % pn)
    for url in src_patches(d):
        patch_file = bb.fetch.decodeurl(url)[2]

        # Check patch file name for CVE ID
        fname_match = cve_file_name_match.search(patch_file)
        if fname_match:
            cve = fname_match.group(1).upper()
            patched_cves.add(cve)
            bb.debug(2, "Found CVE %s from patch file name %s" % (cve, patch_file))

        with open(patch_file, "r", encoding="utf-8") as f:
            try:
                patch_text = f.read()
            except UnicodeDecodeError:
                bb.debug(1, "Failed to read patch %s using UTF-8 encoding"
                        " trying with iso8859-1" %  patch_file)
                f.close()
                with open(patch_file, "r", encoding="iso8859-1") as f:
                    patch_text = f.read()

        # Search for one or more "CVE: " lines
        text_match = False
        for match in cve_match.finditer(patch_text):
            # Get only the CVEs without the "CVE: " tag
            cves = patch_text[match.start()+5:match.end()]
            for cve in cves.split():
                bb.debug(2, "Patch %s solves %s" % (patch_file, cve))
                patched_cves.add(cve)
                text_match = True

        if not fname_match and not text_match:
            bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file)

    return patched_cves


python generate_report_handler() {
    if d.getVarFlag("CVE_REPORT_MODE", "packageonly") != "0":
        return

    import subprocess

    restore = d.getVarFlag("CVE_REPORT_MODE", "restore")

    if not restore:
        restore = cvert_update(d)

    if os.path.exists(d.getVarFlag("CVE_REPORT_MODE", "packagelist")):
        report_foss = d.getVarFlag("CVE_REPORT_MODE", "foss")

        bb.note("Generating CVE FOSS report: %s" % report_foss)

        cmd = [
            "cvert-foss",
            "--restore", restore,
            "--output", report_foss,
            d.getVarFlag("CVE_REPORT_MODE", "packagelist")
        ]

        try:
            bb.debug(2, "Call '%s'" % " ".join(cmd))
            output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode()
            bb.debug(2, "Output: %s" % output)
        except subprocess.CalledProcessError as e:
            bb.error("Failed to run cvert-foss: '%s'\n%s: %s" % (" ".join(cmd), e, e.output))
}

addhandler build_started
build_started[eventmask] = "bb.event.BuildStarted"

python build_started() {
    packagelist = d.getVarFlag("CVE_REPORT_MODE", "packagelist")
    bb.utils.remove(packagelist)
    bb.utils.mkdirhier(os.path.dirname(packagelist))
    bb.note("Package list: ", packagelist)
}

addtask do_report_cve after do_report_patched

do_report_cve[recrdeptask] = "do_report_cve do_report_patched"
do_report_cve[recideptask] = "do_${BB_DEFAULT_TASK}"
do_report_cve[nostamp] = "1"

do_report_cve() {
    :
}

python do_report_patched() {
    if not d.getVar("SRC_URI"):
        return

    cve_product = d.getVar("CVE_PRODUCT")

    if not cve_product:
        return

    cve_version = d.getVar("CVE_VERSION")
    patched_cves = get_patches_cves(d)

    with open(d.getVarFlag("CVE_REPORT_MODE", "packagelist"), "a") as fil:
        fil.write("%s,%s,%s\n" % (cve_product, cve_version, " ".join(patched_cves)))
        bb.debug(2, "Append to package-list: '%s,%s,%s'" % (cve_product, cve_version, " ".join(patched_cves)))
}

addtask do_report_patched after do_unpack before do_build

do_report_patched[nostamp] = "1"

python() {
    for b in d.getVarFlag("CVE_REPORT_MODE", "blacklist").split(","):
        if bb.data.inherits_class(b, d):
            bb.build.deltask("do_report_patched", d)
            break
}