Borjan Tchakaloff | 72ee6d6 | 2018-12-04 10:29:38 +0100 | [diff] [blame] | 1 | import os |
| 2 | import re |
| 3 | import zipfile |
| 4 | |
| 5 | from manifest import ProjectNotFoundError |
| 6 | from utility import exec_command as utility_exec_command |
| 7 | |
| 8 | |
| 9 | class 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 Tausche | f414272 | 2019-01-30 11:20:35 +0100 | [diff] [blame] | 93 | if self.args.apply_to_branch: |
Borjan Tchakaloff | 72ee6d6 | 2018-12-04 10:29:38 +0100 | [diff] [blame] | 94 | 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 Tausche | f414272 | 2019-01-30 11:20:35 +0100 | [diff] [blame] | 165 | if not self.args.skip_amend and not self.args.cleanup_after: |
Borjan Tchakaloff | 72ee6d6 | 2018-12-04 10:29:38 +0100 | [diff] [blame] | 166 | self.amend() |
| 167 | |
| 168 | def exec_command(self, command): |
| 169 | return utility_exec_command(command, self.args.quiet) |
| 170 | |
| 171 | |
| 172 | class PlatformPatch(SecurityPatch): |
| 173 | pass |
| 174 | |
| 175 | |
| 176 | class 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 | |
| 201 | class 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 Tausche | e237bc8 | 2019-02-01 12:15:15 +0100 | [diff] [blame] | 282 | 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 Tchakaloff | 72ee6d6 | 2018-12-04 10:29:38 +0100 | [diff] [blame] | 294 | ) |
Karsten Tausche | e237bc8 | 2019-02-01 12:15:15 +0100 | [diff] [blame] | 295 | except ProjectNotFoundError as e: |
| 296 | print("[BE]: WARNING: {}".format(e)) |
Borjan Tchakaloff | 72ee6d6 | 2018-12-04 10:29:38 +0100 | [diff] [blame] | 297 | 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 Tausche | e237bc8 | 2019-02-01 12:15:15 +0100 | [diff] [blame] | 305 | try: |
| 306 | patches.append( |
| 307 | PlatformPatch( |
| 308 | self._archive, item, issues, self._manifest, self._args |
| 309 | ) |
Borjan Tchakaloff | 72ee6d6 | 2018-12-04 10:29:38 +0100 | [diff] [blame] | 310 | ) |
Karsten Tausche | e237bc8 | 2019-02-01 12:15:15 +0100 | [diff] [blame] | 311 | except ProjectNotFoundError as e: |
| 312 | print("[BE]: WARNING: {}".format(e)) |
Borjan Tchakaloff | 72ee6d6 | 2018-12-04 10:29:38 +0100 | [diff] [blame] | 313 | |
| 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() |