blob: 041efafed30c2c571ca8a9cf02681a7125734271 [file] [log] [blame]
Borjan Tchakaloff72ee6d62018-12-04 10:29:38 +01001import os
2import re
3import zipfile
4
5from manifest import ProjectNotFoundError
6from utility import exec_command as utility_exec_command
7
8
9class SecurityPatch(object):
10 """Abstract representation of a Security Patch from a Security Bulletin."""
11
12 def __init__(self, zip_file, filename, android_ids, manifest, args):
13 self.args = args
14 self.android_ids = android_ids
15 self.aosp_project_path = self._get_aosp_project_path(filename)
16
17 if self.aosp_project_path in manifest.projects:
18 self.project_rel_path = manifest.projects[self.aosp_project_path]
19 self.project_abs_path = os.path.join(
20 self.args.aosp_root, self.project_rel_path
21 )
22 else:
23 raise ProjectNotFoundError(self.aosp_project_path)
24
25 self.filename = filename
26 self.zip_file = zip_file
27 self.change_id = self.find_last_change_id()
28
29 def __repr__(self):
30 return self.filename
31
32 def _get_aosp_project_path(self, filename):
33 """Get project path as expected by the AndroidManifest class.
34
35 The returned path matches or is close to the naming conventions for the
36 `path` attribute in AOSP manifest XMLs. The AndroidManifest class knows
37 how to map this path to actual file system paths.
38
39 """
40 dirs_to_skip = (
41 2 if self.args.use_old_format else self.args.ignore_n_leading
42 )
43 return "/".join(filename.split("/")[dirs_to_skip:-1])
44
45 def find_last_change_id(self):
46 """Find the Change-Id mentioned last in the patch.
47
48 Returns:
49 The Change-Id identifying the patch, or `None` if none was found.
50
51 """
52 change_id = None
53 with self.zip_file.open(self.filename) as patchfile:
54 for raw_line in patchfile:
55 # TODO: Currently we actually expect a patch file here, but also
56 # get passed in zipped kernel patch files, which of course can't
57 # be parsed directly. This causes the decoding from bytes to
58 # string to fail, which we ignore for now.
59 line = raw_line.decode("utf-8", errors="ignore")
60 if line.startswith("Change-Id"):
61 change_id = line.split()[1]
62 if line.startswith("---"):
63 break
64 return change_id
65
66 def find_issues_for_patch(self):
67 return self.android_ids
68
69 def get_description(self):
70 if self.args.display_all:
71 return "{} {} {}".format(
72 str(self.find_issues_for_patch()),
73 self.aosp_project_path,
74 os.path.basename(self.filename),
75 )
76 elif self.args.use_a_number:
77 return self.find_issues_for_patch()
78 else:
79 return (
80 self.aosp_project_path + " " + os.path.basename(self.filename)
81 )
82
83 def check_and_apply(self):
84 if not self.change_id:
85 print(
86 "[BE]: Unable to verify patch, could not determine its "
87 "Change-Id: {}.".format(self.get_description())
88 )
89 return
90
91 if not self.is_applied():
92 print("[BE]: Not yet applied: {}".format(self.get_description()))
Karsten Tauschef4142722019-01-30 11:20:35 +010093 if self.args.apply_to_branch:
Borjan Tchakaloff72ee6d62018-12-04 10:29:38 +010094 self.apply()
95 else:
96 print("[BE]: Already applied: {}".format(self.get_description()))
97
98 def is_applied(self):
99 assert self.change_id
100 found = (
101 self.exec_command(
102 "( cd {path} && git log --oneline --grep={changeid} | "
103 " grep [a-Z] >/dev/null )".format(
104 path=self.project_abs_path, changeid=self.change_id
105 )
106 )
107 == 0
108 )
109 return found
110
111 def rollback(self):
112 print("[BE]: Rolling back... ".format(self.filename))
113 self.exec_command(
114 "( cd {path} && git am --abort && git reset --hard )".format(
115 path=self.project_abs_path
116 )
117 )
118 self.exec_command(
119 "( cd {path} && git reset --hard )".format(
120 path=self.project_abs_path
121 )
122 )
123 self.exec_command(
124 "( cd {path} && repo sync .)".format(path=self.project_abs_path)
125 )
126
127 def amend(self):
128 print(
129 "[BE]: This patch addresses {} ".format(
130 self.find_issues_for_patch()
131 )
132 )
133 try:
134 input("Press enter to continue and change the commit message.")
135 except (KeyboardInterrupt, EOFError):
136 pass
137
138 os.system(
139 "( cd {path} && git commit --amend )".format(
140 path=self.project_abs_path,
141 tmpbranchname=self.args.apply_to_branch,
142 )
143 )
144
145 def apply(self):
146 self.zip_file.extract(self.filename, "/tmp/")
147 print("[BE]: Applying {}.".format(self.filename.split("/")[-1]))
148 print("[BE]: for Project {}.".format(self.aosp_project_path))
149 applies = (
150 self.exec_command(
151 "( cd {path} && repo start {tmpbranchname} . ;"
152 " git am -3 {filename} )".format(
153 path=self.project_abs_path,
154 tmpbranchname=self.args.apply_to_branch,
155 filename="/tmp/" + self.filename,
156 )
157 )
158 == 0
159 )
160 if not applies:
161 print("[BE]: FAIL: doesn't apply.".format(self.filename))
162 self.rollback()
163 else:
164 print("[BE]: SUCCESS: applied.".format(self.filename))
Karsten Tauschef4142722019-01-30 11:20:35 +0100165 if not self.args.skip_amend and not self.args.cleanup_after:
Borjan Tchakaloff72ee6d62018-12-04 10:29:38 +0100166 self.amend()
167
168 def exec_command(self, command):
169 return utility_exec_command(command, self.args.quiet)
170
171
172class PlatformPatch(SecurityPatch):
173 pass
174
175
176class KernelPatch(SecurityPatch):
177 def __init__(
178 self,
179 zip_file,
180 filename,
181 android_id,
182 manifest,
183 args,
184 subpatch,
185 kernel,
186 extra,
187 ):
188 super().__init__(zip_file, filename, [android_id], manifest, args)
189
190 self.subpatch = subpatch
191 self.kernel = kernel
192 self.extra = extra
193
194 def _get_aosp_project_path(self, filename):
195 return "kernel/msm"
196
197 def get_description(self):
198 return "(snippet) " + str(super().get_description())
199
200
201class SecurityBulletin:
202 """A collection of patches for the Android platform and kernel.
203
204 The Android Security Bulletins regroup security patches needed for an
205 Android operating system in order to comply with a so-called *Security Patch
206 Level*.
207
208 """
209
210 # Handle special kernel snippet names before the machine-readable metadata
211 # introduced in August 2017. Before that:
212 # - ASB of 2017-05 introduces e.g.:
213 # - ANDROID-32402303
214 # - ANDROID-36000515_1
215 # - ASB of 2017-06 introduces e.g.:
216 # - ANDROID-35472278_dsx
217 # - ASB of 2017-07 introduces e.g.:
218 # - ANDROID-34620535_kernel3.10
219 KERNEL_SNIPPETS_FILENAME_PATTERN = re.compile(
220 r"code_snippets/ANDROID-(?P<android_id>[0-9]+)"
221 r"(_(?P<subpatch>[0-9]+)|_(kernel)?(?P<kernel>[0-9]\.[0-9]+)|"
222 r"(?P<extra>.*))?$"
223 )
224 #:
225 _ISSUE_MAPPING_FILENAME = re.compile(
226 r"patches/issue-mapping/ANDROID-(?P<android_id>\d+)"
227 )
228
229 def __init__(self, args, manifest):
230 self._args = args
231 #: The archive containing the bulletin patches and metadata.
232 self._archive = zipfile.ZipFile(self._args.zipfile)
233 #: The mapping of patch files to Android bug ids.
234 self._issue_mapping = self._get_issue_mapping()
235 self._manifest = manifest
236
237 def _get_issue_mapping(self):
238 """Get the mapping of patch files to Android bug ids.
239
240 Returns:
241 The mapping of patch files to a list of Android bug id.
242
243 """
244 mapping = {}
245
246 for filename in self._archive.namelist():
247 match = self._ISSUE_MAPPING_FILENAME.match(filename)
248 if not match:
249 continue
250 android_id = "A-" + match.group("android_id")
251
252 with self._archive.open(filename) as issue_file:
253 for raw_line in issue_file:
254 line = raw_line.decode("utf-8").rstrip()
255 if line not in mapping:
256 mapping[line] = []
257 mapping[line].append(android_id)
258
259 return mapping
260
261 def _get_patches(self, android_version, platform, kernel):
262 """Find the patches grouped in this bulletin.
263
264 Arguments:
265 android_version: The version of the Android operating system to
266 find patches for.
267 platform (optional): Whether to find platform security patches.
268 kernel (optional): Whether to find kernel snippets.
269 Returns:
270 The list of patches found.
271
272 """
273 patches = []
274
275 for item in self._archive.namelist():
276 snippet_match = (
277 self.KERNEL_SNIPPETS_FILENAME_PATTERN.search(item)
278 if kernel
279 else False
280 )
281 if snippet_match:
Karsten Tauschee237bc82019-02-01 12:15:15 +0100282 try:
283 patches.append(
284 KernelPatch(
285 self._archive,
286 item,
287 "A-" + snippet_match.group("android_id"),
288 self._manifest,
289 self._args,
290 snippet_match.group("subpatch"),
291 snippet_match.group("kernel"),
292 snippet_match.group("extra"),
293 )
Borjan Tchakaloff72ee6d62018-12-04 10:29:38 +0100294 )
Karsten Tauschee237bc82019-02-01 12:15:15 +0100295 except ProjectNotFoundError as e:
296 print("[BE]: WARNING: {}".format(e))
Borjan Tchakaloff72ee6d62018-12-04 10:29:38 +0100297 elif (
298 platform
299 and "android-{}".format(android_version) in item
300 and ".patch" in item
301 ):
302 issues = self._issue_mapping.get(item, None)
303 # TODO: `issues` will always be None, also before the latest
304 # refactoring.
Karsten Tauschee237bc82019-02-01 12:15:15 +0100305 try:
306 patches.append(
307 PlatformPatch(
308 self._archive, item, issues, self._manifest, self._args
309 )
Borjan Tchakaloff72ee6d62018-12-04 10:29:38 +0100310 )
Karsten Tauschee237bc82019-02-01 12:15:15 +0100311 except ProjectNotFoundError as e:
312 print("[BE]: WARNING: {}".format(e))
Borjan Tchakaloff72ee6d62018-12-04 10:29:38 +0100313
314 return patches
315
316 def apply(self):
317 """Apply all not yet applied patches in this bulletin.
318
319 This high-level function groups all functionality of this
320 SecurityBulletin class together: It finds all patches included in the
321 bulletin archive, checks if they are applied and applies them if
322 necessary.
323
324 """
325 patches = self._get_patches(
326 self._args.version,
327 platform=not self._args.skip_patches,
328 kernel=not self._args.skip_snippets,
329 )
330
331 for patch in sorted(patches, key=lambda p: p.filename):
332 patch.check_and_apply()