summaryrefslogtreecommitdiffstats
path: root/meta/lib/oe/cve_check.py
diff options
context:
space:
mode:
Diffstat (limited to 'meta/lib/oe/cve_check.py')
-rw-r--r--meta/lib/oe/cve_check.py154
1 files changed, 153 insertions, 1 deletions
diff --git a/meta/lib/oe/cve_check.py b/meta/lib/oe/cve_check.py
index ce755f940a..ed4af18ced 100644
--- a/meta/lib/oe/cve_check.py
+++ b/meta/lib/oe/cve_check.py
@@ -11,8 +11,13 @@ _Version = collections.namedtuple(
11class Version(): 11class Version():
12 12
13 def __init__(self, version, suffix=None): 13 def __init__(self, version, suffix=None):
14
15 suffixes = ["alphabetical", "patch"]
16
14 if str(suffix) == "alphabetical": 17 if str(suffix) == "alphabetical":
15 version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<patch>[-_\.]?(?P<patch_l>[a-z]))?(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?""" 18 version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<patch>[-_\.]?(?P<patch_l>[a-z]))?(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?"""
19 elif str(suffix) == "patch":
20 version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<patch>[-_\.]?(p|patch)(?P<patch_l>[0-9]+))?(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?"""
16 else: 21 else:
17 version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?""" 22 version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?"""
18 regex = re.compile(r"^\s*" + version_pattern + r"\s*$", re.VERBOSE | re.IGNORECASE) 23 regex = re.compile(r"^\s*" + version_pattern + r"\s*$", re.VERBOSE | re.IGNORECASE)
@@ -23,7 +28,7 @@ class Version():
23 28
24 self._version = _Version( 29 self._version = _Version(
25 release=tuple(int(i) for i in match.group("release").replace("-",".").split(".")), 30 release=tuple(int(i) for i in match.group("release").replace("-",".").split(".")),
26 patch_l=match.group("patch_l") if str(suffix) == "alphabetical" and match.group("patch_l") else "", 31 patch_l=match.group("patch_l") if str(suffix) in suffixes and match.group("patch_l") else "",
27 pre_l=match.group("pre_l"), 32 pre_l=match.group("pre_l"),
28 pre_v=match.group("pre_v") 33 pre_v=match.group("pre_v")
29 ) 34 )
@@ -58,3 +63,150 @@ def _cmpkey(release, patch_l, pre_l, pre_v):
58 else: 63 else:
59 _pre = float(pre_v) if pre_v else float('-inf') 64 _pre = float(pre_v) if pre_v else float('-inf')
60 return _release, _patch, _pre 65 return _release, _patch, _pre
66
67def cve_check_merge_jsons(output, data):
68 """
69 Merge the data in the "package" property to the main data file
70 output
71 """
72 if output["version"] != data["version"]:
73 bb.error("Version mismatch when merging JSON outputs")
74 return
75
76 for product in output["package"]:
77 if product["name"] == data["package"][0]["name"]:
78 bb.error("Error adding the same package %s twice" % product["name"])
79 return
80
81 output["package"].append(data["package"][0])
82
83def update_symlinks(target_path, link_path):
84 """
85 Update a symbolic link link_path to point to target_path.
86 Remove the link and recreate it if exist and is different.
87 """
88 if link_path != target_path and os.path.exists(target_path):
89 if os.path.exists(os.path.realpath(link_path)):
90 os.remove(link_path)
91 os.symlink(os.path.basename(target_path), link_path)
92
93def get_patched_cves(d):
94 """
95 Get patches that solve CVEs using the "CVE: " tag.
96 """
97
98 import re
99 import oe.patch
100
101 pn = d.getVar("PN")
102 cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+")
103
104 # Matches the last "CVE-YYYY-ID" in the file name, also if written
105 # in lowercase. Possible to have multiple CVE IDs in a single
106 # file name, but only the last one will be detected from the file name.
107 # However, patch files contents addressing multiple CVE IDs are supported
108 # (cve_match regular expression)
109
110 cve_file_name_match = re.compile(".*([Cc][Vv][Ee]\-\d{4}\-\d+)")
111
112 patched_cves = set()
113 bb.debug(2, "Looking for patches that solves CVEs for %s" % pn)
114 for url in oe.patch.src_patches(d):
115 patch_file = bb.fetch.decodeurl(url)[2]
116
117 # Check patch file name for CVE ID
118 fname_match = cve_file_name_match.search(patch_file)
119 if fname_match:
120 cve = fname_match.group(1).upper()
121 patched_cves.add(cve)
122 bb.debug(2, "Found CVE %s from patch file name %s" % (cve, patch_file))
123
124 # Remote patches won't be present and compressed patches won't be
125 # unpacked, so say we're not scanning them
126 if not os.path.isfile(patch_file):
127 bb.note("%s is remote or compressed, not scanning content" % patch_file)
128 continue
129
130 with open(patch_file, "r", encoding="utf-8") as f:
131 try:
132 patch_text = f.read()
133 except UnicodeDecodeError:
134 bb.debug(1, "Failed to read patch %s using UTF-8 encoding"
135 " trying with iso8859-1" % patch_file)
136 f.close()
137 with open(patch_file, "r", encoding="iso8859-1") as f:
138 patch_text = f.read()
139
140 # Search for one or more "CVE: " lines
141 text_match = False
142 for match in cve_match.finditer(patch_text):
143 # Get only the CVEs without the "CVE: " tag
144 cves = patch_text[match.start()+5:match.end()]
145 for cve in cves.split():
146 bb.debug(2, "Patch %s solves %s" % (patch_file, cve))
147 patched_cves.add(cve)
148 text_match = True
149
150 if not fname_match and not text_match:
151 bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file)
152
153 return patched_cves
154
155
156def get_cpe_ids(cve_product, version):
157 """
158 Get list of CPE identifiers for the given product and version
159 """
160
161 version = version.split("+git")[0]
162
163 cpe_ids = []
164 for product in cve_product.split():
165 # CVE_PRODUCT in recipes may include vendor information for CPE identifiers. If not,
166 # use wildcard for vendor.
167 if ":" in product:
168 vendor, product = product.split(":", 1)
169 else:
170 vendor = "*"
171
172 cpe_id = 'cpe:2.3:a:{}:{}:{}:*:*:*:*:*:*:*'.format(vendor, product, version)
173 cpe_ids.append(cpe_id)
174
175 return cpe_ids
176
177def convert_cve_version(version):
178 """
179 This function converts from CVE format to Yocto version format.
180 eg 8.3_p1 -> 8.3p1, 6.2_rc1 -> 6.2-rc1
181
182 Unless it is redefined using CVE_VERSION in the recipe,
183 cve_check uses the version in the name of the recipe (${PV})
184 to check vulnerabilities against a CVE in the database downloaded from NVD.
185
186 When the version has an update, i.e.
187 "p1" in OpenSSH 8.3p1,
188 "-rc1" in linux kernel 6.2-rc1,
189 the database stores the version as version_update (8.3_p1, 6.2_rc1).
190 Therefore, we must transform this version before comparing to the
191 recipe version.
192
193 In this case, the parameter of the function is 8.3_p1.
194 If the version uses the Release Candidate format, "rc",
195 this function replaces the '_' by '-'.
196 If the version uses the Update format, "p",
197 this function removes the '_' completely.
198 """
199 import re
200
201 matches = re.match('^([0-9.]+)_((p|rc)[0-9]+)$', version)
202
203 if not matches:
204 return version
205
206 version = matches.group(1)
207 update = matches.group(2)
208
209 if matches.group(3) == "rc":
210 return version + '-' + update
211
212 return version + update