1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.security.state 18 19 import android.annotation.SuppressLint 20 import android.content.Context 21 import android.net.Uri 22 import android.os.Build 23 import androidx.annotation.RequiresApi 24 import androidx.annotation.StringDef 25 import androidx.annotation.WorkerThread 26 import androidx.security.state.SecurityStateManagerCompat.Companion.KEY_KERNEL_VERSION 27 import androidx.security.state.SecurityStateManagerCompat.Companion.KEY_SYSTEM_SPL 28 import androidx.security.state.SecurityStateManagerCompat.Companion.KEY_VENDOR_SPL 29 import java.text.ParseException 30 import java.text.SimpleDateFormat 31 import java.util.Calendar 32 import java.util.Date 33 import java.util.Locale 34 import java.util.regex.Pattern 35 import kotlinx.serialization.SerialName 36 import kotlinx.serialization.Serializable 37 import kotlinx.serialization.SerializationException 38 import kotlinx.serialization.json.Json 39 40 /** 41 * Provides methods to access and manage security state information for various components within a 42 * system. This class handles operations related to security patch levels, vulnerability reports, 43 * and update management. 44 * 45 * Usage examples include: 46 * - Fetching the current security patch level for specific system components. 47 * - Retrieving published security patch levels to compare against current levels. 48 * - Listing and applying security updates from designated update providers. 49 * 50 * The class uses a combination of local data storage and external data fetching to maintain and 51 * update security states. 52 * 53 * Recommended pattern of usage: 54 * - call [getVulnerabilityReportUrl] and make a request to download the JSON file containing 55 * vulnerability report data 56 * - create SecurityPatchState object, passing in the downloaded JSON as a [String] 57 * - call [getPublishedSecurityPatchLevel] or other APIs 58 * 59 * @param context Application context used for accessing shared preferences, resources, and other 60 * context-dependent features. 61 * @param systemModulePackageNames A list of system module package names, defaults to Google 62 * provided system modules if none are provided. The first module on the list must be the system 63 * modules metadata provider package. 64 * @param customSecurityStateManagerCompat An optional custom manager for obtaining security state 65 * information. If null, a default manager is instantiated. 66 * @param vulnerabilityReportJsonString A JSON string containing vulnerability data to initialize a 67 * [VulnerabilityReport] object. 68 * 69 * If you only care about the Device SPL, this parameter is optional. If you need access to 70 * Published SPL and Available SPL, you must provide this JSON string, either here in the 71 * constructor, or later using [loadVulnerabilityReport]. 72 * 73 * @constructor Creates an instance of SecurityPatchState. 74 */ 75 public open class SecurityPatchState 76 @JvmOverloads 77 constructor( 78 private val context: Context, 79 private val systemModulePackageNames: List<String> = DEFAULT_SYSTEM_MODULES, 80 private val customSecurityStateManagerCompat: SecurityStateManagerCompat? = null, 81 vulnerabilityReportJsonString: String? = null 82 ) { 83 init { 84 if (vulnerabilityReportJsonString != null) { 85 loadVulnerabilityReport(vulnerabilityReportJsonString) 86 } 87 } 88 89 private val securityStateManagerCompat = 90 customSecurityStateManagerCompat ?: SecurityStateManagerCompat(context = context) 91 private var vulnerabilityReport: VulnerabilityReport? = null 92 93 public companion object { 94 /** Default list of Android Mainline system modules. */ 95 @JvmField 96 public val DEFAULT_SYSTEM_MODULES: List<String> = 97 listOf( 98 "com.google.android.modulemetadata", 99 "com.google.mainline.telemetry", 100 "com.google.mainline.adservices", 101 "com.google.mainline.go.primary", 102 "com.google.mainline.go.telemetry" 103 ) 104 105 /** URL for the Google-provided data of vulnerabilities from Android Security Bulletin. */ 106 public const val DEFAULT_VULNERABILITY_REPORTS_URL: String = 107 "https://storage.googleapis.com/osv-android-api" 108 109 /** 110 * System component providing ro.build.version.security_patch property value as 111 * DateBasedSpl. 112 */ 113 public const val COMPONENT_SYSTEM: String = "SYSTEM" 114 115 /** System modules component providing DateBasedSpl of system modules patch level. */ 116 public const val COMPONENT_SYSTEM_MODULES: String = "SYSTEM_MODULES" 117 118 /** Kernel component providing kernel version as VersionedSpl. */ 119 public const val COMPONENT_KERNEL: String = "KERNEL" 120 121 /** 122 * Vendor component providing ro.vendor.build.security_patch property value as DateBasedSpl. 123 */ 124 internal const val COMPONENT_VENDOR: String = "VENDOR" 125 126 /** Disabled until Android provides sufficient guidelines for the usage of Vendor SPL. */ 127 internal var USE_VENDOR_SPL = false 128 129 /** 130 * Retrieves the specific security patch level for a given component based on a security 131 * patch level string. This method determines the type of [SecurityPatchLevel] to construct 132 * based on the component type, interpreting the string as a date for date-based components 133 * or as a version number for versioned components. 134 * 135 * @param component The component indicating which type of component's patch level is being 136 * requested. 137 * @param securityPatchLevel The string representation of the security patch level, which 138 * could be a date or a version number. 139 * @return A [SecurityPatchLevel] instance corresponding to the specified component and 140 * patch level string. 141 * @throws IllegalArgumentException If the input string is not in a valid format for the 142 * specified component type, or if the component requires a specific format that the 143 * string does not meet. 144 */ 145 @JvmStatic 146 public fun getComponentSecurityPatchLevel( 147 @Component component: String, 148 securityPatchLevel: String 149 ): SecurityPatchLevel { 150 val exception = IllegalArgumentException("Unknown component: $component") 151 return when (component) { 152 COMPONENT_SYSTEM, 153 COMPONENT_SYSTEM_MODULES, 154 COMPONENT_VENDOR -> { 155 if (component == COMPONENT_VENDOR && !USE_VENDOR_SPL) { 156 throw exception 157 } 158 // These components are expected to use DateBasedSpl 159 DateBasedSecurityPatchLevel.fromString(securityPatchLevel) 160 } 161 COMPONENT_KERNEL -> { 162 // These components are expected to use VersionedSpl 163 VersionedSecurityPatchLevel.fromString(securityPatchLevel) 164 } 165 else -> throw exception 166 } 167 } 168 169 /** 170 * Constructs a URL for fetching vulnerability reports based on the device's Android 171 * version. 172 * 173 * @param serverUrl The base URL of the server where vulnerability reports are stored. 174 * @return A fully constructed URL pointing to the specific vulnerability report for this 175 * device. 176 */ 177 @JvmStatic 178 @RequiresApi(26) 179 public fun getVulnerabilityReportUrl( 180 serverUrl: Uri = Uri.parse(DEFAULT_VULNERABILITY_REPORTS_URL) 181 ): Uri { 182 val newEndpoint = "v1/android_sdk_${Build.VERSION.SDK_INT}.json" 183 return serverUrl.buildUpon().appendEncodedPath(newEndpoint).build() 184 } 185 } 186 187 /** Annotation for defining the component to use. */ 188 @Retention(AnnotationRetention.SOURCE) 189 @StringDef( 190 open = true, 191 value = 192 [ 193 COMPONENT_SYSTEM, 194 COMPONENT_SYSTEM_MODULES, 195 COMPONENT_KERNEL, 196 COMPONENT_VENDOR, 197 ] 198 ) 199 internal annotation class Component 200 201 /** Severity of reported security issues. */ 202 public enum class Severity { 203 /** Critical severity issues from Android Security Bulletin. */ 204 CRITICAL, 205 /** High severity issues from Android Security Bulletin. */ 206 HIGH, 207 /** Moderate severity issues from Android Security Bulletin. */ 208 MODERATE, 209 /** Low severity issues from Android Security Bulletin. */ 210 LOW 211 } 212 213 /** Abstract base class representing a security patch level. */ 214 public abstract class SecurityPatchLevel : Comparable<SecurityPatchLevel> { 215 abstract override fun toString(): String 216 } 217 218 /** Implementation of [SecurityPatchLevel] for a simple string patch level. */ 219 public class GenericStringSecurityPatchLevel(private val patchLevel: String) : 220 SecurityPatchLevel() { 221 222 override fun toString(): String = patchLevel 223 224 override fun compareTo(other: SecurityPatchLevel): Int { 225 return when (other) { 226 is GenericStringSecurityPatchLevel -> patchLevel.compareTo(other.patchLevel) 227 else -> 228 throw IllegalArgumentException( 229 "Cannot compare GenericStringSpl with different type." 230 ) 231 } 232 } 233 } 234 235 /** Implementation of [SecurityPatchLevel] for a date-based patch level. */ 236 public class DateBasedSecurityPatchLevel( 237 private val year: Int, 238 private val month: Int, 239 private val day: Int 240 ) : SecurityPatchLevel() { 241 242 public companion object { 243 private val DATE_FORMATS = listOf("yyyy-MM", "yyyy-MM-dd") 244 245 /** 246 * Creates a new [DateBasedSecurityPatchLevel] from a string representation of the date. 247 * 248 * @param value The date string in the format of [DATE_FORMATS]. 249 * @return A new [DateBasedSecurityPatchLevel] representing the date. 250 * @throws IllegalArgumentException if the date string is not in the correct format. 251 */ 252 @JvmStatic 253 public fun fromString(value: String): DateBasedSecurityPatchLevel { 254 var date: Date? = null 255 for (dateFormat in DATE_FORMATS) { 256 try { 257 date = 258 SimpleDateFormat(dateFormat, Locale.US) 259 .apply { 260 isLenient = false // Set the date parsing to be strict 261 } 262 .parse(value) 263 } catch (e: ParseException) { 264 // Ignore and try other date format. 265 } 266 } 267 if (date != null) { 268 val calendar = Calendar.getInstance() 269 calendar.time = date 270 val year = calendar.get(Calendar.YEAR) 271 /* Calendar.MONTH is zero-based */ 272 val month = calendar.get(Calendar.MONTH) + 1 273 val day = calendar.get(Calendar.DAY_OF_MONTH) 274 return DateBasedSecurityPatchLevel(year, month, day) 275 } else { 276 throw IllegalArgumentException( 277 "Invalid date format. Expected formats: $DATE_FORMATS", 278 ) 279 } 280 } 281 } 282 283 @SuppressLint("DefaultLocale") 284 override fun toString(): String = String.format("%d-%02d-%02d", year, month, day) 285 286 override fun compareTo(other: SecurityPatchLevel): Int { 287 if (other is DateBasedSecurityPatchLevel) { 288 return when { 289 year != other.year -> year - other.year 290 month != other.month -> month - other.month 291 else -> day - other.day 292 } 293 } else { 294 throw IllegalArgumentException("Cannot compare DateBasedSpl with different type.") 295 } 296 } 297 298 /** Year of the security patch level. */ 299 public fun getYear(): Int = year 300 301 /** Month of the security patch level. */ 302 public fun getMonth(): Int = month 303 304 /** Day of the security patch level. */ 305 public fun getDay(): Int = day 306 } 307 308 /** Implementation of [SecurityPatchLevel] for a versioned patch level. */ 309 public class VersionedSecurityPatchLevel( 310 private val majorVersion: Int, 311 private val minorVersion: Int, 312 private val buildVersion: Int = 0, 313 private val patchVersion: Int = 0 314 ) : SecurityPatchLevel() { 315 316 public companion object { 317 /** 318 * Creates a new [VersionedSecurityPatchLevel] from a string representation of the 319 * version. 320 * 321 * @param value The version string in the format of "major.minor.build.patch". 322 * @return A new [VersionedSecurityPatchLevel] representing the version. 323 * @throws IllegalArgumentException if the version string is not in the correct format. 324 */ 325 @JvmStatic 326 public fun fromString(value: String): VersionedSecurityPatchLevel { 327 val parts = value.split(".") 328 if (parts.size < 2) { 329 throw IllegalArgumentException( 330 "Invalid version format. Expected at least major and minor versions." 331 ) 332 } 333 334 val major = 335 parts[0].toIntOrNull() 336 ?: throw IllegalArgumentException("Major version is not a valid number.") 337 val minor = 338 parts[1].toIntOrNull() 339 ?: throw IllegalArgumentException("Minor version is not a valid number.") 340 val patch: Int 341 val build: Int 342 if (parts.size > 3) { 343 build = parts[2].toIntOrNull() ?: 0 344 patch = parts[3].toIntOrNull() ?: 0 345 } else if (parts.size == 3) { 346 build = 0 347 patch = parts[2].toIntOrNull() ?: 0 348 } else { 349 build = 0 350 patch = 0 351 } 352 353 return VersionedSecurityPatchLevel(major, minor, build, patch) 354 } 355 } 356 357 @SuppressLint("DefaultLocale") 358 override fun toString(): String { 359 // Include the build version if it is non-zero 360 return when { 361 buildVersion > 0 -> 362 String.format( 363 "%d.%d.%d.%d", 364 majorVersion, 365 minorVersion, 366 buildVersion, 367 patchVersion 368 ) 369 patchVersion > 0 -> 370 String.format("%d.%d.%d", majorVersion, minorVersion, patchVersion) 371 else -> String.format("%d.%d", majorVersion, minorVersion) 372 } 373 } 374 375 override fun compareTo(other: SecurityPatchLevel): Int { 376 if (other is VersionedSecurityPatchLevel) { 377 return when { 378 majorVersion != other.majorVersion -> majorVersion - other.majorVersion 379 minorVersion != other.minorVersion -> minorVersion - other.minorVersion 380 patchVersion != other.patchVersion -> patchVersion - other.patchVersion 381 else -> buildVersion - other.buildVersion 382 } 383 } else { 384 throw IllegalArgumentException( 385 "Cannot compare VersionedSecurityPatchLevel with different type" 386 ) 387 } 388 } 389 390 /** Major version of the security patch level. */ 391 public fun getMajorVersion(): Int = majorVersion 392 393 /** Minor version of the security patch level. */ 394 public fun getMinorVersion(): Int = minorVersion 395 396 /** Patch version of the security patch level. */ 397 public fun getPatchVersion(): Int = patchVersion 398 399 /** Build version of the security patch level. */ 400 public fun getBuildVersion(): Int = buildVersion 401 } 402 403 @Serializable 404 private data class VulnerabilityReport( 405 /* Key is the SPL date yyyy-MM-dd */ 406 val vulnerabilities: Map<String, List<VulnerabilityGroup>>, 407 408 /* Key is the SPL date yyyy-MM-dd, values are kernel versions */ 409 @SerialName("kernel_lts_versions") val kernelLtsVersions: Map<String, List<String>> 410 ) 411 412 @Serializable 413 private data class VulnerabilityGroup( 414 @SerialName("cve_identifiers") val cveIdentifiers: List<String>, 415 @SerialName("asb_identifiers") val asbIdentifiers: List<String>, 416 val severity: String, 417 val components: List<String> 418 ) 419 420 /** 421 * Retrieves a list of all system modules, defaulting to a predefined list of Google system 422 * modules if no custom modules are provided. 423 * 424 * @return A list of strings representing system module identifiers. 425 */ 426 internal fun getSystemModules(): List<String> { 427 return systemModulePackageNames.ifEmpty { DEFAULT_SYSTEM_MODULES } 428 } 429 430 /** 431 * Parses a JSON string to extract vulnerability report data. This method validates the format 432 * of the input JSON and constructs a [VulnerabilityReport] object, preparing the class to 433 * provide published and available security state information. 434 * 435 * @param jsonString The JSON string containing the vulnerability data. 436 * @throws IllegalArgumentException if the JSON input is malformed or contains invalid data. 437 */ 438 @WorkerThread 439 public fun loadVulnerabilityReport(jsonString: String) { 440 val result: VulnerabilityReport 441 442 try { 443 val json = Json { ignoreUnknownKeys = true } 444 result = json.decodeFromString<VulnerabilityReport>(jsonString) 445 } catch (e: SerializationException) { 446 throw IllegalArgumentException("Malformed JSON input: ${e.message}") 447 } 448 449 val dateFormat = SimpleDateFormat("yyyy-MM-dd") 450 dateFormat.isLenient = false 451 452 result.vulnerabilities.keys.forEach { date -> 453 try { 454 dateFormat.parse(date) 455 } catch (e: Exception) { 456 throw IllegalArgumentException( 457 "Invalid format in date key for vulnerabilities (yyyy-MM-dd): $date" 458 ) 459 } 460 } 461 462 result.kernelLtsVersions.forEach { kv -> 463 try { 464 dateFormat.parse(kv.key) 465 } catch (e: Exception) { 466 throw IllegalArgumentException( 467 "Invalid format in date key for kernel LTS versions (yyyy-MM-dd): ${kv.key}" 468 ) 469 } 470 471 kv.value.forEach { 472 val majorVersion: Int 473 try { 474 majorVersion = VersionedSecurityPatchLevel.fromString(it).getMajorVersion() 475 } catch (e: Exception) { 476 throw IllegalArgumentException("Invalid format in kernel LTS version: $it") 477 } 478 479 if (majorVersion < 4 || majorVersion > 20) { 480 throw IllegalArgumentException("Invalid format in kernel LTS version: $it") 481 } 482 } 483 } 484 485 val cvePattern = Pattern.compile("CVE-\\d{4}-\\d{4,}") 486 val asbPattern = Pattern.compile("(ASB|PUB)-A-\\d{4,}") 487 488 result.vulnerabilities.values.flatten().forEach { group -> 489 group.cveIdentifiers.forEach { cve -> 490 if (!cvePattern.matcher(cve).matches()) { 491 throw IllegalArgumentException( 492 "CVE identifier does not match the required format (CVE-XXXX-XXXX): $cve" 493 ) 494 } 495 } 496 497 group.asbIdentifiers.forEach { asb -> 498 if (!asbPattern.matcher(asb).matches()) { 499 throw IllegalArgumentException( 500 "ASB identifier $asb does not match the required format: $asbPattern" 501 ) 502 } 503 } 504 505 try { 506 Severity.valueOf(group.severity.uppercase(Locale.US)) 507 } catch (e: IllegalArgumentException) { 508 throw IllegalArgumentException( 509 "Severity must be: critical, high, moderate, low. Found: ${group.severity}" 510 ) 511 } 512 } 513 514 vulnerabilityReport = result 515 } 516 517 private fun getMaxComponentSecurityPatchLevel( 518 @Component component: String 519 ): DateBasedSecurityPatchLevel? { 520 if (vulnerabilityReport == null) return null 521 522 // Iterate through all SPL dates, find the latest date where 523 // the specified component is included 524 return vulnerabilityReport!! 525 .vulnerabilities 526 .filter { entry -> entry.value.any { group -> component in group.components } } 527 .keys 528 .maxByOrNull { it } 529 ?.let { latestDate -> DateBasedSecurityPatchLevel.fromString(latestDate) } 530 } 531 532 private fun componentToString(@Component component: String): String { 533 return component.lowercase(Locale.US) 534 } 535 536 private fun checkVulnerabilityReport() { 537 if (vulnerabilityReport == null) 538 throw IllegalStateException("No vulnerability report data available.") 539 } 540 541 /** 542 * Returns min SPL of the unpatched system modules, or max SPL of the system modules if all of 543 * them are fully patched. 544 */ 545 private fun getSystemModulesSecurityPatchLevel(): DateBasedSecurityPatchLevel { 546 checkVulnerabilityReport() 547 548 val modules: List<String> = getSystemModules() 549 var minSpl = DateBasedSecurityPatchLevel(1970, 1, 1) 550 var maxSpl = DateBasedSecurityPatchLevel(1970, 1, 1) 551 var unpatched = false 552 modules.forEach { module -> 553 val maxComponentSpl = getMaxComponentSecurityPatchLevel(module) ?: return@forEach 554 val packageSpl: DateBasedSecurityPatchLevel 555 try { 556 packageSpl = 557 DateBasedSecurityPatchLevel.fromString( 558 securityStateManagerCompat.getPackageVersion(module) 559 ) 560 } catch (e: Exception) { 561 // Prevent malformed package versions from interrupting the loop. 562 return@forEach 563 } 564 565 if (packageSpl < maxComponentSpl) { 566 if (unpatched) { 567 if (minSpl > packageSpl) minSpl = packageSpl 568 } else { 569 minSpl = packageSpl 570 unpatched = true 571 } 572 } 573 if (maxComponentSpl > maxSpl) { 574 maxSpl = maxComponentSpl 575 } 576 } 577 578 if (unpatched) { 579 return minSpl 580 } 581 if (maxSpl.getYear() == 1970) { 582 throw IllegalStateException("No SPL data available for system modules.") 583 } 584 return maxSpl 585 } 586 587 private fun getSystemModulesPublishedSecurityPatchLevel(): DateBasedSecurityPatchLevel { 588 checkVulnerabilityReport() 589 590 val modules: List<String> = getSystemModules() 591 var maxSpl = DateBasedSecurityPatchLevel(1970, 1, 1) 592 modules.forEach { module -> 593 val maxComponentSpl = getMaxComponentSecurityPatchLevel(module) ?: return@forEach 594 595 if (maxComponentSpl > maxSpl) { 596 maxSpl = maxComponentSpl 597 } 598 } 599 return maxSpl 600 } 601 602 /** 603 * Retrieves the current security patch level for a specified component. 604 * 605 * @param component The component for which the security patch level is requested. 606 * @return A [SecurityPatchLevel] representing the current patch level of the component. 607 * @throws IllegalStateException if the patch level data is not available. 608 * @throws IllegalArgumentException if the component name is unrecognized. 609 */ 610 public open fun getDeviceSecurityPatchLevel(@Component component: String): SecurityPatchLevel { 611 val globalSecurityState = 612 securityStateManagerCompat.getGlobalSecurityState(getSystemModules()[0]) 613 614 return when (component) { 615 COMPONENT_SYSTEM_MODULES -> { 616 getSystemModulesSecurityPatchLevel() 617 } 618 COMPONENT_KERNEL -> { 619 val kernelVersion = 620 globalSecurityState.getString(KEY_KERNEL_VERSION) 621 ?: throw IllegalStateException("Kernel version not available.") 622 623 VersionedSecurityPatchLevel.fromString(kernelVersion) 624 } 625 COMPONENT_SYSTEM -> { 626 val systemSpl = 627 globalSecurityState.getString(KEY_SYSTEM_SPL) 628 ?: throw IllegalStateException("System SPL not available.") 629 630 DateBasedSecurityPatchLevel.fromString(systemSpl) 631 } 632 COMPONENT_VENDOR -> { 633 val vendorSpl = 634 globalSecurityState.getString(KEY_VENDOR_SPL) 635 ?: throw IllegalStateException("Vendor SPL not available.") 636 637 DateBasedSecurityPatchLevel.fromString(vendorSpl) 638 } 639 else -> throw IllegalArgumentException("Unknown component: $component") 640 } 641 } 642 643 /** 644 * Retrieves the published security patch level for a specified component. This patch level is 645 * based on the most recent vulnerability reports, which is a machine-readable data from Android 646 * and other security bulletins. 647 * 648 * The published security patch level is the most recent value published in a bulletin. 649 * 650 * @param component The component for which the published patch level is requested. 651 * @return A list of [SecurityPatchLevel] representing the published patch levels. The list 652 * contains single element for all components, except for KERNEL, where it lists kernel LTS 653 * version numbers for all supported major kernel versions. For example: ``` [ "4.19.314", 654 * "5.15.159", "6.1.91" ] ``` 655 * @throws IllegalStateException if the vulnerability report is not loaded or if patch level 656 * data is unavailable. 657 * @throws IllegalArgumentException if the component name is unrecognized. 658 */ 659 public open fun getPublishedSecurityPatchLevel( 660 @Component component: String 661 ): List<SecurityPatchLevel> { 662 checkVulnerabilityReport() 663 664 return when (component) { 665 COMPONENT_SYSTEM_MODULES -> listOf(getSystemModulesPublishedSecurityPatchLevel()) 666 COMPONENT_SYSTEM, 667 COMPONENT_VENDOR -> { 668 val exception = IllegalStateException("SPL data not available: $component") 669 if (component == COMPONENT_VENDOR && !USE_VENDOR_SPL) { 670 throw exception 671 } 672 listOf( 673 getMaxComponentSecurityPatchLevel(componentToString(component)) 674 ?: throw exception 675 ) 676 } 677 COMPONENT_KERNEL -> getPublishedKernelVersions() 678 else -> throw IllegalArgumentException("Unknown component: $component") 679 } 680 } 681 682 /** 683 * Retrieves a list of the latest kernel LTS versions from the vulnerability report. 684 * 685 * @return A list of [VersionedSecurityPatchLevel] representing kernel LTS versions, or an empty 686 * list if no data is available. 687 */ 688 private fun getPublishedKernelVersions(): List<VersionedSecurityPatchLevel> { 689 vulnerabilityReport?.let { (_, kernelLtsVersions) -> 690 if (kernelLtsVersions.isEmpty()) { 691 return emptyList() 692 } 693 // A map from a kernel LTS version (major.minor) to its latest published version. 694 // For example, version 5.4 would map to 5.4.123 if that's the latest published version. 695 val kernelVersionToLatest = mutableMapOf<String, VersionedSecurityPatchLevel>() 696 // Reduce all the published kernel LTS versions from each SPL into one list. 697 val publishedKernelLtsVersions = 698 kernelLtsVersions.values 699 .reduce { versions, version -> versions + version } 700 .map { VersionedSecurityPatchLevel.fromString(it) } 701 702 // Update the map so that each kernel LTS version maps to its latest (largest) published 703 // version. 704 publishedKernelLtsVersions.forEach { version -> 705 val kernelVersion = "${version.getMajorVersion()}.${version.getMinorVersion()}" 706 707 kernelVersionToLatest[kernelVersion]?.let { 708 if (version > it) { 709 kernelVersionToLatest[kernelVersion] = version 710 } 711 } ?: run { kernelVersionToLatest[kernelVersion] = version } 712 } 713 return kernelVersionToLatest.values.toList() 714 } 715 return emptyList() 716 } 717 718 /** 719 * Lists all security fixes applied on the current device since the baseline Android release of 720 * the current system image, filtered for a specified component and patch level, categorized by 721 * severity. 722 * 723 * @param component The component for which security fixes are listed. 724 * @param spl The security patch level for which fixes are retrieved. 725 * @return A map categorizing CVE identifiers by their severity for the specified patch level. 726 * For example: ``` { Severity.CRITICAL: ["CVE-2023-1234", "CVE-2023-5678"], Severity.HIGH: 727 * ["CVE-2023-9012"], Severity.MODERATE: ["CVE-2023-3456"] } ``` 728 * @throws IllegalArgumentException if the specified component is not valid for fetching 729 * security fixes. 730 * @throws IllegalStateException if the vulnerability report is not loaded. 731 */ 732 public open fun getPatchedCves( 733 @Component component: String, 734 spl: SecurityPatchLevel 735 ): Map<Severity, Set<String>> { 736 // Check if the component is valid for this operation 737 val validComponents = 738 listOfNotNull( 739 COMPONENT_SYSTEM, 740 if (USE_VENDOR_SPL) COMPONENT_VENDOR else null, 741 COMPONENT_SYSTEM_MODULES 742 ) 743 if (component !in validComponents) { 744 throw IllegalArgumentException( 745 "Component must be one of $validComponents but was $component" 746 ) 747 } 748 checkVulnerabilityReport() 749 750 vulnerabilityReport!!.let { report -> 751 val relevantFixes = mutableMapOf<Severity, MutableList<String>>() 752 753 // Iterate through all vulnerabilities and filter based on component and patch level 754 report.vulnerabilities.forEach { (patchLevel, groups) -> 755 if (spl.toString() >= patchLevel) { 756 groups 757 .filter { group -> 758 when (component) { 759 COMPONENT_SYSTEM_MODULES -> 760 group.components.any { it in getSystemModules() } 761 else -> group.components.contains(componentToString(component)) 762 } 763 } 764 .forEach { group -> 765 val severity = Severity.valueOf(group.severity.uppercase(Locale.US)) 766 relevantFixes 767 .getOrPut(severity, ::mutableListOf) 768 .addAll(group.cveIdentifiers) 769 } 770 } 771 } 772 return relevantFixes.mapValues { it.value.toSet() }.toMap() 773 } 774 } 775 776 /** 777 * Checks if all components of the device have their security patch levels up to date with the 778 * published security patch levels. This method compares the device's current security patch 779 * level against the latest published levels for each component. 780 * 781 * @return true if all components are fully updated, false otherwise. 782 * @throws IllegalArgumentException if device or published security patch level for a component 783 * cannot be accessed. 784 */ 785 public fun isDeviceFullyUpdated(): Boolean { 786 checkVulnerabilityReport() 787 788 val components = 789 listOf( 790 COMPONENT_SYSTEM, 791 COMPONENT_SYSTEM_MODULES, 792 COMPONENT_VENDOR, 793 COMPONENT_KERNEL, 794 ) 795 796 components.forEach { component -> 797 if (component == COMPONENT_VENDOR && !USE_VENDOR_SPL) return@forEach 798 val deviceSpl = 799 try { 800 getDeviceSecurityPatchLevel(component) 801 } catch (e: Exception) { 802 throw IllegalStateException( 803 "Failed to retrieve device SPL for component: $component", 804 e 805 ) 806 } 807 808 try { 809 if (component != COMPONENT_KERNEL) { 810 val publishedSpl = getPublishedSecurityPatchLevel(component)[0] 811 812 if (deviceSpl < publishedSpl) { 813 return false 814 } 815 } else { 816 val publishedVersions = getPublishedKernelVersions() 817 val kernelVersion = deviceSpl as VersionedSecurityPatchLevel 818 819 if ( 820 publishedVersions 821 .filter { it.getMajorVersion() == kernelVersion.getMajorVersion() } 822 .any { it > kernelVersion } 823 ) { 824 return false 825 } 826 } 827 } catch (e: Exception) { 828 throw IllegalStateException( 829 "Published SPL not available for component: $component", 830 e 831 ) 832 } 833 } 834 return true 835 } 836 837 /** 838 * Verifies if all specified CVEs have been patched in the system. This method aggregates the 839 * CVEs patched across specified system components and checks if the list includes all CVEs 840 * provided. 841 * 842 * @param cveList A list of CVE identifiers as strings in the form "CVE-YYYY-NNNNN", where YYYY 843 * denotes year, and NNNNN is a number with 3 to 5 digits. 844 * @return true if all provided CVEs are patched, false otherwise. 845 */ 846 public fun areCvesPatched(cveList: List<String>): Boolean { 847 val componentsToCheck = 848 listOfNotNull( 849 COMPONENT_SYSTEM, 850 if (USE_VENDOR_SPL) COMPONENT_VENDOR else null, 851 COMPONENT_SYSTEM_MODULES 852 ) 853 val allPatchedCves = mutableSetOf<String>() 854 855 // Aggregate all CVEs from security fixes across necessary components 856 for (component in componentsToCheck) { 857 val spl = getDeviceSecurityPatchLevel(component) 858 val fixes = getPatchedCves(component, spl) 859 allPatchedCves.addAll(fixes.values.flatten()) 860 } 861 862 // Check if all provided CVEs are in the patched CVEs list 863 return cveList.all { allPatchedCves.contains(it) } 864 } 865 } 866